use crate::rate_limit::RateLimitInfo;
use crate::license::License;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use deadpool_postgres::Pool;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;
use uuid::Uuid;
#[derive(Clone)]
pub struct AppState {
pub instances: Arc<tokio::sync::RwLock<HashMap<String, PostgresInstance>>>,
pub connections: Arc<DashMap<String, Pool>>,
pub accounts: Arc<tokio::sync::RwLock<HashMap<String, Account>>>,
pub rate_limiter: Arc<Mutex<HashMap<String, RateLimitInfo>>>,
pub active_connections: Arc<DashMap<String, u32>>, pub license: Arc<Option<License>>, }
impl AppState {
pub async fn new() -> anyhow::Result<Self> {
let license = match crate::license::LicenseValidator::validate_from_env() {
Ok(lic) => {
tracing::info!("License validated successfully: {:?}", lic.license_type);
Some(lic)
}
Err(e) => {
tracing::warn!("License validation failed: {}", e);
None
}
};
Ok(Self {
instances: Arc::new(tokio::sync::RwLock::new(load_instances().await?)),
connections: Arc::new(DashMap::new()),
accounts: Arc::new(tokio::sync::RwLock::new(load_accounts().await?)),
rate_limiter: Arc::new(Mutex::new(HashMap::new())),
active_connections: Arc::new(DashMap::new()),
license: Arc::new(license),
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PostgresInstance {
pub id: String,
pub name: String,
pub host: String,
pub port: u16,
pub superuser: String,
pub superuser_password: String,
pub instance_type: InstanceType,
pub created_at: DateTime<Utc>,
pub status: InstanceStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InstanceType {
Single,
Primary,
Replica,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InstanceStatus {
Active,
Maintenance,
Degraded,
Offline,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Account {
pub id: String,
pub name: String,
pub api_key: String,
pub instance_id: String,
pub databases: Vec<DatabaseAccess>,
pub role: AccountRole,
pub created_at: DateTime<Utc>,
pub last_used: DateTime<Utc>,
pub rate_limit: u32,
pub max_connections: u32,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub notes: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DatabaseAccess {
pub database: String,
pub username: String,
pub password: String,
pub permissions: Vec<Permission>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum Permission {
Select,
Insert,
Update,
Delete,
Create,
Drop,
Truncate,
References,
Trigger,
Execute,
Usage,
All,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AccountRole {
Owner,
Admin,
Developer,
ReadWrite,
ReadOnly,
}
#[derive(Debug, Deserialize)]
pub struct QueryRequest {
pub query: String,
pub database: String,
#[serde(default)]
pub params: Vec<Value>,
#[serde(default)]
#[allow(dead_code)]
pub options: QueryOptions,
}
#[derive(Debug, Deserialize, Default)]
pub struct QueryOptions {
#[serde(default)]
#[allow(dead_code)]
pub timeout_ms: Option<u64>,
#[serde(default)]
#[allow(dead_code)]
pub read_only: bool,
#[serde(default)]
#[allow(dead_code)]
pub as_transaction: bool,
}
#[derive(Debug, Serialize)]
pub struct ApiResponse<T> {
pub success: bool,
pub data: Option<T>,
pub error: Option<ErrorInfo>,
pub metadata: ResponseMetadata,
}
#[derive(Debug, Serialize)]
pub struct ErrorInfo {
pub code: String,
pub message: String,
pub details: Option<Value>,
}
#[derive(Debug, Serialize)]
pub struct ResponseMetadata {
pub request_id: String,
pub execution_time_ms: u128,
#[serde(skip_serializing_if = "Option::is_none")]
pub rows_affected: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance_id: Option<String>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Serialize)]
pub struct QueryResult {
pub rows: Vec<Value>,
pub fields: Vec<FieldMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query_plan: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct FieldMetadata {
pub name: String,
pub data_type: String,
pub nullable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<i32>,
}
impl<T: Serialize> ApiResponse<T> {
pub fn success(data: T, metadata: ResponseMetadata) -> Self {
Self {
success: true,
data: Some(data),
error: None,
metadata,
}
}
pub fn error(code: &str, message: String, metadata: ResponseMetadata) -> Self {
Self {
success: false,
data: None,
error: Some(ErrorInfo {
code: code.to_string(),
message,
details: None,
}),
metadata,
}
}
}
async fn load_instances() -> anyhow::Result<HashMap<String, PostgresInstance>> {
let mut instances = HashMap::new();
instances.insert(
"default".to_string(),
PostgresInstance {
id: "default".to_string(),
name: "Primary Instance".to_string(),
host: "127.0.0.1".to_string(),
port: 5432,
superuser: "postgres".to_string(),
superuser_password: "postgres".to_string(),
instance_type: InstanceType::Single,
created_at: Utc::now(),
status: InstanceStatus::Active,
},
);
Ok(instances)
}
async fn load_accounts() -> anyhow::Result<HashMap<String, Account>> {
let mut accounts = HashMap::new();
let config_dir = std::env::var("CONFIG_DIR").unwrap_or_else(|_| "/etc/pg-api".to_string());
let config_path = std::path::PathBuf::from(&config_dir).join("accounts.json");
eprintln!("[ACCOUNT LOADING] Loading accounts from: {}", config_path.display());
tracing::info!("Loading accounts from: {}", config_path.display());
match tokio::fs::read_to_string(&config_path).await {
Ok(content) => {
match serde_json::from_str::<Vec<Account>>(&content) {
Ok(loaded_accounts) => {
eprintln!("[ACCOUNT LOADING] Loaded {} accounts from config file", loaded_accounts.len());
tracing::info!("Loaded {} accounts from config file", loaded_accounts.len());
for account in loaded_accounts {
eprintln!("[ACCOUNT LOADING] Loading account: {} with key: {}...", account.name, &account.api_key[..20.min(account.api_key.len())]);
tracing::debug!("Loading account: {} with key: {}...", account.name, &account.api_key[..20.min(account.api_key.len())]);
accounts.insert(account.api_key.clone(), account);
}
}
Err(e) => {
eprintln!("[ACCOUNT LOADING ERROR] Failed to parse accounts.json: {}", e);
tracing::error!("Failed to parse accounts.json: {}", e);
return Err(anyhow::anyhow!("Failed to parse accounts configuration: {}", e));
}
}
}
Err(e) => {
eprintln!("[ACCOUNT LOADING] Could not read accounts file from {}: {}", config_path.display(), e);
tracing::warn!("Could not read accounts file from {}: {}", config_path.display(), e);
if e.kind() == std::io::ErrorKind::NotFound {
tracing::info!("Using default account configuration");
accounts.insert(
"TWDo79SIVGM21sdZqbIW68q5FuW+SQTL9jl88t2iF1j5vfP2poxU0wp43NHSVXdI".to_string(),
Account {
id: Uuid::new_v4().to_string(),
name: "sentric-production".to_string(),
api_key: "TWDo79SIVGM21sdZqbIW68q5FuW+SQTL9jl88t2iF1j5vfP2poxU0wp43NHSVXdI".to_string(),
instance_id: "default".to_string(),
databases: vec![
DatabaseAccess {
database: "camera".to_string(),
username: "sentric".to_string(),
password: "X5pzEXGqAyLVy9CQQKEbCsrF".to_string(),
permissions: vec![Permission::All],
},
DatabaseAccess {
database: "sentric".to_string(),
username: "sentric".to_string(),
password: "X5pzEXGqAyLVy9CQQKEbCsrF".to_string(),
permissions: vec![Permission::All],
},
],
role: AccountRole::Developer,
created_at: Utc::now(),
last_used: Utc::now(),
rate_limit: 1000,
max_connections: 50,
notes: Some("Default account - replace with proper configuration".to_string()),
},
);
} else {
return Err(anyhow::anyhow!("Failed to read accounts configuration: {}", e));
}
}
}
Ok(accounts)
}