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}