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

This commit is contained in:
Lewis
2026-03-20 13:39:11 +00:00
committed by Tangled
parent 7b7936d539
commit a3f96b6367
11 changed files with 224 additions and 408 deletions
@@ -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<AppState>,
_auth: Auth<Admin>,
Json(input): Json<DeleteAccountInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, 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 {}))
}
@@ -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<AppState>,
_auth: Auth<Admin>,
Json(input): Json<SendEmailInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<SendEmailOutput>, 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 }))
}
}
}
+28 -47
View File
@@ -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<AppState>,
_auth: Auth<Admin>,
Query(params): Query<GetAccountInfoParams>,
) -> Result<Response, ApiError> {
) -> Result<Json<AccountInfo>, ApiError> {
let account = state
.infra_repo
.get_admin_account_info_by_did(&params.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<InviteCodeInfo> {
@@ -133,16 +128,10 @@ async fn get_invites_for_user(
.await
.ok()?;
let uses_by_code: HashMap<String, Vec<InviteCodeUseInfo>> =
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<InviteCodeInfo> = invite_codes
.into_iter()
@@ -195,7 +184,7 @@ pub async fn get_account_infos(
State(state): State<AppState>,
_auth: Auth<Admin>,
RawQuery(raw_query): RawQuery,
) -> Result<Response, ApiError> {
) -> Result<Json<GetAccountInfosOutput>, ApiError> {
let dids: Vec<String> =
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<String, Vec<InviteCodeUseInfo>> =
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<uuid::Uuid, Vec<InviteCodeInfo>>,
@@ -304,5 +285,5 @@ pub async fn get_account_infos(
})
.collect();
Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response())
Ok(Json(GetAccountInfosOutput { infos }))
}
@@ -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<AppState>,
_auth: Auth<Admin>,
Query(params): Query<SearchAccountsParams>,
) -> Result<Response, ApiError> {
) -> Result<Json<SearchAccountsOutput>, 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,
}))
}
@@ -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<AppState>,
_auth: Auth<Admin>,
Json(input): Json<UpdateAccountEmailInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, 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<AppState>,
_auth: Auth<Admin>,
Json(input): Json<UpdateAccountHandleInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, 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<AppState>,
_auth: Auth<Admin>,
Json(input): Json<UpdateAccountPasswordInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, 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))
+6 -6
View File
@@ -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<String>,
pub primary_color_dark: Option<String>,
@@ -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<AppState>,
) -> Result<Json<ServerConfigResponse>, ApiError> {
) -> Result<Json<ServerConfigOutput>, ApiError> {
let keys = &[
"server_name",
"primary_color",
@@ -60,7 +60,7 @@ pub async fn get_server_config(
let config_map: std::collections::HashMap<String, String> = 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<AppState>,
_auth: Auth<Admin>,
Json(req): Json<UpdateServerConfigRequest>,
) -> Result<Json<UpdateServerConfigResponse>, ApiError> {
) -> Result<Json<UpdateServerConfigOutput>, 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 }))
}
+23 -33
View File
@@ -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<AppState>,
_auth: Auth<Admin>,
Json(input): Json<DisableInviteCodesInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, 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<AppState>,
_auth: Auth<Admin>,
Query(params): Query<GetInviteCodesParams>,
) -> Result<Response, ApiError> {
) -> Result<Json<GetInviteCodesOutput>, 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<String, Vec<InviteCodeUseInfo>> =
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<InviteCodeInfo> = 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<AppState>,
_auth: Auth<Admin>,
Json(input): Json<DisableAccountInvitesInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, 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<AppState>,
_auth: Auth<Admin>,
Json(input): Json<EnableAccountInvitesInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, 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);
+5 -10
View File
@@ -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<AppState>,
_auth: Auth<Admin>,
) -> Result<Response, ApiError> {
) -> Result<Json<ServerStatsOutput>, 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())
}))
}
+53 -79
View File
@@ -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<AppState>,
_auth: Auth<Admin>,
Query(params): Query<GetSubjectStatusParams>,
) -> Result<Response, ApiError> {
) -> Result<Json<SubjectStatus>, 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<AppState>,
_auth: Auth<Admin>,
Json(input): Json<UpdateSubjectStatusInput>,
) -> Result<Response, ApiError> {
let subject_type = input.subject.get("$type").and_then(|t| t.as_str());
) -> Result<Json<serde_json::Value>, 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
}))
})));
}
}
_ => {}
+83 -185
View File
@@ -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::<Vec<DidWebVerificationMethod>>(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::<Vec<_>>(),
"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<u8> =
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(), &current_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<AppState>, Path(handle): Path<Stri
let service_endpoint = migrated_to_pds.unwrap_or_else(|| format!("https://{}", hostname));
if let Some((ovr, parsed)) = overrides.as_ref().and_then(|ovr| {
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 = common::resolve_also_known_as(overrides.as_ref(), &current_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<Vec<serde_json::Value>, Response> {
if let Some(parsed) = overrides.and_then(|ovr| {
serde_json::from_value::<Vec<DidWebVerificationMethod>>(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::<Vec<_>>(),
"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<u8> =
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<AppState>,
auth: Auth<NotTakendown>,
) -> Result<Response, ApiError> {
) -> Result<Json<GetRecommendedDidCredentialsOutput>, 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<AppState>,
auth: Auth<NotTakendown>,
Json(input): Json<UpdateHandleInput>,
) -> Result<Response, ApiError> {
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
) -> Result<Json<EmptyResponse>, 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::<HandleUpdateLimit>(
&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(
+7 -10
View File
@@ -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<bool, Response> {
) -> Result<bool, ApiError> {
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<CanAddControllers<'a>, Response> {
) -> Result<CanAddControllers<'a>, 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<CanControlAccounts<'a>, Response> {
) -> Result<CanControlAccounts<'a>, ApiError> {
check_delegation_flag(
state,
&user.did,