Our Personal Data Server from scratch! tranquil.farm
atproto pds rust postgresql fun oauth

refactor(api): centralize DID document building, update admin endpoints #82

merged opened by oyster.cafe targeting main from refactor/api
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mhi3qdcvxs22
+224 -408
Diff #0
+3 -7
crates/tranquil-api/src/admin/account/delete.rs
··· 1 - use axum::{ 2 - Json, 3 - extract::State, 4 - response::{IntoResponse, Response}, 5 - }; 1 + use axum::{Json, extract::State}; 6 2 use serde::Deserialize; 7 3 use tracing::warn; 8 4 use tranquil_pds::api::EmptyResponse; ··· 20 16 State(state): State<AppState>, 21 17 _auth: Auth<Admin>, 22 18 Json(input): Json<DeleteAccountInput>, 23 - ) -> Result<Response, ApiError> { 19 + ) -> Result<Json<EmptyResponse>, ApiError> { 24 20 let did = &input.did; 25 21 let (user_id, handle) = state 26 22 .user_repo ··· 52 48 .cache 53 49 .delete(&tranquil_pds::cache_keys::handle_key(&handle)) 54 50 .await; 55 - Ok(EmptyResponse::ok().into_response()) 51 + Ok(Json(EmptyResponse {})) 56 52 }
+4 -9
crates/tranquil-api/src/admin/account/email.rs
··· 1 - use axum::{ 2 - Json, 3 - extract::State, 4 - http::StatusCode, 5 - response::{IntoResponse, Response}, 6 - }; 1 + use axum::{Json, extract::State}; 7 2 use serde::{Deserialize, Serialize}; 8 3 use tracing::warn; 9 4 use tranquil_pds::api::error::{ApiError, DbResultExt}; ··· 30 25 State(state): State<AppState>, 31 26 _auth: Auth<Admin>, 32 27 Json(input): Json<SendEmailInput>, 33 - ) -> Result<Response, ApiError> { 28 + ) -> Result<Json<SendEmailOutput>, ApiError> { 34 29 let content = input.content.trim(); 35 30 if content.is_empty() { 36 31 return Err(ApiError::InvalidRequest("content is required".into())); ··· 68 63 handle, 69 64 input.recipient_did 70 65 ); 71 - Ok((StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()) 66 + Ok(Json(SendEmailOutput { sent: true })) 72 67 } 73 68 Err(e) => { 74 69 warn!("Failed to enqueue admin email: {:?}", e); 75 - Ok((StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response()) 70 + Ok(Json(SendEmailOutput { sent: false })) 76 71 } 77 72 } 78 73 }
+28 -47
crates/tranquil-api/src/admin/account/info.rs
··· 1 + use crate::common; 1 2 use axum::{ 2 3 Json, 3 4 extract::{Query, RawQuery, State}, 4 - http::StatusCode, 5 - response::{IntoResponse, Response}, 6 5 }; 7 6 use serde::{Deserialize, Serialize}; 8 7 use std::collections::HashMap; ··· 68 67 State(state): State<AppState>, 69 68 _auth: Auth<Admin>, 70 69 Query(params): Query<GetAccountInfoParams>, 71 - ) -> Result<Response, ApiError> { 70 + ) -> Result<Json<AccountInfo>, ApiError> { 72 71 let account = state 73 72 .infra_repo 74 73 .get_admin_account_info_by_did(&params.did) ··· 79 78 let invited_by = get_invited_by(&state, account.id).await; 80 79 let invites = get_invites_for_user(&state, account.id).await; 81 80 82 - Ok(( 83 - StatusCode::OK, 84 - Json(AccountInfo { 85 - did: account.did, 86 - handle: account.handle, 87 - email: account.email, 88 - indexed_at: account.created_at.to_rfc3339(), 89 - invite_note: None, 90 - invites_disabled: account.invites_disabled, 91 - email_confirmed_at: if account.email_verified { 92 - Some(account.created_at.to_rfc3339()) 93 - } else { 94 - None 95 - }, 96 - deactivated_at: account.deactivated_at.map(|dt| dt.to_rfc3339()), 97 - invited_by, 98 - invites, 99 - }), 100 - ) 101 - .into_response()) 81 + Ok(Json(AccountInfo { 82 + did: account.did, 83 + handle: account.handle, 84 + email: account.email, 85 + indexed_at: account.created_at.to_rfc3339(), 86 + invite_note: None, 87 + invites_disabled: account.invites_disabled, 88 + email_confirmed_at: if account.email_verified { 89 + Some(account.created_at.to_rfc3339()) 90 + } else { 91 + None 92 + }, 93 + deactivated_at: account.deactivated_at.map(|dt| dt.to_rfc3339()), 94 + invited_by, 95 + invites, 96 + })) 102 97 } 103 98 104 99 async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> { ··· 133 128 .await 134 129 .ok()?; 135 130 136 - let uses_by_code: HashMap<String, Vec<InviteCodeUseInfo>> = 137 - uses.into_iter().fold(HashMap::new(), |mut acc, u| { 138 - acc.entry(u.code.clone()) 139 - .or_default() 140 - .push(InviteCodeUseInfo { 141 - used_by: u.used_by_did, 142 - used_at: u.used_at.to_rfc3339(), 143 - }); 144 - acc 145 - }); 131 + let uses_by_code = common::group_invite_uses_by_code(uses, |u| InviteCodeUseInfo { 132 + used_by: u.used_by_did, 133 + used_at: u.used_at.to_rfc3339(), 134 + }); 146 135 147 136 let invites: Vec<InviteCodeInfo> = invite_codes 148 137 .into_iter() ··· 195 184 State(state): State<AppState>, 196 185 _auth: Auth<Admin>, 197 186 RawQuery(raw_query): RawQuery, 198 - ) -> Result<Response, ApiError> { 187 + ) -> Result<Json<GetAccountInfosOutput>, ApiError> { 199 188 let dids: Vec<String> = 200 189 tranquil_pds::util::parse_repeated_query_param(raw_query.as_deref(), "dids") 201 190 .into_iter() ··· 244 233 .into_iter() 245 234 .collect(); 246 235 247 - let uses_by_code: HashMap<String, Vec<InviteCodeUseInfo>> = 248 - all_invite_uses 249 - .into_iter() 250 - .fold(HashMap::new(), |mut acc, u| { 251 - acc.entry(u.code.clone()) 252 - .or_default() 253 - .push(InviteCodeUseInfo { 254 - used_by: u.used_by_did, 255 - used_at: u.used_at.to_rfc3339(), 256 - }); 257 - acc 258 - }); 236 + let uses_by_code = common::group_invite_uses_by_code(all_invite_uses, |u| InviteCodeUseInfo { 237 + used_by: u.used_by_did, 238 + used_at: u.used_at.to_rfc3339(), 239 + }); 259 240 260 241 let (codes_by_user, code_info_map): ( 261 242 HashMap<uuid::Uuid, Vec<InviteCodeInfo>>, ··· 304 285 }) 305 286 .collect(); 306 287 307 - Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()) 288 + Ok(Json(GetAccountInfosOutput { infos })) 308 289 }
+5 -11
crates/tranquil-api/src/admin/account/search.rs
··· 1 1 use axum::{ 2 2 Json, 3 3 extract::{Query, State}, 4 - http::StatusCode, 5 - response::{IntoResponse, Response}, 6 4 }; 7 5 use serde::{Deserialize, Serialize}; 8 6 use tranquil_pds::api::error::{ApiError, DbResultExt}; ··· 51 49 State(state): State<AppState>, 52 50 _auth: Auth<Admin>, 53 51 Query(params): Query<SearchAccountsParams>, 54 - ) -> Result<Response, ApiError> { 52 + ) -> Result<Json<SearchAccountsOutput>, ApiError> { 55 53 let limit = params.limit.clamp(1, 100); 56 54 let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); 57 55 let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); ··· 91 89 } else { 92 90 None 93 91 }; 94 - Ok(( 95 - StatusCode::OK, 96 - Json(SearchAccountsOutput { 97 - cursor: next_cursor, 98 - accounts, 99 - }), 100 - ) 101 - .into_response()) 92 + Ok(Json(SearchAccountsOutput { 93 + cursor: next_cursor, 94 + accounts, 95 + })) 102 96 }
+7 -11
crates/tranquil-api/src/admin/account/update.rs
··· 1 - use axum::{ 2 - Json, 3 - extract::State, 4 - response::{IntoResponse, Response}, 5 - }; 1 + use axum::{Json, extract::State}; 6 2 use serde::Deserialize; 7 3 use tracing::{error, warn}; 8 4 use tranquil_pds::api::EmptyResponse; ··· 21 17 State(state): State<AppState>, 22 18 _auth: Auth<Admin>, 23 19 Json(input): Json<UpdateAccountEmailInput>, 24 - ) -> Result<Response, ApiError> { 20 + ) -> Result<Json<EmptyResponse>, ApiError> { 25 21 let account = input.account.trim(); 26 22 let email = input.email.trim(); 27 23 if account.is_empty() || email.is_empty() { ··· 39 35 .await 40 36 { 41 37 Ok(0) => Err(ApiError::AccountNotFound), 42 - Ok(_) => Ok(EmptyResponse::ok().into_response()), 38 + Ok(_) => Ok(Json(EmptyResponse {})), 43 39 Err(e) => { 44 40 error!("DB error updating email: {:?}", e); 45 41 Err(ApiError::InternalError(None)) ··· 57 53 State(state): State<AppState>, 58 54 _auth: Auth<Admin>, 59 55 Json(input): Json<UpdateAccountHandleInput>, 60 - ) -> Result<Response, ApiError> { 56 + ) -> Result<Json<EmptyResponse>, ApiError> { 61 57 let did = &input.did; 62 58 let input_handle = input.handle.trim(); 63 59 if input_handle.is_empty() { ··· 125 121 { 126 122 warn!("Failed to update PLC handle for admin handle update: {}", e); 127 123 } 128 - Ok(EmptyResponse::ok().into_response()) 124 + Ok(Json(EmptyResponse {})) 129 125 } 130 126 Err(e) => { 131 127 error!("DB error updating handle: {:?}", e); ··· 144 140 State(state): State<AppState>, 145 141 _auth: Auth<Admin>, 146 142 Json(input): Json<UpdateAccountPasswordInput>, 147 - ) -> Result<Response, ApiError> { 143 + ) -> Result<Json<EmptyResponse>, ApiError> { 148 144 let did = &input.did; 149 145 let password = input.password.trim(); 150 146 if password.is_empty() { ··· 161 157 .await 162 158 { 163 159 Ok(0) => Err(ApiError::AccountNotFound), 164 - Ok(_) => Ok(EmptyResponse::ok().into_response()), 160 + Ok(_) => Ok(Json(EmptyResponse {})), 165 161 Err(e) => { 166 162 error!("DB error updating password: {:?}", e); 167 163 Err(ApiError::InternalError(None))
+6 -6
crates/tranquil-api/src/admin/config.rs
··· 8 8 9 9 #[derive(Serialize)] 10 10 #[serde(rename_all = "camelCase")] 11 - pub struct ServerConfigResponse { 11 + pub struct ServerConfigOutput { 12 12 pub server_name: String, 13 13 pub primary_color: Option<String>, 14 14 pub primary_color_dark: Option<String>, ··· 29 29 } 30 30 31 31 #[derive(Serialize)] 32 - pub struct UpdateServerConfigResponse { 32 + pub struct UpdateServerConfigOutput { 33 33 pub success: bool, 34 34 } 35 35 ··· 42 42 43 43 pub async fn get_server_config( 44 44 State(state): State<AppState>, 45 - ) -> Result<Json<ServerConfigResponse>, ApiError> { 45 + ) -> Result<Json<ServerConfigOutput>, ApiError> { 46 46 let keys = &[ 47 47 "server_name", 48 48 "primary_color", ··· 60 60 61 61 let config_map: std::collections::HashMap<String, String> = rows.into_iter().collect(); 62 62 63 - Ok(Json(ServerConfigResponse { 63 + Ok(Json(ServerConfigOutput { 64 64 server_name: config_map 65 65 .get("server_name") 66 66 .cloned() ··· 77 77 State(state): State<AppState>, 78 78 _auth: Auth<Admin>, 79 79 Json(req): Json<UpdateServerConfigRequest>, 80 - ) -> Result<Json<UpdateServerConfigResponse>, ApiError> { 80 + ) -> Result<Json<UpdateServerConfigOutput>, ApiError> { 81 81 if let Some(server_name) = req.server_name { 82 82 let trimmed = server_name.trim(); 83 83 if trimmed.is_empty() || trimmed.len() > 100 { ··· 224 224 } 225 225 } 226 226 227 - Ok(Json(UpdateServerConfigResponse { success: true })) 227 + Ok(Json(UpdateServerConfigOutput { success: true })) 228 228 }
+23 -33
crates/tranquil-api/src/admin/invite.rs
··· 1 + use crate::common; 1 2 use axum::{ 2 3 Json, 3 4 extract::{Query, State}, 4 - http::StatusCode, 5 - response::{IntoResponse, Response}, 6 5 }; 7 6 use serde::{Deserialize, Serialize}; 8 7 use tracing::error; ··· 23 22 State(state): State<AppState>, 24 23 _auth: Auth<Admin>, 25 24 Json(input): Json<DisableInviteCodesInput>, 26 - ) -> Result<Response, ApiError> { 25 + ) -> Result<Json<EmptyResponse>, ApiError> { 27 26 if let Some(codes) = &input.codes 28 27 && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await 29 28 { ··· 40 39 error!("DB error disabling invite codes by account: {:?}", e); 41 40 } 42 41 } 43 - Ok(EmptyResponse::ok().into_response()) 42 + Ok(Json(EmptyResponse {})) 44 43 } 45 44 46 45 #[derive(Deserialize)] ··· 80 79 State(state): State<AppState>, 81 80 _auth: Auth<Admin>, 82 81 Query(params): Query<GetInviteCodesParams>, 83 - ) -> Result<Response, ApiError> { 82 + ) -> Result<Json<GetInviteCodesOutput>, ApiError> { 84 83 let limit = params.limit.unwrap_or(100).clamp(1, 500); 85 84 let sort_order = match params.sort.as_deref() { 86 85 Some("usage") => InviteCodeSortOrder::Usage, ··· 104 103 .into_iter() 105 104 .collect(); 106 105 107 - let uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> = 108 - if code_strings.is_empty() { 109 - std::collections::HashMap::new() 110 - } else { 106 + let uses_by_code = if code_strings.is_empty() { 107 + std::collections::HashMap::new() 108 + } else { 109 + common::group_invite_uses_by_code( 111 110 state 112 111 .infra_repo 113 112 .get_invite_code_uses_batch(&code_strings) 114 113 .await 115 - .unwrap_or_default() 116 - .into_iter() 117 - .fold(std::collections::HashMap::new(), |mut acc, u| { 118 - acc.entry(u.code.clone()) 119 - .or_default() 120 - .push(InviteCodeUseInfo { 121 - used_by: u.used_by_did.to_string(), 122 - used_at: u.used_at.to_rfc3339(), 123 - }); 124 - acc 125 - }) 126 - }; 114 + .unwrap_or_default(), 115 + |u| InviteCodeUseInfo { 116 + used_by: u.used_by_did.to_string(), 117 + used_at: u.used_at.to_rfc3339(), 118 + }, 119 + ) 120 + }; 127 121 128 122 let codes: Vec<InviteCodeInfo> = codes_rows 129 123 .iter() ··· 149 143 } else { 150 144 None 151 145 }; 152 - Ok(( 153 - StatusCode::OK, 154 - Json(GetInviteCodesOutput { 155 - cursor: next_cursor, 156 - codes, 157 - }), 158 - ) 159 - .into_response()) 146 + Ok(Json(GetInviteCodesOutput { 147 + cursor: next_cursor, 148 + codes, 149 + })) 160 150 } 161 151 162 152 #[derive(Deserialize)] ··· 168 158 State(state): State<AppState>, 169 159 _auth: Auth<Admin>, 170 160 Json(input): Json<DisableAccountInvitesInput>, 171 - ) -> Result<Response, ApiError> { 161 + ) -> Result<Json<EmptyResponse>, ApiError> { 172 162 let account = input.account.trim(); 173 163 if account.is_empty() { 174 164 return Err(ApiError::InvalidRequest("account is required".into())); ··· 182 172 .set_invites_disabled(&account_did, true) 183 173 .await 184 174 { 185 - Ok(true) => Ok(EmptyResponse::ok().into_response()), 175 + Ok(true) => Ok(Json(EmptyResponse {})), 186 176 Ok(false) => Err(ApiError::AccountNotFound), 187 177 Err(e) => { 188 178 error!("DB error disabling account invites: {:?}", e); ··· 200 190 State(state): State<AppState>, 201 191 _auth: Auth<Admin>, 202 192 Json(input): Json<EnableAccountInvitesInput>, 203 - ) -> Result<Response, ApiError> { 193 + ) -> Result<Json<EmptyResponse>, ApiError> { 204 194 let account = input.account.trim(); 205 195 if account.is_empty() { 206 196 return Err(ApiError::InvalidRequest("account is required".into())); ··· 214 204 .set_invites_disabled(&account_did, false) 215 205 .await 216 206 { 217 - Ok(true) => Ok(EmptyResponse::ok().into_response()), 207 + Ok(true) => Ok(Json(EmptyResponse {})), 218 208 Ok(false) => Err(ApiError::AccountNotFound), 219 209 Err(e) => { 220 210 error!("DB error enabling account invites: {:?}", e);
+5 -10
crates/tranquil-api/src/admin/server_stats.rs
··· 1 - use axum::{ 2 - Json, 3 - extract::State, 4 - response::{IntoResponse, Response}, 5 - }; 1 + use axum::{Json, extract::State}; 6 2 use serde::Serialize; 7 3 use tranquil_pds::api::error::ApiError; 8 4 use tranquil_pds::auth::{Admin, Auth}; ··· 10 6 11 7 #[derive(Serialize)] 12 8 #[serde(rename_all = "camelCase")] 13 - pub struct ServerStatsResponse { 9 + pub struct ServerStatsOutput { 14 10 pub user_count: i64, 15 11 pub repo_count: i64, 16 12 pub record_count: i64, ··· 20 16 pub async fn get_server_stats( 21 17 State(state): State<AppState>, 22 18 _auth: Auth<Admin>, 23 - ) -> Result<Response, ApiError> { 19 + ) -> Result<Json<ServerStatsOutput>, ApiError> { 24 20 let user_count = state.user_repo.count_users().await.unwrap_or(0); 25 21 let repo_count = state.repo_repo.count_repos().await.unwrap_or(0); 26 22 let record_count = state.repo_repo.count_all_records().await.unwrap_or(0); 27 23 let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0); 28 24 29 - Ok(Json(ServerStatsResponse { 25 + Ok(Json(ServerStatsOutput { 30 26 user_count, 31 27 repo_count, 32 28 record_count, 33 29 blob_storage_bytes, 34 - }) 35 - .into_response()) 30 + })) 36 31 }
+53 -79
crates/tranquil-api/src/admin/status.rs
··· 1 1 use axum::{ 2 2 Json, 3 3 extract::{Query, State}, 4 - http::StatusCode, 5 - response::{IntoResponse, Response}, 6 4 }; 7 5 use serde::{Deserialize, Serialize}; 8 - use serde_json::json; 6 + use serde_json::{Value, json}; 9 7 use tracing::{error, warn}; 10 8 use tranquil_pds::api::error::ApiError; 11 9 use tranquil_pds::auth::{Admin, Auth}; ··· 37 35 State(state): State<AppState>, 38 36 _auth: Auth<Admin>, 39 37 Query(params): Query<GetSubjectStatusParams>, 40 - ) -> Result<Response, ApiError> { 38 + ) -> Result<Json<SubjectStatus>, ApiError> { 41 39 if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { 42 40 return Err(ApiError::InvalidRequest( 43 41 "Must provide did, uri, or blob".into(), ··· 57 55 applied: true, 58 56 r#ref: Some(r.clone()), 59 57 }); 60 - return Ok(( 61 - StatusCode::OK, 62 - Json(SubjectStatus { 63 - subject: json!({ 64 - "$type": "com.atproto.admin.defs#repoRef", 65 - "did": did_str 66 - }), 67 - takedown, 68 - deactivated, 58 + return Ok(Json(SubjectStatus { 59 + subject: json!({ 60 + "$type": "com.atproto.admin.defs#repoRef", 61 + "did": did_str 69 62 }), 70 - ) 71 - .into_response()); 63 + takedown, 64 + deactivated, 65 + })); 72 66 } 73 67 Ok(None) => { 74 68 return Err(ApiError::SubjectNotFound); ··· 89 83 applied: true, 90 84 r#ref: Some(r.clone()), 91 85 }); 92 - return Ok(( 93 - StatusCode::OK, 94 - Json(SubjectStatus { 95 - subject: json!({ 96 - "$type": "com.atproto.repo.strongRef", 97 - "uri": uri_str, 98 - "cid": uri_str 99 - }), 100 - takedown, 101 - deactivated: None, 86 + return Ok(Json(SubjectStatus { 87 + subject: json!({ 88 + "$type": "com.atproto.repo.strongRef", 89 + "uri": uri_str, 90 + "cid": uri_str 102 91 }), 103 - ) 104 - .into_response()); 92 + takedown, 93 + deactivated: None, 94 + })); 105 95 } 106 96 Ok(None) => { 107 97 return Err(ApiError::RecordNotFound); ··· 125 115 applied: true, 126 116 r#ref: Some(r.clone()), 127 117 }); 128 - return Ok(( 129 - StatusCode::OK, 130 - Json(SubjectStatus { 131 - subject: json!({ 132 - "$type": "com.atproto.admin.defs#repoBlobRef", 133 - "did": did, 134 - "cid": blob.cid 135 - }), 136 - takedown, 137 - deactivated: None, 118 + return Ok(Json(SubjectStatus { 119 + subject: json!({ 120 + "$type": "com.atproto.admin.defs#repoBlobRef", 121 + "did": did, 122 + "cid": blob.cid 138 123 }), 139 - ) 140 - .into_response()); 124 + takedown, 125 + deactivated: None, 126 + })); 141 127 } 142 128 Ok(None) => { 143 129 return Err(ApiError::BlobNotFound(None)); ··· 169 155 State(state): State<AppState>, 170 156 _auth: Auth<Admin>, 171 157 Json(input): Json<UpdateSubjectStatusInput>, 172 - ) -> Result<Response, ApiError> { 173 - let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 158 + ) -> Result<Json<serde_json::Value>, ApiError> { 159 + let subject_type = input.subject.get("$type").and_then(Value::as_str); 174 160 match subject_type { 175 161 Some("com.atproto.admin.defs#repoRef") => { 176 - let did_str = input.subject.get("did").and_then(|d| d.as_str()); 162 + let did_str = input.subject.get("did").and_then(Value::as_str); 177 163 if let Some(did_str) = did_str { 178 164 let did: Did = match did_str.parse() { 179 165 Ok(d) => d, ··· 238 224 .delete(&tranquil_pds::cache_keys::handle_key(&handle)) 239 225 .await; 240 226 } 241 - return Ok(( 242 - StatusCode::OK, 243 - Json(json!({ 244 - "subject": input.subject, 245 - "takedown": input.takedown.as_ref().map(|t| json!({ 246 - "applied": t.applied, 247 - "ref": t.r#ref 248 - })), 249 - "deactivated": input.deactivated.as_ref().map(|d| json!({ 250 - "applied": d.applied 251 - })) 227 + return Ok(Json(json!({ 228 + "subject": input.subject, 229 + "takedown": input.takedown.as_ref().map(|t| json!({ 230 + "applied": t.applied, 231 + "ref": t.r#ref 252 232 })), 253 - ) 254 - .into_response()); 233 + "deactivated": input.deactivated.as_ref().map(|d| json!({ 234 + "applied": d.applied 235 + })) 236 + }))); 255 237 } 256 238 } 257 239 Some("com.atproto.repo.strongRef") => { 258 - let uri_str = input.subject.get("uri").and_then(|u| u.as_str()); 240 + let uri_str = input.subject.get("uri").and_then(Value::as_str); 259 241 if let Some(uri_str) = uri_str { 260 242 let cid: CidLink = uri_str 261 243 .parse() ··· 278 260 ApiError::InternalError(Some("Failed to update takedown status".into())) 279 261 })?; 280 262 } 281 - return Ok(( 282 - StatusCode::OK, 283 - Json(json!({ 284 - "subject": input.subject, 285 - "takedown": input.takedown.as_ref().map(|t| json!({ 286 - "applied": t.applied, 287 - "ref": t.r#ref 288 - })) 289 - })), 290 - ) 291 - .into_response()); 263 + return Ok(Json(json!({ 264 + "subject": input.subject, 265 + "takedown": input.takedown.as_ref().map(|t| json!({ 266 + "applied": t.applied, 267 + "ref": t.r#ref 268 + })) 269 + }))); 292 270 } 293 271 } 294 272 Some("com.atproto.admin.defs#repoBlobRef") => { 295 - let cid_str = input.subject.get("cid").and_then(|c| c.as_str()); 273 + let cid_str = input.subject.get("cid").and_then(Value::as_str); 296 274 if let Some(cid_str) = cid_str { 297 275 let cid: CidLink = cid_str 298 276 .parse() ··· 315 293 ApiError::InternalError(Some("Failed to update takedown status".into())) 316 294 })?; 317 295 } 318 - return Ok(( 319 - StatusCode::OK, 320 - Json(json!({ 321 - "subject": input.subject, 322 - "takedown": input.takedown.as_ref().map(|t| json!({ 323 - "applied": t.applied, 324 - "ref": t.r#ref 325 - })) 326 - })), 327 - ) 328 - .into_response()); 296 + return Ok(Json(json!({ 297 + "subject": input.subject, 298 + "takedown": input.takedown.as_ref().map(|t| json!({ 299 + "applied": t.applied, 300 + "ref": t.r#ref 301 + })) 302 + }))); 329 303 } 330 304 } 331 305 _ => {}
+83 -185
crates/tranquil-api/src/identity/did.rs
··· 1 + use crate::common; 1 2 use axum::{ 2 3 Json, 3 4 extract::{Path, Query, State}, ··· 10 11 use serde::{Deserialize, Serialize}; 11 12 use serde_json::json; 12 13 use tracing::{error, warn}; 14 + use tranquil_pds::api::error::DbResultExt; 13 15 use tranquil_pds::api::{ApiError, DidResponse, EmptyResponse}; 14 16 use tranquil_pds::auth::{Auth, NotTakendown}; 15 17 use tranquil_pds::plc::signing_key_to_did_key; ··· 188 190 189 191 let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname)); 190 192 191 - if let Some((ovr, parsed)) = overrides.as_ref().and_then(|ovr| { 192 - serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone()) 193 - .ok() 194 - .filter(|p| !p.is_empty()) 195 - .map(|p| (ovr, p)) 196 - }) { 197 - let also_known_as = if !ovr.also_known_as.is_empty() { 198 - ovr.also_known_as.clone() 199 - } else { 200 - vec![format!("at://{}", current_handle)] 201 - }; 202 - 203 - return Json(json!({ 204 - "@context": [ 205 - "https://www.w3.org/ns/did/v1", 206 - "https://w3id.org/security/multikey/v1", 207 - "https://w3id.org/security/suites/secp256k1-2019/v1" 208 - ], 209 - "id": did, 210 - "alsoKnownAs": also_known_as, 211 - "verificationMethod": parsed.iter().map(|m| json!({ 212 - "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }), 213 - "type": m.method_type, 214 - "controller": did, 215 - "publicKeyMultibase": m.public_key_multibase 216 - })).collect::<Vec<_>>(), 217 - "service": [{ 218 - "id": "#atproto_pds", 219 - "type": tranquil_pds::plc::ServiceType::Pds.as_str(), 220 - "serviceEndpoint": service_endpoint 221 - }] 222 - })) 223 - .into_response(); 224 - } 225 - 226 - let key_info = match state.user_repo.get_user_key_by_id(user_id).await { 227 - Ok(Some(k)) => k, 228 - Ok(None) => return ApiError::InternalError(None).into_response(), 229 - Err(_) => return ApiError::InternalError(None).into_response(), 230 - }; 231 - let key_bytes: Vec<u8> = 232 - match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { 233 - Ok(k) => k, 234 - Err(_) => { 235 - return ApiError::InternalError(None).into_response(); 236 - } 237 - }; 238 - let public_key_multibase = match get_public_key_multibase(&key_bytes) { 239 - Ok(pk) => pk, 240 - Err(e) => { 241 - tracing::error!("Failed to generate public key multibase: {}", e); 242 - return ApiError::InternalError(None).into_response(); 243 - } 244 - }; 245 - 246 - let also_known_as = if let Some(ref ovr) = overrides { 247 - if !ovr.also_known_as.is_empty() { 248 - ovr.also_known_as.clone() 249 - } else { 250 - vec![format!("at://{}", current_handle)] 251 - } 252 - } else { 253 - vec![format!("at://{}", current_handle)] 193 + let verification_methods = 194 + build_override_or_key_verification_methods(state, user_id, &did, overrides.as_ref()).await; 195 + let verification_methods = match verification_methods { 196 + Ok(vm) => vm, 197 + Err(resp) => return resp, 254 198 }; 255 199 256 - Json(json!({ 257 - "@context": [ 258 - "https://www.w3.org/ns/did/v1", 259 - "https://w3id.org/security/multikey/v1", 260 - "https://w3id.org/security/suites/secp256k1-2019/v1" 261 - ], 262 - "id": did, 263 - "alsoKnownAs": also_known_as, 264 - "verificationMethod": [{ 265 - "id": format!("{}#atproto", did), 266 - "type": "Multikey", 267 - "controller": did, 268 - "publicKeyMultibase": public_key_multibase 269 - }], 270 - "service": [{ 271 - "id": "#atproto_pds", 272 - "type": tranquil_pds::plc::ServiceType::Pds.as_str(), 273 - "serviceEndpoint": service_endpoint 274 - }] 275 - })) 200 + let also_known_as = common::resolve_also_known_as(overrides.as_ref(), &current_handle); 201 + Json(common::build_did_document( 202 + &did, 203 + also_known_as, 204 + verification_methods, 205 + &service_endpoint, 206 + )) 276 207 .into_response() 277 208 } 278 209 ··· 323 254 324 255 let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname)); 325 256 326 - if let Some((ovr, parsed)) = overrides.as_ref().and_then(|ovr| { 257 + let verification_methods = 258 + build_override_or_key_verification_methods(&state, user_id, &did, overrides.as_ref()).await; 259 + let verification_methods = match verification_methods { 260 + Ok(vm) => vm, 261 + Err(resp) => return resp, 262 + }; 263 + 264 + let also_known_as = common::resolve_also_known_as(overrides.as_ref(), &current_handle); 265 + Json(common::build_did_document( 266 + &did, 267 + also_known_as, 268 + verification_methods, 269 + &service_endpoint, 270 + )) 271 + .into_response() 272 + } 273 + 274 + async fn build_override_or_key_verification_methods( 275 + state: &AppState, 276 + user_id: uuid::Uuid, 277 + did: &str, 278 + overrides: Option<&tranquil_db_traits::DidWebOverrides>, 279 + ) -> Result<Vec<serde_json::Value>, Response> { 280 + if let Some(parsed) = overrides.and_then(|ovr| { 327 281 serde_json::from_value::<Vec<DidWebVerificationMethod>>(ovr.verification_methods.clone()) 328 282 .ok() 329 283 .filter(|p| !p.is_empty()) 330 - .map(|p| (ovr, p)) 331 284 }) { 332 - let also_known_as = if !ovr.also_known_as.is_empty() { 333 - ovr.also_known_as.clone() 334 - } else { 335 - vec![format!("at://{}", current_handle)] 336 - }; 337 - 338 - return Json(json!({ 339 - "@context": [ 340 - "https://www.w3.org/ns/did/v1", 341 - "https://w3id.org/security/multikey/v1", 342 - "https://w3id.org/security/suites/secp256k1-2019/v1" 343 - ], 344 - "id": did, 345 - "alsoKnownAs": also_known_as, 346 - "verificationMethod": parsed.iter().map(|m| json!({ 347 - "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }), 348 - "type": m.method_type, 349 - "controller": did, 350 - "publicKeyMultibase": m.public_key_multibase 351 - })).collect::<Vec<_>>(), 352 - "service": [{ 353 - "id": "#atproto_pds", 354 - "type": tranquil_pds::plc::ServiceType::Pds.as_str(), 355 - "serviceEndpoint": service_endpoint 356 - }] 357 - })) 358 - .into_response(); 285 + return Ok(parsed 286 + .iter() 287 + .map(|m| { 288 + json!({ 289 + "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }), 290 + "type": m.method_type, 291 + "controller": did, 292 + "publicKeyMultibase": m.public_key_multibase 293 + }) 294 + }) 295 + .collect()); 359 296 } 360 297 361 298 let key_info = match state.user_repo.get_user_key_by_id(user_id).await { 362 299 Ok(Some(k)) => k, 363 - Ok(None) => return ApiError::InternalError(None).into_response(), 364 - Err(_) => return ApiError::InternalError(None).into_response(), 365 - }; 366 - let key_bytes: Vec<u8> = 367 - match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { 368 - Ok(k) => k, 369 - Err(_) => { 370 - return ApiError::InternalError(None).into_response(); 371 - } 372 - }; 373 - let public_key_multibase = match get_public_key_multibase(&key_bytes) { 374 - Ok(pk) => pk, 375 - Err(e) => { 376 - tracing::error!("Failed to generate public key multibase: {}", e); 377 - return ApiError::InternalError(None).into_response(); 378 - } 379 - }; 380 - 381 - let also_known_as = if let Some(ref ovr) = overrides { 382 - if !ovr.also_known_as.is_empty() { 383 - ovr.also_known_as.clone() 384 - } else { 385 - vec![format!("at://{}", current_handle)] 386 - } 387 - } else { 388 - vec![format!("at://{}", current_handle)] 300 + _ => return Err(ApiError::InternalError(None).into_response()), 389 301 }; 302 + let key_bytes = 303 + tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) 304 + .map_err(|_| ApiError::InternalError(None).into_response())?; 305 + let public_key_multibase = get_public_key_multibase(&key_bytes).map_err(|e| { 306 + tracing::error!("Failed to generate public key multibase: {}", e); 307 + ApiError::InternalError(None).into_response() 308 + })?; 390 309 391 - Json(json!({ 392 - "@context": [ 393 - "https://www.w3.org/ns/did/v1", 394 - "https://w3id.org/security/multikey/v1", 395 - "https://w3id.org/security/suites/secp256k1-2019/v1" 396 - ], 397 - "id": did, 398 - "alsoKnownAs": also_known_as, 399 - "verificationMethod": [{ 400 - "id": format!("{}#atproto", did), 401 - "type": "Multikey", 402 - "controller": did, 403 - "publicKeyMultibase": public_key_multibase 404 - }], 405 - "service": [{ 406 - "id": "#atproto_pds", 407 - "type": tranquil_pds::plc::ServiceType::Pds.as_str(), 408 - "serviceEndpoint": service_endpoint 409 - }] 410 - })) 411 - .into_response() 310 + Ok(vec![json!({ 311 + "id": format!("{}#atproto", did), 312 + "type": "Multikey", 313 + "controller": did, 314 + "publicKeyMultibase": public_key_multibase 315 + })]) 412 316 } 413 317 414 318 #[derive(Debug, thiserror::Error)] ··· 562 466 pub async fn get_recommended_did_credentials( 563 467 State(state): State<AppState>, 564 468 auth: Auth<NotTakendown>, 565 - ) -> Result<Response, ApiError> { 469 + ) -> Result<Json<GetRecommendedDidCredentialsOutput>, ApiError> { 566 470 let handle = state 567 471 .user_repo 568 472 .get_handle_by_did(&auth.did) 569 473 .await 570 - .map_err(|_| ApiError::InternalError(None))? 474 + .log_db_err("fetching handle for DID credentials")? 571 475 .ok_or(ApiError::InternalError(None))?; 572 476 573 477 let key_bytes = auth.key_bytes.clone().ok_or_else(|| { ··· 593 497 }; 594 498 vec![server_rotation_key] 595 499 }; 596 - Ok(( 597 - StatusCode::OK, 598 - Json(GetRecommendedDidCredentialsOutput { 599 - rotation_keys, 600 - also_known_as: vec![format!("at://{}", handle)], 601 - verification_methods: VerificationMethods { atproto: did_key }, 602 - services: Services { 603 - atproto_pds: AtprotoPds { 604 - service_type: tranquil_pds::plc::ServiceType::Pds.as_str().to_string(), 605 - endpoint: pds_endpoint, 606 - }, 500 + Ok(Json(GetRecommendedDidCredentialsOutput { 501 + rotation_keys, 502 + also_known_as: vec![format!("at://{}", handle)], 503 + verification_methods: VerificationMethods { atproto: did_key }, 504 + services: Services { 505 + atproto_pds: AtprotoPds { 506 + service_type: tranquil_pds::plc::ServiceType::Pds.as_str().to_string(), 507 + endpoint: pds_endpoint, 607 508 }, 608 - }), 609 - ) 610 - .into_response()) 509 + }, 510 + })) 611 511 } 612 512 613 513 #[derive(Deserialize)] ··· 619 519 State(state): State<AppState>, 620 520 auth: Auth<NotTakendown>, 621 521 Json(input): Json<UpdateHandleInput>, 622 - ) -> Result<Response, ApiError> { 623 - if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope( 522 + ) -> Result<Json<EmptyResponse>, ApiError> { 523 + tranquil_pds::auth::scope_check::check_identity_scope( 624 524 &auth.auth_source, 625 525 auth.scope.as_deref(), 626 526 tranquil_pds::oauth::scopes::IdentityAttr::Handle, 627 - ) { 628 - return Ok(e); 629 - } 527 + )?; 630 528 let did = auth.did.clone(); 631 529 let _rate_limit = check_user_rate_limit_with_message::<HandleUpdateLimit>( 632 530 &state, ··· 644 542 .user_repo 645 543 .get_id_and_handle_by_did(&did) 646 544 .await 647 - .map_err(|_| ApiError::InternalError(None))? 545 + .log_db_err("fetching user for handle update")? 648 546 .ok_or(ApiError::InternalError(None))?; 649 547 let user_id = user_row.id; 650 548 let current_handle = user_row.handle; ··· 710 608 { 711 609 warn!("Failed to sequence identity event for handle update: {}", e); 712 610 } 713 - return Ok(EmptyResponse::ok().into_response()); 611 + return Ok(Json(EmptyResponse {})); 714 612 } 715 613 if short_part.contains('.') { 716 614 return Err(ApiError::InvalidHandle(Some( ··· 736 634 { 737 635 warn!("Failed to sequence identity event for handle update: {}", e); 738 636 } 739 - return Ok(EmptyResponse::ok().into_response()); 637 + return Ok(Json(EmptyResponse {})); 740 638 } 741 639 match tranquil_pds::handle::verify_handle_ownership(&new_handle, &did).await { 742 640 Ok(()) => {} ··· 766 664 .user_repo 767 665 .check_handle_exists(&handle_typed, user_id) 768 666 .await 769 - .map_err(|_| ApiError::InternalError(None))?; 667 + .log_db_err("checking handle existence")?; 770 668 if handle_exists { 771 669 return Err(ApiError::HandleTaken); 772 670 } ··· 797 695 if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await { 798 696 warn!("Failed to update PLC handle: {}", e); 799 697 } 800 - Ok(EmptyResponse::ok().into_response()) 698 + Ok(Json(EmptyResponse {})) 801 699 } 802 700 803 701 pub async fn update_plc_handle(
+7 -10
crates/tranquil-pds/src/delegation/roles.rs
··· 1 1 use std::marker::PhantomData; 2 2 3 - use axum::response::{IntoResponse, Response}; 4 - 5 3 use crate::api::error::ApiError; 6 4 use crate::auth::AuthenticatedUser; 7 5 use crate::state::AppState; ··· 29 27 did: &Did, 30 28 check_is_delegated: bool, 31 29 error_msg: &str, 32 - ) -> Result<bool, Response> { 30 + ) -> Result<bool, ApiError> { 33 31 let result = if check_is_delegated { 34 32 state.delegation_repo.is_delegated_account(did).await 35 33 } else { 36 34 state.delegation_repo.controls_any_accounts(did).await 37 35 }; 38 36 match result { 39 - Ok(true) => Err(ApiError::InvalidDelegation(error_msg.into()).into_response()), 37 + Ok(true) => Err(ApiError::InvalidDelegation(error_msg.into())), 40 38 Ok(false) => Ok(false), 41 39 Err(e) => { 42 40 tracing::error!("Failed to check delegation status: {:?}", e); 43 - Err( 44 - ApiError::InternalError(Some("Failed to verify delegation status".into())) 45 - .into_response(), 46 - ) 41 + Err(ApiError::InternalError(Some( 42 + "Failed to verify delegation status".into(), 43 + ))) 47 44 } 48 45 } 49 46 } ··· 51 48 pub async fn verify_can_add_controllers<'a>( 52 49 state: &AppState, 53 50 user: &'a AuthenticatedUser, 54 - ) -> Result<CanAddControllers<'a>, Response> { 51 + ) -> Result<CanAddControllers<'a>, ApiError> { 55 52 check_delegation_flag( 56 53 state, 57 54 &user.did, ··· 68 65 pub async fn verify_can_control_accounts<'a>( 69 66 state: &AppState, 70 67 user: &'a AuthenticatedUser, 71 - ) -> Result<CanControlAccounts<'a>, Response> { 68 + ) -> Result<CanControlAccounts<'a>, ApiError> { 72 69 check_delegation_flag( 73 70 state, 74 71 &user.did,

History

1 round 0 comments
sign up or login to add to the discussion
oyster.cafe submitted #0
1 commit
expand
refactor(api): centralize DID document building, update admin endpoints
expand 0 comments
pull request successfully merged