Skip to main content

bitrouter_runtime/
keys.rs

1//! HTTP endpoints for API key lifecycle.
2//!
3//! Mounted at `/key/*`, these allow callers with **admin** access (master key)
4//! to create and revoke virtual keys.
5
6use std::sync::Arc;
7
8use sea_orm::DatabaseConnection;
9use serde::{Deserialize, Serialize};
10use uuid::Uuid;
11use warp::Filter;
12
13use bitrouter_accounts::identity::{Identity, Scope};
14use bitrouter_accounts::service::AccountService;
15
16use crate::auth::{self, AuthContext, Unauthorized, hash_key};
17
18// ── request / response DTOs ───────────────────────────────────
19
20#[derive(Debug, Deserialize)]
21pub struct GenerateKeyRequest {
22    /// Human-readable name for the key.
23    #[serde(default = "default_key_name")]
24    pub name: String,
25    /// Account to associate with. If omitted, a new account is created.
26    pub account_id: Option<Uuid>,
27}
28
29fn default_key_name() -> String {
30    "default".into()
31}
32
33#[derive(Debug, Serialize)]
34pub struct GenerateKeyResponse {
35    /// The plaintext key — **only returned once**.
36    pub key: String,
37    /// Display prefix (e.g. `sk-br-12...`).
38    pub prefix: String,
39    /// The account that owns this key.
40    pub account_id: Uuid,
41}
42
43#[derive(Debug, Deserialize)]
44pub struct RevokeKeyRequest {
45    pub key_id: Uuid,
46}
47
48#[derive(Debug, Serialize)]
49pub struct RevokeKeyResponse {
50    pub revoked: bool,
51}
52
53// ── route builder ─────────────────────────────────────────────
54
55/// Build the `/key/generate` and `/key/revoke` routes.
56///
57/// Both require admin scope (master key). If no DB is configured, the routes
58/// will still be mounted but will reject with a 503.
59pub fn key_routes(
60    auth_ctx: Arc<AuthContext>,
61    db: Option<Arc<DatabaseConnection>>,
62) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
63    let generate = warp::path!("key" / "generate")
64        .and(warp::post())
65        .and(auth::management_auth(auth_ctx.clone()))
66        .and(warp::body::json::<GenerateKeyRequest>())
67        .and(require_db(db.clone()))
68        .and_then(handle_generate);
69
70    let revoke = warp::path!("key" / "revoke")
71        .and(warp::post())
72        .and(auth::management_auth(auth_ctx))
73        .and(warp::body::json::<RevokeKeyRequest>())
74        .and(require_db(db))
75        .and_then(handle_revoke);
76
77    generate.or(revoke)
78}
79
80fn require_db(
81    db: Option<Arc<DatabaseConnection>>,
82) -> impl Filter<Extract = (Arc<DatabaseConnection>,), Error = warp::Rejection> + Clone {
83    warp::any().and_then(move || {
84        let db = db.clone();
85        async move {
86            db.ok_or_else(|| warp::reject::custom(KeyError("database not configured".into())))
87        }
88    })
89}
90
91// ── handlers ──────────────────────────────────────────────────
92
93async fn handle_generate(
94    identity: Identity,
95    req: GenerateKeyRequest,
96    db: Arc<DatabaseConnection>,
97) -> Result<impl warp::Reply, warp::Rejection> {
98    require_admin(&identity)?;
99
100    let svc = AccountService::new(&db);
101
102    // Resolve or create account.
103    let account_id = match req.account_id {
104        Some(id) => bitrouter_accounts::identity::AccountId(id),
105        None => {
106            let acct = svc
107                .create_account(&req.name)
108                .await
109                .map_err(|e| warp::reject::custom(KeyError(e.to_string())))?;
110            bitrouter_accounts::identity::AccountId(acct.id)
111        }
112    };
113
114    // Generate a random key with a recognizable prefix.
115    let plaintext = format!("sk-br-{}", Uuid::new_v4().as_simple());
116    let hashed = hash_key(&plaintext);
117
118    let model = svc
119        .create_api_key(account_id, &req.name, &plaintext, &hashed)
120        .await
121        .map_err(|e| warp::reject::custom(KeyError(e.to_string())))?;
122
123    Ok(warp::reply::json(&GenerateKeyResponse {
124        key: plaintext,
125        prefix: model.prefix,
126        account_id: account_id.0,
127    }))
128}
129
130async fn handle_revoke(
131    identity: Identity,
132    req: RevokeKeyRequest,
133    db: Arc<DatabaseConnection>,
134) -> Result<impl warp::Reply, warp::Rejection> {
135    require_admin(&identity)?;
136
137    let svc = AccountService::new(&db);
138    svc.revoke_api_key(req.key_id)
139        .await
140        .map_err(|e| warp::reject::custom(KeyError(e.to_string())))?;
141
142    Ok(warp::reply::json(&RevokeKeyResponse { revoked: true }))
143}
144
145fn require_admin(identity: &Identity) -> Result<(), warp::Rejection> {
146    if identity.scope >= Scope::Admin {
147        Ok(())
148    } else {
149        Err(warp::reject::custom(Unauthorized("admin access required")))
150    }
151}
152
153// ── error type ────────────────────────────────────────────────
154
155#[derive(Debug)]
156struct KeyError(String);
157
158impl std::fmt::Display for KeyError {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        write!(f, "key operation failed: {}", self.0)
161    }
162}
163
164impl warp::reject::Reject for KeyError {}