     1→//! Server configuration
     2→//!
     3→//! Configuration is loaded from environment variables.
     4→//! Required variables:
     5→//! - DATABASE_URL: PostgreSQL connection string
     6→//! - JWT_SECRET: Secret key for JWT token signing
     7→//!
     8→//! Optional variables:
     9→//! - SERVER_HOST: Host to bind to (default: 0.0.0.0)
    10→//! - SERVER_PORT: Port to bind to (default: 3001)
    11→//! - DB_MAX_CONNECTIONS: Max database connections (default: 10)
    12→//! - DOWNLOADS_DIR: Directory containing agent binaries (default: /var/www/downloads)
    13→//! - DOWNLOADS_BASE_URL: Base URL for downloads (default: http://localhost:3001/downloads)
    14→//! - AUTO_UPDATE_ENABLED: Enable automatic agent updates (default: true)
    15→//! - UPDATE_TIMEOUT_SECS: Timeout for agent updates (default: 180)
    16→
    17→use std::path::PathBuf;
    18→
    19→use anyhow::{Context, Result};
    20→use serde::{Deserialize, Serialize};
    21→
    22→/// Root server configuration
    23→#[derive(Debug, Clone, Serialize, Deserialize)]
    24→pub struct ServerConfig {
    25→    /// Server binding configuration
    26→    pub server: ServerBindConfig,
    27→
    28→    /// Database configuration
    29→    pub database: DatabaseConfig,
    30→
    31→    /// Authentication configuration
    32→    pub auth: AuthConfig,
    33→
    34→    /// Agent updates configuration
    35→    pub updates: UpdatesConfig,
    36→}
    37→
    38→/// Server binding configuration
    39→#[derive(Debug, Clone, Serialize, Deserialize)]
    40→pub struct ServerBindConfig {
    41→    /// Host to bind to
    42→    pub host: String,
    43→
    44→    /// Port to bind to
    45→    pub port: u16,
    46→}
    47→
    48→/// Database configuration
    49→#[derive(Debug, Clone, Serialize, Deserialize)]
    50→pub struct DatabaseConfig {
    51→    /// PostgreSQL connection URL
    52→    pub url: String,
    53→
    54→    /// Maximum number of connections in the pool
    55→    pub max_connections: u32,
    56→}
    57→
    58→/// Authentication configuration
    59→#[derive(Debug, Clone, Serialize, Deserialize)]
    60→pub struct AuthConfig {
    61→    /// JWT signing secret
    62→    pub jwt_secret: String,
    63→
    64→    /// JWT token expiry in hours
    65→    pub jwt_expiry_hours: u64,
    66→
    67→    /// API key prefix for agents
    68→    pub api_key_prefix: String,
    69→}
    70→
    71→/// Agent updates configuration
    72→#[derive(Debug, Clone, Serialize, Deserialize)]
    73→pub struct UpdatesConfig {
    74→    /// Directory containing agent binaries
    75→    pub downloads_dir: PathBuf,
    76→
    77→    /// Base URL for agent downloads
    78→    pub downloads_base_url: String,
    79→
    80→    /// Enable automatic agent updates
    81→    pub auto_update_enabled: bool,
    82→
    83→    /// Timeout for agent updates in seconds
    84→    pub update_timeout_secs: u64,
    85→
    86→    /// Interval for scanning downloads directory (seconds)
    87→    pub scan_interval_secs: u64,
    88→}
    89→
    90→impl ServerConfig {
    91→    /// Load configuration from environment variables
    92→    pub fn from_env() -> Result<Self> {
    93→        let database_url =
    94→            std::env::var("DATABASE_URL").context("DATABASE_URL environment variable not set")?;
    95→
    96→        let jwt_secret =
    97→            std::env::var("JWT_SECRET").context("JWT_SECRET environment variable not set")?;
    98→
    99→        let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
   100→
   101→        let port = std::env::var("SERVER_PORT")
   102→            .ok()
   103→            .and_then(|p| p.parse().ok())
   104→            .unwrap_or(3001);
   105→
   106→        let max_connections = std::env::var("DB_MAX_CONNECTIONS")
   107→            .ok()
   108→            .and_then(|p| p.parse().ok())
   109→            .unwrap_or(10);
   110→
   111→        let jwt_expiry_hours = std::env::var("JWT_EXPIRY_HOURS")
   112→            .ok()
   113→            .and_then(|p| p.parse().ok())
   114→            .unwrap_or(24);
   115→
   116→        let api_key_prefix =
   117→            std::env::var("API_KEY_PREFIX").unwrap_or_else(|_| "grmm_".to_string());
   118→
   119→        // Updates configuration
   120→        let downloads_dir = std::env::var("DOWNLOADS_DIR")
   121→            .map(PathBuf::from)
   122→            .unwrap_or_else(|_| PathBuf::from("/var/www/downloads"));
   123→
   124→        let downloads_base_url = std::env::var("DOWNLOADS_BASE_URL")
   125→            .unwrap_or_else(|_| format!("http://{}:{}/downloads", host, port));
   126→
   127→        let auto_update_enabled = std::env::var("AUTO_UPDATE_ENABLED")
   128→            .map(|v| v.to_lowercase() == "true" || v == "1")
   129→            .unwrap_or(true);
   130→
   131→        let update_timeout_secs = std::env::var("UPDATE_TIMEOUT_SECS")
   132→            .ok()
   133→            .and_then(|p| p.parse().ok())
   134→            .unwrap_or(180);
   135→
   136→        let scan_interval_secs = std::env::var("SCAN_INTERVAL_SECS")
   137→            .ok()
   138→            .and_then(|p| p.parse().ok())
   139→            .unwrap_or(300); // 5 minutes
   140→
   141→        Ok(Self {
   142→            server: ServerBindConfig { host, port },
   143→            database: DatabaseConfig {
   144→                url: database_url,
   145→                max_connections,
   146→            },
   147→            auth: AuthConfig {
   148→                jwt_secret,
   149→                jwt_expiry_hours,
   150→                api_key_prefix,
   151→            },
   152→            updates: UpdatesConfig {
   153→                downloads_dir,
   154→                downloads_base_url,
   155→                auto_update_enabled,
   156→                update_timeout_secs,
   157→                scan_interval_secs,
   158→            },
   159→        })
   160→    }
   161→}
   162→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Server configuration
//!
//! Configuration is loaded from environment variables.
//! Required variables:
//! - DATABASE_URL: PostgreSQL connection string
//! - JWT_SECRET: Secret key for JWT token signing
//!
//! Optional variables:
//! - SERVER_HOST: Host to bind to (default: 0.0.0.0)
//! - SERVER_PORT: Port to bind to (default: 3001)
//! - DB_MAX_CONNECTIONS: Max database connections (default: 10)
//! - DOWNLOADS_DIR: Directory containing agent binaries (default: /var/www/downloads)
//! - DOWNLOADS_BASE_URL: Base URL for downloads (default: http://localhost:3001/downloads)
//! - AUTO_UPDATE_ENABLED: Enable automatic agent updates (default: true)
//! - UPDATE_TIMEOUT_SECS: Timeout for agent updates (default: 180)

use std::path::PathBuf;

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

/// Root server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
    /// Server binding configuration
    pub server: ServerBindConfig,

    /// Database configuration
    pub database: DatabaseConfig,

    /// Authentication configuration
    pub auth: AuthConfig,

    /// Agent updates configuration
    pub updates: UpdatesConfig,
}

/// Server binding configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerBindConfig {
    /// Host to bind to
    pub host: String,

    /// Port to bind to
    pub port: u16,
}

/// Database configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
    /// PostgreSQL connection URL
    pub url: String,

    /// Maximum number of connections in the pool
    pub max_connections: u32,
}

/// Authentication configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
    /// JWT signing secret
    pub jwt_secret: String,

    /// JWT token expiry in hours
    pub jwt_expiry_hours: u64,

    /// API key prefix for agents
    pub api_key_prefix: String,
}

/// Agent updates configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdatesConfig {
    /// Directory containing agent binaries
    pub downloads_dir: PathBuf,

    /// Base URL for agent downloads
    pub downloads_base_url: String,

    /// Enable automatic agent updates
    pub auto_update_enabled: bool,

    /// Timeout for agent updates in seconds
    pub update_timeout_secs: u64,

    /// Interval for scanning downloads directory (seconds)
    pub scan_interval_secs: u64,
}

impl ServerConfig {
    /// Load configuration from environment variables
    pub fn from_env() -> Result<Self> {
        let database_url =
            std::env::var("DATABASE_URL").context("DATABASE_URL environment variable not set")?;

        let jwt_secret =
            std::env::var("JWT_SECRET").context("JWT_SECRET environment variable not set")?;

        let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());

        let port = std::env::var("SERVER_PORT")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(3001);

        let max_connections = std::env::var("DB_MAX_CONNECTIONS")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(10);

        let jwt_expiry_hours = std::env::var("JWT_EXPIRY_HOURS")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(24);

        let api_key_prefix =
            std::env::var("API_KEY_PREFIX").unwrap_or_else(|_| "grmm_".to_string());

        // Updates configuration
        let downloads_dir = std::env::var("DOWNLOADS_DIR")
            .map(PathBuf::from)
            .unwrap_or_else(|_| PathBuf::from("/var/www/downloads"));

        let downloads_base_url = std::env::var("DOWNLOADS_BASE_URL")
            .unwrap_or_else(|_| format!("http://{}:{}/downloads", host, port));

        let auto_update_enabled = std::env::var("AUTO_UPDATE_ENABLED")
            .map(|v| v.to_lowercase() == "true" || v == "1")
            .unwrap_or(true);

        let update_timeout_secs = std::env::var("UPDATE_TIMEOUT_SECS")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(180);

        let scan_interval_secs = std::env::var("SCAN_INTERVAL_SECS")
            .ok()
            .and_then(|p| p.parse().ok())
            .unwrap_or(300); // 5 minutes

        Ok(Self {
            server: ServerBindConfig { host, port },
            database: DatabaseConfig {
                url: database_url,
                max_connections,
            },
            auth: AuthConfig {
                jwt_secret,
                jwt_expiry_hours,
                api_key_prefix,
            },
            updates: UpdatesConfig {
                downloads_dir,
                downloads_base_url,
                auto_update_enabled,
                update_timeout_secs,
                scan_interval_secs,
            },
        })
    }
}


Let me check the API routes to see how they're organized and if there's any tenant/client scoping:


     1→//! REST API routes
     2→//!
     3→//! Provides endpoints for:
     4→//! - Agent management (registration, listing, deletion)
     5→//! - Client and site management
     6→//! - Metrics retrieval
     7→//! - Command execution
     8→//! - User authentication
     9→
    10→pub mod agents;
    11→pub mod auth;
    12→pub mod clients;
    13→pub mod commands;
    14→pub mod metrics;
    15→pub mod sites;
    16→
    17→use axum::{
    18→    routing::{delete, get, post, put},
    19→    Router,
    20→};
    21→
    22→use crate::AppState;
    23→
    24→/// Build all API routes
    25→pub fn routes() -> Router<AppState> {
    26→    Router::new()
    27→        // Authentication
    28→        .route("/auth/login", post(auth::login))
    29→        .route("/auth/register", post(auth::register))
    30→        .route("/auth/me", get(auth::me))
    31→        // Clients
    32→        .route("/clients", get(clients::list_clients))
    33→        .route("/clients", post(clients::create_client))
    34→        .route("/clients/:id", get(clients::get_client))
    35→        .route("/clients/:id", put(clients::update_client))
    36→        .route("/clients/:id", delete(clients::delete_client))
    37→        .route("/clients/:id/sites", get(sites::list_sites_by_client))
    38→        // Sites
    39→        .route("/sites", get(sites::list_sites))
    40→        .route("/sites", post(sites::create_site))
    41→        .route("/sites/:id", get(sites::get_site))
    42→        .route("/sites/:id", put(sites::update_site))
    43→        .route("/sites/:id", delete(sites::delete_site))
    44→        .route("/sites/:id/regenerate-key", post(sites::regenerate_api_key))
    45→        // Agents
    46→        .route("/agents", get(agents::list_agents_with_details))
    47→        .route("/agents", post(agents::register_agent))
    48→        .route("/agents/stats", get(agents::get_stats))
    49→        .route("/agents/unassigned", get(agents::list_unassigned_agents))
    50→        .route("/agents/:id", get(agents::get_agent))
    51→        .route("/agents/:id", delete(agents::delete_agent))
    52→        .route("/agents/:id/move", post(agents::move_agent))
    53→        .route("/agents/:id/state", get(agents::get_agent_state))
    54→        .route("/agents/:id/update", post(agents::trigger_update))
    55→        // Metrics
    56→        .route("/agents/:id/metrics", get(metrics::get_agent_metrics))
    57→        .route("/metrics/summary", get(metrics::get_summary))
    58→        // Commands
    59→        .route("/agents/:id/command", post(commands::send_command))
    60→        .route("/commands", get(commands::list_commands))
    61→        .route("/commands/:id", get(commands::get_command))
    62→        // Legacy Agent (PowerShell for 2008 R2)
    63→        .route("/agent/register-legacy", post(agents::register_legacy))
    64→        .route("/agent/heartbeat", post(agents::heartbeat))
    65→        .route("/agent/command-result", post(agents::command_result))
    66→}
    67→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! REST API routes
//!
//! Provides endpoints for:
//! - Agent management (registration, listing, deletion)
//! - Client and site management
//! - Metrics retrieval
//! - Command execution
//! - User authentication

pub mod agents;
pub mod auth;
pub mod clients;
pub mod commands;
pub mod metrics;
pub mod sites;

use axum::{
    routing::{delete, get, post, put},
    Router,
};

use crate::AppState;

/// Build all API routes
pub fn routes() -> Router<AppState> {
    Router::new()
        // Authentication
        .route("/auth/login", post(auth::login))
        .route("/auth/register", post(auth::register))
        .route("/auth/me", get(auth::me))
        // Clients
        .route("/clients", get(clients::list_clients))
        .route("/clients", post(clients::create_client))
        .route("/clients/:id", get(clients::get_client))
        .route("/clients/:id", put(clients::update_client))
        .route("/clients/:id", delete(clients::delete_client))
        .route("/clients/:id/sites", get(sites::list_sites_by_client))
        // Sites
        .route("/sites", get(sites::list_sites))
        .route("/sites", post(sites::create_site))
        .route("/sites/:id", get(sites::get_site))
        .route("/sites/:id", put(sites::update_site))
        .route("/sites/:id", delete(sites::delete_site))
        .route("/sites/:id/regenerate-key", post(sites::regenerate_api_key))
        // Agents
        .route("/agents", get(agents::list_agents_with_details))
        .route("/agents", post(agents::register_agent))
        .route("/agents/stats", get(agents::get_stats))
        .route("/agents/unassigned", get(agents::list_unassigned_agents))
        .route("/agents/:id", get(agents::get_agent))
        .route("/agents/:id", delete(agents::delete_agent))
        .route("/agents/:id/move", post(agents::move_agent))
        .route("/agents/:id/state", get(agents::get_agent_state))
        .route("/agents/:id/update", post(agents::trigger_update))
        // Metrics
        .route("/agents/:id/metrics", get(metrics::get_agent_metrics))
        .route("/metrics/summary", get(metrics::get_summary))
        // Commands
        .route("/agents/:id/command", post(commands::send_command))
        .route("/commands", get(commands::list_commands))
        .route("/commands/:id", get(commands::get_command))
        // Legacy Agent (PowerShell for 2008 R2)
        .route("/agent/register-legacy", post(agents::register_legacy))
        .route("/agent/heartbeat", post(agents::heartbeat))
        .route("/agent/command-result", post(agents::command_result))
}


No matches found


Let me check if there's any user-to-client/tenant relationship by searching the database modules more broadly:


No files found


Let me verify by looking at the actual clients.rs API implementation:


     1→//! Client management API endpoints
     2→
     3→use axum::{
     4→    extract::{Path, State},
     5→    http::StatusCode,
     6→    Json,
     7→};
     8→use serde::{Deserialize, Serialize};
     9→use uuid::Uuid;
    10→
    11→use crate::auth::AuthUser;
    12→use crate::db;
    13→use crate::AppState;
    14→
    15→/// Response for client operations
    16→#[derive(Debug, Serialize)]
    17→pub struct ClientResponse {
    18→    pub id: Uuid,
    19→    pub name: String,
    20→    pub code: Option<String>,
    21→    pub notes: Option<String>,
    22→    pub is_active: bool,
    23→    pub created_at: chrono::DateTime<chrono::Utc>,
    24→    pub site_count: i64,
    25→}
    26→
    27→/// Request to create a new client
    28→#[derive(Debug, Deserialize)]
    29→pub struct CreateClientRequest {
    30→    pub name: String,
    31→    pub code: Option<String>,
    32→    pub notes: Option<String>,
    33→}
    34→
    35→/// Request to update a client
    36→#[derive(Debug, Deserialize)]
    37→pub struct UpdateClientRequest {
    38→    pub name: Option<String>,
    39→    pub code: Option<String>,
    40→    pub notes: Option<String>,
    41→    pub is_active: Option<bool>,
    42→}
    43→
    44→/// List all clients
    45→pub async fn list_clients(
    46→    _user: AuthUser,
    47→    State(state): State<AppState>,
    48→) -> Result<Json<Vec<ClientResponse>>, (StatusCode, String)> {
    49→    let clients = db::get_all_clients_with_counts(&state.db)
    50→        .await
    51→        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    52→
    53→    let responses: Vec<ClientResponse> = clients
    54→        .into_iter()
    55→        .map(|c| ClientResponse {
    56→            id: c.id,
    57→            name: c.name,
    58→            code: c.code,
    59→            notes: c.notes,
    60→            is_active: c.is_active,
    61→            created_at: c.created_at,
    62→            site_count: c.site_count,
    63→        })
    64→        .collect();
    65→
    66→    Ok(Json(responses))
    67→}
    68→
    69→/// Create a new client
    70→pub async fn create_client(
    71→    _user: AuthUser,
    72→    State(state): State<AppState>,
    73→    Json(req): Json<CreateClientRequest>,
    74→) -> Result<Json<ClientResponse>, (StatusCode, String)> {
    75→    let create = db::CreateClient {
    76→        name: req.name,
    77→        code: req.code,
    78→        notes: req.notes,
    79→    };
    80→
    81→    let client = db::create_client(&state.db, create)
    82→        .await
    83→        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
    84→
    85→    Ok(Json(ClientResponse {
    86→        id: client.id,
    87→        name: client.name,
    88→        code: client.code,
    89→        notes: client.notes,
    90→        is_active: client.is_active,
    91→        created_at: client.created_at,
    92→        site_count: 0,
    93→    }))
    94→}
    95→
    96→/// Get a specific client
    97→pub async fn get_client(
    98→    _user: AuthUser,
    99→    State(state): State<AppState>,
   100→    Path(id): Path<Uuid>,

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Client management API endpoints

use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::auth::AuthUser;
use crate::db;
use crate::AppState;

/// Response for client operations
#[derive(Debug, 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: i64,
}

/// Request to create a new client
#[derive(Debug, Deserialize)]
pub struct CreateClientRequest {
    pub name: String,
    pub code: Option<String>,
    pub notes: Option<String>,
}

/// Request to update a client
#[derive(Debug, Deserialize)]
pub struct UpdateClientRequest {
    pub name: Option<String>,
    pub code: Option<String>,
    pub notes: Option<String>,
    pub is_active: Option<bool>,
}

/// List all clients
pub async fn list_clients(
    _user: AuthUser,
    State(state): State<AppState>,
) -> Result<Json<Vec<ClientResponse>>, (StatusCode, String)> {
    let clients = db::get_all_clients_with_counts(&state.db)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    let responses: Vec<ClientResponse> = clients
        .into_iter()
        .map(|c| 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: c.site_count,
        })
        .collect();

    Ok(Json(responses))
}

/// Create a new client
pub async fn create_client(
    _user: AuthUser,
    State(state): State<AppState>,
    Json(req): Json<CreateClientRequest>,
) -> Result<Json<ClientResponse>, (StatusCode, String)> {
    let create = db::CreateClient {
        name: req.name,
        code: req.code,
        notes: req.notes,
    };

    let client = db::create_client(&state.db, create)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    Ok(Json(ClientResponse {
        id: client.id,
        name: client.name,
        code: client.code,
        notes: client.notes,
        is_active: client.is_active,
        created_at: client.created_at,
        site_count: 0,
    }))
}

/// Get a specific client
pub async fn get_client(
    _user: AuthUser,
    State(state): State<AppState>,
    Path(id): Path<Uuid>,


Perfect! Now let me create a comprehensive summary document by gathering all the information I've found. Let me compile this into a clear report.

## GuruRMM User Database Schema Analysis

Based on my exploration of the GuruRMM server codebase, here's a comprehensive understanding of the user database structure and related schemas:

### 1. USER TABLE STRUCTURE

**Current User Table Definition** (`migrations/001_initial.sql`):

```sql
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255),
    name VARCHAR(255),
    role VARCHAR(50) DEFAULT 'user',
    sso_provider VARCHAR(50),
    sso_id VARCHAR(255),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    last_login TIMESTAMPTZ
);
```

**File Location:** `C:\Users\MikeSwanson\claude-projects\gururmm\server\migrations\001_initial.sql`

**User Struct** (`C:\Users\MikeSwanson\claude-projects\gururmm\server\src\db\users.rs`):

```rust
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct User {
    pub id: Uuid,
    pub email: String,
    #[serde(skip_serializing)]
    pub password_hash: Option<String>,
    pub name: Option<String>,
    pub role: String,
    pub sso_provider: Option<String>,
    pub sso_id: Option<String>,
    pub created_at: DateTime<Utc>,
    pub last_login: Option<DateTime<Utc>>,
}
```

#### SSO Fields Already Present:
- **sso_provider** (VARCHAR(50)): Name of the SSO provider (e.g., "oauth2", "saml", "azure-ad")
- **sso_id** (VARCHAR(255)): The unique identifier from the SSO provider
- **password_hash** (Optional): Can be NULL for SSO-only users
- **Index exists:** `idx_users_sso` on (sso_provider, sso_id) for quick SSO lookups

### 2. USER CREATION & UPDATE OPERATIONS

**Local User Creation** (`users.rs` lines 49-73):
```rust
pub async fn create_user(pool: &PgPool, user: CreateUser) -> Result<User, sqlx::Error> {
    sqlx::query_as::<_, User>(
        r#"
        INSERT INTO users (email, password_hash, name, role)
        VALUES ($1, $2, $3, COALESCE($4, 'user'))
        RETURNING *
        "#,
    )
    // ... binds values
}
```

**SSO User Upsert** (`users.rs` lines 75-104) - Key for SSO integration:
```rust
pub async fn upsert_sso_user(pool: &PgPool, user: UpsertSsoUser) -> Result<User, sqlx::Error> {
    sqlx::query_as::<_, User>(
        r#"
        INSERT INTO users (email, name, sso_provider, sso_id, role)
        VALUES ($1, $2, $3, $4, 'user')
        ON CONFLICT (email)
        DO UPDATE SET
            name = COALESCE(EXCLUDED.name, users.name),
            sso_provider = EXCLUDED.sso_provider,
            sso_id = EXCLUDED.sso_id,
            last_login = NOW()
        RETURNING *
        "#,
    )
}
```

This upsert operation:
- Creates a new user if email doesn't exist
- On email conflict, updates SSO provider/ID and last_login timestamp
- Preserves existing name if new one is NULL

**Other Update Operations:**
- `update_last_login()` - Updates last_login timestamp
- `update_user_role()` - Changes user role
- `update_user_password()` - Updates password hash (for local auth users)

### 3. TENANT/ORGANIZATION STRUCTURE

**Important Finding:** The current schema has **NO direct user-to-client/tenant relationship**.

The multi-tenant architecture exists through:
- **Clients Table** (Migration 002): Represents organizations/companies
  - `id`, `name`, `code`, `notes`, `is_active`, `created_at`, `updated_at`

- **Sites Table** (Migration 002): Locations under a client
  - `id`, `client_id` (references clients), `name`, `site_code`, `api_key_hash`, etc.

- **Agents Table**: Belongs to sites, which belong to clients
  - Contains `site_id` to link agents to specific sites

**Current Authorization Model:** All authenticated users have global access to all clients and sites. There's no row-level security or user-to-client assignment.

**File Locations:**
- `C:\Users\MikeSwanson\claude-projects\gururmm\server\migrations\002_clients_sites.sql`
- `C:\Users\MikeSwanson\claude-projects\gururmm\server\src\db\clients.rs`
- `C:\Users\MikeSwanson\claude-projects\gururmm\server\src\db\sites.rs`

### 4. AUTHENTICATION & SSO IMPLEMENTATION

**Current Auth Module** (`C:\Users\MikeSwanson\claude-projects\gururmm\server\src\auth\mod.rs`):
- JWT token creation with claims: `sub` (user_id), `role`, `exp`, `iat`
- Password hashing using Argon2
- Password verification
- AuthUser extractor for Axum routes

**API Auth Endpoints** (`C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\auth.rs`):
- `POST /api/auth/login` - Email/password login (returns JWT + UserResponse)
- `POST /api/auth/register` - Create first admin user (only if no users exist)
- `GET /api/auth/me` - Get current authenticated user info

**Key Finding:** The `upsert_sso_user()` function is already present but **NOT YET wired into any API endpoints**. This is the entry point for SSO integration.

### 5. DATABASE MIGRATIONS & SCHEMA VERSIONING

**Migration Files:**
- `001_initial.sql` - Core schema: agents, metrics, users, commands, watchdog_events
- `002_clients_sites.sql` - Multi-tenant support: clients, sites, registration_tokens
- `003_extended_metrics.sql` - Extended agent metrics: agent_state table with JSONB network_interfaces
- `004_agent_updates.sql` - Agent update tracking

**File Location:** `C:\Users\MikeSwanson\claude-projects\gururmm\server\migrations\`

Database is using SQLx for migration management (loaded in main.rs: `sqlx::migrate!("./migrations")`).

### 6. SCHEMA CHANGES NEEDED FOR SSO SUPPORT

Based on the current schema, here's what you'll need to consider:

**Already Supported:**
- SSO provider and ID fields exist
- Upsert logic for SSO users already implemented
- JWT-based authentication ready

**Recommended Additions:**
1. **User-to-Client/Tenant Association** (for role-based access):
   ```sql
   ALTER TABLE users ADD COLUMN client_id UUID REFERENCES clients(id) ON DELETE CASCADE;
   CREATE INDEX idx_users_client ON users(client_id);
   ```

2. **SSO Configuration Table** (for managing SSO settings):
   ```sql
   CREATE TABLE sso_providers (
       id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
       provider_name VARCHAR(50) UNIQUE NOT NULL,
       client_id VARCHAR(255),
       client_secret VARCHAR(255),
       issuer_url VARCHAR(255),
       authorization_endpoint VARCHAR(255),
       token_endpoint VARCHAR(255),
       userinfo_endpoint VARCHAR(255),
       config_json JSONB,
       is_active BOOLEAN DEFAULT TRUE,
       created_at TIMESTAMPTZ DEFAULT NOW(),
       updated_at TIMESTAMPTZ DEFAULT NOW()
   );
   ```

3. **Extended User Claims** (optional, for richer JWT tokens):
   - Add `organization_id` or `tenant_id` to Claims struct
   - Add `sso_verified_at` timestamp to users table
   - Add `sso_attributes` JSONB column to preserve SSO profile data

### 7. KEY FILES SUMMARY

| File | Purpose |
|------|---------|
| `C:\Users\MikeSwanson\claude-projects\gururmm\server\src\db\users.rs` | User model and database operations |
| `C:\Users\MikeSwanson\claude-projects\gururmm\server\src\auth\mod.rs` | JWT and password handling |
| `C:\Users\MikeSwanson\claude-projects\gururmm\server\src\api\auth.rs` | Authentication endpoints |
| `C:\Users\MikeSwanson\claude-projects\gururmm\server\migrations\001_initial.sql` | Initial schema including users table |
| `C:\Users\MikeSwanson\claude-projects\gururmm\server\migrations\002_clients_sites.sql` | Multi-tenant schema (clients/sites) |
| `C:\Users\MikeSwanson\claude-projects\gururmm\server\src\db\clients.rs` | Client/organization model |
| `C:\Users\MikeSwanson\claude-projects\gururmm\server\src\db\sites.rs` | Site/location model |

The infrastructure is well-prepared for SSO! The foundation is already in place - you primarily need to:
1. Create SSO provider configuration endpoints
2. Create OAuth/SAML callback endpoints
3. Hook in the existing `upsert_sso_user()` function
4. Consider adding user-to-client relationships for proper multi-tenant access control