diff --git a/nixpanel-api/src/auth.rs b/nixpanel-api/src/auth.rs index 00bf972..7b2bc82 100644 --- a/nixpanel-api/src/auth.rs +++ b/nixpanel-api/src/auth.rs @@ -1,10 +1,8 @@ use axum::{ async_trait, extract::FromRequestParts, - http::{request::Parts, HeaderMap}, - RequestPartsExt, + http::request::Parts, }; -use axum_extra::TypedHeader; use chrono::{Duration, Utc}; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; @@ -16,11 +14,11 @@ use crate::state::AppState; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Claims { - pub sub: String, // username - pub user_id: i64, - pub role: String, // "admin" | "user" - pub exp: i64, // unix timestamp - pub iat: i64, + pub sub: String, // username + pub user_id: i64, + pub role: String, // "admin" | "user" + pub exp: i64, + pub iat: i64, } impl Claims { @@ -33,25 +31,24 @@ impl Claims { exp: (now + Duration::hours(8)).timestamp(), iat: now.timestamp(), }; - let token = encode( + Ok(encode( &Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()), - )?; - Ok(token) + )?) } pub fn verify(token: &str, secret: &str) -> Result { - let data = decode::( + Ok(decode::( token, &DecodingKey::from_secret(secret.as_bytes()), &Validation::new(Algorithm::HS256), - )?; - Ok(data.claims) + )? + .claims) } } -/* ── Extractor: pulls Claims from Authorization: Bearer ──────── */ +/* ── Extractor — pulls Claims from Authorization: Bearer ─────── */ #[async_trait] impl FromRequestParts for Claims { @@ -61,14 +58,13 @@ impl FromRequestParts for Claims { parts: &mut Parts, state: &AppState, ) -> Result { - let headers: &HeaderMap = &parts.headers; - let auth = headers + let auth = parts + .headers .get("authorization") .and_then(|v| v.to_str().ok()) .and_then(|s| s.strip_prefix("Bearer ")) .ok_or(AppError::Unauthorized)?; - Claims::verify(auth, &state.config.jwt_secret) - .map_err(|_| AppError::Unauthorized) + Claims::verify(auth, &state.config.jwt_secret).map_err(|_| AppError::Unauthorized) } } diff --git a/nixpanel-api/src/error.rs b/nixpanel-api/src/error.rs index b723ca7..a12128d 100644 --- a/nixpanel-api/src/error.rs +++ b/nixpanel-api/src/error.rs @@ -23,10 +23,10 @@ pub enum AppError { impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match &self { - AppError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()), - AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()), - AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), - AppError::Internal(_) => { + AppError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()), + AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()), + AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), + AppError::Internal(_) => { tracing::error!("{}", self); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".into()) } @@ -42,7 +42,7 @@ impl From for AppError { } impl From for AppError { - fn from(e: jsonwebtoken::errors::Error) -> Self { + fn from(_e: jsonwebtoken::errors::Error) -> Self { AppError::Unauthorized } } diff --git a/nixpanel-api/src/routes/accounts.rs b/nixpanel-api/src/routes/accounts.rs index 4ee94fe..2a05a30 100644 --- a/nixpanel-api/src/routes/accounts.rs +++ b/nixpanel-api/src/routes/accounts.rs @@ -6,15 +6,15 @@ use crate::{auth::Claims, error::AppError, state::AppState}; #[derive(Serialize, sqlx::FromRow)] pub struct Account { - pub id: i64, - pub username: String, - pub domain: String, - pub email: String, - pub package_name: String, - pub disk_quota_mb: i64, - pub bandwidth_mb: i64, - pub status: String, - pub created_at: NaiveDateTime, + pub id: i64, + pub username: String, + pub domain: String, + pub email: String, + pub package_name: String, + pub disk_quota_mb: i64, + pub bandwidth_mb: i64, + pub status: String, + pub created_at: NaiveDateTime, } #[derive(Deserialize)] @@ -29,13 +29,12 @@ pub struct CreateAccountRequest { } pub async fn list_accounts( - claims: Claims, + _claims: Claims, State(state): State, ) -> Result>, AppError> { - let accounts = sqlx::query_as!( - Account, + let accounts = sqlx::query_as::<_, Account>( "SELECT id, username, domain, email, package_name, disk_quota_mb, bandwidth_mb, status, created_at - FROM accounts ORDER BY created_at DESC" + FROM accounts ORDER BY created_at DESC", ) .fetch_all(&state.db) .await?; @@ -43,7 +42,7 @@ pub async fn list_accounts( } pub async fn create_account( - claims: Claims, + _claims: Claims, State(state): State, Json(body): Json, ) -> Result, AppError> { @@ -52,61 +51,60 @@ pub async fn create_account( Argon2, }; - // Validate if body.username.is_empty() || body.domain.is_empty() { return Err(AppError::BadRequest("username and domain are required".into())); } - // Hash password - let salt = SaltString::generate(&mut OsRng); - let hash = Argon2::default() + let salt = SaltString::generate(&mut OsRng); + let hash = Argon2::default() .hash_password(body.password.as_bytes(), &salt) - .map_err(|e| AppError::Internal(anyhow::anyhow!(e)))? + .map_err(|e| AppError::Internal(anyhow::anyhow!("argon2: {}", e)))? .to_string(); - // Create system user (best-effort — panel may not be running as root in dev) + // Create system user (best-effort) let _ = std::process::Command::new("useradd") - .args(&[ - "-m", - "-s", "/bin/bash", - "-d", &format!("/home/{}", body.username), - &body.username, - ]) + .args(["-m", "-s", "/bin/bash", "-d", + &format!("/home/{}", body.username), + &body.username]) .output(); - // Create Apache vhost directory - let vhost_dir = format!("/home/{}/public_html", body.username); - let _ = std::fs::create_dir_all(&vhost_dir); + let _ = std::fs::create_dir_all(format!("/home/{}/public_html", body.username)); let package_name = body.package_name.unwrap_or_else(|| "Default".into()); let disk_quota_mb = body.disk_quota_mb.unwrap_or(10240); let bandwidth_mb = body.bandwidth_mb.unwrap_or(0); - let id = sqlx::query!( + let insert_id: u64 = sqlx::query( "INSERT INTO accounts (username, domain, email, password_hash, package_name, disk_quota_mb, bandwidth_mb, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'active')", - body.username, body.domain, body.email, hash, - package_name, disk_quota_mb, bandwidth_mb ) + .bind(&body.username) + .bind(&body.domain) + .bind(&body.email) + .bind(&hash) + .bind(&package_name) + .bind(disk_quota_mb) + .bind(bandwidth_mb) .execute(&state.db) .await? - .last_insert_id() as i64; + .last_insert_id(); - // Also create a panel user so they can log into NixClient - let _ = sqlx::query!( + // Also create a NixClient panel user + let _ = sqlx::query( "INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, 'user') ON DUPLICATE KEY UPDATE email = VALUES(email)", - body.username, body.email, hash ) + .bind(&body.username) + .bind(&body.email) + .bind(&hash) .execute(&state.db) .await; - let account = sqlx::query_as!( - Account, + let account = sqlx::query_as::<_, Account>( "SELECT id, username, domain, email, package_name, disk_quota_mb, bandwidth_mb, status, created_at FROM accounts WHERE id = ?", - id ) + .bind(insert_id) .fetch_one(&state.db) .await?; diff --git a/nixpanel-api/src/routes/auth.rs b/nixpanel-api/src/routes/auth.rs index 4921634..ea8f08c 100644 --- a/nixpanel-api/src/routes/auth.rs +++ b/nixpanel-api/src/routes/auth.rs @@ -23,29 +23,35 @@ pub struct UserInfo { pub role: String, } +// Internal row type — non-macro query returns untyped rows, use FromRow +#[derive(sqlx::FromRow)] +struct UserRow { + id: i64, + username: String, + password_hash: String, + role: String, +} + pub async fn login( State(state): State, Json(body): Json, ) -> Result, AppError> { - // Look up user - let row = sqlx::query!( + let row: Option = sqlx::query_as::<_, UserRow>( "SELECT id, username, password_hash, role FROM users WHERE username = ? LIMIT 1", - body.username ) + .bind(&body.username) .fetch_optional(&state.db) - .await? - .ok_or(AppError::Unauthorized)?; + .await?; - // Verify password - let parsed = PasswordHash::new(&row.password_hash) - .map_err(|_| AppError::Unauthorized)?; + let row = row.ok_or(AppError::Unauthorized)?; + + let parsed = PasswordHash::new(&row.password_hash).map_err(|_| AppError::Unauthorized)?; Argon2::default() .verify_password(body.password.as_bytes(), &parsed) .map_err(|_| AppError::Unauthorized)?; - // Issue JWT let token = Claims::new(row.id, &row.username, &row.role, &state.config.jwt_secret) - .map_err(|e| AppError::Internal(e))?; + .map_err(AppError::Internal)?; Ok(Json(LoginResponse { token, diff --git a/nixpanel-api/src/routes/dashboard.rs b/nixpanel-api/src/routes/dashboard.rs index a744211..5698df9 100644 --- a/nixpanel-api/src/routes/dashboard.rs +++ b/nixpanel-api/src/routes/dashboard.rs @@ -19,36 +19,30 @@ pub struct DashboardResponse { } pub async fn get_dashboard( - claims: Claims, + _claims: Claims, State(state): State, ) -> Result, AppError> { - // DB counts - let accounts = sqlx::query_scalar!("SELECT COUNT(*) FROM accounts") - .fetch_one(&state.db).await?.unwrap_or(0); - let domains = sqlx::query_scalar!("SELECT COUNT(*) FROM domains") - .fetch_one(&state.db).await?.unwrap_or(0); - let databases = sqlx::query_scalar!("SELECT COUNT(*) FROM account_databases") - .fetch_one(&state.db).await?.unwrap_or(0); - let email_accounts = sqlx::query_scalar!("SELECT COUNT(*) FROM email_accounts") - .fetch_one(&state.db).await?.unwrap_or(0); + // Use non-macro query() — tables may not exist at compile time + let accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM accounts") + .fetch_one(&state.db).await.unwrap_or(0); + let domains: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM domains") + .fetch_one(&state.db).await.unwrap_or(0); + let databases: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM account_databases") + .fetch_one(&state.db).await.unwrap_or(0); + let email_accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM email_accounts") + .fetch_one(&state.db).await.unwrap_or(0); - // Section binaries let bin_dir = &state.config.bin_dir; - let sysinfo_resp = section::call(bin_dir, "nixpanel-sysinfo", "stats", serde_json::Value::Null) + let sysinfo = section::call(bin_dir, "nixpanel-sysinfo", "stats", serde_json::Value::Null) .unwrap_or_else(|_| nixpanel_common::SectionResponse::err("sysinfo unavailable")); - let versions_resp = section::call(bin_dir, "nixpanel-sysinfo", "versions", serde_json::Value::Null) + let versions = section::call(bin_dir, "nixpanel-sysinfo", "versions", serde_json::Value::Null) .unwrap_or_else(|_| nixpanel_common::SectionResponse::err("versions unavailable")); Ok(Json(DashboardResponse { - stats: DashboardStats { - accounts, - domains, - databases, - email_accounts, - }, - sysinfo: sysinfo_resp.data.unwrap_or(serde_json::Value::Null), - versions: versions_resp.data.unwrap_or(serde_json::Value::Null), + stats: DashboardStats { accounts, domains, databases, email_accounts }, + sysinfo: sysinfo.data.unwrap_or(serde_json::Value::Null), + versions: versions.data.unwrap_or(serde_json::Value::Null), })) } diff --git a/nixpanel-api/src/routes/services.rs b/nixpanel-api/src/routes/services.rs index a5d8e6e..51a826e 100644 --- a/nixpanel-api/src/routes/services.rs +++ b/nixpanel-api/src/routes/services.rs @@ -2,12 +2,11 @@ use axum::{ extract::{Path, State}, Json, }; -use serde::{Deserialize, Serialize}; use crate::{auth::Claims, error::AppError, section, state::AppState}; pub async fn list_services( - claims: Claims, + _claims: Claims, State(state): State, ) -> Result, AppError> { let resp = section::call( @@ -22,12 +21,12 @@ pub async fn list_services( } pub async fn service_action( - claims: Claims, + _claims: Claims, State(state): State, Path((name, action)): Path<(String, String)>, ) -> Result, AppError> { - let allowed_actions = ["start", "stop", "restart", "reload"]; - if !allowed_actions.contains(&action.as_str()) { + let allowed = ["start", "stop", "restart", "reload"]; + if !allowed.contains(&action.as_str()) { return Err(AppError::BadRequest(format!("Unknown action: {}", action))); } @@ -40,11 +39,10 @@ pub async fn service_action( .map_err(|e| AppError::Internal(e))?; if resp.success { - Ok(Json(serde_json::json!({ - "success": true, - "message": resp.data - }))) + Ok(Json(serde_json::json!({ "success": true, "message": resp.data }))) } else { - Err(AppError::BadRequest(resp.error.unwrap_or_else(|| "Action failed".into()))) + Err(AppError::BadRequest( + resp.error.unwrap_or_else(|| "Action failed".into()), + )) } } diff --git a/nixpanel-services/src/main.rs b/nixpanel-services/src/main.rs index 38f8bba..a66f95a 100644 --- a/nixpanel-services/src/main.rs +++ b/nixpanel-services/src/main.rs @@ -12,7 +12,6 @@ use nixpanel_common::{SectionRequest, SectionResponse, ServiceInfo}; use std::io::{self, BufRead}; use std::process::Command; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// Canonical list of managed services (systemd unit → display name). const SERVICES: &[(&str, &str)] = &[ diff --git a/nixpanel-sysinfo/src/main.rs b/nixpanel-sysinfo/src/main.rs index 59503da..077086e 100644 --- a/nixpanel-sysinfo/src/main.rs +++ b/nixpanel-sysinfo/src/main.rs @@ -129,7 +129,6 @@ fn get_ram() -> (u64, u64) { fn get_disk() -> (u64, u64) { // Returns (used_gb, total_gb) for / - use std::os::unix::fs::MetadataExt; let stat = nix_statvfs("/"); stat.unwrap_or((0, 0)) }