use chrono::{DateTime, Utc};
use sea_orm::sea_query::OnConflict;
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, DbErr, EntityTrait, QueryFilter, Set,
};
use bitrouter_sdk::{BitrouterError, Result};
use crate::auth::entities::{api_keys, users};
#[derive(Debug, Clone)]
pub struct ApiKeyRecord {
pub id: String,
pub user_id: String,
pub spend_limit_micro_usd: Option<i64>,
pub rpm_limit: Option<i64>,
pub policy_id: Option<String>,
pub expires_at: Option<DateTime<Utc>>,
pub active: bool,
}
pub async fn find_key_by_hash(
db: &DatabaseConnection,
key_hash: &str,
) -> Result<Option<ApiKeyRecord>> {
let row = api_keys::Entity::find()
.filter(api_keys::Column::KeyHash.eq(key_hash))
.one(db)
.await
.map_err(|e| BitrouterError::internal(format!("api_keys lookup: {e}")))?;
let Some(row) = row else {
return Ok(None);
};
let expires_at = row.expires_at.as_deref().and_then(|s| {
DateTime::parse_from_rfc3339(s)
.ok()
.map(|d| d.with_timezone(&Utc))
});
Ok(Some(ApiKeyRecord {
id: row.id,
user_id: row.user_id,
spend_limit_micro_usd: row.spend_limit_micro_usd,
rpm_limit: row.rpm_limit,
policy_id: row.policy_id,
expires_at,
active: row.active != 0,
}))
}
const RESERVED_USER_IDS: &[&str] = &["local", "anonymous"];
pub fn is_reserved_user_id(user_id: &str) -> bool {
RESERVED_USER_IDS.contains(&user_id)
}
pub async fn upsert_user(db: &DatabaseConnection, user_id: &str) -> Result<()> {
if is_reserved_user_id(user_id) {
return Err(BitrouterError::bad_request(format!(
"user id '{user_id}' is reserved for synthesised callers"
)));
}
let row = users::ActiveModel {
id: Set(user_id.to_string()),
created_at: Set(Utc::now().to_rfc3339()),
};
match users::Entity::insert(row)
.on_conflict(
OnConflict::column(users::Column::Id)
.do_nothing()
.to_owned(),
)
.exec(db)
.await
{
Ok(_) | Err(DbErr::RecordNotInserted) => Ok(()),
Err(e) => Err(BitrouterError::internal(format!("upsert user: {e}"))),
}
}
#[derive(Debug, Clone)]
pub struct NewApiKey {
pub id: String,
pub key_hash: String,
pub user_id: String,
pub spend_limit_micro_usd: Option<i64>,
pub rpm_limit: Option<i64>,
pub policy_id: Option<String>,
}
pub async fn insert_api_key(db: &DatabaseConnection, key: &NewApiKey) -> Result<()> {
let row = api_keys::ActiveModel {
id: Set(key.id.clone()),
key_hash: Set(key.key_hash.clone()),
user_id: Set(key.user_id.clone()),
spend_limit_micro_usd: Set(key.spend_limit_micro_usd),
rpm_limit: Set(key.rpm_limit),
policy_id: Set(key.policy_id.clone()),
expires_at: Set(None),
active: Set(1),
created_at: Set(Utc::now().to_rfc3339()),
};
row.insert(db)
.await
.map_err(|e| BitrouterError::internal(format!("insert api key: {e}")))?;
Ok(())
}