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
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:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (¶ms.company, ¶ms.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))
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user