use chrono::Utc;
use diesel::prelude::*;
use uuid::Uuid;
use super::ApiKeyInfo;
use crate::dal::unified::DAL;
use crate::database::schema::postgres::api_keys;
use crate::error::ValidationError;
#[derive(Queryable, Debug)]
#[diesel(table_name = api_keys)]
struct ApiKeyRow {
pub id: Uuid,
#[allow(dead_code)]
pub key_hash: String,
pub name: String,
pub permissions: String,
pub created_at: chrono::NaiveDateTime,
pub revoked_at: Option<chrono::NaiveDateTime>,
pub tenant_id: Option<String>,
pub is_admin: bool,
}
#[derive(Insertable)]
#[diesel(table_name = api_keys)]
struct NewApiKey {
pub id: Uuid,
pub key_hash: String,
pub name: String,
pub permissions: String,
pub tenant_id: Option<String>,
pub is_admin: bool,
}
fn to_info(row: ApiKeyRow) -> ApiKeyInfo {
ApiKeyInfo {
id: row.id,
name: row.name,
permissions: row.permissions,
created_at: row.created_at.and_utc(),
revoked: row.revoked_at.is_some(),
tenant_id: row.tenant_id,
is_admin: row.is_admin,
}
}
pub async fn create_key(
dal: &DAL,
key_hash: &str,
name: &str,
tenant_id: Option<&str>,
is_admin: bool,
role: &str,
) -> Result<ApiKeyInfo, ValidationError> {
let conn = dal
.database
.get_postgres_connection()
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))?;
let id = Uuid::new_v4();
let new_key = NewApiKey {
id,
key_hash: key_hash.to_string(),
name: name.to_string(),
permissions: role.to_string(),
tenant_id: tenant_id.map(|s| s.to_string()),
is_admin,
};
let row: ApiKeyRow = conn
.interact(move |conn| {
diesel::insert_into(api_keys::table)
.values(&new_key)
.get_result(conn)
})
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))??;
Ok(to_info(row))
}
pub async fn validate_hash(
dal: &DAL,
key_hash: &str,
) -> Result<Option<ApiKeyInfo>, ValidationError> {
let conn = dal
.database
.get_postgres_connection()
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))?;
let hash = key_hash.to_string();
let result: Option<ApiKeyRow> = conn
.interact(move |conn| {
api_keys::table
.filter(api_keys::key_hash.eq(&hash))
.filter(api_keys::revoked_at.is_null())
.first(conn)
.optional()
})
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))??;
Ok(result.map(to_info))
}
pub async fn has_any_keys(dal: &DAL) -> Result<bool, ValidationError> {
let conn = dal
.database
.get_postgres_connection()
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))?;
let result: Option<ApiKeyRow> = conn
.interact(move |conn| {
api_keys::table
.filter(api_keys::revoked_at.is_null())
.first(conn)
.optional()
})
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))??;
Ok(result.is_some())
}
pub async fn list_keys(dal: &DAL) -> Result<Vec<ApiKeyInfo>, ValidationError> {
let conn = dal
.database
.get_postgres_connection()
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))?;
let results: Vec<ApiKeyRow> = conn
.interact(move |conn| {
api_keys::table
.order(api_keys::created_at.desc())
.load(conn)
})
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))??;
Ok(results.into_iter().map(to_info).collect())
}
pub async fn revoke_key(dal: &DAL, id: Uuid) -> Result<bool, ValidationError> {
let conn = dal
.database
.get_postgres_connection()
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))?;
let now = Utc::now().naive_utc();
let rows: usize = conn
.interact(move |conn| {
diesel::update(
api_keys::table
.find(id)
.filter(api_keys::revoked_at.is_null()),
)
.set(api_keys::revoked_at.eq(Some(now)))
.execute(conn)
})
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))??;
Ok(rows > 0)
}