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())).into_response(); 124 } 125 }; 126 let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 127 { 128 Ok(k) => k, 129 Err(e) => { 130 error!("Failed to decrypt user key: {}", e); 131 return ApiError::InternalError(None).into_response(); 132 } 133 }; 134 let signing_key = match SigningKey::from_slice(&key_bytes) { 135 Ok(k) => k, 136 Err(e) => { 137 error!("Failed to create signing key: {:?}", e); 138 return ApiError::InternalError(None).into_response(); 139 } 140 }; 141 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 142 let did_clone = did.clone(); 143 let last_op = match with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 144 plc_client.get_last_op(&did_clone).await 145 }) 146 .await 147 { 148 Ok(op) => op, 149 Err(e) => return ApiError::from(e).into_response(), 150 }; 151 if last_op.is_tombstone() { 152 return ApiError::from(PlcError::Tombstoned).into_response(); 153 } 154 let services = input.services.map(|s| { 155 s.into_iter() 156 .map(|(k, v)| { 157 ( 158 k, 159 PlcService { 160 service_type: v.service_type, 161 endpoint: v.endpoint, 162 }, 163 ) 164 }) 165 .collect() 166 }); 167 let unsigned_op = match create_update_op( 168 &last_op, 169 input.rotation_keys, 170 input.verification_methods, 171 input.also_known_as, 172 services, 173 ) { 174 Ok(op) => op, 175 Err(PlcError::Tombstoned) => { 176 return ApiError::InvalidRequest("Cannot update tombstoned DID".into()).into_response(); 177 } 178 Err(e) => { 179 error!("Failed to create PLC operation: {:?}", e); 180 return ApiError::InternalError(None).into_response(); 181 } 182 }; 183 let signed_op = match sign_operation(&unsigned_op, &signing_key) { 184 Ok(op) => op, 185 Err(e) => { 186 error!("Failed to sign PLC operation: {:?}", e); 187 return ApiError::InternalError(None).into_response(); 188 } 189 }; 190 let _ = sqlx::query!( 191 "DELETE FROM plc_operation_tokens WHERE id = $1", 192 token_row.id 193 ) 194 .execute(&state.db) 195 .await; 196 info!("Signed PLC operation for user {}", did); 197 ( 198 StatusCode::OK, 199 Json(SignPlcOperationOutput { 200 operation: signed_op, 201 }), 202 ) 203 .into_response() 204}