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}