fix: Schema alignment and code corrections (COA 2)
All checks were successful
Test Asgard Runner / test (push) Successful in 3s

- Replace owner_id → owner_user_id in all queries
- Replace auth_token → companion_agent_token in server_connections
- Replace l.active → (l.status = 'active') checks using ENUM
- Fix AppError → ApiError in all new API files
- Add missing imports (Path, PanelAdapter trait)
- Fix StoreConfig nullable type mismatches

Resolves 122 compilation errors. Only sqlx cache generation remains.

Phase 3: EXECUTE complete per V4_WORKFLOW
This commit is contained in:
Vantz Stockwell
2026-02-15 18:23:33 -05:00
parent d7dddca106
commit 500d92cbe3
8 changed files with 342 additions and 82 deletions

View File

@@ -1,5 +1,5 @@
use axum::{
extract::{Request, State},
extract::{Path, Request, State},
http::StatusCode,
middleware::{self, Next},
response::{IntoResponse, Response},
@@ -11,7 +11,7 @@ use std::sync::Arc;
use uuid::Uuid;
use crate::{
models::error::AppError,
models::error::ApiError,
services::host_provisioning::{HostProvisioningService, ProvisionedLicense},
AppState,
};
@@ -29,16 +29,16 @@ async fn host_auth_middleware(
State(state): State<Arc<AppState>>,
mut req: Request,
next: Next,
) -> Result<Response, AppError> {
) -> Result<Response, ApiError> {
// Extract API key from Authorization header (Bearer token)
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".to_string()))?;
.ok_or_else(|| ApiError::Unauthorized("Missing Authorization header".to_string()))?;
if !auth_header.starts_with("Bearer ") {
return Err(AppError::Unauthorized("Invalid Authorization format. Use: Bearer <api_key>".to_string()));
return Err(ApiError::Unauthorized("Invalid Authorization format. Use: Bearer <api_key>".to_string()));
}
let api_key = &auth_header[7..]; // Skip "Bearer "
@@ -48,7 +48,7 @@ async fn host_auth_middleware(
let host_id = service
.authenticate_host(api_key)
.await
.map_err(|_| AppError::Unauthorized("Invalid or inactive API key".to_string()))?;
.map_err(|_| ApiError::Unauthorized("Invalid or inactive API key".to_string()))?;
// Store host_id in request extensions for handlers to access
req.extensions_mut().insert(HostContext { host_id });
@@ -87,7 +87,7 @@ async fn provision_license(
State(state): State<Arc<AppState>>,
axum::extract::Extension(ctx): axum::extract::Extension<HostContext>,
Json(req): Json<ProvisionRequest>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let service = HostProvisioningService::new(state.db.clone());
// Use hostname if provided, otherwise use server_id
@@ -125,13 +125,13 @@ struct HostLicenseInfo {
async fn list_host_licenses(
State(state): State<Arc<AppState>>,
axum::extract::Extension(ctx): axum::extract::Extension<HostContext>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let licenses = sqlx::query!(
"SELECT
l.license_key,
l.server_name,
l.subdomain,
l.active,
(l.status = 'active') as \"active!\",
hl.customer_email,
hl.last_seen_at,
hl.provisioned_at
@@ -187,10 +187,10 @@ async fn get_billing_report(
State(state): State<Arc<AppState>>,
axum::extract::Extension(ctx): axum::extract::Extension<HostContext>,
Path(month): axum::extract::Path<String>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
// Parse month (format: YYYY-MM)
let billing_month = chrono::NaiveDate::parse_from_str(&format!("{}-01", month), "%Y-%m-%d")
.map_err(|_| AppError::BadRequest("Invalid month format. Use YYYY-MM (e.g., 2026-02)".to_string()))?;
.map_err(|_| ApiError::BadRequest("Invalid month format. Use YYYY-MM (e.g., 2026-02)".to_string()))?;
// Get billing record
let record = sqlx::query!(
@@ -223,7 +223,7 @@ async fn get_billing_report(
"SELECT
l.license_key,
l.server_name,
l.active,
(l.status = 'active') as \"active!\",
hl.customer_email,
hl.last_seen_at
FROM host_licenses hl

View File

@@ -10,7 +10,7 @@ use std::sync::Arc;
use uuid::Uuid;
use crate::{
models::error::AppError,
models::error::ApiError,
services::payment_processor::PayPalProcessor,
AppState,
};
@@ -35,14 +35,14 @@ struct PublicStoreInfo {
async fn get_store_info(
State(state): State<Arc<AppState>>,
Path(subdomain): Path<String>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
// Get license_id from subdomain
let license = sqlx::query!("SELECT id FROM licenses WHERE subdomain = $1", subdomain)
.fetch_optional(&state.db)
.await?;
let license_id = license
.ok_or_else(|| AppError::NotFound("Store not found".to_string()))?
.ok_or_else(|| ApiError::NotFound("Store not found".to_string()))?
.id;
// Get store config
@@ -58,11 +58,11 @@ async fn get_store_info(
if let Some(config) = config {
if !config.enabled {
return Err(AppError::NotFound("Store is currently disabled".to_string()));
return Err(ApiError::NotFound("Store is currently disabled".to_string()));
}
Ok(Json(config))
} else {
Err(AppError::NotFound("Store not configured".to_string()))
Err(ApiError::NotFound("Store not configured".to_string()))
}
}
@@ -82,13 +82,13 @@ struct PublicStoreItem {
async fn get_store_items(
State(state): State<Arc<AppState>>,
Path(subdomain): Path<String>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license = sqlx::query!("SELECT id FROM licenses WHERE subdomain = $1", subdomain)
.fetch_optional(&state.db)
.await?;
let license_id = license
.ok_or_else(|| AppError::NotFound("Store not found".to_string()))?
.ok_or_else(|| ApiError::NotFound("Store not found".to_string()))?
.id;
// Check if store is enabled
@@ -98,7 +98,7 @@ async fn get_store_items(
.unwrap_or(false);
if !enabled {
return Err(AppError::NotFound("Store is currently disabled".to_string()));
return Err(ApiError::NotFound("Store is currently disabled".to_string()));
}
type Row = (
@@ -166,13 +166,13 @@ async fn create_purchase_order(
State(state): State<Arc<AppState>>,
Path(subdomain): Path<String>,
Json(req): Json<CreatePurchaseRequest>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license = sqlx::query!("SELECT id FROM licenses WHERE subdomain = $1", subdomain)
.fetch_optional(&state.db)
.await?;
let license_id = license
.ok_or_else(|| AppError::NotFound("Store not found".to_string()))?
.ok_or_else(|| ApiError::NotFound("Store not found".to_string()))?
.id;
// Get store config and check if enabled
@@ -184,10 +184,10 @@ async fn create_purchase_order(
)
.fetch_optional(&state.db)
.await?
.ok_or_else(|| AppError::NotFound("Store not configured".to_string()))?;
.ok_or_else(|| ApiError::NotFound("Store not configured".to_string()))?;
if !store_config.enabled {
return Err(AppError::BadRequest("Store is currently disabled".to_string()));
return Err(ApiError::BadRequest("Store is currently disabled".to_string()));
}
// Get item details
@@ -198,7 +198,7 @@ async fn create_purchase_order(
)
.fetch_optional(&state.db)
.await?
.ok_or_else(|| AppError::NotFound("Item not found or disabled".to_string()))?;
.ok_or_else(|| ApiError::NotFound("Item not found or disabled".to_string()))?;
// Check purchase limit if set
if let Some(limit) = sqlx::query_scalar!(
@@ -219,17 +219,17 @@ async fn create_purchase_order(
.unwrap_or(0);
if purchase_count >= limit as i64 {
return Err(AppError::BadRequest("Purchase limit reached for this item".to_string()));
return Err(ApiError::BadRequest("Purchase limit reached for this item".to_string()));
}
}
// Create PayPal processor using store owner's credentials
let client_id = store_config.paypal_client_id.ok_or_else(|| {
AppError::Internal("Store PayPal credentials not configured".to_string())
ApiError::Internal("Store PayPal credentials not configured".to_string())
})?;
let client_secret = store_config.paypal_client_secret.ok_or_else(|| {
AppError::Internal("Store PayPal credentials not configured".to_string())
ApiError::Internal("Store PayPal credentials not configured".to_string())
})?;
// TODO: Decrypt client_secret using encryption service
@@ -282,7 +282,7 @@ async fn handle_purchase_webhook(
State(state): State<Arc<AppState>>,
Path(subdomain): Path<String>,
Json(webhook): Json<serde_json::Value>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
tracing::info!("Store purchase webhook for {}: {:?}", subdomain, webhook);
let license = sqlx::query!("SELECT id FROM licenses WHERE subdomain = $1", subdomain)
@@ -290,7 +290,7 @@ async fn handle_purchase_webhook(
.await?;
let license_id = license
.ok_or_else(|| AppError::NotFound("Store not found".to_string()))?
.ok_or_else(|| ApiError::NotFound("Store not found".to_string()))?
.id;
// TODO: Verify webhook signature
@@ -320,11 +320,11 @@ async fn handle_payment_completed(
state: &AppState,
license_id: Uuid,
webhook: serde_json::Value,
) -> Result<(), AppError> {
) -> Result<(), ApiError> {
let order_id = webhook
.pointer("/resource/supplementary_data/related_ids/order_id")
.and_then(|v| v.as_str())
.ok_or_else(|| AppError::Internal("Missing order_id in webhook".to_string()))?;
.ok_or_else(|| ApiError::Internal("Missing order_id in webhook".to_string()))?;
// Update transaction status
let transaction = sqlx::query!(
@@ -367,7 +367,7 @@ async fn handle_payment_completed(
let payload = serde_json::json!({ "command": command });
nats.publish(subject, serde_json::to_vec(&payload)?.into())
.await
.map_err(|e| AppError::Internal(format!("NATS publish failed: {}", e)))?;
.map_err(|e| ApiError::Internal(format!("NATS publish failed: {}", e)))?;
}
// Mark as delivered
@@ -391,11 +391,11 @@ async fn handle_payment_failed(
state: &AppState,
license_id: Uuid,
webhook: serde_json::Value,
) -> Result<(), AppError> {
) -> Result<(), ApiError> {
let order_id = webhook
.pointer("/resource/supplementary_data/related_ids/order_id")
.and_then(|v| v.as_str())
.ok_or_else(|| AppError::Internal("Missing order_id in webhook".to_string()))?;
.ok_or_else(|| ApiError::Internal("Missing order_id in webhook".to_string()))?;
sqlx::query!(
"UPDATE store_transactions SET status = 'failed' WHERE paypal_order_id = $1 AND license_id = $2",

View File

@@ -11,7 +11,7 @@ use uuid::Uuid;
use crate::{
middleware::jwt::Claims,
models::error::AppError,
models::error::ApiError,
services::subscription_processor::{SubscriptionProcessor, SubscriptionDetails},
AppState,
};
@@ -59,10 +59,10 @@ async fn create_subscription(
State(state): State<Arc<AppState>>,
claims: Claims,
Json(req): Json<CreateSubscriptionRequest>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
// Get license_id from JWT claims
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
// Check if subscription already exists
@@ -74,16 +74,16 @@ async fn create_subscription(
.await?;
if existing.is_some() {
return Err(AppError::BadRequest(
return Err(ApiError::BadRequest(
"You already have an active webstore subscription".to_string(),
));
}
// Get PayPal credentials from env
let client_id = std::env::var("PAYPAL_CLIENT_ID")
.map_err(|_| AppError::Internal("PayPal credentials not configured".to_string()))?;
.map_err(|_| ApiError::Internal("PayPal credentials not configured".to_string()))?;
let client_secret = std::env::var("PAYPAL_CLIENT_SECRET")
.map_err(|_| AppError::Internal("PayPal credentials not configured".to_string()))?;
.map_err(|_| ApiError::Internal("PayPal credentials not configured".to_string()))?;
let sandbox_mode = std::env::var("PAYPAL_SANDBOX")
.unwrap_or_else(|_| "true".to_string())
.parse()
@@ -114,9 +114,9 @@ struct SubscriptionStatusResponse {
async fn get_subscription_status(
State(state): State<Arc<AppState>>,
claims: Claims,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let subscription = sqlx::query!(
@@ -179,9 +179,9 @@ async fn cancel_subscription(
State(state): State<Arc<AppState>>,
claims: Claims,
Json(req): Json<CancelSubscriptionRequest>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let subscription = sqlx::query!(
@@ -195,13 +195,13 @@ async fn cancel_subscription(
.await?;
let subscription = subscription.ok_or_else(|| {
AppError::NotFound("No active subscription found".to_string())
ApiError::NotFound("No active subscription found".to_string())
})?;
let client_id = std::env::var("PAYPAL_CLIENT_ID")
.map_err(|_| AppError::Internal("PayPal credentials not configured".to_string()))?;
.map_err(|_| ApiError::Internal("PayPal credentials not configured".to_string()))?;
let client_secret = std::env::var("PAYPAL_CLIENT_SECRET")
.map_err(|_| AppError::Internal("PayPal credentials not configured".to_string()))?;
.map_err(|_| ApiError::Internal("PayPal credentials not configured".to_string()))?;
let sandbox_mode = std::env::var("PAYPAL_SANDBOX")
.unwrap_or_else(|_| "true".to_string())
.parse()
@@ -233,7 +233,7 @@ async fn cancel_subscription(
async fn handle_subscription_webhook(
State(state): State<Arc<AppState>>,
Json(event): Json<serde_json::Value>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
// TODO: Verify webhook signature using PayPal webhook ID
// For now, just log and process
@@ -270,18 +270,18 @@ async fn handle_subscription_webhook(
struct StoreConfig {
store_name: String,
description: Option<String>,
currency: String,
currency: Option<String>,
paypal_client_id: Option<String>,
sandbox_mode: bool,
enabled: bool,
sandbox_mode: Option<bool>,
enabled: Option<bool>,
}
async fn get_store_config(
State(state): State<Arc<AppState>>,
claims: Claims,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let config = sqlx::query_as!(
@@ -313,9 +313,9 @@ async fn update_store_config(
State(state): State<Arc<AppState>>,
claims: Claims,
Json(config): Json<StoreConfig>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
// Upsert store config
@@ -362,9 +362,9 @@ struct StoreCategory {
async fn list_categories(
State(state): State<Arc<AppState>>,
claims: Claims,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let categories = sqlx::query_as!(
@@ -394,9 +394,9 @@ async fn create_category(
State(state): State<Arc<AppState>>,
claims: Claims,
Json(req): Json<CreateCategoryRequest>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let id = sqlx::query_scalar!(
@@ -421,9 +421,9 @@ async fn update_category(
claims: Claims,
Path(id): Path<Uuid>,
Json(req): Json<CreateCategoryRequest>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let result = sqlx::query!(
@@ -442,7 +442,7 @@ async fn update_category(
.await?;
if result.rows_affected() == 0 {
return Err(AppError::NotFound("Category not found".to_string()));
return Err(ApiError::NotFound("Category not found".to_string()));
}
Ok(StatusCode::NO_CONTENT)
@@ -452,9 +452,9 @@ async fn delete_category(
State(state): State<Arc<AppState>>,
claims: Claims,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let result = sqlx::query!(
@@ -466,7 +466,7 @@ async fn delete_category(
.await?;
if result.rows_affected() == 0 {
return Err(AppError::NotFound("Category not found".to_string()));
return Err(ApiError::NotFound("Category not found".to_string()));
}
Ok(StatusCode::NO_CONTENT)
@@ -493,9 +493,9 @@ struct StoreItem {
async fn list_items(
State(state): State<Arc<AppState>>,
claims: Claims,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let items = sqlx::query_as!(
@@ -529,9 +529,9 @@ async fn create_item(
State(state): State<Arc<AppState>>,
claims: Claims,
Json(req): Json<CreateItemRequest>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let id = sqlx::query_scalar!(
@@ -560,9 +560,9 @@ async fn update_item(
claims: Claims,
Path(id): Path<Uuid>,
Json(req): Json<CreateItemRequest>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let result = sqlx::query!(
@@ -585,7 +585,7 @@ async fn update_item(
.await?;
if result.rows_affected() == 0 {
return Err(AppError::NotFound("Item not found".to_string()));
return Err(ApiError::NotFound("Item not found".to_string()));
}
Ok(StatusCode::NO_CONTENT)
@@ -595,9 +595,9 @@ async fn delete_item(
State(state): State<Arc<AppState>>,
claims: Claims,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let result = sqlx::query!(
@@ -609,7 +609,7 @@ async fn delete_item(
.await?;
if result.rows_affected() == 0 {
return Err(AppError::NotFound("Item not found".to_string()));
return Err(ApiError::NotFound("Item not found".to_string()));
}
Ok(StatusCode::NO_CONTENT)
@@ -638,9 +638,9 @@ struct StoreTransaction {
async fn list_transactions(
State(state): State<Arc<AppState>>,
claims: Claims,
) -> Result<impl IntoResponse, AppError> {
) -> Result<impl IntoResponse, ApiError> {
let license_id = claims.license_id.ok_or_else(|| {
AppError::Unauthorized("No license associated with this account".to_string())
ApiError::Unauthorized("No license associated with this account".to_string())
})?;
let transactions = sqlx::query_as!(

View File

@@ -52,7 +52,7 @@ impl HostProvisioningService {
// Create license
let license_id = sqlx::query_scalar!(
"INSERT INTO licenses (license_key, owner_id, server_name, subdomain)
"INSERT INTO licenses (license_key, owner_user_id, server_name, subdomain)
VALUES ($1, $2, $3, $4)
RETURNING id",
license_key,
@@ -79,9 +79,9 @@ impl HostProvisioningService {
// Store companion token
sqlx::query!(
"INSERT INTO server_connections (license_id, connection_type, auth_token)
"INSERT INTO server_connections (license_id, connection_type, companion_agent_token)
VALUES ($1, 'bare_metal', $2)
ON CONFLICT (license_id) DO UPDATE SET auth_token = $2",
ON CONFLICT (license_id) DO UPDATE SET companion_agent_token = $2",
license_id,
companion_token
)
@@ -150,7 +150,7 @@ impl HostProvisioningService {
let count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM host_licenses hl
INNER JOIN licenses l ON l.id = hl.license_id
WHERE hl.host_id = $1 AND l.active = true",
WHERE hl.host_id = $1 AND l.status = 'active'",
host_id
)
.fetch_one(&self.db)

View File

@@ -6,6 +6,7 @@ use uuid::Uuid;
use super::amp_adapter::AmpAdapter;
use super::encryption;
use super::nats_bridge::NatsBridge;
use super::panel_adapter::PanelAdapter;
use super::pterodactyl_adapter::PterodactylAdapter;
/// Default timeout for module installation operations (60 seconds).