use std::sync::Arc;
use axum::{
extract::State,
response::{IntoResponse, Response},
Json,
};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
use crate::errors::{LicenseError, LicenseResult};
use crate::server::api_error::ApiError;
use crate::server::database::{Database, License};
#[cfg(feature = "jwt-auth")]
use crate::server::auth::AuthState;
#[derive(Clone)]
pub struct AppState {
pub db: Arc<Database>,
#[cfg(feature = "jwt-auth")]
pub auth: AuthState,
}
impl IntoResponse for LicenseError {
fn into_response(self) -> Response {
let api_error: ApiError = self.into();
api_error.into_response()
}
}
#[derive(Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct LicenseRequest {
pub license_id: String,
pub client_id: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct LicenseResponse {
pub success: bool,
}
#[derive(Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct HeartbeatRequest {
pub license_id: String,
pub client_id: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct HeartbeatResponse {
pub success: bool,
}
#[cfg_attr(feature = "openapi", utoipa::path(
post,
path = "/activate",
tag = "legacy",
request_body = LicenseRequest,
responses(
(status = 200, description = "License activated", body = LicenseResponse),
(status = 500, description = "Server error"),
)
))]
pub async fn activate_license_handler(
State(state): State<AppState>,
Json(payload): Json<LicenseRequest>,
) -> LicenseResult<Json<LicenseResponse>> {
info!(
"Activating license_id={} for client_id={}",
payload.license_id, payload.client_id
);
let now = Utc::now().naive_utc();
let license = License {
license_id: payload.license_id.clone(),
client_id: Some(payload.client_id.clone()),
status: "active".to_string(),
features: None,
issued_at: now,
expires_at: None,
hardware_id: None,
signature: None,
last_heartbeat: Some(now),
org_id: None,
org_name: None,
license_key: None,
tier: None,
device_name: None,
device_info: None,
bound_at: None,
last_seen_at: None,
suspended_at: None,
revoked_at: None,
revoke_reason: None,
grace_period_ends_at: None,
suspension_message: None,
is_blacklisted: None,
blacklisted_at: None,
blacklist_reason: None,
metadata: None,
bandwidth_used_bytes: None,
bandwidth_limit_bytes: None,
quota_exceeded: None,
};
state.db.insert_license(license).await?;
Ok(Json(LicenseResponse { success: true }))
}
#[cfg_attr(feature = "openapi", utoipa::path(
post,
path = "/validate",
tag = "legacy",
request_body = LicenseRequest,
responses(
(status = 200, description = "Validation result", body = LicenseResponse),
(status = 500, description = "Server error"),
)
))]
pub async fn validate_license_handler(
State(state): State<AppState>,
Json(payload): Json<LicenseRequest>,
) -> LicenseResult<Json<LicenseResponse>> {
info!(
"Validating license_id={} for client_id={}",
payload.license_id, payload.client_id
);
let license_opt = state.db.get_license(&payload.license_id).await?;
let success = match license_opt {
Some(license) => {
if license.client_id.as_deref() != Some(payload.client_id.as_str()) {
warn!(
"Client ID mismatch for license_id={} (expected={:?}, got={})",
payload.license_id, license.client_id, payload.client_id
);
false
} else if license.status != "active" {
warn!(
"License is not active for license_id={} (status={})",
payload.license_id, license.status
);
false
} else {
true
}
}
None => {
warn!("License not found for license_id={}", payload.license_id);
false
}
};
Ok(Json(LicenseResponse { success }))
}
#[cfg_attr(feature = "openapi", utoipa::path(
post,
path = "/deactivate",
tag = "legacy",
request_body = LicenseRequest,
responses(
(status = 200, description = "Deactivation result", body = LicenseResponse),
(status = 500, description = "Server error"),
)
))]
pub async fn deactivate_license_handler(
State(state): State<AppState>,
Json(payload): Json<LicenseRequest>,
) -> LicenseResult<Json<LicenseResponse>> {
info!(
"Deactivating license_id={} for client_id={}",
payload.license_id, payload.client_id
);
let license_opt = state.db.get_license(&payload.license_id).await?;
let success = if let Some(mut license) = license_opt {
if license.client_id.as_deref() == Some(payload.client_id.as_str()) {
license.status = "inactive".to_string();
state.db.insert_license(license).await?;
info!(
"License deactivated for license_id={} client_id={}",
payload.license_id, payload.client_id
);
true
} else {
warn!(
"Client ID mismatch during deactivation for license_id={} (expected={:?}, got={})",
payload.license_id, license.client_id, payload.client_id
);
false
}
} else {
warn!(
"Deactivation requested for non-existent license_id={}",
payload.license_id
);
false
};
Ok(Json(LicenseResponse { success }))
}
#[cfg_attr(feature = "openapi", utoipa::path(
post,
path = "/heartbeat",
tag = "legacy",
request_body = HeartbeatRequest,
responses(
(status = 200, description = "Heartbeat result", body = HeartbeatResponse),
(status = 500, description = "Server error"),
)
))]
pub async fn heartbeat_handler(
State(state): State<AppState>,
Json(payload): Json<HeartbeatRequest>,
) -> LicenseResult<Json<HeartbeatResponse>> {
info!(
"Received heartbeat for license_id={} client_id={}",
payload.license_id, payload.client_id
);
let updated = state
.db
.update_last_heartbeat(&payload.license_id, &payload.client_id)
.await?;
if !updated {
warn!(
"Failed to update heartbeat: no matching license for license_id={} client_id={}",
payload.license_id, payload.client_id
);
}
Ok(Json(HeartbeatResponse { success: updated }))
}
#[cfg_attr(feature = "openapi", utoipa::path(
get,
path = "/health",
tag = "system",
responses(
(status = 200, description = "Service health status", body = crate::server::logging::HealthResponse),
)
))]
pub async fn health_handler(
State(state): State<AppState>,
) -> Json<crate::server::logging::HealthResponse> {
let db_connected = state.db.health_check().await;
let db_type = state.db.db_type();
Json(crate::server::logging::HealthResponse::healthy(
db_connected,
db_type,
))
}