use std::collections::HashMap;
use better_auth_core::{AuthContext, AuthResult, CreateApiKey, UpdateApiKey};
use super::ApiKeyPlugin;
use super::types::*;
use crate::plugins::helpers;
pub(super) fn check_permissions(key_permissions_json: &str, required: &serde_json::Value) -> bool {
let required_map = match required.as_object() {
Some(m) => m,
None => return false,
};
let key_map: HashMap<String, Vec<String>> = match serde_json::from_str(key_permissions_json) {
Ok(v) => v,
Err(_) => return false,
};
for (resource, requested_actions) in required_map {
let allowed_actions = match key_map.get(resource) {
Some(a) => a,
None => return false,
};
if let Some(actions_array) = requested_actions.as_array() {
for action_val in actions_array {
let action = match action_val.as_str() {
Some(s) => s,
None => return false,
};
if !allowed_actions.iter().any(|a| a == action) {
return false;
}
}
} else if let Some(obj) = requested_actions.as_object() {
let actions = match obj.get("actions").and_then(|v| v.as_array()) {
Some(a) => a,
None => return false,
};
let connector = obj
.get("connector")
.and_then(|v| v.as_str())
.unwrap_or("AND");
if connector == "OR" {
let any_allowed = actions.iter().any(|action_val| {
action_val
.as_str()
.is_some_and(|action| allowed_actions.iter().any(|a| a == action))
});
if !any_allowed {
return false;
}
} else {
for action_val in actions {
let action = match action_val.as_str() {
Some(s) => s,
None => return false,
};
if !allowed_actions.iter().any(|a| a == action) {
return false;
}
}
}
} else {
return false;
}
}
true
}
pub(crate) async fn create_key_core(
body: &CreateKeyRequest,
user_id: impl AsRef<str>,
plugin: &ApiKeyPlugin,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<CreateKeyResponse> {
if body.refill_amount.is_some()
|| body.refill_interval.is_some()
|| body.rate_limit_max.is_some()
|| body.rate_limit_time_window.is_some()
|| body.rate_limit_enabled.is_some()
|| body.permissions.is_some()
|| body.remaining.is_some()
{
return Err(super::api_key_error(
super::ApiKeyErrorCode::ServerOnlyProperty,
));
}
let config = plugin.resolve_configuration(body.config_id.as_deref())?;
ApiKeyPlugin::validate_prefix(config, body.prefix.as_deref())?;
ApiKeyPlugin::validate_name(config, body.name.as_deref(), true)?;
ApiKeyPlugin::validate_metadata(config, &body.metadata)?;
ApiKeyPlugin::validate_refill(body.refill_interval, body.refill_amount)?;
let effective_expires_in = ApiKeyPlugin::validate_expires_in(config, body.expires_in)?;
let reference_id = match config.references {
super::ApiKeyReferences::User => user_id.as_ref().to_string(),
super::ApiKeyReferences::Organization => {
let organization_id = body.organization_id.as_deref().ok_or_else(|| {
super::api_key_error(super::ApiKeyErrorCode::OrganizationIdRequired)
})?;
helpers::require_org_api_key_permission(
ctx,
user_id.as_ref(),
organization_id,
"create",
)
.await?;
organization_id.to_string()
}
};
let (full_key, hash, start) = ApiKeyPlugin::generate_key(config, body.prefix.as_deref());
let expires_at = helpers::expires_in_to_at(effective_expires_in)?;
let remaining = body.remaining.or(config.default_remaining);
let store_start = if config.store_starting_characters {
Some(start)
} else {
None
};
let input = CreateApiKey {
reference_id,
config_id: config.config_id.clone(),
name: body.name.clone(),
prefix: body.prefix.clone().or_else(|| config.prefix.clone()),
key_hash: hash,
start: store_start,
expires_at,
remaining,
rate_limit_enabled: body.rate_limit_enabled.unwrap_or(config.rate_limit.enabled),
rate_limit_time_window: body
.rate_limit_time_window
.or(Some(config.rate_limit.time_window)),
rate_limit_max: body.rate_limit_max.or(Some(config.rate_limit.max_requests)),
refill_interval: body.refill_interval,
refill_amount: body.refill_amount,
permissions: body
.permissions
.as_ref()
.map(|v| serde_json::to_string(v).unwrap_or_default()),
metadata: body
.metadata
.as_ref()
.map(|v| serde_json::to_string(v).unwrap_or_default()),
enabled: true,
};
let api_key = ctx.database.create_api_key(input).await?;
plugin.maybe_delete_expired(ctx).await;
Ok(CreateKeyResponse {
key: full_key,
api_key: ApiKeyView::from(&api_key),
})
}
pub(crate) async fn get_key_core(
id: &str,
config_id: Option<&str>,
user_id: impl AsRef<str>,
plugin: &ApiKeyPlugin,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<ApiKeyView> {
let config = plugin.resolve_configuration(config_id)?;
let api_key = helpers::get_owned_api_key(ctx, config, id, user_id.as_ref(), "read").await?;
plugin.maybe_delete_expired(ctx).await;
Ok(ApiKeyView::from(&api_key))
}
pub(crate) async fn list_keys_core(
user_id: impl AsRef<str>,
query: &ListKeysQuery,
plugin: &ApiKeyPlugin,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<ListKeysResponse> {
let config = plugin.resolve_configuration(query.config_id.as_deref())?;
let reference_id = match config.references {
super::ApiKeyReferences::User => user_id.as_ref().to_string(),
super::ApiKeyReferences::Organization => {
let organization_id = query.organization_id.as_deref().ok_or_else(|| {
super::api_key_error(super::ApiKeyErrorCode::OrganizationIdRequired)
})?;
helpers::require_org_api_key_permission(ctx, user_id.as_ref(), organization_id, "read")
.await?;
organization_id.to_string()
}
};
let keys = ctx
.database
.list_api_keys_by_reference(&reference_id)
.await?;
let mut views: Vec<ApiKeyView> = keys
.iter()
.map(ApiKeyView::from)
.filter(|view| super::config_id_matches(&view.config_id, &config.config_id))
.collect();
if let Some(sort_by) = query.sort_by.as_deref() {
sort_views(&mut views, sort_by, query.sort_direction.as_deref());
}
let total = views.len();
if let Some(offset) = query.offset {
views = views.split_off(offset.min(views.len()));
}
if let Some(limit) = query.limit {
views.truncate(limit);
}
plugin.maybe_delete_expired(ctx).await;
Ok(ListKeysResponse {
api_keys: views,
total,
limit: query.limit,
offset: query.offset,
})
}
fn sort_views(views: &mut [ApiKeyView], sort_by: &str, direction: Option<&str>) {
match sort_by {
"createdAt" => views.sort_by(|a, b| a.created_at.cmp(&b.created_at)),
"updatedAt" => views.sort_by(|a, b| a.updated_at.cmp(&b.updated_at)),
"name" => views.sort_by(|a, b| a.name.cmp(&b.name)),
"expiresAt" => views.sort_by(|a, b| a.expires_at.cmp(&b.expires_at)),
_ => return,
}
if direction == Some("desc") {
views.reverse();
}
}
pub(crate) async fn update_key_core(
body: &UpdateKeyRequest,
user_id: impl AsRef<str>,
plugin: &ApiKeyPlugin,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<ApiKeyView> {
if body.refill_amount.is_some()
|| body.refill_interval.is_some()
|| body.rate_limit_max.is_some()
|| body.rate_limit_time_window.is_some()
|| body.rate_limit_enabled.is_some()
|| body.remaining.is_some()
|| body.permissions.is_some()
{
return Err(super::api_key_error(
super::ApiKeyErrorCode::ServerOnlyProperty,
));
}
if body.name.is_none()
&& body.enabled.is_none()
&& body.expires_in.is_none()
&& body.metadata.is_none()
{
return Err(super::api_key_error(
super::ApiKeyErrorCode::NoValuesToUpdate,
));
}
let config = plugin.resolve_configuration(body.config_id.as_deref())?;
ApiKeyPlugin::validate_name(config, body.name.as_deref(), false)?;
ApiKeyPlugin::validate_metadata(config, &body.metadata)?;
let _existing =
helpers::get_owned_api_key(ctx, config, &body.key_id, user_id.as_ref(), "update").await?;
let expires_at = match body.expires_in {
None => None,
Some(_) if config.key_expiration.disable_custom_expires_time => {
return Err(super::api_key_error(
super::ApiKeyErrorCode::KeyDisabledExpiration,
));
}
Some(None) => Some(None),
Some(Some(secs)) => {
let validated = ApiKeyPlugin::validate_expires_in(config, Some(secs))?;
helpers::expires_in_to_at(validated)?.map(Some)
}
};
let update = UpdateApiKey {
name: body.name.clone(),
enabled: body.enabled,
remaining: body.remaining,
rate_limit_enabled: body.rate_limit_enabled,
rate_limit_time_window: body.rate_limit_time_window,
rate_limit_max: body.rate_limit_max,
refill_interval: body.refill_interval,
refill_amount: body.refill_amount,
permissions: body
.permissions
.as_ref()
.map(|v| serde_json::to_string(v).unwrap_or_default()),
metadata: body
.metadata
.as_ref()
.map(|v| serde_json::to_string(v).unwrap_or_default()),
expires_at,
last_request: None,
request_count: None,
last_refill_at: None,
};
let updated = ctx.database.update_api_key(&body.key_id, update).await?;
plugin.maybe_delete_expired(ctx).await;
Ok(ApiKeyView::from(&updated))
}
pub(crate) async fn delete_key_core(
body: &DeleteKeyRequest,
user_id: impl AsRef<str>,
plugin: &ApiKeyPlugin,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<serde_json::Value> {
let config = plugin.resolve_configuration(body.config_id.as_deref())?;
let _existing =
helpers::get_owned_api_key(ctx, config, &body.key_id, user_id.as_ref(), "delete").await?;
ctx.database.delete_api_key(&body.key_id).await?;
Ok(serde_json::json!({ "success": true }))
}