mod database;
mod keys;
mod listing;
mod secondary;
use std::sync::Arc;
use rustauth_core::context::AuthContext;
use rustauth_core::crypto::random::generate_random_string;
use rustauth_core::db::{DbAdapter, DbValue, Update, Where};
use rustauth_core::error::RustAuthError;
use rustauth_core::options::SecondaryStorage;
use time::OffsetDateTime;
use super::models::ApiKeyRecord;
use super::options::{ApiKeyConfiguration, ApiKeyStorageMode};
pub(super) use listing::ListOptions;
const DEFAULT_ID_LENGTH: usize = 32;
pub struct ApiKeyStore<'a> {
pub(super) context: &'a AuthContext,
pub(super) adapter: Option<Arc<dyn DbAdapter>>,
pub(super) options: &'a ApiKeyConfiguration,
}
impl<'a> ApiKeyStore<'a> {
pub fn new(context: &'a AuthContext, options: &'a ApiKeyConfiguration) -> Self {
Self {
context,
adapter: context.adapter(),
options,
}
}
pub async fn create(&self, mut api_key: ApiKeyRecord) -> Result<ApiKeyRecord, RustAuthError> {
match self.options.storage {
ApiKeyStorageMode::Database => self.create_database(api_key).await,
ApiKeyStorageMode::SecondaryStorage if self.options.fallback_to_database => {
let created = self.create_database(api_key).await?;
self.set_secondary(&created).await?;
Ok(created)
}
ApiKeyStorageMode::SecondaryStorage => {
api_key.id = generate_random_string(DEFAULT_ID_LENGTH);
self.set_secondary(&api_key).await?;
Ok(api_key)
}
}
}
pub async fn get_by_hash(
&self,
hashed_key: &str,
) -> Result<Option<ApiKeyRecord>, RustAuthError> {
match self.options.storage {
ApiKeyStorageMode::Database => self.get_database("key", hashed_key).await,
ApiKeyStorageMode::SecondaryStorage if self.options.fallback_to_database => {
if let Some(storage) = self.secondary_storage() {
if let Some(api_key) =
secondary::get_secondary(&*storage, &keys::storage_key_by_hash(hashed_key))
.await?
{
if self.options.revalidate_secondary_against_database {
return self.revalidate_cache_hit(api_key, "key", hashed_key).await;
}
return Ok(Some(api_key));
}
}
let api_key = self.get_database("key", hashed_key).await?;
if let Some(api_key) = &api_key {
self.set_secondary(api_key).await?;
}
Ok(api_key)
}
ApiKeyStorageMode::SecondaryStorage => {
let Some(storage) = self.secondary_storage() else {
return Ok(None);
};
secondary::get_secondary(&*storage, &keys::storage_key_by_hash(hashed_key)).await
}
}
}
pub async fn get_by_id(&self, id: &str) -> Result<Option<ApiKeyRecord>, RustAuthError> {
match self.options.storage {
ApiKeyStorageMode::Database => self.get_database("id", id).await,
ApiKeyStorageMode::SecondaryStorage if self.options.fallback_to_database => {
if let Some(storage) = self.secondary_storage() {
if let Some(api_key) =
secondary::get_secondary(&*storage, &keys::storage_key_by_id(id)).await?
{
if self.options.revalidate_secondary_against_database {
return self.revalidate_cache_hit(api_key, "id", id).await;
}
return Ok(Some(api_key));
}
}
let api_key = self.get_database("id", id).await?;
if let Some(api_key) = &api_key {
self.set_secondary(api_key).await?;
}
Ok(api_key)
}
ApiKeyStorageMode::SecondaryStorage => {
let Some(storage) = self.secondary_storage() else {
return Ok(None);
};
secondary::get_secondary(&*storage, &keys::storage_key_by_id(id)).await
}
}
}
pub async fn update(
&self,
api_key: &ApiKeyRecord,
) -> Result<Option<ApiKeyRecord>, RustAuthError> {
match self.options.storage {
ApiKeyStorageMode::Database => self.update_database(api_key).await,
ApiKeyStorageMode::SecondaryStorage if self.options.fallback_to_database => {
let updated = self.update_database(api_key).await?;
if let Some(updated) = &updated {
self.set_secondary(updated).await?;
}
Ok(updated)
}
ApiKeyStorageMode::SecondaryStorage => {
self.set_secondary(api_key).await?;
Ok(Some(api_key.clone()))
}
}
}
pub async fn update_if_unchanged(
&self,
api_key: &ApiKeyRecord,
expected_updated_at: OffsetDateTime,
) -> Result<Option<ApiKeyRecord>, RustAuthError> {
match self.options.storage {
ApiKeyStorageMode::Database => {
self.update_database_if_unchanged(api_key, expected_updated_at)
.await
}
ApiKeyStorageMode::SecondaryStorage if self.options.fallback_to_database => {
let updated = self
.update_database_if_unchanged(api_key, expected_updated_at)
.await?;
if let Some(updated) = &updated {
self.set_secondary(updated).await?;
}
Ok(updated)
}
ApiKeyStorageMode::SecondaryStorage => {
self.set_secondary(api_key).await?;
Ok(Some(api_key.clone()))
}
}
}
pub async fn delete(&self, api_key: &ApiKeyRecord) -> Result<(), RustAuthError> {
match self.options.storage {
ApiKeyStorageMode::Database => self.delete_database(&api_key.id).await,
ApiKeyStorageMode::SecondaryStorage if self.options.fallback_to_database => {
self.delete_secondary(api_key).await?;
self.delete_database(&api_key.id).await
}
ApiKeyStorageMode::SecondaryStorage => self.delete_secondary(api_key).await,
}
}
pub async fn migrate_metadata_if_needed(&self, api_key: &mut ApiKeyRecord) {
if !api_key.needs_metadata_migration()
|| matches!(self.options.storage, ApiKeyStorageMode::SecondaryStorage)
&& !self.options.fallback_to_database
{
return;
}
let Some(metadata) = api_key.normalized_metadata() else {
return;
};
let Some(adapter) = &self.adapter else {
api_key.metadata = Some(metadata);
return;
};
let update = Update::new(super::API_KEY_MODEL)
.where_clause(Where::new("id", DbValue::String(api_key.id.clone())))
.data("metadata", DbValue::Json(metadata.clone()));
if adapter.update(update).await.is_ok() {
api_key.metadata = Some(metadata);
}
}
async fn revalidate_cache_hit(
&self,
cached: ApiKeyRecord,
field: &str,
value: &str,
) -> Result<Option<ApiKeyRecord>, RustAuthError> {
match self.get_database(field, value).await? {
None => {
self.delete_secondary(&cached).await?;
Ok(None)
}
Some(fresh) => {
if fresh.updated_at > cached.updated_at {
self.set_secondary(&fresh).await?;
Ok(Some(fresh))
} else {
Ok(Some(cached))
}
}
}
}
pub(super) fn secondary_storage(&self) -> Option<Arc<dyn SecondaryStorage>> {
self.options
.custom_storage
.clone()
.or_else(|| self.context.secondary_storage())
}
pub(super) fn required_adapter(&self) -> Result<Arc<dyn DbAdapter>, RustAuthError> {
self.context.require_adapter()
}
}