systemprompt-api 0.14.3

Axum-based HTTP server and API gateway for systemprompt.io AI governance infrastructure. Exposes governed agents, MCP, A2A, and admin endpoints with rate limiting and RBAC.
Documentation
use axum::extract::{Extension, Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::{delete, post};
use axum::{Json, Router};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use systemprompt_identifiers::{ApiKeyId, UserId};
use systemprompt_models::RequestContext;
use systemprompt_runtime::AppContext;
use systemprompt_users::{ApiKeyService, IssueApiKeyParams, UserApiKey};

use crate::error::ApiHttpError;

pub(super) fn router() -> Router<AppContext> {
    Router::new()
        .route("/", post(issue_key).get(list_keys))
        .route("/{key_id}", delete(revoke_key))
}

#[derive(Debug, Deserialize)]
pub(super) struct IssueApiKeyRequest {
    pub name: String,
    #[serde(default)]
    pub target_user_id: Option<String>,
    #[serde(default)]
    pub expires_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Serialize)]
pub(super) struct IssueApiKeyResponse {
    pub id: String,
    pub name: String,
    pub key_prefix: String,
    pub secret: String,
    pub created_at: Option<DateTime<Utc>>,
    pub expires_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Serialize)]
pub(super) struct ApiKeyView {
    pub id: String,
    pub name: String,
    pub key_prefix: String,
    pub created_at: Option<DateTime<Utc>>,
    pub last_used_at: Option<DateTime<Utc>>,
    pub expires_at: Option<DateTime<Utc>>,
    pub revoked_at: Option<DateTime<Utc>>,
}

impl From<UserApiKey> for ApiKeyView {
    fn from(k: UserApiKey) -> Self {
        Self {
            id: k.id.as_str().to_owned(),
            name: k.name,
            key_prefix: k.key_prefix,
            created_at: k.created_at,
            last_used_at: k.last_used_at,
            expires_at: k.expires_at,
            revoked_at: k.revoked_at,
        }
    }
}

async fn issue_key(
    State(ctx): State<AppContext>,
    Extension(req_ctx): Extension<RequestContext>,
    Json(body): Json<IssueApiKeyRequest>,
) -> Result<impl IntoResponse, ApiHttpError> {
    let target_user = resolve_target_user(&req_ctx, body.target_user_id.as_deref());
    let service = ApiKeyService::new(ctx.db_pool())?;

    let issued = service
        .issue(IssueApiKeyParams {
            user_id: &target_user,
            name: &body.name,
            expires_at: body.expires_at,
        })
        .await?;

    Ok((
        StatusCode::CREATED,
        Json(IssueApiKeyResponse {
            id: issued.record.id.as_str().to_owned(),
            name: issued.record.name,
            key_prefix: issued.record.key_prefix,
            secret: issued.secret,
            created_at: issued.record.created_at,
            expires_at: issued.record.expires_at,
        }),
    ))
}

async fn list_keys(
    State(ctx): State<AppContext>,
    Extension(req_ctx): Extension<RequestContext>,
) -> Result<Json<Vec<ApiKeyView>>, ApiHttpError> {
    let service = ApiKeyService::new(ctx.db_pool())?;

    let keys = service.list_for_user(req_ctx.user_id()).await?;

    Ok(Json(keys.into_iter().map(ApiKeyView::from).collect()))
}

async fn revoke_key(
    State(ctx): State<AppContext>,
    Extension(req_ctx): Extension<RequestContext>,
    Path(key_id): Path<String>,
) -> Result<StatusCode, ApiHttpError> {
    let service = ApiKeyService::new(ctx.db_pool())?;

    let id = ApiKeyId::new(key_id);
    let revoked = service.revoke(&id, req_ctx.user_id()).await?;

    if revoked {
        Ok(StatusCode::NO_CONTENT)
    } else {
        Err(ApiHttpError::not_found("API key not found"))
    }
}

fn resolve_target_user(req_ctx: &RequestContext, override_user_id: Option<&str>) -> UserId {
    match override_user_id {
        Some(value) if !value.is_empty() => UserId::new(value.to_owned()),
        _ => req_ctx.user_id().clone(),
    }
}