newton-core 0.4.16

newton protocol core sdk
use super::{ApiPermission, DatabaseManager};
use alloy::primitives::Address;
use chrono::{DateTime, Utc};
use serde_json::Value as JsonValue;
use sqlx::{postgres::PgRow, Row};
use std::collections::HashSet;
use tracing::{error, info};
use uuid::Uuid;

/// Database model for API keys with additional metadata
#[derive(Debug, Clone)]
pub struct ApiKeyRecord {
    /// Unique identifier for this API key
    pub id: Uuid,
    /// User ID associated with this API key
    pub user_id: Uuid,
    /// Ethereum wallet address associated with this user
    pub address: Address,
    /// API key string (bearer token)
    pub api_key: String,
    /// Human-readable name for the API key
    pub name: String,
    /// Set of permissions granted to this key
    pub permissions: HashSet<ApiPermission>,
    /// Optional rate limit override (requests per minute)
    pub rate_limit: Option<u32>,
    /// Timestamp when the key was created
    pub created_at: DateTime<Utc>,
    /// Timestamp when the key was last updated
    pub updated_at: DateTime<Utc>,
    /// Whether the key is currently active
    pub is_active: bool,
    /// Optional expiration timestamp
    pub expires_at: Option<DateTime<Utc>>,
    /// Optional description of the key's purpose
    pub description: Option<String>,
}

impl ApiKeyRecord {
    /// Checks if the API key is currently valid
    pub fn is_valid(&self) -> bool {
        if !self.is_active {
            return false;
        }

        if let Some(expires_at) = self.expires_at {
            if expires_at < Utc::now() {
                return false;
            }
        }

        true
    }

    /// Checks if the API key has the specified permission
    pub fn has_permission(&self, permission: &ApiPermission) -> bool {
        self.permissions.iter().any(|p| p.implies(permission))
    }
}

/// Repository for API key CRUD operations
#[derive(Debug, Clone)]
pub struct ApiKeyRepository {
    db: DatabaseManager,
}

/// Parameters for updating an existing API key record.
#[derive(Debug, Default)]
pub struct ApiKeyUpdate {
    /// Optional new name for the API key.
    pub name: Option<String>,
    /// Optional new set of permissions.
    pub permissions: Option<HashSet<ApiPermission>>,
    /// Optional new rate limit.
    pub rate_limit: Option<Option<u32>>,
    /// Optional updated active flag.
    pub is_active: Option<bool>,
    /// Optional updated description.
    pub description: Option<Option<String>>,
    /// Optional updated expiration timestamp.
    pub expires_at: Option<Option<DateTime<Utc>>>,
}

impl ApiKeyRepository {
    /// Creates a new API key repository
    pub fn new(db: DatabaseManager) -> Self {
        Self { db }
    }

    /// Retrieves an API key by its key string
    ///
    /// # Arguments
    ///
    /// * `key` - The API key string
    ///
    /// # Returns
    ///
    /// Returns the API key record if found
    ///
    /// # Errors
    ///
    /// Returns an error if the database query fails
    pub async fn get_by_key(&self, key: &str) -> sqlx::Result<Option<ApiKeyRecord>> {
        let row = sqlx::query(
            r#"
            SELECT id, user_id, address, api_key, name, permissions, rate_limit, created_at, updated_at,
                   is_active, expires_at, description
            FROM api_keys
            WHERE api_key = $1
            "#,
        )
        .bind(key)
        .fetch_optional(self.db.pool())
        .await?;

        Ok(row.map(Self::row_to_record))
    }

    /// Retrieves all active API keys
    ///
    /// # Returns
    ///
    /// Returns a vector of all active API key records
    ///
    /// # Errors
    ///
    /// Returns an error if the database query fails
    pub async fn get_all_active(&self) -> sqlx::Result<Vec<ApiKeyRecord>> {
        let rows = sqlx::query(
            r#"
            SELECT id, user_id, address, api_key, name, permissions, rate_limit, created_at, updated_at,
                   is_active, expires_at, description
            FROM api_keys
            WHERE is_active = true
            ORDER BY created_at DESC
            "#,
        )
        .fetch_all(self.db.pool())
        .await?;

        Ok(rows.into_iter().map(Self::row_to_record).collect())
    }

    /// Creates a new API key
    ///
    /// # Arguments
    ///
    /// * `key` - The API key string
    /// * `name` - Human-readable name for the key
    /// * `permissions` - Set of permissions to grant
    /// * `rate_limit` - Optional rate limit (requests per minute)
    /// * `description` - Optional description
    /// * `expires_at` - Optional expiration timestamp
    /// * `user_id` - UUID of the user who owns this API key
    ///
    /// # Returns
    ///
    /// Returns the created API key record
    ///
    /// # Errors
    ///
    /// Returns an error if the database insert fails
    #[allow(clippy::too_many_arguments)]
    pub async fn create(
        &self,
        user_id: Uuid,
        api_key: String,
        name: String,
        permissions: HashSet<ApiPermission>,
        rate_limit: Option<u32>,
        description: Option<String>,
        expires_at: Option<DateTime<Utc>>,
    ) -> sqlx::Result<ApiKeyRecord> {
        let permissions_json = Self::permissions_to_json(&permissions);

        let row = sqlx::query(
            r#"
            INSERT INTO api_keys (user_id, address, api_key, name, permissions, rate_limit, description, expires_at)
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
            RETURNING id, user_id, address, api_key, name, permissions, rate_limit, created_at, updated_at,
                      is_active, expires_at, description
            "#,
        )
        // Default to zero-address; callers should set the actual wallet address elsewhere.
        .bind(user_id)
        .bind(Address::ZERO.as_slice())
        .bind(&api_key)
        .bind(&name)
        .bind(permissions_json)
        .bind(rate_limit.map(|r| r as i32))
        .bind(description)
        .bind(expires_at)
        .fetch_one(self.db.pool())
        .await?;

        info!("Created new API key: {} ({})", name, api_key);
        Ok(Self::row_to_record(row))
    }

    /// Updates an existing API key
    ///
    /// # Arguments
    ///
    /// * `key` - The API key string
    /// * `name` - Optional new name
    /// * `permissions` - Optional new permissions set
    /// * `rate_limit` - Optional new rate limit
    /// * `is_active` - Optional new active status
    /// * `description` - Optional new description
    /// * `expires_at` - Optional new expiration timestamp
    ///
    /// # Returns
    ///
    /// Returns the updated API key record
    ///
    /// # Errors
    ///
    /// Returns an error if the database update fails or key not found
    pub async fn update(&self, key: &str, update: ApiKeyUpdate) -> sqlx::Result<ApiKeyRecord> {
        // Build dynamic update query
        let mut query = String::from("UPDATE api_keys SET updated_at = NOW()");
        let mut param_count = 1;

        if update.name.is_some() {
            param_count += 1;
            query.push_str(&format!(", name = ${}", param_count));
        }
        if update.permissions.is_some() {
            param_count += 1;
            query.push_str(&format!(", permissions = ${}", param_count));
        }
        if update.rate_limit.is_some() {
            param_count += 1;
            query.push_str(&format!(", rate_limit = ${}", param_count));
        }
        if update.is_active.is_some() {
            param_count += 1;
            query.push_str(&format!(", is_active = ${}", param_count));
        }
        if update.description.is_some() {
            param_count += 1;
            query.push_str(&format!(", description = ${}", param_count));
        }
        if update.expires_at.is_some() {
            param_count += 1;
            query.push_str(&format!(", expires_at = ${}", param_count));
        }

        query.push_str(
            " WHERE api_key = $1 RETURNING id, user_id, address, api_key, name, permissions, rate_limit, created_at, updated_at, is_active, expires_at, description",
        );

        let mut q = sqlx::query(&query).bind(key);

        if let Some(n) = update.name {
            q = q.bind(n);
        }
        if let Some(p) = update.permissions {
            q = q.bind(Self::permissions_to_json(&p));
        }
        if let Some(r) = update.rate_limit {
            q = q.bind(r.map(|v| v as i32));
        }
        if let Some(a) = update.is_active {
            q = q.bind(a);
        }
        if let Some(d) = update.description {
            q = q.bind(d);
        }
        if let Some(e) = update.expires_at {
            q = q.bind(e);
        }

        let row = q.fetch_one(self.db.pool()).await?;

        info!("Updated API key: {}", key);
        Ok(Self::row_to_record(row))
    }

    /// Deletes an API key
    ///
    /// # Arguments
    ///
    /// * `key` - The API key string to delete
    ///
    /// # Returns
    ///
    /// Returns true if the key was deleted, false if not found
    ///
    /// # Errors
    ///
    /// Returns an error if the database delete fails
    pub async fn delete(&self, key: &str) -> sqlx::Result<bool> {
        let result = sqlx::query("DELETE FROM api_keys WHERE api_key = $1")
            .bind(key)
            .execute(self.db.pool())
            .await?;

        let deleted = result.rows_affected() > 0;
        if deleted {
            info!("Deleted API key: {}", key);
        }
        Ok(deleted)
    }

    /// Deactivates an API key (soft delete)
    ///
    /// # Arguments
    ///
    /// * `key` - The API key string to deactivate
    ///
    /// # Returns
    ///
    /// Returns true if the key was deactivated
    ///
    /// # Errors
    ///
    /// Returns an error if the database update fails
    pub async fn deactivate(&self, key: &str) -> sqlx::Result<bool> {
        let result = sqlx::query("UPDATE api_keys SET is_active = false, updated_at = NOW() WHERE api_key = $1")
            .bind(key)
            .execute(self.db.pool())
            .await?;

        let deactivated = result.rows_affected() > 0;
        if deactivated {
            info!("Deactivated API key: {}", key);
        }
        Ok(deactivated)
    }

    /// Converts a database row to ApiKeyRecord
    fn row_to_record(row: PgRow) -> ApiKeyRecord {
        let permissions_json: JsonValue = row.get("permissions");
        let permissions = Self::json_to_permissions(&permissions_json);

        let address_bytes: Vec<u8> = row.get("address");
        let address = Address::from_slice(&address_bytes);

        ApiKeyRecord {
            id: row.get("id"),
            user_id: row.get("user_id"),
            address,
            api_key: row.get("api_key"),
            name: row.get("name"),
            permissions,
            rate_limit: row.get::<Option<i32>, _>("rate_limit").map(|r| r as u32),
            created_at: row.get("created_at"),
            updated_at: row.get("updated_at"),
            is_active: row.get("is_active"),
            expires_at: row.get("expires_at"),
            description: row.get("description"),
        }
    }

    /// Converts permissions HashSet to JSON array
    fn permissions_to_json(permissions: &HashSet<ApiPermission>) -> JsonValue {
        let perms: Vec<String> = permissions
            .iter()
            .filter_map(|p| serde_json::to_value(p).ok())
            .filter_map(|v| v.as_str().map(String::from))
            .collect();
        JsonValue::Array(perms.into_iter().map(JsonValue::String).collect())
    }

    /// Converts JSON array to permissions HashSet
    fn json_to_permissions(json: &JsonValue) -> HashSet<ApiPermission> {
        match json {
            JsonValue::Array(arr) => arr
                .iter()
                .filter_map(|v| v.as_str())
                .filter_map(|s| serde_json::from_value(JsonValue::String(s.to_string())).ok())
                .collect(),
            _ => {
                error!("Invalid permissions JSON format: {:?}", json);
                HashSet::new()
            }
        }
    }
}