use axum::extract::FromRequestParts;
use axum::http::HeaderMap;
use axum::http::header::AUTHORIZATION;
use axum::http::request::Parts;
use codoseo_store::accounts::Account;
use uuid::Uuid;
use super::error::AgentError;
use super::keys;
use crate::metrics::{self, Surface, Tier};
use crate::state::AppState;
#[derive(Debug, Clone)]
pub struct ApiCaller {
pub account: Account,
pub key_id: Uuid,
}
const NO_KEY: &str = "Missing API key. Send it as the header \"Authorization: Bearer <key>\".";
const BAD_KEY: &str = "That API key is not valid, or it was revoked.";
pub fn bearer_key(headers: &HeaderMap) -> Result<Option<&str>, AgentError> {
let Some(value) = headers.get(AUTHORIZATION) else {
return Ok(None);
};
let bad = || AgentError::Unauthorized(BAD_KEY.to_owned());
let value = value.to_str().map_err(|_| bad())?;
let (scheme, key) = value.split_once(' ').ok_or_else(bad)?;
if !scheme.eq_ignore_ascii_case("bearer") {
return Err(AgentError::Unauthorized(
"Use the header \"Authorization: Bearer <key>\".".to_owned(),
));
}
let key = key.trim();
if keys::is_well_formed(key) {
Ok(Some(key))
} else {
Err(bad())
}
}
pub async fn resolve_key(state: &AppState, key: &str) -> Result<ApiCaller, AgentError> {
match codoseo_store::api_keys::authenticate(&state.pool, &keys::hash_key(key)).await? {
Some((key_id, account)) => Ok(ApiCaller { account, key_id }),
None => Err(AgentError::Unauthorized(BAD_KEY.to_owned())),
}
}
pub async fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<ApiCaller, AgentError> {
match bearer_key(headers)? {
Some(key) => resolve_key(state, key).await,
None => Err(AgentError::Unauthorized(NO_KEY.to_owned())),
}
}
impl FromRequestParts<AppState> for ApiCaller {
type Rejection = AgentError;
async fn from_request_parts(
parts: &mut Parts,
state: &AppState,
) -> Result<ApiCaller, AgentError> {
let caller = authenticate(state, &parts.headers).await;
if let Err(e) = &caller {
metrics::api_request(Surface::Rest, Tier::Key, e.code());
}
caller
}
}