this repo has no description
1use crate::api::ApiError; 2use crate::circuit_breaker::with_circuit_breaker; 3use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation}; 4use crate::state::AppState; 5use axum::{ 6 Json, 7 extract::State, 8 http::StatusCode, 9 response::{IntoResponse, Response}, 10}; 11use chrono::Utc; 12use k256::ecdsa::SigningKey; 13use serde::{Deserialize, Serialize}; 14use serde_json::Value; 15use std::collections::HashMap; 16use tracing::{error, info}; 17 18#[derive(Debug, Deserialize)] 19#[serde(rename_all = "camelCase")] 20pub struct SignPlcOperationInput { 21 pub token: Option<String>, 22 pub rotation_keys: Option<Vec<String>>, 23 pub also_known_as: Option<Vec<String>>, 24 pub verification_methods: Option<HashMap<String, String>>, 25 pub services: Option<HashMap<String, ServiceInput>>, 26} 27 28#[derive(Debug, Deserialize, Clone)] 29pub struct ServiceInput { 30 #[serde(rename = "type")] 31 pub service_type: String, 32 pub endpoint: String, 33} 34 35#[derive(Debug, Serialize)] 36pub struct SignPlcOperationOutput { 37 pub operation: Value, 38} 39 40pub async fn sign_plc_operation( 41 State(state): State<AppState>, 42 headers: axum::http::HeaderMap, 43 Json(input): Json<SignPlcOperationInput>, 44) -> Response { 45 let bearer = match crate::auth::extract_bearer_token_from_header( 46 headers.get("Authorization").and_then(|h| h.to_str().ok()), 47 ) { 48 Some(t) => t, 49 None => return ApiError::AuthenticationRequired.into_response(), 50 }; 51 let auth_user = 52 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await { 53 Ok(user) => user, 54 Err(e) => return ApiError::from(e).into_response(), 55 }; 56 if let Err(e) = crate::auth::scope_check::check_identity_scope( 57 auth_user.is_oauth, 58 auth_user.scope.as_deref(), 59 crate::oauth::scopes::IdentityAttr::Wildcard, 60 ) { 61 return e; 62 } 63 let did = &auth_user.did; 64 if did.starts_with("did:web:") { 65 return ApiError::InvalidRequest( 66 "PLC operations are only valid for did:plc identities".into(), 67 ) 68 .into_response(); 69 } 70 let token = match &input.token { 71 Some(t) => t, 72 None => { 73 return ApiError::InvalidRequest( 74 "Email confirmation token required to sign PLC operations".into(), 75 ) 76 .into_response(); 77 } 78 }; 79 let user = match sqlx::query!("SELECT id FROM users WHERE did = $1", did) 80 .fetch_optional(&state.db) 81 .await 82 { 83 Ok(Some(row)) => row, 84 _ => { 85 return ApiError::AccountNotFound.into_response(); 86 } 87 }; 88 let token_row = match sqlx::query!( 89 "SELECT id, expires_at FROM plc_operation_tokens WHERE user_id = $1 AND token = $2", 90 user.id, 91 token 92 ) 93 .fetch_optional(&state.db) 94 .await 95 { 96 Ok(Some(row)) => row, 97 Ok(None) => { 98 return ApiError::InvalidToken(Some("Invalid or expired token".into())).into_response(); 99 } 100 Err(e) => { 101 error!("DB error: {:?}", e); 102 return ApiError::InternalError(None).into_response(); 103 } 104 }; 105 if Utc::now() > token_row.expires_at { 106 let _ = sqlx::query!( 107 "DELETE FROM plc_operation_tokens WHERE id = $1", 108 token_row.id 109 ) 110 .execute(&state.db) 111 .await; 112 return ApiError::ExpiredToken(Some("Token has expired".into())).into_response(); 113 } 114 let key_row = match sqlx::query!( 115 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 116 user.id 117 ) 118 .fetch_optional(&state.db) 119 .await 120 { 121 Ok(Some(row)) => row, 122 _ => { 123 return ApiError::InternalError(Some("User signing key not found".into())) 124 .into_response(); 125 } 126 }; 127 let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 128 { 129 Ok(k) => k, 130 Err(e) => { 131 error!("Failed to decrypt user key: {}", e); 132 return ApiError::InternalError(None).into_response(); 133 } 134 }; 135 let signing_key = match SigningKey::from_slice(&key_bytes) { 136 Ok(k) => k, 137 Err(e) => { 138 error!("Failed to create signing key: {:?}", e); 139 return ApiError::InternalError(None).into_response(); 140 } 141 }; 142 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 143 let did_clone = did.clone(); 144 let last_op = match with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 145 plc_client.get_last_op(&did_clone).await 146 }) 147 .await 148 { 149 Ok(op) => op, 150 Err(e) => return ApiError::from(e).into_response(), 151 }; 152 if last_op.is_tombstone() { 153 return ApiError::from(PlcError::Tombstoned).into_response(); 154 } 155 let services = input.services.map(|s| { 156 s.into_iter() 157 .map(|(k, v)| { 158 ( 159 k, 160 PlcService { 161 service_type: v.service_type, 162 endpoint: v.endpoint, 163 }, 164 ) 165 }) 166 .collect() 167 }); 168 let unsigned_op = match create_update_op( 169 &last_op, 170 input.rotation_keys, 171 input.verification_methods, 172 input.also_known_as, 173 services, 174 ) { 175 Ok(op) => op, 176 Err(PlcError::Tombstoned) => { 177 return ApiError::InvalidRequest("Cannot update tombstoned DID".into()).into_response(); 178 } 179 Err(e) => { 180 error!("Failed to create PLC operation: {:?}", e); 181 return ApiError::InternalError(None).into_response(); 182 } 183 }; 184 let signed_op = match sign_operation(&unsigned_op, &signing_key) { 185 Ok(op) => op, 186 Err(e) => { 187 error!("Failed to sign PLC operation: {:?}", e); 188 return ApiError::InternalError(None).into_response(); 189 } 190 }; 191 let _ = sqlx::query!( 192 "DELETE FROM plc_operation_tokens WHERE id = $1", 193 token_row.id 194 ) 195 .execute(&state.db) 196 .await; 197 info!("Signed PLC operation for user {}", did); 198 ( 199 StatusCode::OK, 200 Json(SignPlcOperationOutput { 201 operation: signed_op, 202 }), 203 ) 204 .into_response() 205}