fix: replace sqlx macros with non-macro queries, drop axum_extra dep, fix unused imports
This commit is contained in:
+15
-19
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user