this repo has no description
1use crate::api::ApiError; 2use crate::plc::{ 3 create_update_op, sign_operation, PlcClient, PlcError, PlcService, 4}; 5use crate::state::AppState; 6use axum::{ 7 extract::State, 8 http::StatusCode, 9 response::{IntoResponse, Response}, 10 Json, 11}; 12use chrono::Utc; 13use k256::ecdsa::SigningKey; 14use serde::{Deserialize, Serialize}; 15use serde_json::{json, Value}; 16use std::collections::HashMap; 17use tracing::{error, info}; 18 19#[derive(Debug, Deserialize)] 20#[serde(rename_all = "camelCase")] 21pub struct SignPlcOperationInput { 22 pub token: Option<String>, 23 pub rotation_keys: Option<Vec<String>>, 24 pub also_known_as: Option<Vec<String>>, 25 pub verification_methods: Option<HashMap<String, String>>, 26 pub services: Option<HashMap<String, ServiceInput>>, 27} 28 29#[derive(Debug, Deserialize, Clone)] 30pub struct ServiceInput { 31 #[serde(rename = "type")] 32 pub service_type: String, 33 pub endpoint: String, 34} 35 36#[derive(Debug, Serialize)] 37pub struct SignPlcOperationOutput { 38 pub operation: Value, 39} 40 41pub async fn sign_plc_operation( 42 State(state): State<AppState>, 43 headers: axum::http::HeaderMap, 44 Json(input): Json<SignPlcOperationInput>, 45) -> Response { 46 let bearer = match crate::auth::extract_bearer_token_from_header( 47 headers.get("Authorization").and_then(|h| h.to_str().ok()), 48 ) { 49 Some(t) => t, 50 None => return ApiError::AuthenticationRequired.into_response(), 51 }; 52 53 let auth_user = match crate::auth::validate_bearer_token(&state.db, &bearer).await { 54 Ok(user) => user, 55 Err(e) => return ApiError::from(e).into_response(), 56 }; 57 58 let did = &auth_user.did; 59 60 let token = match &input.token { 61 Some(t) => t, 62 None => { 63 return ApiError::InvalidRequest( 64 "Email confirmation token required to sign PLC operations".into() 65 ).into_response(); 66 } 67 }; 68 69 let user = match sqlx::query!("SELECT id FROM users WHERE did = $1", did) 70 .fetch_optional(&state.db) 71 .await 72 { 73 Ok(Some(row)) => row, 74 _ => { 75 return ( 76 StatusCode::NOT_FOUND, 77 Json(json!({"error": "AccountNotFound"})), 78 ) 79 .into_response(); 80 } 81 }; 82 83 let token_row = match sqlx::query!( 84 "SELECT id, expires_at FROM plc_operation_tokens WHERE user_id = $1 AND token = $2", 85 user.id, 86 token 87 ) 88 .fetch_optional(&state.db) 89 .await 90 { 91 Ok(Some(row)) => row, 92 Ok(None) => { 93 return ( 94 StatusCode::BAD_REQUEST, 95 Json(json!({ 96 "error": "InvalidToken", 97 "message": "Invalid or expired token" 98 })), 99 ) 100 .into_response(); 101 } 102 Err(e) => { 103 error!("DB error: {:?}", e); 104 return ( 105 StatusCode::INTERNAL_SERVER_ERROR, 106 Json(json!({"error": "InternalError"})), 107 ) 108 .into_response(); 109 } 110 }; 111 112 if Utc::now() > token_row.expires_at { 113 let _ = sqlx::query!("DELETE FROM plc_operation_tokens WHERE id = $1", token_row.id) 114 .execute(&state.db) 115 .await; 116 return ( 117 StatusCode::BAD_REQUEST, 118 Json(json!({ 119 "error": "ExpiredToken", 120 "message": "Token has expired" 121 })), 122 ) 123 .into_response(); 124 } 125 126 let key_row = match sqlx::query!( 127 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 128 user.id 129 ) 130 .fetch_optional(&state.db) 131 .await 132 { 133 Ok(Some(row)) => row, 134 _ => { 135 return ( 136 StatusCode::INTERNAL_SERVER_ERROR, 137 Json(json!({"error": "InternalError", "message": "User signing key not found"})), 138 ) 139 .into_response(); 140 } 141 }; 142 143 let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 144 { 145 Ok(k) => k, 146 Err(e) => { 147 error!("Failed to decrypt user key: {}", e); 148 return ( 149 StatusCode::INTERNAL_SERVER_ERROR, 150 Json(json!({"error": "InternalError"})), 151 ) 152 .into_response(); 153 } 154 }; 155 156 let signing_key = match SigningKey::from_slice(&key_bytes) { 157 Ok(k) => k, 158 Err(e) => { 159 error!("Failed to create signing key: {:?}", e); 160 return ( 161 StatusCode::INTERNAL_SERVER_ERROR, 162 Json(json!({"error": "InternalError"})), 163 ) 164 .into_response(); 165 } 166 }; 167 168 let plc_client = PlcClient::new(None); 169 let last_op = match plc_client.get_last_op(did).await { 170 Ok(op) => op, 171 Err(PlcError::NotFound) => { 172 return ( 173 StatusCode::NOT_FOUND, 174 Json(json!({ 175 "error": "NotFound", 176 "message": "DID not found in PLC directory" 177 })), 178 ) 179 .into_response(); 180 } 181 Err(e) => { 182 error!("Failed to fetch PLC operation: {:?}", e); 183 return ( 184 StatusCode::BAD_GATEWAY, 185 Json(json!({ 186 "error": "UpstreamError", 187 "message": "Failed to communicate with PLC directory" 188 })), 189 ) 190 .into_response(); 191 } 192 }; 193 194 if last_op.is_tombstone() { 195 return ( 196 StatusCode::BAD_REQUEST, 197 Json(json!({ 198 "error": "InvalidRequest", 199 "message": "DID is tombstoned" 200 })), 201 ) 202 .into_response(); 203 } 204 205 let services = input.services.map(|s| { 206 s.into_iter() 207 .map(|(k, v)| { 208 ( 209 k, 210 PlcService { 211 service_type: v.service_type, 212 endpoint: v.endpoint, 213 }, 214 ) 215 }) 216 .collect() 217 }); 218 219 let unsigned_op = match create_update_op( 220 &last_op, 221 input.rotation_keys, 222 input.verification_methods, 223 input.also_known_as, 224 services, 225 ) { 226 Ok(op) => op, 227 Err(PlcError::Tombstoned) => { 228 return ( 229 StatusCode::BAD_REQUEST, 230 Json(json!({ 231 "error": "InvalidRequest", 232 "message": "Cannot update tombstoned DID" 233 })), 234 ) 235 .into_response(); 236 } 237 Err(e) => { 238 error!("Failed to create PLC operation: {:?}", e); 239 return ( 240 StatusCode::INTERNAL_SERVER_ERROR, 241 Json(json!({"error": "InternalError"})), 242 ) 243 .into_response(); 244 } 245 }; 246 247 let signed_op = match sign_operation(&unsigned_op, &signing_key) { 248 Ok(op) => op, 249 Err(e) => { 250 error!("Failed to sign PLC operation: {:?}", e); 251 return ( 252 StatusCode::INTERNAL_SERVER_ERROR, 253 Json(json!({"error": "InternalError"})), 254 ) 255 .into_response(); 256 } 257 }; 258 259 let _ = sqlx::query!("DELETE FROM plc_operation_tokens WHERE id = $1", token_row.id) 260 .execute(&state.db) 261 .await; 262 263 info!("Signed PLC operation for user {}", did); 264 265 ( 266 StatusCode::OK, 267 Json(SignPlcOperationOutput { 268 operation: signed_op, 269 }), 270 ) 271 .into_response() 272}