bitrouter_runtime/
keys.rs1use 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#[derive(Debug, Deserialize)]
21pub struct GenerateKeyRequest {
22 #[serde(default = "default_key_name")]
24 pub name: String,
25 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 pub key: String,
37 pub prefix: String,
39 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
53pub 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
91async 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 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 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#[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 {}