diff --git a/crates/tranquil-api/src/admin/account/delete.rs b/crates/tranquil-api/src/admin/account/delete.rs index 0e783ad..fb4e7b0 100644 --- a/crates/tranquil-api/src/admin/account/delete.rs +++ b/crates/tranquil-api/src/admin/account/delete.rs @@ -1,8 +1,4 @@ -use axum::{ - Json, - extract::State, - response::{IntoResponse, Response}, -}; +use axum::{Json, extract::State}; use serde::Deserialize; use tracing::warn; use tranquil_pds::api::EmptyResponse; @@ -20,7 +16,7 @@ pub async fn delete_account( State(state): State, _auth: Auth, Json(input): Json, -) -> Result { +) -> Result, ApiError> { let did = &input.did; let (user_id, handle) = state .user_repo @@ -52,5 +48,5 @@ pub async fn delete_account( .cache .delete(&tranquil_pds::cache_keys::handle_key(&handle)) .await; - Ok(EmptyResponse::ok().into_response()) + Ok(Json(EmptyResponse {})) } diff --git a/crates/tranquil-api/src/admin/account/email.rs b/crates/tranquil-api/src/admin/account/email.rs index f78a15c..dbf0e17 100644 --- a/crates/tranquil-api/src/admin/account/email.rs +++ b/crates/tranquil-api/src/admin/account/email.rs @@ -1,9 +1,4 @@ -use axum::{ - Json, - extract::State, - http::StatusCode, - response::{IntoResponse, Response}, -}; +use axum::{Json, extract::State}; use serde::{Deserialize, Serialize}; use tracing::warn; use tranquil_pds::api::error::{ApiError, DbResultExt}; @@ -30,7 +25,7 @@ pub async fn send_email( State(state): State, _auth: Auth, Json(input): Json, -) -> Result { +) -> Result, ApiError> { let content = input.content.trim(); if content.is_empty() { return Err(ApiError::InvalidRequest("content is required".into())); @@ -68,11 +63,11 @@ pub async fn send_email( handle, input.recipient_did ); - Ok((StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()) + Ok(Json(SendEmailOutput { sent: true })) } Err(e) => { warn!("Failed to enqueue admin email: {:?}", e); - Ok((StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response()) + Ok(Json(SendEmailOutput { sent: false })) } } } diff --git a/crates/tranquil-api/src/admin/account/info.rs b/crates/tranquil-api/src/admin/account/info.rs index b19d42e..1b75f21 100644 --- a/crates/tranquil-api/src/admin/account/info.rs +++ b/crates/tranquil-api/src/admin/account/info.rs @@ -1,8 +1,7 @@ +use crate::common; use axum::{ Json, extract::{Query, RawQuery, State}, - http::StatusCode, - response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -68,7 +67,7 @@ pub async fn get_account_info( State(state): State, _auth: Auth, Query(params): Query, -) -> Result { +) -> Result, ApiError> { let account = state .infra_repo .get_admin_account_info_by_did(¶ms.did) @@ -79,26 +78,22 @@ pub async fn get_account_info( let invited_by = get_invited_by(&state, account.id).await; let invites = get_invites_for_user(&state, account.id).await; - Ok(( - StatusCode::OK, - Json(AccountInfo { - did: account.did, - handle: account.handle, - email: account.email, - indexed_at: account.created_at.to_rfc3339(), - invite_note: None, - invites_disabled: account.invites_disabled, - email_confirmed_at: if account.email_verified { - Some(account.created_at.to_rfc3339()) - } else { - None - }, - deactivated_at: account.deactivated_at.map(|dt| dt.to_rfc3339()), - invited_by, - invites, - }), - ) - .into_response()) + Ok(Json(AccountInfo { + did: account.did, + handle: account.handle, + email: account.email, + indexed_at: account.created_at.to_rfc3339(), + invite_note: None, + invites_disabled: account.invites_disabled, + email_confirmed_at: if account.email_verified { + Some(account.created_at.to_rfc3339()) + } else { + None + }, + deactivated_at: account.deactivated_at.map(|dt| dt.to_rfc3339()), + invited_by, + invites, + })) } async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option { @@ -133,16 +128,10 @@ async fn get_invites_for_user( .await .ok()?; - let uses_by_code: HashMap> = - uses.into_iter().fold(HashMap::new(), |mut acc, u| { - acc.entry(u.code.clone()) - .or_default() - .push(InviteCodeUseInfo { - used_by: u.used_by_did, - used_at: u.used_at.to_rfc3339(), - }); - acc - }); + let uses_by_code = common::group_invite_uses_by_code(uses, |u| InviteCodeUseInfo { + used_by: u.used_by_did, + used_at: u.used_at.to_rfc3339(), + }); let invites: Vec = invite_codes .into_iter() @@ -195,7 +184,7 @@ pub async fn get_account_infos( State(state): State, _auth: Auth, RawQuery(raw_query): RawQuery, -) -> Result { +) -> Result, ApiError> { let dids: Vec = tranquil_pds::util::parse_repeated_query_param(raw_query.as_deref(), "dids") .into_iter() @@ -244,18 +233,10 @@ pub async fn get_account_infos( .into_iter() .collect(); - let uses_by_code: HashMap> = - all_invite_uses - .into_iter() - .fold(HashMap::new(), |mut acc, u| { - acc.entry(u.code.clone()) - .or_default() - .push(InviteCodeUseInfo { - used_by: u.used_by_did, - used_at: u.used_at.to_rfc3339(), - }); - acc - }); + let uses_by_code = common::group_invite_uses_by_code(all_invite_uses, |u| InviteCodeUseInfo { + used_by: u.used_by_did, + used_at: u.used_at.to_rfc3339(), + }); let (codes_by_user, code_info_map): ( HashMap>, @@ -304,5 +285,5 @@ pub async fn get_account_infos( }) .collect(); - Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()) + Ok(Json(GetAccountInfosOutput { infos })) } diff --git a/crates/tranquil-api/src/admin/account/search.rs b/crates/tranquil-api/src/admin/account/search.rs index 96cab7c..f96e2f8 100644 --- a/crates/tranquil-api/src/admin/account/search.rs +++ b/crates/tranquil-api/src/admin/account/search.rs @@ -1,8 +1,6 @@ use axum::{ Json, extract::{Query, State}, - http::StatusCode, - response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; use tranquil_pds::api::error::{ApiError, DbResultExt}; @@ -51,7 +49,7 @@ pub async fn search_accounts( State(state): State, _auth: Auth, Query(params): Query, -) -> Result { +) -> Result, ApiError> { let limit = params.limit.clamp(1, 100); let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); @@ -91,12 +89,8 @@ pub async fn search_accounts( } else { None }; - Ok(( - StatusCode::OK, - Json(SearchAccountsOutput { - cursor: next_cursor, - accounts, - }), - ) - .into_response()) + Ok(Json(SearchAccountsOutput { + cursor: next_cursor, + accounts, + })) } diff --git a/crates/tranquil-api/src/admin/account/update.rs b/crates/tranquil-api/src/admin/account/update.rs index 5953176..791a9af 100644 --- a/crates/tranquil-api/src/admin/account/update.rs +++ b/crates/tranquil-api/src/admin/account/update.rs @@ -1,8 +1,4 @@ -use axum::{ - Json, - extract::State, - response::{IntoResponse, Response}, -}; +use axum::{Json, extract::State}; use serde::Deserialize; use tracing::{error, warn}; use tranquil_pds::api::EmptyResponse; @@ -21,7 +17,7 @@ pub async fn update_account_email( State(state): State, _auth: Auth, Json(input): Json, -) -> Result { +) -> Result, ApiError> { let account = input.account.trim(); let email = input.email.trim(); if account.is_empty() || email.is_empty() { @@ -39,7 +35,7 @@ pub async fn update_account_email( .await { Ok(0) => Err(ApiError::AccountNotFound), - Ok(_) => Ok(EmptyResponse::ok().into_response()), + Ok(_) => Ok(Json(EmptyResponse {})), Err(e) => { error!("DB error updating email: {:?}", e); Err(ApiError::InternalError(None)) @@ -57,7 +53,7 @@ pub async fn update_account_handle( State(state): State, _auth: Auth, Json(input): Json, -) -> Result { +) -> Result, ApiError> { let did = &input.did; let input_handle = input.handle.trim(); if input_handle.is_empty() { @@ -125,7 +121,7 @@ pub async fn update_account_handle( { warn!("Failed to update PLC handle for admin handle update: {}", e); } - Ok(EmptyResponse::ok().into_response()) + Ok(Json(EmptyResponse {})) } Err(e) => { error!("DB error updating handle: {:?}", e); @@ -144,7 +140,7 @@ pub async fn update_account_password( State(state): State, _auth: Auth, Json(input): Json, -) -> Result { +) -> Result, ApiError> { let did = &input.did; let password = input.password.trim(); if password.is_empty() { @@ -161,7 +157,7 @@ pub async fn update_account_password( .await { Ok(0) => Err(ApiError::AccountNotFound), - Ok(_) => Ok(EmptyResponse::ok().into_response()), + Ok(_) => Ok(Json(EmptyResponse {})), Err(e) => { error!("DB error updating password: {:?}", e); Err(ApiError::InternalError(None)) diff --git a/crates/tranquil-api/src/admin/config.rs b/crates/tranquil-api/src/admin/config.rs index 4c20ca8..e43d61a 100644 --- a/crates/tranquil-api/src/admin/config.rs +++ b/crates/tranquil-api/src/admin/config.rs @@ -8,7 +8,7 @@ use tranquil_types::CidLink; #[derive(Serialize)] #[serde(rename_all = "camelCase")] -pub struct ServerConfigResponse { +pub struct ServerConfigOutput { pub server_name: String, pub primary_color: Option, pub primary_color_dark: Option, @@ -29,7 +29,7 @@ pub struct UpdateServerConfigRequest { } #[derive(Serialize)] -pub struct UpdateServerConfigResponse { +pub struct UpdateServerConfigOutput { pub success: bool, } @@ -42,7 +42,7 @@ fn is_valid_hex_color(s: &str) -> bool { pub async fn get_server_config( State(state): State, -) -> Result, ApiError> { +) -> Result, ApiError> { let keys = &[ "server_name", "primary_color", @@ -60,7 +60,7 @@ pub async fn get_server_config( let config_map: std::collections::HashMap = rows.into_iter().collect(); - Ok(Json(ServerConfigResponse { + Ok(Json(ServerConfigOutput { server_name: config_map .get("server_name") .cloned() @@ -77,7 +77,7 @@ pub async fn update_server_config( State(state): State, _auth: Auth, Json(req): Json, -) -> Result, ApiError> { +) -> Result, ApiError> { if let Some(server_name) = req.server_name { let trimmed = server_name.trim(); if trimmed.is_empty() || trimmed.len() > 100 { @@ -224,5 +224,5 @@ pub async fn update_server_config( } } - Ok(Json(UpdateServerConfigResponse { success: true })) + Ok(Json(UpdateServerConfigOutput { success: true })) } diff --git a/crates/tranquil-api/src/admin/invite.rs b/crates/tranquil-api/src/admin/invite.rs index 9079ef0..26d02af 100644 --- a/crates/tranquil-api/src/admin/invite.rs +++ b/crates/tranquil-api/src/admin/invite.rs @@ -1,8 +1,7 @@ +use crate::common; use axum::{ Json, extract::{Query, State}, - http::StatusCode, - response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; use tracing::error; @@ -23,7 +22,7 @@ pub async fn disable_invite_codes( State(state): State, _auth: Auth, Json(input): Json, -) -> Result { +) -> Result, ApiError> { if let Some(codes) = &input.codes && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await { @@ -40,7 +39,7 @@ pub async fn disable_invite_codes( error!("DB error disabling invite codes by account: {:?}", e); } } - Ok(EmptyResponse::ok().into_response()) + Ok(Json(EmptyResponse {})) } #[derive(Deserialize)] @@ -80,7 +79,7 @@ pub async fn get_invite_codes( State(state): State, _auth: Auth, Query(params): Query, -) -> Result { +) -> Result, ApiError> { let limit = params.limit.unwrap_or(100).clamp(1, 500); let sort_order = match params.sort.as_deref() { Some("usage") => InviteCodeSortOrder::Usage, @@ -104,26 +103,21 @@ pub async fn get_invite_codes( .into_iter() .collect(); - let uses_by_code: std::collections::HashMap> = - if code_strings.is_empty() { - std::collections::HashMap::new() - } else { + let uses_by_code = if code_strings.is_empty() { + std::collections::HashMap::new() + } else { + common::group_invite_uses_by_code( state .infra_repo .get_invite_code_uses_batch(&code_strings) .await - .unwrap_or_default() - .into_iter() - .fold(std::collections::HashMap::new(), |mut acc, u| { - acc.entry(u.code.clone()) - .or_default() - .push(InviteCodeUseInfo { - used_by: u.used_by_did.to_string(), - used_at: u.used_at.to_rfc3339(), - }); - acc - }) - }; + .unwrap_or_default(), + |u| InviteCodeUseInfo { + used_by: u.used_by_did.to_string(), + used_at: u.used_at.to_rfc3339(), + }, + ) + }; let codes: Vec = codes_rows .iter() @@ -149,14 +143,10 @@ pub async fn get_invite_codes( } else { None }; - Ok(( - StatusCode::OK, - Json(GetInviteCodesOutput { - cursor: next_cursor, - codes, - }), - ) - .into_response()) + Ok(Json(GetInviteCodesOutput { + cursor: next_cursor, + codes, + })) } #[derive(Deserialize)] @@ -168,7 +158,7 @@ pub async fn disable_account_invites( State(state): State, _auth: Auth, Json(input): Json, -) -> Result { +) -> Result, ApiError> { let account = input.account.trim(); if account.is_empty() { return Err(ApiError::InvalidRequest("account is required".into())); @@ -182,7 +172,7 @@ pub async fn disable_account_invites( .set_invites_disabled(&account_did, true) .await { - Ok(true) => Ok(EmptyResponse::ok().into_response()), + Ok(true) => Ok(Json(EmptyResponse {})), Ok(false) => Err(ApiError::AccountNotFound), Err(e) => { error!("DB error disabling account invites: {:?}", e); @@ -200,7 +190,7 @@ pub async fn enable_account_invites( State(state): State, _auth: Auth, Json(input): Json, -) -> Result { +) -> Result, ApiError> { let account = input.account.trim(); if account.is_empty() { return Err(ApiError::InvalidRequest("account is required".into())); @@ -214,7 +204,7 @@ pub async fn enable_account_invites( .set_invites_disabled(&account_did, false) .await { - Ok(true) => Ok(EmptyResponse::ok().into_response()), + Ok(true) => Ok(Json(EmptyResponse {})), Ok(false) => Err(ApiError::AccountNotFound), Err(e) => { error!("DB error enabling account invites: {:?}", e); diff --git a/crates/tranquil-api/src/admin/server_stats.rs b/crates/tranquil-api/src/admin/server_stats.rs index 6f26c2b..f5ead06 100644 --- a/crates/tranquil-api/src/admin/server_stats.rs +++ b/crates/tranquil-api/src/admin/server_stats.rs @@ -1,8 +1,4 @@ -use axum::{ - Json, - extract::State, - response::{IntoResponse, Response}, -}; +use axum::{Json, extract::State}; use serde::Serialize; use tranquil_pds::api::error::ApiError; use tranquil_pds::auth::{Admin, Auth}; @@ -10,7 +6,7 @@ use tranquil_pds::state::AppState; #[derive(Serialize)] #[serde(rename_all = "camelCase")] -pub struct ServerStatsResponse { +pub struct ServerStatsOutput { pub user_count: i64, pub repo_count: i64, pub record_count: i64, @@ -20,17 +16,16 @@ pub struct ServerStatsResponse { pub async fn get_server_stats( State(state): State, _auth: Auth, -) -> Result { +) -> Result, ApiError> { let user_count = state.user_repo.count_users().await.unwrap_or(0); let repo_count = state.repo_repo.count_repos().await.unwrap_or(0); let record_count = state.repo_repo.count_all_records().await.unwrap_or(0); let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0); - Ok(Json(ServerStatsResponse { + Ok(Json(ServerStatsOutput { user_count, repo_count, record_count, blob_storage_bytes, - }) - .into_response()) + })) } diff --git a/crates/tranquil-api/src/admin/status.rs b/crates/tranquil-api/src/admin/status.rs index 49570a8..227de4a 100644 --- a/crates/tranquil-api/src/admin/status.rs +++ b/crates/tranquil-api/src/admin/status.rs @@ -1,11 +1,9 @@ use axum::{ Json, extract::{Query, State}, - http::StatusCode, - response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; -use serde_json::json; +use serde_json::{Value, json}; use tracing::{error, warn}; use tranquil_pds::api::error::ApiError; use tranquil_pds::auth::{Admin, Auth}; @@ -37,7 +35,7 @@ pub async fn get_subject_status( State(state): State, _auth: Auth, Query(params): Query, -) -> Result { +) -> Result, ApiError> { if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { return Err(ApiError::InvalidRequest( "Must provide did, uri, or blob".into(), @@ -57,18 +55,14 @@ pub async fn get_subject_status( applied: true, r#ref: Some(r.clone()), }); - return Ok(( - StatusCode::OK, - Json(SubjectStatus { - subject: json!({ - "$type": "com.atproto.admin.defs#repoRef", - "did": did_str - }), - takedown, - deactivated, + return Ok(Json(SubjectStatus { + subject: json!({ + "$type": "com.atproto.admin.defs#repoRef", + "did": did_str }), - ) - .into_response()); + takedown, + deactivated, + })); } Ok(None) => { return Err(ApiError::SubjectNotFound); @@ -89,19 +83,15 @@ pub async fn get_subject_status( applied: true, r#ref: Some(r.clone()), }); - return Ok(( - StatusCode::OK, - Json(SubjectStatus { - subject: json!({ - "$type": "com.atproto.repo.strongRef", - "uri": uri_str, - "cid": uri_str - }), - takedown, - deactivated: None, + return Ok(Json(SubjectStatus { + subject: json!({ + "$type": "com.atproto.repo.strongRef", + "uri": uri_str, + "cid": uri_str }), - ) - .into_response()); + takedown, + deactivated: None, + })); } Ok(None) => { return Err(ApiError::RecordNotFound); @@ -125,19 +115,15 @@ pub async fn get_subject_status( applied: true, r#ref: Some(r.clone()), }); - return Ok(( - StatusCode::OK, - Json(SubjectStatus { - subject: json!({ - "$type": "com.atproto.admin.defs#repoBlobRef", - "did": did, - "cid": blob.cid - }), - takedown, - deactivated: None, + return Ok(Json(SubjectStatus { + subject: json!({ + "$type": "com.atproto.admin.defs#repoBlobRef", + "did": did, + "cid": blob.cid }), - ) - .into_response()); + takedown, + deactivated: None, + })); } Ok(None) => { return Err(ApiError::BlobNotFound(None)); @@ -169,11 +155,11 @@ pub async fn update_subject_status( State(state): State, _auth: Auth, Json(input): Json, -) -> Result { - let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); +) -> Result, ApiError> { + let subject_type = input.subject.get("$type").and_then(Value::as_str); match subject_type { Some("com.atproto.admin.defs#repoRef") => { - let did_str = input.subject.get("did").and_then(|d| d.as_str()); + let did_str = input.subject.get("did").and_then(Value::as_str); if let Some(did_str) = did_str { let did: Did = match did_str.parse() { Ok(d) => d, @@ -238,24 +224,20 @@ pub async fn update_subject_status( .delete(&tranquil_pds::cache_keys::handle_key(&handle)) .await; } - return Ok(( - StatusCode::OK, - Json(json!({ - "subject": input.subject, - "takedown": input.takedown.as_ref().map(|t| json!({ - "applied": t.applied, - "ref": t.r#ref - })), - "deactivated": input.deactivated.as_ref().map(|d| json!({ - "applied": d.applied - })) + return Ok(Json(json!({ + "subject": input.subject, + "takedown": input.takedown.as_ref().map(|t| json!({ + "applied": t.applied, + "ref": t.r#ref })), - ) - .into_response()); + "deactivated": input.deactivated.as_ref().map(|d| json!({ + "applied": d.applied + })) + }))); } } Some("com.atproto.repo.strongRef") => { - let uri_str = input.subject.get("uri").and_then(|u| u.as_str()); + let uri_str = input.subject.get("uri").and_then(Value::as_str); if let Some(uri_str) = uri_str { let cid: CidLink = uri_str .parse() @@ -278,21 +260,17 @@ pub async fn update_subject_status( ApiError::InternalError(Some("Failed to update takedown status".into())) })?; } - return Ok(( - StatusCode::OK, - Json(json!({ - "subject": input.subject, - "takedown": input.takedown.as_ref().map(|t| json!({ - "applied": t.applied, - "ref": t.r#ref - })) - })), - ) - .into_response()); + return Ok(Json(json!({ + "subject": input.subject, + "takedown": input.takedown.as_ref().map(|t| json!({ + "applied": t.applied, + "ref": t.r#ref + })) + }))); } } Some("com.atproto.admin.defs#repoBlobRef") => { - let cid_str = input.subject.get("cid").and_then(|c| c.as_str()); + let cid_str = input.subject.get("cid").and_then(Value::as_str); if let Some(cid_str) = cid_str { let cid: CidLink = cid_str .parse() @@ -315,17 +293,13 @@ pub async fn update_subject_status( ApiError::InternalError(Some("Failed to update takedown status".into())) })?; } - return Ok(( - StatusCode::OK, - Json(json!({ - "subject": input.subject, - "takedown": input.takedown.as_ref().map(|t| json!({ - "applied": t.applied, - "ref": t.r#ref - })) - })), - ) - .into_response()); + return Ok(Json(json!({ + "subject": input.subject, + "takedown": input.takedown.as_ref().map(|t| json!({ + "applied": t.applied, + "ref": t.r#ref + })) + }))); } } _ => {} diff --git a/crates/tranquil-api/src/identity/did.rs b/crates/tranquil-api/src/identity/did.rs index 3405b58..361fde4 100644 --- a/crates/tranquil-api/src/identity/did.rs +++ b/crates/tranquil-api/src/identity/did.rs @@ -1,3 +1,4 @@ +use crate::common; use axum::{ Json, extract::{Path, Query, State}, @@ -10,6 +11,7 @@ use k256::elliptic_curve::sec1::ToEncodedPoint; use serde::{Deserialize, Serialize}; use serde_json::json; use tracing::{error, warn}; +use tranquil_pds::api::error::DbResultExt; use tranquil_pds::api::{ApiError, DidResponse, EmptyResponse}; use tranquil_pds::auth::{Auth, NotTakendown}; use tranquil_pds::plc::signing_key_to_did_key; @@ -188,91 +190,20 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname)); - if let Some((ovr, parsed)) = overrides.as_ref().and_then(|ovr| { - serde_json::from_value::>(ovr.verification_methods.clone()) - .ok() - .filter(|p| !p.is_empty()) - .map(|p| (ovr, p)) - }) { - let also_known_as = if !ovr.also_known_as.is_empty() { - ovr.also_known_as.clone() - } else { - vec![format!("at://{}", current_handle)] - }; - - return Json(json!({ - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/multikey/v1", - "https://w3id.org/security/suites/secp256k1-2019/v1" - ], - "id": did, - "alsoKnownAs": also_known_as, - "verificationMethod": parsed.iter().map(|m| json!({ - "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }), - "type": m.method_type, - "controller": did, - "publicKeyMultibase": m.public_key_multibase - })).collect::>(), - "service": [{ - "id": "#atproto_pds", - "type": tranquil_pds::plc::ServiceType::Pds.as_str(), - "serviceEndpoint": service_endpoint - }] - })) - .into_response(); - } - - let key_info = match state.user_repo.get_user_key_by_id(user_id).await { - Ok(Some(k)) => k, - Ok(None) => return ApiError::InternalError(None).into_response(), - Err(_) => return ApiError::InternalError(None).into_response(), - }; - let key_bytes: Vec = - match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { - Ok(k) => k, - Err(_) => { - return ApiError::InternalError(None).into_response(); - } - }; - let public_key_multibase = match get_public_key_multibase(&key_bytes) { - Ok(pk) => pk, - Err(e) => { - tracing::error!("Failed to generate public key multibase: {}", e); - return ApiError::InternalError(None).into_response(); - } + let verification_methods = + build_override_or_key_verification_methods(state, user_id, &did, overrides.as_ref()).await; + let verification_methods = match verification_methods { + Ok(vm) => vm, + Err(resp) => return resp, }; - let also_known_as = if let Some(ref ovr) = overrides { - if !ovr.also_known_as.is_empty() { - ovr.also_known_as.clone() - } else { - vec![format!("at://{}", current_handle)] - } - } else { - vec![format!("at://{}", current_handle)] - }; - - Json(json!({ - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/multikey/v1", - "https://w3id.org/security/suites/secp256k1-2019/v1" - ], - "id": did, - "alsoKnownAs": also_known_as, - "verificationMethod": [{ - "id": format!("{}#atproto", did), - "type": "Multikey", - "controller": did, - "publicKeyMultibase": public_key_multibase - }], - "service": [{ - "id": "#atproto_pds", - "type": tranquil_pds::plc::ServiceType::Pds.as_str(), - "serviceEndpoint": service_endpoint - }] - })) + let also_known_as = common::resolve_also_known_as(overrides.as_ref(), ¤t_handle); + Json(common::build_did_document( + &did, + also_known_as, + verification_methods, + &service_endpoint, + )) .into_response() } @@ -323,92 +254,65 @@ pub async fn user_did_doc(State(state): State, Path(handle): Path vm, + Err(resp) => return resp, + }; + + let also_known_as = common::resolve_also_known_as(overrides.as_ref(), ¤t_handle); + Json(common::build_did_document( + &did, + also_known_as, + verification_methods, + &service_endpoint, + )) + .into_response() +} + +async fn build_override_or_key_verification_methods( + state: &AppState, + user_id: uuid::Uuid, + did: &str, + overrides: Option<&tranquil_db_traits::DidWebOverrides>, +) -> Result, Response> { + if let Some(parsed) = overrides.and_then(|ovr| { serde_json::from_value::>(ovr.verification_methods.clone()) .ok() .filter(|p| !p.is_empty()) - .map(|p| (ovr, p)) }) { - let also_known_as = if !ovr.also_known_as.is_empty() { - ovr.also_known_as.clone() - } else { - vec![format!("at://{}", current_handle)] - }; - - return Json(json!({ - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/multikey/v1", - "https://w3id.org/security/suites/secp256k1-2019/v1" - ], - "id": did, - "alsoKnownAs": also_known_as, - "verificationMethod": parsed.iter().map(|m| json!({ - "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }), - "type": m.method_type, - "controller": did, - "publicKeyMultibase": m.public_key_multibase - })).collect::>(), - "service": [{ - "id": "#atproto_pds", - "type": tranquil_pds::plc::ServiceType::Pds.as_str(), - "serviceEndpoint": service_endpoint - }] - })) - .into_response(); + return Ok(parsed + .iter() + .map(|m| { + json!({ + "id": format!("{}{}", did, if m.id.starts_with('#') { m.id.clone() } else { format!("#{}", m.id) }), + "type": m.method_type, + "controller": did, + "publicKeyMultibase": m.public_key_multibase + }) + }) + .collect()); } let key_info = match state.user_repo.get_user_key_by_id(user_id).await { Ok(Some(k)) => k, - Ok(None) => return ApiError::InternalError(None).into_response(), - Err(_) => return ApiError::InternalError(None).into_response(), - }; - let key_bytes: Vec = - match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) { - Ok(k) => k, - Err(_) => { - return ApiError::InternalError(None).into_response(); - } - }; - let public_key_multibase = match get_public_key_multibase(&key_bytes) { - Ok(pk) => pk, - Err(e) => { - tracing::error!("Failed to generate public key multibase: {}", e); - return ApiError::InternalError(None).into_response(); - } + _ => return Err(ApiError::InternalError(None).into_response()), }; + let key_bytes = + tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) + .map_err(|_| ApiError::InternalError(None).into_response())?; + let public_key_multibase = get_public_key_multibase(&key_bytes).map_err(|e| { + tracing::error!("Failed to generate public key multibase: {}", e); + ApiError::InternalError(None).into_response() + })?; - let also_known_as = if let Some(ref ovr) = overrides { - if !ovr.also_known_as.is_empty() { - ovr.also_known_as.clone() - } else { - vec![format!("at://{}", current_handle)] - } - } else { - vec![format!("at://{}", current_handle)] - }; - - Json(json!({ - "@context": [ - "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/multikey/v1", - "https://w3id.org/security/suites/secp256k1-2019/v1" - ], - "id": did, - "alsoKnownAs": also_known_as, - "verificationMethod": [{ - "id": format!("{}#atproto", did), - "type": "Multikey", - "controller": did, - "publicKeyMultibase": public_key_multibase - }], - "service": [{ - "id": "#atproto_pds", - "type": tranquil_pds::plc::ServiceType::Pds.as_str(), - "serviceEndpoint": service_endpoint - }] - })) - .into_response() + Ok(vec![json!({ + "id": format!("{}#atproto", did), + "type": "Multikey", + "controller": did, + "publicKeyMultibase": public_key_multibase + })]) } #[derive(Debug, thiserror::Error)] @@ -562,12 +466,12 @@ pub struct AtprotoPds { pub async fn get_recommended_did_credentials( State(state): State, auth: Auth, -) -> Result { +) -> Result, ApiError> { let handle = state .user_repo .get_handle_by_did(&auth.did) .await - .map_err(|_| ApiError::InternalError(None))? + .log_db_err("fetching handle for DID credentials")? .ok_or(ApiError::InternalError(None))?; let key_bytes = auth.key_bytes.clone().ok_or_else(|| { @@ -593,21 +497,17 @@ pub async fn get_recommended_did_credentials( }; vec![server_rotation_key] }; - Ok(( - StatusCode::OK, - Json(GetRecommendedDidCredentialsOutput { - rotation_keys, - also_known_as: vec![format!("at://{}", handle)], - verification_methods: VerificationMethods { atproto: did_key }, - services: Services { - atproto_pds: AtprotoPds { - service_type: tranquil_pds::plc::ServiceType::Pds.as_str().to_string(), - endpoint: pds_endpoint, - }, + Ok(Json(GetRecommendedDidCredentialsOutput { + rotation_keys, + also_known_as: vec![format!("at://{}", handle)], + verification_methods: VerificationMethods { atproto: did_key }, + services: Services { + atproto_pds: AtprotoPds { + service_type: tranquil_pds::plc::ServiceType::Pds.as_str().to_string(), + endpoint: pds_endpoint, }, - }), - ) - .into_response()) + }, + })) } #[derive(Deserialize)] @@ -619,14 +519,12 @@ pub async fn update_handle( State(state): State, auth: Auth, Json(input): Json, -) -> Result { - if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope( +) -> Result, ApiError> { + tranquil_pds::auth::scope_check::check_identity_scope( &auth.auth_source, auth.scope.as_deref(), tranquil_pds::oauth::scopes::IdentityAttr::Handle, - ) { - return Ok(e); - } + )?; let did = auth.did.clone(); let _rate_limit = check_user_rate_limit_with_message::( &state, @@ -644,7 +542,7 @@ pub async fn update_handle( .user_repo .get_id_and_handle_by_did(&did) .await - .map_err(|_| ApiError::InternalError(None))? + .log_db_err("fetching user for handle update")? .ok_or(ApiError::InternalError(None))?; let user_id = user_row.id; let current_handle = user_row.handle; @@ -710,7 +608,7 @@ pub async fn update_handle( { warn!("Failed to sequence identity event for handle update: {}", e); } - return Ok(EmptyResponse::ok().into_response()); + return Ok(Json(EmptyResponse {})); } if short_part.contains('.') { return Err(ApiError::InvalidHandle(Some( @@ -736,7 +634,7 @@ pub async fn update_handle( { warn!("Failed to sequence identity event for handle update: {}", e); } - return Ok(EmptyResponse::ok().into_response()); + return Ok(Json(EmptyResponse {})); } match tranquil_pds::handle::verify_handle_ownership(&new_handle, &did).await { Ok(()) => {} @@ -766,7 +664,7 @@ pub async fn update_handle( .user_repo .check_handle_exists(&handle_typed, user_id) .await - .map_err(|_| ApiError::InternalError(None))?; + .log_db_err("checking handle existence")?; if handle_exists { return Err(ApiError::HandleTaken); } @@ -797,7 +695,7 @@ pub async fn update_handle( if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await { warn!("Failed to update PLC handle: {}", e); } - Ok(EmptyResponse::ok().into_response()) + Ok(Json(EmptyResponse {})) } pub async fn update_plc_handle( diff --git a/crates/tranquil-pds/src/delegation/roles.rs b/crates/tranquil-pds/src/delegation/roles.rs index d3a5a93..ef3b9bb 100644 --- a/crates/tranquil-pds/src/delegation/roles.rs +++ b/crates/tranquil-pds/src/delegation/roles.rs @@ -1,7 +1,5 @@ use std::marker::PhantomData; -use axum::response::{IntoResponse, Response}; - use crate::api::error::ApiError; use crate::auth::AuthenticatedUser; use crate::state::AppState; @@ -29,21 +27,20 @@ async fn check_delegation_flag( did: &Did, check_is_delegated: bool, error_msg: &str, -) -> Result { +) -> Result { let result = if check_is_delegated { state.delegation_repo.is_delegated_account(did).await } else { state.delegation_repo.controls_any_accounts(did).await }; match result { - Ok(true) => Err(ApiError::InvalidDelegation(error_msg.into()).into_response()), + Ok(true) => Err(ApiError::InvalidDelegation(error_msg.into())), Ok(false) => Ok(false), Err(e) => { tracing::error!("Failed to check delegation status: {:?}", e); - Err( - ApiError::InternalError(Some("Failed to verify delegation status".into())) - .into_response(), - ) + Err(ApiError::InternalError(Some( + "Failed to verify delegation status".into(), + ))) } } } @@ -51,7 +48,7 @@ async fn check_delegation_flag( pub async fn verify_can_add_controllers<'a>( state: &AppState, user: &'a AuthenticatedUser, -) -> Result, Response> { +) -> Result, ApiError> { check_delegation_flag( state, &user.did, @@ -68,7 +65,7 @@ pub async fn verify_can_add_controllers<'a>( pub async fn verify_can_control_accounts<'a>( state: &AppState, user: &'a AuthenticatedUser, -) -> Result, Response> { +) -> Result, ApiError> { check_delegation_flag( state, &user.did,