fix: replace sqlx macros with non-macro queries, drop axum_extra dep, fix unused imports

This commit is contained in:
NixReaper
2026-04-25 16:48:14 -05:00
parent 5247c77132
commit d3b27bb90c
8 changed files with 96 additions and 106 deletions
+11 -15
View File
@@ -1,10 +1,8 @@
use axum::{ use axum::{
async_trait, async_trait,
extract::FromRequestParts, extract::FromRequestParts,
http::{request::Parts, HeaderMap}, http::request::Parts,
RequestPartsExt,
}; };
use axum_extra::TypedHeader;
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -19,7 +17,7 @@ pub struct Claims {
pub sub: String, // username pub sub: String, // username
pub user_id: i64, pub user_id: i64,
pub role: String, // "admin" | "user" pub role: String, // "admin" | "user"
pub exp: i64, // unix timestamp pub exp: i64,
pub iat: i64, pub iat: i64,
} }
@@ -33,25 +31,24 @@ impl Claims {
exp: (now + Duration::hours(8)).timestamp(), exp: (now + Duration::hours(8)).timestamp(),
iat: now.timestamp(), iat: now.timestamp(),
}; };
let token = encode( Ok(encode(
&Header::default(), &Header::default(),
&claims, &claims,
&EncodingKey::from_secret(secret.as_bytes()), &EncodingKey::from_secret(secret.as_bytes()),
)?; )?)
Ok(token)
} }
pub fn verify(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> { pub fn verify(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let data = decode::<Claims>( Ok(decode::<Claims>(
token, token,
&DecodingKey::from_secret(secret.as_bytes()), &DecodingKey::from_secret(secret.as_bytes()),
&Validation::new(Algorithm::HS256), &Validation::new(Algorithm::HS256),
)?; )?
Ok(data.claims) .claims)
} }
} }
/* ── Extractor: pulls Claims from Authorization: Bearer <token> ──────── */ /* ── Extractor — pulls Claims from Authorization: Bearer <token> ─────── */
#[async_trait] #[async_trait]
impl FromRequestParts<AppState> for Claims { impl FromRequestParts<AppState> for Claims {
@@ -61,14 +58,13 @@ impl FromRequestParts<AppState> for Claims {
parts: &mut Parts, parts: &mut Parts,
state: &AppState, state: &AppState,
) -> Result<Self, Self::Rejection> { ) -> Result<Self, Self::Rejection> {
let headers: &HeaderMap = &parts.headers; let auth = parts
let auth = headers .headers
.get("authorization") .get("authorization")
.and_then(|v| v.to_str().ok()) .and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer ")) .and_then(|s| s.strip_prefix("Bearer "))
.ok_or(AppError::Unauthorized)?; .ok_or(AppError::Unauthorized)?;
Claims::verify(auth, &state.config.jwt_secret) Claims::verify(auth, &state.config.jwt_secret).map_err(|_| AppError::Unauthorized)
.map_err(|_| AppError::Unauthorized)
} }
} }
+1 -1
View File
@@ -42,7 +42,7 @@ impl From<sqlx::Error> for AppError {
} }
impl From<jsonwebtoken::errors::Error> for AppError { impl From<jsonwebtoken::errors::Error> for AppError {
fn from(e: jsonwebtoken::errors::Error) -> Self { fn from(_e: jsonwebtoken::errors::Error) -> Self {
AppError::Unauthorized AppError::Unauthorized
} }
} }
+26 -28
View File
@@ -29,13 +29,12 @@ pub struct CreateAccountRequest {
} }
pub async fn list_accounts( pub async fn list_accounts(
claims: Claims, _claims: Claims,
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<Json<Vec<Account>>, AppError> { ) -> Result<Json<Vec<Account>>, AppError> {
let accounts = sqlx::query_as!( let accounts = sqlx::query_as::<_, Account>(
Account,
"SELECT id, username, domain, email, package_name, disk_quota_mb, bandwidth_mb, status, created_at "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) .fetch_all(&state.db)
.await?; .await?;
@@ -43,7 +42,7 @@ pub async fn list_accounts(
} }
pub async fn create_account( pub async fn create_account(
claims: Claims, _claims: Claims,
State(state): State<AppState>, State(state): State<AppState>,
Json(body): Json<CreateAccountRequest>, Json(body): Json<CreateAccountRequest>,
) -> Result<Json<Account>, AppError> { ) -> Result<Json<Account>, AppError> {
@@ -52,61 +51,60 @@ pub async fn create_account(
Argon2, Argon2,
}; };
// Validate
if body.username.is_empty() || body.domain.is_empty() { if body.username.is_empty() || body.domain.is_empty() {
return Err(AppError::BadRequest("username and domain are required".into())); return Err(AppError::BadRequest("username and domain are required".into()));
} }
// Hash password
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let hash = Argon2::default() let hash = Argon2::default()
.hash_password(body.password.as_bytes(), &salt) .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(); .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") let _ = std::process::Command::new("useradd")
.args(&[ .args(["-m", "-s", "/bin/bash", "-d",
"-m", &format!("/home/{}", body.username),
"-s", "/bin/bash", &body.username])
"-d", &format!("/home/{}", body.username),
&body.username,
])
.output(); .output();
// Create Apache vhost directory let _ = std::fs::create_dir_all(format!("/home/{}/public_html", body.username));
let vhost_dir = format!("/home/{}/public_html", body.username);
let _ = std::fs::create_dir_all(&vhost_dir);
let package_name = body.package_name.unwrap_or_else(|| "Default".into()); let package_name = body.package_name.unwrap_or_else(|| "Default".into());
let disk_quota_mb = body.disk_quota_mb.unwrap_or(10240); let disk_quota_mb = body.disk_quota_mb.unwrap_or(10240);
let bandwidth_mb = body.bandwidth_mb.unwrap_or(0); 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) "INSERT INTO accounts (username, domain, email, password_hash, package_name, disk_quota_mb, bandwidth_mb, status)
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')", 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) .execute(&state.db)
.await? .await?
.last_insert_id() as i64; .last_insert_id();
// Also create a panel user so they can log into NixClient // Also create a NixClient panel user
let _ = sqlx::query!( let _ = sqlx::query(
"INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, 'user') "INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, 'user')
ON DUPLICATE KEY UPDATE email = VALUES(email)", ON DUPLICATE KEY UPDATE email = VALUES(email)",
body.username, body.email, hash
) )
.bind(&body.username)
.bind(&body.email)
.bind(&hash)
.execute(&state.db) .execute(&state.db)
.await; .await;
let account = sqlx::query_as!( let account = sqlx::query_as::<_, Account>(
Account,
"SELECT id, username, domain, email, package_name, disk_quota_mb, bandwidth_mb, status, created_at "SELECT id, username, domain, email, package_name, disk_quota_mb, bandwidth_mb, status, created_at
FROM accounts WHERE id = ?", FROM accounts WHERE id = ?",
id
) )
.bind(insert_id)
.fetch_one(&state.db) .fetch_one(&state.db)
.await?; .await?;
+16 -10
View File
@@ -23,29 +23,35 @@ pub struct UserInfo {
pub role: String, 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( pub async fn login(
State(state): State<AppState>, State(state): State<AppState>,
Json(body): Json<LoginRequest>, Json(body): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, AppError> { ) -> Result<Json<LoginResponse>, AppError> {
// Look up user let row: Option<UserRow> = sqlx::query_as::<_, UserRow>(
let row = sqlx::query!(
"SELECT id, username, password_hash, role FROM users WHERE username = ? LIMIT 1", "SELECT id, username, password_hash, role FROM users WHERE username = ? LIMIT 1",
body.username
) )
.bind(&body.username)
.fetch_optional(&state.db) .fetch_optional(&state.db)
.await? .await?;
.ok_or(AppError::Unauthorized)?;
// Verify password let row = row.ok_or(AppError::Unauthorized)?;
let parsed = PasswordHash::new(&row.password_hash)
.map_err(|_| AppError::Unauthorized)?; let parsed = PasswordHash::new(&row.password_hash).map_err(|_| AppError::Unauthorized)?;
Argon2::default() Argon2::default()
.verify_password(body.password.as_bytes(), &parsed) .verify_password(body.password.as_bytes(), &parsed)
.map_err(|_| AppError::Unauthorized)?; .map_err(|_| AppError::Unauthorized)?;
// Issue JWT
let token = Claims::new(row.id, &row.username, &row.role, &state.config.jwt_secret) 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 { Ok(Json(LoginResponse {
token, token,
+15 -21
View File
@@ -19,36 +19,30 @@ pub struct DashboardResponse {
} }
pub async fn get_dashboard( pub async fn get_dashboard(
claims: Claims, _claims: Claims,
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<Json<DashboardResponse>, AppError> { ) -> Result<Json<DashboardResponse>, AppError> {
// DB counts // Use non-macro query() — tables may not exist at compile time
let accounts = sqlx::query_scalar!("SELECT COUNT(*) FROM accounts") let accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM accounts")
.fetch_one(&state.db).await?.unwrap_or(0); .fetch_one(&state.db).await.unwrap_or(0);
let domains = sqlx::query_scalar!("SELECT COUNT(*) FROM domains") let domains: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM domains")
.fetch_one(&state.db).await?.unwrap_or(0); .fetch_one(&state.db).await.unwrap_or(0);
let databases = sqlx::query_scalar!("SELECT COUNT(*) FROM account_databases") let databases: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM account_databases")
.fetch_one(&state.db).await?.unwrap_or(0); .fetch_one(&state.db).await.unwrap_or(0);
let email_accounts = sqlx::query_scalar!("SELECT COUNT(*) FROM email_accounts") let email_accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM email_accounts")
.fetch_one(&state.db).await?.unwrap_or(0); .fetch_one(&state.db).await.unwrap_or(0);
// Section binaries
let bin_dir = &state.config.bin_dir; 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")); .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")); .unwrap_or_else(|_| nixpanel_common::SectionResponse::err("versions unavailable"));
Ok(Json(DashboardResponse { Ok(Json(DashboardResponse {
stats: DashboardStats { stats: DashboardStats { accounts, domains, databases, email_accounts },
accounts, sysinfo: sysinfo.data.unwrap_or(serde_json::Value::Null),
domains, versions: versions.data.unwrap_or(serde_json::Value::Null),
databases,
email_accounts,
},
sysinfo: sysinfo_resp.data.unwrap_or(serde_json::Value::Null),
versions: versions_resp.data.unwrap_or(serde_json::Value::Null),
})) }))
} }
+8 -10
View File
@@ -2,12 +2,11 @@ use axum::{
extract::{Path, State}, extract::{Path, State},
Json, Json,
}; };
use serde::{Deserialize, Serialize};
use crate::{auth::Claims, error::AppError, section, state::AppState}; use crate::{auth::Claims, error::AppError, section, state::AppState};
pub async fn list_services( pub async fn list_services(
claims: Claims, _claims: Claims,
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, AppError> { ) -> Result<Json<serde_json::Value>, AppError> {
let resp = section::call( let resp = section::call(
@@ -22,12 +21,12 @@ pub async fn list_services(
} }
pub async fn service_action( pub async fn service_action(
claims: Claims, _claims: Claims,
State(state): State<AppState>, State(state): State<AppState>,
Path((name, action)): Path<(String, String)>, Path((name, action)): Path<(String, String)>,
) -> Result<Json<serde_json::Value>, AppError> { ) -> Result<Json<serde_json::Value>, AppError> {
let allowed_actions = ["start", "stop", "restart", "reload"]; let allowed = ["start", "stop", "restart", "reload"];
if !allowed_actions.contains(&action.as_str()) { if !allowed.contains(&action.as_str()) {
return Err(AppError::BadRequest(format!("Unknown action: {}", action))); return Err(AppError::BadRequest(format!("Unknown action: {}", action)));
} }
@@ -40,11 +39,10 @@ pub async fn service_action(
.map_err(|e| AppError::Internal(e))?; .map_err(|e| AppError::Internal(e))?;
if resp.success { if resp.success {
Ok(Json(serde_json::json!({ Ok(Json(serde_json::json!({ "success": true, "message": resp.data })))
"success": true,
"message": resp.data
})))
} else { } else {
Err(AppError::BadRequest(resp.error.unwrap_or_else(|| "Action failed".into()))) Err(AppError::BadRequest(
resp.error.unwrap_or_else(|| "Action failed".into()),
))
} }
} }
-1
View File
@@ -12,7 +12,6 @@
use nixpanel_common::{SectionRequest, SectionResponse, ServiceInfo}; use nixpanel_common::{SectionRequest, SectionResponse, ServiceInfo};
use std::io::{self, BufRead}; use std::io::{self, BufRead};
use std::process::Command; use std::process::Command;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
/// Canonical list of managed services (systemd unit → display name). /// Canonical list of managed services (systemd unit → display name).
const SERVICES: &[(&str, &str)] = &[ const SERVICES: &[(&str, &str)] = &[
-1
View File
@@ -129,7 +129,6 @@ fn get_ram() -> (u64, u64) {
fn get_disk() -> (u64, u64) { fn get_disk() -> (u64, u64) {
// Returns (used_gb, total_gb) for / // Returns (used_gb, total_gb) for /
use std::os::unix::fs::MetadataExt;
let stat = nix_statvfs("/"); let stat = nix_statvfs("/");
stat.unwrap_or((0, 0)) stat.unwrap_or((0, 0))
} }