#![allow(clippy::unused_async)]
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use crate::proxy::{AppState, error_response, extract_admin_bearer, is_admin_authorised};
use crate::token::{ADMIN_SCOPE, IssueRequest, TokenError};
pub async fn issue_token(
State(state): State<AppState>,
headers: HeaderMap,
axum::Json(req): axum::Json<IssueTokenRequest>,
) -> impl IntoResponse {
if !is_admin_authorised(&state, &headers) {
return error_response(
StatusCode::UNAUTHORIZED,
"authentication_error",
"missing or invalid admin Bearer key",
);
}
let ttl = req.ttl_hours.unwrap_or(24);
let label = req.label.unwrap_or_default();
let scope = req.scope.unwrap_or_default();
let request = IssueRequest {
ttl_hours: ttl,
label: &label,
account: req.account.as_deref(),
max_requests: req.max_requests,
max_tokens: req.max_tokens,
rate_limit_per_minute: req.rate_limit_per_minute,
scope: &scope,
github_repos: req.github_repos.clone().unwrap_or_default(),
sliding_window_seconds: req
.sliding_expiry
.unwrap_or(false)
.then(|| ttl.saturating_mul(3_600)),
client_kind: None,
principal_id: None,
};
if let Err(message) = request.validate() {
return error_response(StatusCode::BAD_REQUEST, "invalid_request_error", &message);
}
match state.token_manager.issue(&request) {
Ok(token) => {
state.metrics.record_token_issued();
(
StatusCode::OK,
axum::Json(serde_json::json!({
"token": token,
"ttl_hours": ttl,
"label": label,
"account": req.account,
"max_requests": req.max_requests,
"max_tokens": req.max_tokens,
"rate_limit_per_minute": req.rate_limit_per_minute,
"scope": scope,
})),
)
.into_response()
}
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"api_error",
&format!("Failed to issue token: {e}"),
),
}
}
pub async fn issue_client_token(
State(state): State<AppState>,
headers: HeaderMap,
axum::Json(req): axum::Json<IssueClientTokenRequest>,
) -> impl IntoResponse {
if !is_admin_authorised(&state, &headers) {
return error_response(
StatusCode::UNAUTHORIZED,
"authentication_error",
"missing or invalid admin Bearer key",
);
}
let Some(client) = crate::clients::ClientKind::from_str_opt(&req.client_kind) else {
return error_response(
StatusCode::BAD_REQUEST,
"invalid_request_error",
"unknown Router client kind",
);
};
if matches!(client, crate::clients::ClientKind::Cursor) {
return error_response(
StatusCode::BAD_REQUEST,
"invalid_request_error",
"Cursor has no native Router adapter and cannot receive a bound token",
);
}
let ttl = req.ttl_hours.unwrap_or(24);
let label = req
.label
.unwrap_or_else(|| format!("client-{}", client.canonical_name()));
let principal = crate::credential_recovery_store::PRIMARY_ACCOUNT;
let request = IssueRequest {
ttl_hours: ttl,
label: &label,
account: Some(principal),
max_requests: req.max_requests,
max_tokens: None,
rate_limit_per_minute: None,
scope: "",
github_repos: Vec::new(),
sliding_window_seconds: req
.sliding_expiry
.unwrap_or(false)
.then(|| ttl.saturating_mul(3_600)),
client_kind: Some(client.canonical_name()),
principal_id: Some(principal),
};
if let Err(message) = request.validate() {
return error_response(StatusCode::BAD_REQUEST, "invalid_request_error", &message);
}
match state.token_manager.issue(&request) {
Ok(token) => {
state.metrics.record_token_issued();
(
StatusCode::OK,
axum::Json(serde_json::json!({
"token": token,
"ttl_hours": ttl,
"label": label,
"client_kind": client.canonical_name(),
"principal_id": principal,
})),
)
.into_response()
}
Err(error) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"api_error",
&format!("Failed to issue bound client token: {error}"),
),
}
}
pub async fn list_tokens(State(state): State<AppState>, headers: HeaderMap) -> impl IntoResponse {
if !is_admin_authorised(&state, &headers) {
return error_response(
StatusCode::UNAUTHORIZED,
"authentication_error",
"admin Bearer key required",
);
}
match state.token_manager.list_tokens() {
Ok(records) => (
StatusCode::OK,
axum::Json(serde_json::json!({"data": records})),
)
.into_response(),
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"api_error",
&format!("{e}"),
),
}
}
pub async fn revoke_token(
State(state): State<AppState>,
headers: HeaderMap,
axum::Json(req): axum::Json<RevokeTokenRequest>,
) -> impl IntoResponse {
if !is_admin_authorised(&state, &headers) {
return error_response(
StatusCode::UNAUTHORIZED,
"authentication_error",
"admin Bearer key required",
);
}
match state.token_manager.revoke_token(&req.id) {
Ok(()) => {
state.metrics.record_token_revoked();
(
StatusCode::OK,
axum::Json(serde_json::json!({"revoked": req.id})),
)
.into_response()
}
Err(e @ TokenError::NotFound(_)) => {
error_response(StatusCode::NOT_FOUND, "not_found", &format!("{e}"))
}
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"api_error",
&format!("{e}"),
),
}
}
pub async fn rotate_admin_token(
State(state): State<AppState>,
headers: HeaderMap,
axum::Json(req): axum::Json<RotateTokenRequest>,
) -> impl IntoResponse {
let Some(bearer) = extract_admin_bearer(&headers) else {
return error_response(
StatusCode::UNAUTHORIZED,
"authentication_error",
"admin Bearer token required",
);
};
let Ok(claims) = state.token_manager.validate_admin_token(bearer) else {
return error_response(
StatusCode::BAD_REQUEST,
"invalid_request_error",
"rotation requires an admin-scoped token; the flat admin key has no subject to revoke",
);
};
let ttl = req.ttl_hours.unwrap_or(24);
let label = req.label.unwrap_or_else(|| claims.label.clone());
match state
.token_manager
.rotate_admin_token(&claims.sub, ttl, &label)
{
Ok(token) => {
state.metrics.record_token_issued();
state.metrics.record_token_revoked();
(
StatusCode::OK,
axum::Json(serde_json::json!({
"token": token,
"ttl_hours": ttl,
"label": label,
"scope": ADMIN_SCOPE,
"revoked": claims.sub,
})),
)
.into_response()
}
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"api_error",
&format!("Failed to rotate admin token: {e}"),
),
}
}
pub async fn rotate_client_token(
State(state): State<AppState>,
headers: HeaderMap,
axum::Json(req): axum::Json<RotateClientTokenRequest>,
) -> impl IntoResponse {
if !is_admin_authorised(&state, &headers) {
return error_response(
StatusCode::UNAUTHORIZED,
"authentication_error",
"missing or invalid admin Bearer key",
);
}
match state.token_manager.store().get(&req.id) {
Ok(Some(record)) if record.scope == ADMIN_SCOPE => {
return error_response(
StatusCode::BAD_REQUEST,
"invalid_request_error",
"use /api/management/tokens/rotate to rotate an admin credential",
);
}
Ok(Some(_)) => {}
Ok(None) => {
return error_response(
StatusCode::NOT_FOUND,
"invalid_request_error",
&format!("unknown token id {}", req.id),
);
}
Err(error) => {
return error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"api_error",
&format!("failed to read token: {error}"),
);
}
}
let overrides = crate::token::RotateOverrides {
label: req.label.as_deref(),
ttl_hours: req.ttl_hours,
max_requests: req.max_requests,
max_tokens: req.max_tokens,
rate_limit_per_minute: req.rate_limit_per_minute,
account: req.account.as_deref(),
};
match state.token_manager.rotate_token_with(&req.id, &overrides) {
Ok(token) => {
state.metrics.record_token_issued();
state.metrics.record_token_revoked();
(
StatusCode::OK,
axum::Json(serde_json::json!({
"token": token,
"revoked": req.id,
})),
)
.into_response()
}
Err(crate::token::TokenError::Invalid(message)) => {
error_response(StatusCode::BAD_REQUEST, "invalid_request_error", &message)
}
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"api_error",
&format!("Failed to rotate token: {e}"),
),
}
}
#[derive(serde::Deserialize)]
pub struct RotateClientTokenRequest {
pub id: String,
pub label: Option<String>,
pub ttl_hours: Option<i64>,
pub max_requests: Option<u64>,
pub max_tokens: Option<u64>,
pub rate_limit_per_minute: Option<u64>,
pub account: Option<String>,
}
#[derive(serde::Deserialize)]
pub struct IssueTokenRequest {
pub ttl_hours: Option<i64>,
pub sliding_expiry: Option<bool>,
pub label: Option<String>,
pub account: Option<String>,
pub max_requests: Option<u64>,
pub max_tokens: Option<u64>,
pub rate_limit_per_minute: Option<u64>,
pub scope: Option<String>,
#[serde(default)]
pub github_repos: Option<Vec<String>>,
}
#[derive(serde::Deserialize)]
pub struct IssueClientTokenRequest {
pub client_kind: String,
pub ttl_hours: Option<i64>,
pub sliding_expiry: Option<bool>,
pub label: Option<String>,
pub max_requests: Option<u64>,
}
#[derive(serde::Deserialize, Default)]
pub struct RotateTokenRequest {
pub ttl_hours: Option<i64>,
pub label: Option<String>,
}
#[derive(serde::Deserialize)]
pub struct RevokeTokenRequest {
pub id: String,
}