use super::access::{
authenticate_request, check_auth_result_ownership, check_ownership, filter_and_paginate_keys,
invalidate_api_key_auth_cache, is_auth_enabled, resolve_create_key_scope,
validate_create_key_rate_limits, validate_update_key_permissions,
validate_update_key_rate_limits,
};
use super::types::{
CreateKeyRequest, CreateKeyResponse, KeyErrorResponse, KeyResponse, KeyUsageResponse,
ListKeysQuery, ListKeysResponse, PaginationInfo, RevokeKeyResponse, RotateKeyResponse,
UpdateKeyRequest, VerifyKeyRequest, VerifyKeyResponse,
};
use crate::core::keys::KeyManager;
use crate::core::keys::{CreateKeyConfig, KeyStatus, UpdateKeyConfig};
use crate::core::models::user::types::UserRole;
use crate::server::routes::ApiResponse;
use crate::server::state::AppState;
use actix_web::{HttpRequest, HttpResponse, Result as ActixResult, web};
use tracing::{error, info, warn};
use uuid::Uuid;
pub async fn create_key(
req: HttpRequest,
state: web::Data<AppState>,
request: web::Json<CreateKeyRequest>,
) -> ActixResult<HttpResponse> {
info!("Creating new API key: {}", request.name);
let (resolved_user_id, resolved_team_id) = if is_auth_enabled(&state) {
let auth = match authenticate_request(&req, &state).await {
Err(resp) => return Ok(resp),
Ok(None) => {
let error_response =
KeyErrorResponse::unauthorized("Authentication required to create a key");
return Ok(HttpResponse::Unauthorized()
.json(ApiResponse::<()>::error(error_response.error)));
}
Ok(Some(auth)) => auth,
};
match resolve_create_key_scope(&auth, &request) {
Ok(scope) => scope,
Err(message) => {
let error_response = KeyErrorResponse::forbidden(message);
return Ok(
HttpResponse::Forbidden().json(ApiResponse::<()>::error(error_response.error))
);
}
}
} else {
(request.user_id, request.team_id)
};
let key_manager = get_key_manager(&state);
if let Err(message) = super::api_key_budget::validate_create_key_budget_fields(
state.get_ref(),
request.budget_id,
request.max_budget,
)
.await
{
let error_response = KeyErrorResponse::validation(message);
return Ok(HttpResponse::BadRequest().json(ApiResponse::<()>::error(error_response.error)));
}
if let Err(message) = validate_create_key_rate_limits(&request) {
let error_response = KeyErrorResponse::validation(message);
return Ok(HttpResponse::BadRequest().json(ApiResponse::<()>::error(error_response.error)));
}
let config = CreateKeyConfig {
name: request.name.clone(),
description: request.description.clone(),
user_id: resolved_user_id,
team_id: resolved_team_id,
budget_id: request.budget_id,
permissions: request.permissions.clone().unwrap_or_default(),
rate_limits: request.rate_limits.clone().unwrap_or_default(),
expires_at: request.expires_at,
metadata: request.metadata.clone().unwrap_or(serde_json::Value::Null),
};
match key_manager.generate_key(config).await {
Ok((key_id, raw_key)) => {
let key_info = key_manager
.get_key(key_id)
.await
.map_err(|e| {
error!("Failed to get key info: {}", e);
actix_web::error::ErrorInternalServerError("Failed to get key info")
})?
.ok_or_else(|| {
actix_web::error::ErrorInternalServerError("Key not found after creation")
})?;
let response = CreateKeyResponse {
id: key_id,
key: raw_key,
info: key_info,
warning: "Store this key securely. It will not be shown again.".to_string(),
};
info!("API key created successfully: {}", key_id);
Ok(HttpResponse::Created().json(ApiResponse::success(response)))
}
Err(e) => {
warn!("Failed to create API key: {}", e);
let error_response = KeyErrorResponse::validation(e.to_string());
Ok(HttpResponse::BadRequest().json(ApiResponse::<()>::error(error_response.error)))
}
}
}
pub async fn list_keys(
req: HttpRequest,
state: web::Data<AppState>,
query: web::Query<ListKeysQuery>,
) -> ActixResult<HttpResponse> {
info!("Listing API keys");
let key_manager = get_key_manager(&state);
if is_auth_enabled(&state) {
let auth = match authenticate_request(&req, &state).await {
Err(resp) => return Ok(resp),
Ok(None) => {
let error_response =
KeyErrorResponse::unauthorized("Authentication required to list keys");
return Ok(HttpResponse::Unauthorized()
.json(ApiResponse::<()>::error(error_response.error)));
}
Ok(Some(a)) => a,
};
if let Some(requested_user_id) = query.user_id {
let allowed = auth
.user
.as_ref()
.map(|u| check_ownership(u, Some(requested_user_id), None))
.unwrap_or(false);
if !allowed {
warn!(
"Caller attempted to list keys for user {} without permission",
requested_user_id
);
let error_response =
KeyErrorResponse::forbidden("Not authorized to list keys for this user");
return Ok(
HttpResponse::Forbidden().json(ApiResponse::<()>::error(error_response.error))
);
}
} else if let Some(team_id) = query.team_id {
let allowed = match &auth.user {
Some(user) => {
user.has_role(&UserRole::Admin)
|| (user.has_role(&UserRole::Manager) && user.team_ids.contains(&team_id))
}
None => auth.context.team_id() == Some(team_id),
};
if !allowed {
warn!(
"Caller attempted to list keys for team {} without permission",
team_id
);
let error_response =
KeyErrorResponse::forbidden("Not authorized to list keys for this team");
return Ok(
HttpResponse::Forbidden().json(ApiResponse::<()>::error(error_response.error))
);
}
} else {
let is_admin = auth
.user
.as_ref()
.map(|u| u.has_role(&UserRole::Admin))
.unwrap_or(false);
if !is_admin {
warn!("Non-admin caller attempted to list all keys");
let error_response =
KeyErrorResponse::forbidden("Admin privileges required to list all keys");
return Ok(
HttpResponse::Forbidden().json(ApiResponse::<()>::error(error_response.error))
);
}
}
}
let offset = ((query.page.saturating_sub(1)) * query.limit) as usize;
let limit = query.limit as usize;
let keys_with_total = if let Some(user_id) = query.user_id {
key_manager
.list_user_keys(user_id)
.await
.map(|keys| filter_and_paginate_keys(keys, query.status, limit, offset))
} else if let Some(team_id) = query.team_id {
key_manager
.list_team_keys(team_id)
.await
.map(|keys| filter_and_paginate_keys(keys, query.status, limit, offset))
} else {
match key_manager
.list_keys(query.status, Some(limit), Some(offset))
.await
{
Ok(keys) => key_manager
.count_keys(query.status)
.await
.map(|total| (keys, total)),
Err(e) => Err(e),
}
};
match keys_with_total {
Ok((keys, total)) => {
let response = ListKeysResponse {
keys,
pagination: PaginationInfo::new(total, query.page, query.limit),
};
Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
}
Err(e) => {
error!("Failed to list API keys: {}", e);
let error_response = KeyErrorResponse::internal("Failed to list keys");
Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error_response.error)))
}
}
}
pub async fn get_key(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<Uuid>,
) -> ActixResult<HttpResponse> {
let key_id = path.into_inner();
info!("Getting API key: {}", key_id);
let key_manager = get_key_manager(&state);
let auth_opt = if is_auth_enabled(&state) {
match authenticate_request(&req, &state).await {
Err(resp) => return Ok(resp),
Ok(None) => {
let error_response =
KeyErrorResponse::unauthorized("Authentication required to access a key");
return Ok(HttpResponse::Unauthorized()
.json(ApiResponse::<()>::error(error_response.error)));
}
Ok(auth) => auth,
}
} else {
None
};
match key_manager.get_key(key_id).await {
Ok(Some(key)) => {
if let Some(ref auth) = auth_opt
&& !check_auth_result_ownership(auth, key.user_id, key.team_id)
{
warn!(
"Caller attempted to access key {} owned by user={:?} team={:?}",
key_id, key.user_id, key.team_id
);
let error_response =
KeyErrorResponse::forbidden("Not authorized to access this key");
return Ok(
HttpResponse::Forbidden().json(ApiResponse::<()>::error(error_response.error))
);
}
let response = KeyResponse { key };
Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
}
Ok(None) => {
warn!("API key not found: {}", key_id);
let error_response = KeyErrorResponse::not_found("API key");
Ok(HttpResponse::NotFound().json(ApiResponse::<()>::error(error_response.error)))
}
Err(e) => {
error!("Failed to get API key: {}", e);
let error_response = KeyErrorResponse::internal("Failed to get key");
Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error_response.error)))
}
}
}
pub async fn update_key(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<Uuid>,
request: web::Json<UpdateKeyRequest>,
) -> ActixResult<HttpResponse> {
let key_id = path.into_inner();
info!("Updating API key: {}", key_id);
let key_manager = get_key_manager(&state);
let auth_opt = if is_auth_enabled(&state) {
match authenticate_request(&req, &state).await {
Err(resp) => return Ok(resp),
Ok(None) => {
let error_response =
KeyErrorResponse::unauthorized("Authentication required to update a key");
return Ok(HttpResponse::Unauthorized()
.json(ApiResponse::<()>::error(error_response.error)));
}
Ok(auth) => auth,
}
} else {
None
};
let existing_key = match key_manager.get_key(key_id).await {
Ok(Some(k)) => k,
Ok(None) => {
warn!("API key not found: {}", key_id);
let error_response = KeyErrorResponse::not_found("API key");
return Ok(
HttpResponse::NotFound().json(ApiResponse::<()>::error(error_response.error))
);
}
Err(e) => {
error!("Failed to get API key: {}", e);
let error_response = KeyErrorResponse::internal("Failed to get key");
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error_response.error)));
}
};
if let Some(ref auth) = auth_opt
&& !check_auth_result_ownership(auth, existing_key.user_id, existing_key.team_id)
{
warn!(
"Caller attempted to update key {} owned by user={:?} team={:?}",
key_id, existing_key.user_id, existing_key.team_id
);
let error_response = KeyErrorResponse::forbidden("Not authorized to access this key");
return Ok(HttpResponse::Forbidden().json(ApiResponse::<()>::error(error_response.error)));
}
if let Err(message) = validate_update_key_permissions(auth_opt.as_ref(), &request) {
warn!(
"Caller attempted to grant management permissions to key {} without admin privileges",
key_id
);
let error_response = KeyErrorResponse::forbidden(message);
return Ok(HttpResponse::Forbidden().json(ApiResponse::<()>::error(error_response.error)));
}
if let Err(message) =
super::api_key_budget::validate_update_key_budget_fields(state.get_ref(), &request).await
{
let error_response = KeyErrorResponse::validation(message);
return Ok(HttpResponse::BadRequest().json(ApiResponse::<()>::error(error_response.error)));
}
if let Err(message) = validate_update_key_rate_limits(&request) {
let error_response = KeyErrorResponse::validation(message);
return Ok(HttpResponse::BadRequest().json(ApiResponse::<()>::error(error_response.error)));
}
let config = UpdateKeyConfig {
name: request.name.clone(),
description: request.description.clone(),
permissions: request.permissions.clone(),
rate_limits: request.rate_limits.clone(),
budget_id: request.budget_id,
expires_at: request.expires_at,
metadata: request.metadata.clone(),
};
match key_manager.update_key(key_id, config).await {
Ok(key) => {
invalidate_api_key_auth_cache(&state, key_id).await;
info!("API key updated successfully: {}", key_id);
let response = KeyResponse { key };
Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
}
Err(e) => {
warn!("Failed to update API key: {}", e);
let error_str = e.to_string();
if error_str.contains("not found") {
let error_response = KeyErrorResponse::not_found("API key");
Ok(HttpResponse::NotFound().json(ApiResponse::<()>::error(error_response.error)))
} else if error_str.contains("revoked") {
let error_response = KeyErrorResponse::conflict("Cannot update a revoked key");
Ok(HttpResponse::Conflict().json(ApiResponse::<()>::error(error_response.error)))
} else {
let error_response = KeyErrorResponse::validation(error_str);
Ok(HttpResponse::BadRequest().json(ApiResponse::<()>::error(error_response.error)))
}
}
}
}
pub async fn revoke_key(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<Uuid>,
) -> ActixResult<HttpResponse> {
let key_id = path.into_inner();
info!("Revoking API key: {}", key_id);
let key_manager = get_key_manager(&state);
let auth_opt = if is_auth_enabled(&state) {
match authenticate_request(&req, &state).await {
Err(resp) => return Ok(resp),
Ok(None) => {
let error_response =
KeyErrorResponse::unauthorized("Authentication required to revoke a key");
return Ok(HttpResponse::Unauthorized()
.json(ApiResponse::<()>::error(error_response.error)));
}
Ok(auth) => auth,
}
} else {
None
};
let existing_key = match key_manager.get_key(key_id).await {
Ok(Some(k)) => k,
Ok(None) => {
warn!("API key not found: {}", key_id);
let error_response = KeyErrorResponse::not_found("API key");
return Ok(
HttpResponse::NotFound().json(ApiResponse::<()>::error(error_response.error))
);
}
Err(e) => {
error!("Failed to get API key: {}", e);
let error_response = KeyErrorResponse::internal("Failed to get key");
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error_response.error)));
}
};
if let Some(ref auth) = auth_opt
&& !check_auth_result_ownership(auth, existing_key.user_id, existing_key.team_id)
{
warn!(
"Caller attempted to revoke key {} owned by user={:?} team={:?}",
key_id, existing_key.user_id, existing_key.team_id
);
let error_response = KeyErrorResponse::forbidden("Not authorized to access this key");
return Ok(HttpResponse::Forbidden().json(ApiResponse::<()>::error(error_response.error)));
}
match key_manager.revoke_key(key_id).await {
Ok(()) => {
invalidate_api_key_auth_cache(&state, key_id).await;
info!("API key revoked successfully: {}", key_id);
let response = RevokeKeyResponse {
key_id,
status: KeyStatus::Revoked,
message: "API key has been revoked successfully".to_string(),
};
Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
}
Err(e) => {
warn!("Failed to revoke API key: {}", e);
let error_str = e.to_string();
if error_str.contains("not found") {
let error_response = KeyErrorResponse::not_found("API key");
Ok(HttpResponse::NotFound().json(ApiResponse::<()>::error(error_response.error)))
} else if error_str.contains("already revoked") {
let error_response = KeyErrorResponse::conflict("API key is already revoked");
Ok(HttpResponse::Conflict().json(ApiResponse::<()>::error(error_response.error)))
} else {
let error_response = KeyErrorResponse::internal("Failed to revoke key");
Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error_response.error)))
}
}
}
}
pub async fn rotate_key(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<Uuid>,
) -> ActixResult<HttpResponse> {
let key_id = path.into_inner();
info!("Rotating API key: {}", key_id);
let key_manager = get_key_manager(&state);
let auth_opt = if is_auth_enabled(&state) {
match authenticate_request(&req, &state).await {
Err(resp) => return Ok(resp),
Ok(None) => {
let error_response =
KeyErrorResponse::unauthorized("Authentication required to rotate a key");
return Ok(HttpResponse::Unauthorized()
.json(ApiResponse::<()>::error(error_response.error)));
}
Ok(auth) => auth,
}
} else {
None
};
let existing_key = match key_manager.get_key(key_id).await {
Ok(Some(k)) => k,
Ok(None) => {
warn!("API key not found: {}", key_id);
let error_response = KeyErrorResponse::not_found("API key");
return Ok(
HttpResponse::NotFound().json(ApiResponse::<()>::error(error_response.error))
);
}
Err(e) => {
error!("Failed to get API key: {}", e);
let error_response = KeyErrorResponse::internal("Failed to get key");
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error_response.error)));
}
};
if let Some(ref auth) = auth_opt
&& !check_auth_result_ownership(auth, existing_key.user_id, existing_key.team_id)
{
warn!(
"Caller attempted to rotate key {} owned by user={:?} team={:?}",
key_id, existing_key.user_id, existing_key.team_id
);
let error_response = KeyErrorResponse::forbidden("Not authorized to access this key");
return Ok(HttpResponse::Forbidden().json(ApiResponse::<()>::error(error_response.error)));
}
match key_manager.rotate_key(key_id).await {
Ok((new_key_id, new_raw_key)) => {
invalidate_api_key_auth_cache(&state, key_id).await;
let key_info = key_manager
.get_key(new_key_id)
.await
.map_err(|e| {
error!("Failed to get new key info: {}", e);
actix_web::error::ErrorInternalServerError("Failed to get new key info")
})?
.ok_or_else(|| {
actix_web::error::ErrorInternalServerError("New key not found after rotation")
})?;
info!("API key rotated successfully: {} -> {}", key_id, new_key_id);
let response = RotateKeyResponse {
old_key_id: key_id,
new_key_id,
new_key: new_raw_key,
info: key_info,
warning: "Store this new key securely. It will not be shown again. The old key has been revoked.".to_string(),
};
Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
}
Err(e) => {
warn!("Failed to rotate API key: {}", e);
let error_str = e.to_string();
if error_str.contains("not found") {
let error_response = KeyErrorResponse::not_found("API key");
Ok(HttpResponse::NotFound().json(ApiResponse::<()>::error(error_response.error)))
} else if error_str.contains("revoked") {
let error_response = KeyErrorResponse::conflict("Cannot rotate a revoked key");
Ok(HttpResponse::Conflict().json(ApiResponse::<()>::error(error_response.error)))
} else {
let error_response = KeyErrorResponse::internal("Failed to rotate key");
Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error_response.error)))
}
}
}
}
pub async fn get_key_usage(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<Uuid>,
) -> ActixResult<HttpResponse> {
let key_id = path.into_inner();
info!("Getting usage stats for API key: {}", key_id);
let key_manager = get_key_manager(&state);
let auth_opt = if is_auth_enabled(&state) {
match authenticate_request(&req, &state).await {
Err(resp) => return Ok(resp),
Ok(None) => {
let error_response =
KeyErrorResponse::unauthorized("Authentication required to access key usage");
return Ok(HttpResponse::Unauthorized()
.json(ApiResponse::<()>::error(error_response.error)));
}
Ok(auth) => auth,
}
} else {
None
};
let key = match key_manager.get_key(key_id).await {
Ok(Some(k)) => k,
Ok(None) => {
warn!("API key not found: {}", key_id);
let error_response = KeyErrorResponse::not_found("API key");
return Ok(
HttpResponse::NotFound().json(ApiResponse::<()>::error(error_response.error))
);
}
Err(e) => {
error!("Failed to get API key: {}", e);
let error_response = KeyErrorResponse::internal("Failed to get key");
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error_response.error)));
}
};
if let Some(ref auth) = auth_opt
&& !check_auth_result_ownership(auth, key.user_id, key.team_id)
{
warn!(
"Caller attempted to access usage for key {} owned by user={:?} team={:?}",
key_id, key.user_id, key.team_id
);
let error_response = KeyErrorResponse::forbidden("Not authorized to access this key");
return Ok(HttpResponse::Forbidden().json(ApiResponse::<()>::error(error_response.error)));
}
match key_manager.get_usage_stats(key_id).await {
Ok(usage) => {
let response = KeyUsageResponse { key_id, usage };
Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
}
Err(e) => {
warn!("Failed to get usage stats: {}", e);
let error_str = e.to_string();
if error_str.contains("not found") {
let error_response = KeyErrorResponse::not_found("API key");
Ok(HttpResponse::NotFound().json(ApiResponse::<()>::error(error_response.error)))
} else {
let error_response = KeyErrorResponse::internal("Failed to get usage stats");
Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error_response.error)))
}
}
}
}
pub async fn verify_key(
state: web::Data<AppState>,
request: web::Json<VerifyKeyRequest>,
) -> ActixResult<HttpResponse> {
info!("Verifying API key");
let key_manager = get_key_manager(&state);
match key_manager.validate_key(&request.key).await {
Ok(result) => {
let response = VerifyKeyResponse { result };
Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
}
Err(e) => {
error!("Failed to verify API key: {}", e);
let error_response = KeyErrorResponse::internal("Failed to verify key");
Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error(error_response.error)))
}
}
}
fn get_key_manager(state: &web::Data<AppState>) -> KeyManager {
state.key_manager.clone()
}
#[cfg(test)]
#[path = "handlers_tests.rs"]
mod handler_tests;