use axum::{
extract::{Path, State},
http::HeaderMap,
Json,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
error::{ApiError, ApiResult},
middleware::{resolve_org_context, AuthUser},
models::{AuditEventType, FeatureType, SuspiciousActivityType, TokenScope},
AppState,
};
#[derive(Debug, Deserialize)]
pub struct CreateTokenRequest {
pub name: String,
pub scopes: Vec<String>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Serialize)]
pub struct CreateTokenResponse {
pub token: String, pub token_id: Uuid,
pub token_prefix: String,
pub name: String,
pub scopes: Vec<String>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
pub async fn create_token(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
headers: HeaderMap,
Json(request): Json<CreateTokenRequest>,
) -> ApiResult<Json<CreateTokenResponse>> {
let org_ctx = resolve_org_context(&state, user_id, &headers, None)
.await
.map_err(|_| ApiError::InvalidRequest("Organization not found".to_string()))?;
let scopes: Result<Vec<TokenScope>, _> = request
.scopes
.iter()
.map(|s| {
TokenScope::from_string(s)
.ok_or_else(|| ApiError::InvalidRequest(format!("Invalid scope: {}", s)))
})
.collect();
let scopes = scopes?;
let recent_tokens = state.store.list_api_tokens_by_org(org_ctx.org_id).await?;
let tokens_last_hour = recent_tokens
.iter()
.filter(|t| t.created_at > chrono::Utc::now() - chrono::Duration::hours(1))
.count();
if tokens_last_hour >= 5 {
let ip_address = headers
.get("X-Forwarded-For")
.or_else(|| headers.get("X-Real-IP"))
.and_then(|h| h.to_str().ok())
.map(|s| s.split(',').next().unwrap_or(s).trim());
let user_agent = headers.get("User-Agent").and_then(|h| h.to_str().ok());
state
.store
.record_suspicious_activity(
Some(org_ctx.org_id),
Some(user_id),
SuspiciousActivityType::RapidApiTokenCreation,
"medium",
format!(
"Rapid API token creation detected: {} tokens created in the last hour",
tokens_last_hour + 1
),
Some(serde_json::json!({
"tokens_created_last_hour": tokens_last_hour + 1,
"new_token_name": request.name,
})),
ip_address,
user_agent,
)
.await;
}
let (full_token, token) = state
.store
.create_api_token(org_ctx.org_id, Some(user_id), &request.name, &scopes, request.expires_at)
.await?;
state
.store
.record_feature_usage(
org_ctx.org_id,
Some(user_id),
FeatureType::ApiTokenCreate,
Some(serde_json::json!({
"token_id": token.id,
"name": request.name,
"scopes": request.scopes,
})),
)
.await;
let ip_address = headers
.get("X-Forwarded-For")
.or_else(|| headers.get("X-Real-IP"))
.and_then(|h| h.to_str().ok())
.map(|s| s.split(',').next().unwrap_or(s).trim());
let user_agent = headers.get("User-Agent").and_then(|h| h.to_str().ok());
state
.store
.record_audit_event(
org_ctx.org_id,
Some(user_id),
AuditEventType::ApiTokenCreated,
format!(
"API token '{}' created with scopes: {}",
request.name,
request.scopes.join(", ")
),
Some(serde_json::json!({
"token_id": token.id,
"token_name": request.name,
"scopes": request.scopes,
})),
ip_address,
user_agent,
)
.await;
Ok(Json(CreateTokenResponse {
token: full_token, token_id: token.id,
token_prefix: token.token_prefix,
name: token.name,
scopes: token.scopes,
expires_at: token.expires_at,
created_at: token.created_at,
}))
}
#[derive(Debug, Serialize)]
pub struct TokenListItem {
pub id: Uuid,
pub name: String,
pub token_prefix: String,
pub scopes: Vec<String>,
pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub age_days: i64,
pub needs_rotation: bool, }
pub async fn list_tokens(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
headers: HeaderMap,
) -> ApiResult<Json<Vec<TokenListItem>>> {
let org_ctx = resolve_org_context(&state, user_id, &headers, None)
.await
.map_err(|_| ApiError::InvalidRequest("Organization not found".to_string()))?;
let tokens = state.store.list_api_tokens_by_org(org_ctx.org_id).await?;
let items: Vec<TokenListItem> = tokens
.into_iter()
.map(|t| {
let age_days = t.age_days();
let needs_rotation = t.needs_rotation(90); TokenListItem {
id: t.id,
name: t.name,
token_prefix: t.token_prefix,
scopes: t.scopes,
last_used_at: t.last_used_at,
expires_at: t.expires_at,
created_at: t.created_at,
age_days,
needs_rotation,
}
})
.collect();
Ok(Json(items))
}
#[derive(Debug, Serialize)]
pub struct TokenScopeInfo {
pub value: String,
pub label: String,
pub description: String,
}
pub async fn list_scopes() -> Json<Vec<TokenScopeInfo>> {
Json(vec![
TokenScopeInfo {
value: TokenScope::ReadPackages.to_string(),
label: "Read Packages".to_string(),
description: "Read and search packages".to_string(),
},
TokenScopeInfo {
value: TokenScope::PublishPackages.to_string(),
label: "Publish Packages".to_string(),
description: "Publish new package versions".to_string(),
},
TokenScopeInfo {
value: TokenScope::DeployMocks.to_string(),
label: "Deploy Mocks".to_string(),
description: "Deploy hosted mock services".to_string(),
},
TokenScopeInfo {
value: TokenScope::AdminOrg.to_string(),
label: "Admin Organization".to_string(),
description: "Full organization administration".to_string(),
},
TokenScopeInfo {
value: TokenScope::ReadUsage.to_string(),
label: "Read Usage".to_string(),
description: "Read usage analytics and metrics".to_string(),
},
TokenScopeInfo {
value: TokenScope::ManageBilling.to_string(),
label: "Manage Billing".to_string(),
description: "Manage billing and subscription".to_string(),
},
])
}
pub async fn delete_token(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
headers: HeaderMap,
Path(token_id): Path<Uuid>,
) -> ApiResult<Json<serde_json::Value>> {
let org_ctx = resolve_org_context(&state, user_id, &headers, None)
.await
.map_err(|_| ApiError::InvalidRequest("Organization not found".to_string()))?;
let token = state
.store
.find_api_token_by_id(token_id)
.await?
.ok_or_else(|| ApiError::InvalidRequest("Token not found".to_string()))?;
if token.org_id != org_ctx.org_id {
return Err(ApiError::InvalidRequest(
"Token does not belong to this organization".to_string(),
));
}
let ip_address = headers
.get("X-Forwarded-For")
.or_else(|| headers.get("X-Real-IP"))
.and_then(|h| h.to_str().ok())
.map(|s| s.split(',').next().unwrap_or(s).trim());
let user_agent = headers.get("User-Agent").and_then(|h| h.to_str().ok());
state
.store
.record_audit_event(
org_ctx.org_id,
Some(user_id),
AuditEventType::ApiTokenDeleted,
format!("API token '{}' deleted", token.name),
Some(serde_json::json!({
"token_id": token.id,
"token_name": token.name,
"token_prefix": token.token_prefix,
})),
ip_address,
user_agent,
)
.await;
state.store.delete_api_token(token_id).await?;
Ok(Json(serde_json::json!({ "success": true })))
}