chore: sync repository to current working state
Some checks failed
Build and Test / Build Server (Linux) (push) Has been cancelled
Build and Test / Build Agent (Windows) (push) Has been cancelled
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled

Brings azcomputerguru/guru-connect up to the authoritative working copy that
had been maintained in the claudetools monorepo: Phase 1 security and
infrastructure (middleware, metrics, utils, token blacklist, deployment
scripts, security audits) plus the native-remote-control integration spec.
Preserves the repo .gitignore, .cargo, and server/static/downloads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 06:15:29 -07:00
parent 5b7cf5fb07
commit e3e95f8fa7
73 changed files with 15608 additions and 5757 deletions

View File

@@ -10,6 +10,9 @@ mod auth;
mod api;
mod db;
mod support_codes;
mod middleware;
mod utils;
mod metrics;
pub mod proto {
include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
@@ -22,11 +25,12 @@ use axum::{
extract::{Path, State, Json, Query, Request},
response::{Html, IntoResponse},
http::StatusCode,
middleware::{self, Next},
middleware::{self as axum_middleware, Next},
};
use std::net::SocketAddr;
use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer};
use tower_http::cors::{Any, CorsLayer, AllowOrigin};
use axum::http::{Method, HeaderValue};
use tower_http::trace::TraceLayer;
use tower_http::services::ServeDir;
use tracing::{info, Level};
@@ -34,7 +38,9 @@ use tracing_subscriber::FmtSubscriber;
use serde::Deserialize;
use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation};
use auth::{JwtConfig, hash_password, generate_random_password, AuthenticatedUser};
use auth::{JwtConfig, TokenBlacklist, hash_password, generate_random_password, AuthenticatedUser};
use metrics::SharedMetrics;
use prometheus_client::registry::Registry;
/// Application state
#[derive(Clone)]
@@ -43,17 +49,25 @@ pub struct AppState {
support_codes: SupportCodeManager,
db: Option<db::Database>,
pub jwt_config: Arc<JwtConfig>,
pub token_blacklist: TokenBlacklist,
/// Optional API key for persistent agents (env: AGENT_API_KEY)
pub agent_api_key: Option<String>,
/// Prometheus metrics
pub metrics: SharedMetrics,
/// Prometheus registry (for /metrics endpoint)
pub registry: Arc<std::sync::Mutex<Registry>>,
/// Server start time
pub start_time: Arc<std::time::Instant>,
}
/// Middleware to inject JWT config into request extensions
/// Middleware to inject JWT config and token blacklist into request extensions
async fn auth_layer(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> impl IntoResponse {
request.extensions_mut().insert(state.jwt_config.clone());
request.extensions_mut().insert(Arc::new(state.token_blacklist.clone()));
next.run(request).await
}
@@ -74,11 +88,14 @@ async fn main() -> Result<()> {
let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string());
info!("Loaded configuration, listening on {}", listen_addr);
// JWT configuration
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
tracing::warn!("JWT_SECRET not set, using default (INSECURE for production!)");
"guruconnect-dev-secret-change-me-in-production".to_string()
});
// JWT configuration - REQUIRED for security
let jwt_secret = std::env::var("JWT_SECRET")
.expect("JWT_SECRET environment variable must be set! Generate one with: openssl rand -base64 64");
if jwt_secret.len() < 32 {
panic!("JWT_SECRET must be at least 32 characters long for security!");
}
let jwt_expiry_hours = std::env::var("JWT_EXPIRY_HOURS")
.ok()
.and_then(|s| s.parse().ok())
@@ -126,12 +143,35 @@ async fn main() -> Result<()> {
];
let _ = db::set_user_permissions(db.pool(), user.id, &perms).await;
info!("========================================");
info!(" INITIAL ADMIN USER CREATED");
info!(" Username: admin");
info!(" Password: {}", password);
info!(" (Change this password after first login!)");
info!("========================================");
// SEC-6: Write credentials to secure file instead of logging
let creds_file = ".admin-credentials";
match std::fs::write(creds_file, format!("Username: admin\nPassword: {}\n\nWARNING: Change this password immediately after first login!\nDelete this file after copying the password.\n", password)) {
Ok(_) => {
// Set restrictive permissions (Unix only)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(creds_file, std::fs::Permissions::from_mode(0o600));
}
info!("========================================");
info!(" INITIAL ADMIN USER CREATED");
info!(" Credentials written to: {}", creds_file);
info!(" (Read file, change password, then delete file)");
info!("========================================");
}
Err(e) => {
// Fallback to logging if file write fails (but warn about security)
tracing::warn!("Could not write credentials file: {}", e);
info!("========================================");
info!(" INITIAL ADMIN USER CREATED");
info!(" Username: admin");
info!(" Password: {}", password);
info!(" WARNING: Password logged due to file write failure!");
info!(" (Change this password immediately!)");
info!("========================================");
}
}
}
Err(e) => {
tracing::error!("Failed to create initial admin user: {}", e);
@@ -167,32 +207,63 @@ async fn main() -> Result<()> {
// Agent API key for persistent agents (optional)
let agent_api_key = std::env::var("AGENT_API_KEY").ok();
if agent_api_key.is_some() {
info!("AGENT_API_KEY configured for persistent agents");
if let Some(ref key) = agent_api_key {
// Validate API key strength for security
utils::validation::validate_api_key_strength(key)?;
info!("AGENT_API_KEY configured for persistent agents (validated)");
} else {
info!("No AGENT_API_KEY set - persistent agents will need JWT token or support code");
}
// Initialize Prometheus metrics
let mut registry = Registry::default();
let metrics = Arc::new(metrics::Metrics::new(&mut registry));
let registry = Arc::new(std::sync::Mutex::new(registry));
let start_time = Arc::new(std::time::Instant::now());
// Spawn background task to update uptime metric
let metrics_for_uptime = metrics.clone();
let start_time_for_uptime = start_time.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(10));
loop {
interval.tick().await;
let uptime = start_time_for_uptime.elapsed().as_secs() as i64;
metrics_for_uptime.update_uptime(uptime);
}
});
// Create application state
let token_blacklist = TokenBlacklist::new();
let state = AppState {
sessions,
support_codes: SupportCodeManager::new(),
db: database,
jwt_config,
token_blacklist,
agent_api_key,
metrics,
registry,
start_time,
};
// Build router
let app = Router::new()
// Health check (no auth required)
.route("/health", get(health))
// Prometheus metrics (no auth required - for monitoring)
.route("/metrics", get(prometheus_metrics))
// Auth endpoints (no auth required for login)
// Auth endpoints (TODO: Add rate limiting - see SEC2_RATE_LIMITING_TODO.md)
.route("/api/auth/login", post(api::auth::login))
// Auth endpoints (auth required)
.route("/api/auth/me", get(api::auth::get_me))
.route("/api/auth/change-password", post(api::auth::change_password))
.route("/api/auth/me", get(api::auth::get_me))
.route("/api/auth/logout", post(api::auth_logout::logout))
.route("/api/auth/revoke-token", post(api::auth_logout::revoke_own_token))
.route("/api/auth/admin/revoke-user", post(api::auth_logout::revoke_user_tokens))
.route("/api/auth/blacklist/stats", get(api::auth_logout::get_blacklist_stats))
.route("/api/auth/blacklist/cleanup", post(api::auth_logout::cleanup_blacklist))
// User management (admin only)
.route("/api/users", get(api::users::list_users))
@@ -203,7 +274,7 @@ async fn main() -> Result<()> {
.route("/api/users/:id/permissions", put(api::users::set_permissions))
.route("/api/users/:id/clients", put(api::users::set_client_access))
// Portal API - Support codes
// Portal API - Support codes (TODO: Add rate limiting)
.route("/api/codes", post(create_code))
.route("/api/codes", get(list_codes))
.route("/api/codes/:code/validate", get(validate_code))
@@ -245,19 +316,35 @@ async fn main() -> Result<()> {
// State and middleware
.with_state(state.clone())
.layer(middleware::from_fn_with_state(state, auth_layer))
.layer(axum_middleware::from_fn_with_state(state, auth_layer))
// Serve static files for portal (fallback)
.fallback_service(ServeDir::new("static").append_index_html_on_directories(true))
// Middleware
.layer(axum_middleware::from_fn(middleware::add_security_headers)) // SEC-7 & SEC-12
.layer(TraceLayer::new_for_http())
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any),
);
// SEC-11: Restricted CORS configuration
.layer({
let cors = CorsLayer::new()
// Allow requests from the production domain and localhost (for development)
.allow_origin([
"https://connect.azcomputerguru.com".parse::<HeaderValue>().unwrap(),
"http://localhost:3002".parse::<HeaderValue>().unwrap(),
"http://127.0.0.1:3002".parse::<HeaderValue>().unwrap(),
])
// Allow only necessary HTTP methods
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
// Allow common headers needed for API requests
.allow_headers([
axum::http::header::AUTHORIZATION,
axum::http::header::CONTENT_TYPE,
axum::http::header::ACCEPT,
])
// Allow credentials (cookies, auth headers)
.allow_credentials(true);
cors
});
// Start server
let addr: SocketAddr = listen_addr.parse()?;
@@ -265,7 +352,11 @@ async fn main() -> Result<()> {
info!("Server listening on {}", addr);
axum::serve(listener, app).await?;
// Use into_make_service_with_connect_info to enable IP address extraction
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>()
).await?;
Ok(())
}
@@ -274,6 +365,18 @@ async fn health() -> &'static str {
"OK"
}
/// Prometheus metrics endpoint
async fn prometheus_metrics(
State(state): State<AppState>,
) -> String {
use prometheus_client::encoding::text::encode;
let registry = state.registry.lock().unwrap();
let mut buffer = String::new();
encode(&mut buffer, &registry).unwrap();
buffer
}
// Support code API handlers
async fn create_code(