style: cargo fmt --all — make codebase rustfmt-clean
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 2m59s
Build and Test / Build Agent (Windows) (push) Has started running
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

First run of the build-and-test CI gate (cargo fmt --all -- --check) surfaced
pre-existing formatting drift across the agent and server crates. Apply rustfmt
across the workspace so the codebase meets its own CI gate. Pure formatting; no
logic changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 15:02:12 +00:00
parent f2e0456f8d
commit 1c5c1e78e7
48 changed files with 1174 additions and 797 deletions

View File

@@ -1,15 +1,13 @@
//! Authentication API endpoints
use axum::{
extract::{State, Request},
extract::{Request, State},
http::StatusCode,
Json,
};
use serde::{Deserialize, Serialize};
use crate::auth::{
verify_password, AuthenticatedUser, JwtConfig,
};
use crate::auth::{verify_password, AuthenticatedUser, JwtConfig};
use crate::db;
use crate::AppState;
@@ -89,16 +87,15 @@ pub async fn login(
}
// Verify password
let password_valid = verify_password(&request.password, &user.password_hash)
.map_err(|e| {
tracing::error!("Password verification error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Internal server error".to_string(),
}),
)
})?;
let password_valid = verify_password(&request.password, &user.password_hash).map_err(|e| {
tracing::error!("Password verification error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Internal server error".to_string(),
}),
)
})?;
if !password_valid {
return Err((
@@ -118,21 +115,18 @@ pub async fn login(
let _ = db::update_last_login(db.pool(), user.id).await;
// Create JWT token
let token = state.jwt_config.create_token(
user.id,
&user.username,
&user.role,
permissions.clone(),
)
.map_err(|e| {
tracing::error!("Token creation error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to create token".to_string(),
}),
)
})?;
let token = state
.jwt_config
.create_token(user.id, &user.username, &user.role, permissions.clone())
.map_err(|e| {
tracing::error!("Token creation error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to create token".to_string(),
}),
)
})?;
tracing::info!("User {} logged in successfully", user.username);
@@ -288,16 +282,15 @@ pub async fn change_password(
}
// Hash new password
let new_hash = crate::auth::hash_password(&request.new_password)
.map_err(|e| {
tracing::error!("Password hashing error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to hash password".to_string(),
}),
)
})?;
let new_hash = crate::auth::hash_password(&request.new_password).map_err(|e| {
tracing::error!("Password hashing error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to hash password".to_string(),
}),
)
})?;
// Update password
db::update_user_password(db.pool(), user_id, &new_hash)

View File

@@ -1,13 +1,13 @@
//! Logout and token revocation endpoints
use axum::{
extract::{Request, State, Path},
http::{StatusCode, HeaderMap},
extract::{Path, Request, State},
http::{HeaderMap, StatusCode},
Json,
};
use uuid::Uuid;
use serde::Serialize;
use tracing::{info, warn};
use uuid::Uuid;
use crate::auth::AuthenticatedUser;
use crate::AppState;
@@ -15,7 +15,9 @@ use crate::AppState;
use super::auth::ErrorResponse;
/// Extract JWT token from Authorization header
fn extract_token_from_headers(headers: &HeaderMap) -> Result<String, (StatusCode, Json<ErrorResponse>)> {
fn extract_token_from_headers(
headers: &HeaderMap,
) -> Result<String, (StatusCode, Json<ErrorResponse>)> {
let auth_header = headers
.get("Authorization")
.and_then(|v| v.to_str().ok())
@@ -28,16 +30,14 @@ fn extract_token_from_headers(headers: &HeaderMap) -> Result<String, (StatusCode
)
})?;
let token = auth_header
.strip_prefix("Bearer ")
.ok_or_else(|| {
(
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "Invalid Authorization format".to_string(),
}),
)
})?;
let token = auth_header.strip_prefix("Bearer ").ok_or_else(|| {
(
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "Invalid Authorization format".to_string(),
}),
)
})?;
Ok(token.to_string())
}
@@ -124,7 +124,8 @@ pub async fn revoke_user_tokens(
Err((
StatusCode::NOT_IMPLEMENTED,
Json(ErrorResponse {
error: "User token revocation not yet implemented - requires session tracking table".to_string(),
error: "User token revocation not yet implemented - requires session tracking table"
.to_string(),
}),
))
}
@@ -179,10 +180,16 @@ pub async fn cleanup_blacklist(
));
}
let removed = state.token_blacklist.cleanup_expired(&state.jwt_config).await;
let removed = state
.token_blacklist
.cleanup_expired(&state.jwt_config)
.await;
let remaining = state.token_blacklist.len().await;
info!("Admin {} cleaned up blacklist: {} tokens removed, {} remaining", admin.username, removed, remaining);
info!(
"Admin {} cleaned up blacklist: {} tokens removed, {} remaining",
admin.username, removed, remaining
);
Ok(Json(CleanupResponse {
removed_count: removed,

View File

@@ -13,7 +13,7 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tracing::{info, warn, error};
use tracing::{error, info, warn};
/// Magic marker for embedded configuration (must match agent)
const MAGIC_MARKER: &[u8] = b"GURUCONFIG";
@@ -87,7 +87,7 @@ pub async fn download_viewer() -> impl IntoResponse {
.header(header::CONTENT_TYPE, "application/octet-stream")
.header(
header::CONTENT_DISPOSITION,
"attachment; filename=\"GuruConnect-Viewer.exe\""
"attachment; filename=\"GuruConnect-Viewer.exe\"",
)
.header(header::CONTENT_LENGTH, binary_data.len())
.body(Body::from(binary_data))
@@ -104,9 +104,7 @@ pub async fn download_viewer() -> impl IntoResponse {
}
/// Download support session binary (code embedded in filename)
pub async fn download_support(
Query(params): Query<SupportDownloadParams>,
) -> impl IntoResponse {
pub async fn download_support(Query(params): Query<SupportDownloadParams>) -> impl IntoResponse {
// Validate support code (must be 6 digits)
let code = params.code.trim();
if code.len() != 6 || !code.chars().all(|c| c.is_ascii_digit()) {
@@ -120,7 +118,11 @@ pub async fn download_support(
match std::fs::read(&binary_path) {
Ok(binary_data) => {
info!("Serving support session download for code {} ({} bytes)", code, binary_data.len());
info!(
"Serving support session download for code {} ({} bytes)",
code,
binary_data.len()
);
// Filename includes the support code
let filename = format!("GuruConnect-{}.exe", code);
@@ -130,7 +132,7 @@ pub async fn download_support(
.header(header::CONTENT_TYPE, "application/octet-stream")
.header(
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{}\"", filename)
format!("attachment; filename=\"{}\"", filename),
)
.header(header::CONTENT_LENGTH, binary_data.len())
.body(Body::from(binary_data))
@@ -147,9 +149,7 @@ pub async fn download_support(
}
/// Download permanent agent binary with embedded configuration
pub async fn download_agent(
Query(params): Query<AgentDownloadParams>,
) -> impl IntoResponse {
pub async fn download_agent(Query(params): Query<AgentDownloadParams>) -> impl IntoResponse {
let binary_path = get_base_binary_path();
// Read base binary
@@ -167,10 +167,13 @@ pub async fn download_agent(
// Build embedded config
let config = EmbeddedConfig {
server_url: "wss://connect.azcomputerguru.com/ws/agent".to_string(),
api_key: params.api_key.unwrap_or_else(|| "managed-agent".to_string()),
api_key: params
.api_key
.unwrap_or_else(|| "managed-agent".to_string()),
company: params.company.clone(),
site: params.site.clone(),
tags: params.tags
tags: params
.tags
.as_ref()
.map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
.unwrap_or_default(),
@@ -196,18 +199,25 @@ pub async fn download_agent(
info!(
"Serving permanent agent download: company={:?}, site={:?}, tags={:?} ({} bytes)",
config.company, config.site, config.tags, binary_data.len()
config.company,
config.site,
config.tags,
binary_data.len()
);
// Generate filename based on company/site
let filename = match (&params.company, &params.site) {
(Some(company), Some(site)) => {
format!("GuruConnect-{}-{}-Setup.exe", sanitize_filename(company), sanitize_filename(site))
format!(
"GuruConnect-{}-{}-Setup.exe",
sanitize_filename(company),
sanitize_filename(site)
)
}
(Some(company), None) => {
format!("GuruConnect-{}-Setup.exe", sanitize_filename(company))
}
_ => "GuruConnect-Setup.exe".to_string()
_ => "GuruConnect-Setup.exe".to_string(),
};
Response::builder()
@@ -215,7 +225,7 @@ pub async fn download_agent(
.header(header::CONTENT_TYPE, "application/octet-stream")
.header(
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{}\"", filename)
format!("attachment; filename=\"{}\"", filename),
)
.header(header::CONTENT_LENGTH, binary_data.len())
.body(Body::from(binary_data))

View File

@@ -3,19 +3,19 @@
pub mod auth;
pub mod auth_logout;
pub mod changelog;
pub mod users;
pub mod releases;
pub mod downloads;
pub mod releases;
pub mod users;
use axum::{
extract::{Path, State, Query},
extract::{Path, Query, State},
Json,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::session::SessionManager;
use crate::db;
use crate::session::SessionManager;
/// Viewer info returned by API
#[derive(Debug, Serialize)]
@@ -78,9 +78,7 @@ impl From<crate::session::Session> for SessionInfo {
}
/// List all active sessions
pub async fn list_sessions(
State(sessions): State<SessionManager>,
) -> Json<Vec<SessionInfo>> {
pub async fn list_sessions(State(sessions): State<SessionManager>) -> Json<Vec<SessionInfo>> {
let sessions = sessions.list_sessions().await;
Json(sessions.into_iter().map(SessionInfo::from).collect())
}
@@ -93,7 +91,9 @@ pub async fn get_session(
let session_id = Uuid::parse_str(&id)
.map_err(|_| (axum::http::StatusCode::BAD_REQUEST, "Invalid session ID"))?;
let session = sessions.get_session(session_id).await
let session = sessions
.get_session(session_id)
.await
.ok_or((axum::http::StatusCode::NOT_FOUND, "Session not found"))?;
Ok(Json(SessionInfo::from(session)))

View File

@@ -129,17 +129,15 @@ pub async fn list_releases(
)
})?;
let releases = db::get_all_releases(db.pool())
.await
.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to fetch releases".to_string(),
}),
)
})?;
let releases = db::get_all_releases(db.pool()).await.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to fetch releases".to_string(),
}),
)
})?;
Ok(Json(releases.into_iter().map(ReleaseInfo::from).collect()))
}
@@ -171,7 +169,10 @@ pub async fn create_release(
// Validate checksum format (64 hex chars for SHA-256)
if request.checksum_sha256.len() != 64
|| !request.checksum_sha256.chars().all(|c| c.is_ascii_hexdigit())
|| !request
.checksum_sha256
.chars()
.all(|c| c.is_ascii_hexdigit())
{
return Err((
StatusCode::BAD_REQUEST,
@@ -349,17 +350,15 @@ pub async fn delete_release(
)
})?;
let deleted = db::delete_release(db.pool(), &version)
.await
.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to delete release".to_string(),
}),
)
})?;
let deleted = db::delete_release(db.pool(), &version).await.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to delete release".to_string(),
}),
)
})?;
if deleted {
tracing::info!("Deleted release: {}", version);

View File

@@ -72,17 +72,15 @@ pub async fn list_users(
)
})?;
let users = db::get_all_users(db.pool())
.await
.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to fetch users".to_string(),
}),
)
})?;
let users = db::get_all_users(db.pool()).await.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to fetch users".to_string(),
}),
)
})?;
let mut result = Vec::new();
for user in users {
@@ -210,7 +208,13 @@ pub async fn create_user(
} else {
// Default permissions based on role
let default_perms = match request.role.as_str() {
"admin" => vec!["view", "control", "transfer", "manage_users", "manage_clients"],
"admin" => vec![
"view",
"control",
"transfer",
"manage_users",
"manage_clients",
],
"operator" => vec!["view", "control", "transfer"],
"viewer" => vec!["view"],
_ => vec!["view"],
@@ -455,17 +459,15 @@ pub async fn delete_user(
));
}
let deleted = db::delete_user(db.pool(), user_id)
.await
.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to delete user".to_string(),
}),
)
})?;
let deleted = db::delete_user(db.pool(), user_id).await.map_err(|e| {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to delete user".to_string(),
}),
)
})?;
if deleted {
tracing::info!("Deleted user: {}", id);
@@ -506,13 +508,22 @@ pub async fn set_permissions(
})?;
// Validate permissions
let valid_permissions = ["view", "control", "transfer", "manage_users", "manage_clients"];
let valid_permissions = [
"view",
"control",
"transfer",
"manage_users",
"manage_clients",
];
for perm in &request.permissions {
if !valid_permissions.contains(&perm.as_str()) {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("Invalid permission: {}. Valid: {:?}", perm, valid_permissions),
error: format!(
"Invalid permission: {}. Valid: {:?}",
perm, valid_permissions
),
}),
));
}