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
+15 -19
View File
@@ -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<Claims, jsonwebtoken::errors::Error> {
let data = decode::<Claims>(
Ok(decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::new(Algorithm::HS256),
)?;
Ok(data.claims)
)?
.claims)
}
}
/* ── Extractor: pulls Claims from Authorization: Bearer <token> ──────── */
/* ── Extractor — pulls Claims from Authorization: Bearer <token> ─────── */
#[async_trait]
impl FromRequestParts<AppState> for Claims {
@@ -61,14 +58,13 @@ impl FromRequestParts<AppState> for Claims {
parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
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)
}
}
+5 -5
View File
@@ -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<sqlx::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
}
}
+37 -39
View File
@@ -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<AppState>,
) -> Result<Json<Vec<Account>>, 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<AppState>,
Json(body): Json<CreateAccountRequest>,
) -> Result<Json<Account>, 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?;
+16 -10
View File
@@ -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<AppState>,
Json(body): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, AppError> {
// Look up user
let row = sqlx::query!(
let row: Option<UserRow> = 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,
+15 -21
View File
@@ -19,36 +19,30 @@ pub struct DashboardResponse {
}
pub async fn get_dashboard(
claims: Claims,
_claims: Claims,
State(state): State<AppState>,
) -> Result<Json<DashboardResponse>, 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),
}))
}
+8 -10
View File
@@ -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<AppState>,
) -> Result<Json<serde_json::Value>, 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<AppState>,
Path((name, action)): Path<(String, String)>,
) -> Result<Json<serde_json::Value>, 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()),
))
}
}