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

fix: consolidate auth extractors & standardize usage

+2853 -2844
+7 -12
crates/tranquil-pds/src/api/actor/preferences.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAllowDeactivated; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 32 pub struct GetPreferencesOutput { 33 pub preferences: Vec<Value>, 34 } 35 - pub async fn get_preferences( 36 - State(state): State<AppState>, 37 - auth: BearerAuthAllowDeactivated, 38 - ) -> Response { 39 - let auth_user = auth.0; 40 - let has_full_access = auth_user.permissions().has_full_access(); 41 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth_user.did).await { 42 Ok(Some(id)) => id, 43 _ => { 44 return ApiError::InternalError(Some("User not found".into())).into_response(); ··· 93 } 94 pub async fn put_preferences( 95 State(state): State<AppState>, 96 - auth: BearerAuthAllowDeactivated, 97 Json(input): Json<PutPreferencesInput>, 98 ) -> Response { 99 - let auth_user = auth.0; 100 - let has_full_access = auth_user.permissions().has_full_access(); 101 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth_user.did).await { 102 Ok(Some(id)) => id, 103 _ => { 104 return ApiError::InternalError(Some("User not found".into())).into_response();
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::{Active, Auth}; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 32 pub struct GetPreferencesOutput { 33 pub preferences: Vec<Value>, 34 } 35 + pub async fn get_preferences(State(state): State<AppState>, auth: Auth<Active>) -> Response { 36 + let has_full_access = auth.permissions().has_full_access(); 37 + let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await { 38 Ok(Some(id)) => id, 39 _ => { 40 return ApiError::InternalError(Some("User not found".into())).into_response(); ··· 89 } 90 pub async fn put_preferences( 91 State(state): State<AppState>, 92 + auth: Auth<Active>, 93 Json(input): Json<PutPreferencesInput>, 94 ) -> Response { 95 + let has_full_access = auth.permissions().has_full_access(); 96 + let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await { 97 Ok(Some(id)) => id, 98 _ => { 99 return ApiError::InternalError(Some("User not found".into())).into_response();
+20 -18
crates/tranquil-pds/src/api/admin/account/delete.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAdmin; 4 use crate::state::AppState; 5 use crate::types::Did; 6 use axum::{ ··· 18 19 pub async fn delete_account( 20 State(state): State<AppState>, 21 - _auth: BearerAuthAdmin, 22 Json(input): Json<DeleteAccountInput>, 23 - ) -> Response { 24 let did = &input.did; 25 - let (user_id, handle) = match state.user_repo.get_id_and_handle_by_did(did).await { 26 - Ok(Some(row)) => (row.id, row.handle), 27 - Ok(None) => { 28 - return ApiError::AccountNotFound.into_response(); 29 - } 30 - Err(e) => { 31 error!("DB error in delete_account: {:?}", e); 32 - return ApiError::InternalError(None).into_response(); 33 - } 34 - }; 35 - if let Err(e) = state 36 .user_repo 37 .admin_delete_account_complete(user_id, did) 38 .await 39 - { 40 - error!("Failed to delete account {}: {:?}", did, e); 41 - return ApiError::InternalError(Some("Failed to delete account".into())).into_response(); 42 - } 43 if let Err(e) = 44 crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await 45 { ··· 49 ); 50 } 51 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 52 - EmptyResponse::ok().into_response() 53 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::{Admin, Auth}; 4 use crate::state::AppState; 5 use crate::types::Did; 6 use axum::{ ··· 18 19 pub async fn delete_account( 20 State(state): State<AppState>, 21 + _auth: Auth<Admin>, 22 Json(input): Json<DeleteAccountInput>, 23 + ) -> Result<Response, ApiError> { 24 let did = &input.did; 25 + let (user_id, handle) = state 26 + .user_repo 27 + .get_id_and_handle_by_did(did) 28 + .await 29 + .map_err(|e| { 30 error!("DB error in delete_account: {:?}", e); 31 + ApiError::InternalError(None) 32 + })? 33 + .ok_or(ApiError::AccountNotFound) 34 + .map(|row| (row.id, row.handle))?; 35 + 36 + state 37 .user_repo 38 .admin_delete_account_complete(user_id, did) 39 .await 40 + .map_err(|e| { 41 + error!("Failed to delete account {}: {:?}", did, e); 42 + ApiError::InternalError(Some("Failed to delete account".into())) 43 + })?; 44 + 45 if let Err(e) = 46 crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await 47 { ··· 51 ); 52 } 53 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 54 + Ok(EmptyResponse::ok().into_response()) 55 }
+16 -21
crates/tranquil-pds/src/api/admin/account/email.rs
··· 1 use crate::api::error::{ApiError, AtpJson}; 2 - use crate::auth::BearerAuthAdmin; 3 use crate::state::AppState; 4 use crate::types::Did; 5 use axum::{ ··· 28 29 pub async fn send_email( 30 State(state): State<AppState>, 31 - _auth: BearerAuthAdmin, 32 AtpJson(input): AtpJson<SendEmailInput>, 33 - ) -> Response { 34 let content = input.content.trim(); 35 if content.is_empty() { 36 - return ApiError::InvalidRequest("content is required".into()).into_response(); 37 } 38 - let user = match state.user_repo.get_by_did(&input.recipient_did).await { 39 - Ok(Some(row)) => row, 40 - Ok(None) => { 41 - return ApiError::AccountNotFound.into_response(); 42 - } 43 - Err(e) => { 44 error!("DB error in send_email: {:?}", e); 45 - return ApiError::InternalError(None).into_response(); 46 - } 47 - }; 48 - let email = match user.email { 49 - Some(e) => e, 50 - None => { 51 - return ApiError::NoEmail.into_response(); 52 - } 53 - }; 54 let (user_id, handle) = (user.id, user.handle); 55 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 56 let subject = input ··· 76 handle, 77 input.recipient_did 78 ); 79 - (StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response() 80 } 81 Err(e) => { 82 warn!("Failed to enqueue admin email: {:?}", e); 83 - (StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response() 84 } 85 } 86 }
··· 1 use crate::api::error::{ApiError, AtpJson}; 2 + use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use crate::types::Did; 5 use axum::{ ··· 28 29 pub async fn send_email( 30 State(state): State<AppState>, 31 + _auth: Auth<Admin>, 32 AtpJson(input): AtpJson<SendEmailInput>, 33 + ) -> Result<Response, ApiError> { 34 let content = input.content.trim(); 35 if content.is_empty() { 36 + return Err(ApiError::InvalidRequest("content is required".into())); 37 } 38 + let user = state 39 + .user_repo 40 + .get_by_did(&input.recipient_did) 41 + .await 42 + .map_err(|e| { 43 error!("DB error in send_email: {:?}", e); 44 + ApiError::InternalError(None) 45 + })? 46 + .ok_or(ApiError::AccountNotFound)?; 47 + 48 + let email = user.email.ok_or(ApiError::NoEmail)?; 49 let (user_id, handle) = (user.id, user.handle); 50 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 51 let subject = input ··· 71 handle, 72 input.recipient_did 73 ); 74 + Ok((StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()) 75 } 76 Err(e) => { 77 warn!("Failed to enqueue admin email: {:?}", e); 78 + Ok((StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response()) 79 } 80 } 81 }
+18 -24
crates/tranquil-pds/src/api/admin/account/info.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; 5 use axum::{ ··· 67 68 pub async fn get_account_info( 69 State(state): State<AppState>, 70 - _auth: BearerAuthAdmin, 71 Query(params): Query<GetAccountInfoParams>, 72 - ) -> Response { 73 - let account = match state 74 .infra_repo 75 .get_admin_account_info_by_did(&params.did) 76 .await 77 - { 78 - Ok(Some(a)) => a, 79 - Ok(None) => return ApiError::AccountNotFound.into_response(), 80 - Err(e) => { 81 error!("DB error in get_account_info: {:?}", e); 82 - return ApiError::InternalError(None).into_response(); 83 - } 84 - }; 85 86 let invited_by = get_invited_by(&state, account.id).await; 87 let invites = get_invites_for_user(&state, account.id).await; 88 89 - ( 90 StatusCode::OK, 91 Json(AccountInfo { 92 did: account.did, ··· 105 invites, 106 }), 107 ) 108 - .into_response() 109 } 110 111 async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> { ··· 200 201 pub async fn get_account_infos( 202 State(state): State<AppState>, 203 - _auth: BearerAuthAdmin, 204 RawQuery(raw_query): RawQuery, 205 - ) -> Response { 206 let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids") 207 .into_iter() 208 .filter(|d| !d.is_empty()) 209 .collect(); 210 211 if dids.is_empty() { 212 - return ApiError::InvalidRequest("dids is required".into()).into_response(); 213 } 214 215 let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect(); 216 - let accounts = match state 217 .infra_repo 218 .get_admin_account_infos_by_dids(&dids_typed) 219 .await 220 - { 221 - Ok(accounts) => accounts, 222 - Err(e) => { 223 error!("Failed to fetch account infos: {:?}", e); 224 - return ApiError::InternalError(None).into_response(); 225 - } 226 - }; 227 228 let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect(); 229 ··· 316 }) 317 .collect(); 318 319 - (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response() 320 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; 5 use axum::{ ··· 67 68 pub async fn get_account_info( 69 State(state): State<AppState>, 70 + _auth: Auth<Admin>, 71 Query(params): Query<GetAccountInfoParams>, 72 + ) -> Result<Response, ApiError> { 73 + let account = state 74 .infra_repo 75 .get_admin_account_info_by_did(&params.did) 76 .await 77 + .map_err(|e| { 78 error!("DB error in get_account_info: {:?}", e); 79 + ApiError::InternalError(None) 80 + })? 81 + .ok_or(ApiError::AccountNotFound)?; 82 83 let invited_by = get_invited_by(&state, account.id).await; 84 let invites = get_invites_for_user(&state, account.id).await; 85 86 + Ok(( 87 StatusCode::OK, 88 Json(AccountInfo { 89 did: account.did, ··· 102 invites, 103 }), 104 ) 105 + .into_response()) 106 } 107 108 async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> { ··· 197 198 pub async fn get_account_infos( 199 State(state): State<AppState>, 200 + _auth: Auth<Admin>, 201 RawQuery(raw_query): RawQuery, 202 + ) -> Result<Response, ApiError> { 203 let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids") 204 .into_iter() 205 .filter(|d| !d.is_empty()) 206 .collect(); 207 208 if dids.is_empty() { 209 + return Err(ApiError::InvalidRequest("dids is required".into())); 210 } 211 212 let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect(); 213 + let accounts = state 214 .infra_repo 215 .get_admin_account_infos_by_dids(&dids_typed) 216 .await 217 + .map_err(|e| { 218 error!("Failed to fetch account infos: {:?}", e); 219 + ApiError::InternalError(None) 220 + })?; 221 222 let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect(); 223 ··· 310 }) 311 .collect(); 312 313 + Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()) 314 }
+39 -42
crates/tranquil-pds/src/api/admin/account/search.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; 5 use axum::{ ··· 50 51 pub async fn search_accounts( 52 State(state): State<AppState>, 53 - _auth: BearerAuthAdmin, 54 Query(params): Query<SearchAccountsParams>, 55 - ) -> Response { 56 let limit = params.limit.clamp(1, 100); 57 let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); 58 let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); 59 let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|c| c.parse().ok()); 60 - let result = state 61 .user_repo 62 .search_accounts( 63 cursor_did.as_ref(), ··· 65 handle_filter.as_deref(), 66 limit + 1, 67 ) 68 - .await; 69 - match result { 70 - Ok(rows) => { 71 - let has_more = rows.len() > limit as usize; 72 - let accounts: Vec<AccountView> = rows 73 - .into_iter() 74 - .take(limit as usize) 75 - .map(|row| AccountView { 76 - did: row.did.clone(), 77 - handle: row.handle, 78 - email: row.email, 79 - indexed_at: row.created_at.to_rfc3339(), 80 - email_confirmed_at: if row.email_verified { 81 - Some(row.created_at.to_rfc3339()) 82 - } else { 83 - None 84 - }, 85 - deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()), 86 - invites_disabled: row.invites_disabled, 87 - }) 88 - .collect(); 89 - let next_cursor = if has_more { 90 - accounts.last().map(|a| a.did.to_string()) 91 } else { 92 None 93 - }; 94 - ( 95 - StatusCode::OK, 96 - Json(SearchAccountsOutput { 97 - cursor: next_cursor, 98 - accounts, 99 - }), 100 - ) 101 - .into_response() 102 - } 103 - Err(e) => { 104 - error!("DB error in search_accounts: {:?}", e); 105 - ApiError::InternalError(None).into_response() 106 - } 107 - } 108 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; 5 use axum::{ ··· 50 51 pub async fn search_accounts( 52 State(state): State<AppState>, 53 + _auth: Auth<Admin>, 54 Query(params): Query<SearchAccountsParams>, 55 + ) -> Result<Response, ApiError> { 56 let limit = params.limit.clamp(1, 100); 57 let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); 58 let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); 59 let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|c| c.parse().ok()); 60 + let rows = state 61 .user_repo 62 .search_accounts( 63 cursor_did.as_ref(), ··· 65 handle_filter.as_deref(), 66 limit + 1, 67 ) 68 + .await 69 + .map_err(|e| { 70 + error!("DB error in search_accounts: {:?}", e); 71 + ApiError::InternalError(None) 72 + })?; 73 + 74 + let has_more = rows.len() > limit as usize; 75 + let accounts: Vec<AccountView> = rows 76 + .into_iter() 77 + .take(limit as usize) 78 + .map(|row| AccountView { 79 + did: row.did.clone(), 80 + handle: row.handle, 81 + email: row.email, 82 + indexed_at: row.created_at.to_rfc3339(), 83 + email_confirmed_at: if row.email_verified { 84 + Some(row.created_at.to_rfc3339()) 85 } else { 86 None 87 + }, 88 + deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()), 89 + invites_disabled: row.invites_disabled, 90 + }) 91 + .collect(); 92 + let next_cursor = if has_more { 93 + accounts.last().map(|a| a.did.to_string()) 94 + } else { 95 + None 96 + }; 97 + Ok(( 98 + StatusCode::OK, 99 + Json(SearchAccountsOutput { 100 + cursor: next_cursor, 101 + accounts, 102 + }), 103 + ) 104 + .into_response()) 105 }
+39 -36
crates/tranquil-pds/src/api/admin/account/update.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAdmin; 4 use crate::state::AppState; 5 use crate::types::{Did, Handle, PlainPassword}; 6 use axum::{ ··· 19 20 pub async fn update_account_email( 21 State(state): State<AppState>, 22 - _auth: BearerAuthAdmin, 23 Json(input): Json<UpdateAccountEmailInput>, 24 - ) -> Response { 25 let account = input.account.trim(); 26 let email = input.email.trim(); 27 if account.is_empty() || email.is_empty() { 28 - return ApiError::InvalidRequest("account and email are required".into()).into_response(); 29 } 30 - let account_did: Did = match account.parse() { 31 - Ok(d) => d, 32 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 33 - }; 34 match state 35 .user_repo 36 .admin_update_email(&account_did, email) 37 .await 38 { 39 - Ok(0) => ApiError::AccountNotFound.into_response(), 40 - Ok(_) => EmptyResponse::ok().into_response(), 41 Err(e) => { 42 error!("DB error updating email: {:?}", e); 43 - ApiError::InternalError(None).into_response() 44 } 45 } 46 } ··· 53 54 pub async fn update_account_handle( 55 State(state): State<AppState>, 56 - _auth: BearerAuthAdmin, 57 Json(input): Json<UpdateAccountHandleInput>, 58 - ) -> Response { 59 let did = &input.did; 60 let input_handle = input.handle.trim(); 61 if input_handle.is_empty() { 62 - return ApiError::InvalidRequest("handle is required".into()).into_response(); 63 } 64 if !input_handle 65 .chars() 66 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 67 { 68 - return ApiError::InvalidHandle(None).into_response(); 69 } 70 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 71 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 75 input_handle.to_string() 76 }; 77 let old_handle = state.user_repo.get_handle_by_did(did).await.ok().flatten(); 78 - let user_id = match state.user_repo.get_id_by_did(did).await { 79 - Ok(Some(id)) => id, 80 - _ => return ApiError::AccountNotFound.into_response(), 81 - }; 82 let handle_for_check = Handle::new_unchecked(&handle); 83 if let Ok(true) = state 84 .user_repo 85 .check_handle_exists(&handle_for_check, user_id) 86 .await 87 { 88 - return ApiError::HandleTaken.into_response(); 89 } 90 match state 91 .user_repo 92 .admin_update_handle(did, &handle_for_check) 93 .await 94 { 95 - Ok(0) => ApiError::AccountNotFound.into_response(), 96 Ok(_) => { 97 if let Some(old) = old_handle { 98 let _ = state.cache.delete(&format!("handle:{}", old)).await; ··· 115 { 116 warn!("Failed to update PLC handle for admin handle update: {}", e); 117 } 118 - EmptyResponse::ok().into_response() 119 } 120 Err(e) => { 121 error!("DB error updating handle: {:?}", e); 122 - ApiError::InternalError(None).into_response() 123 } 124 } 125 } ··· 132 133 pub async fn update_account_password( 134 State(state): State<AppState>, 135 - _auth: BearerAuthAdmin, 136 Json(input): Json<UpdateAccountPasswordInput>, 137 - ) -> Response { 138 let did = &input.did; 139 let password = input.password.trim(); 140 if password.is_empty() { 141 - return ApiError::InvalidRequest("password is required".into()).into_response(); 142 } 143 - let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) { 144 - Ok(h) => h, 145 - Err(e) => { 146 - error!("Failed to hash password: {:?}", e); 147 - return ApiError::InternalError(None).into_response(); 148 - } 149 - }; 150 match state 151 .user_repo 152 .admin_update_password(did, &password_hash) 153 .await 154 { 155 - Ok(0) => ApiError::AccountNotFound.into_response(), 156 - Ok(_) => EmptyResponse::ok().into_response(), 157 Err(e) => { 158 error!("DB error updating password: {:?}", e); 159 - ApiError::InternalError(None).into_response() 160 } 161 } 162 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::{Admin, Auth}; 4 use crate::state::AppState; 5 use crate::types::{Did, Handle, PlainPassword}; 6 use axum::{ ··· 19 20 pub async fn update_account_email( 21 State(state): State<AppState>, 22 + _auth: Auth<Admin>, 23 Json(input): Json<UpdateAccountEmailInput>, 24 + ) -> Result<Response, ApiError> { 25 let account = input.account.trim(); 26 let email = input.email.trim(); 27 if account.is_empty() || email.is_empty() { 28 + return Err(ApiError::InvalidRequest( 29 + "account and email are required".into(), 30 + )); 31 } 32 + let account_did: Did = account 33 + .parse() 34 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 35 + 36 match state 37 .user_repo 38 .admin_update_email(&account_did, email) 39 .await 40 { 41 + Ok(0) => Err(ApiError::AccountNotFound), 42 + Ok(_) => Ok(EmptyResponse::ok().into_response()), 43 Err(e) => { 44 error!("DB error updating email: {:?}", e); 45 + Err(ApiError::InternalError(None)) 46 } 47 } 48 } ··· 55 56 pub async fn update_account_handle( 57 State(state): State<AppState>, 58 + _auth: Auth<Admin>, 59 Json(input): Json<UpdateAccountHandleInput>, 60 + ) -> Result<Response, ApiError> { 61 let did = &input.did; 62 let input_handle = input.handle.trim(); 63 if input_handle.is_empty() { 64 + return Err(ApiError::InvalidRequest("handle is required".into())); 65 } 66 if !input_handle 67 .chars() 68 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 69 { 70 + return Err(ApiError::InvalidHandle(None)); 71 } 72 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 73 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 77 input_handle.to_string() 78 }; 79 let old_handle = state.user_repo.get_handle_by_did(did).await.ok().flatten(); 80 + let user_id = state 81 + .user_repo 82 + .get_id_by_did(did) 83 + .await 84 + .ok() 85 + .flatten() 86 + .ok_or(ApiError::AccountNotFound)?; 87 let handle_for_check = Handle::new_unchecked(&handle); 88 if let Ok(true) = state 89 .user_repo 90 .check_handle_exists(&handle_for_check, user_id) 91 .await 92 { 93 + return Err(ApiError::HandleTaken); 94 } 95 match state 96 .user_repo 97 .admin_update_handle(did, &handle_for_check) 98 .await 99 { 100 + Ok(0) => Err(ApiError::AccountNotFound), 101 Ok(_) => { 102 if let Some(old) = old_handle { 103 let _ = state.cache.delete(&format!("handle:{}", old)).await; ··· 120 { 121 warn!("Failed to update PLC handle for admin handle update: {}", e); 122 } 123 + Ok(EmptyResponse::ok().into_response()) 124 } 125 Err(e) => { 126 error!("DB error updating handle: {:?}", e); 127 + Err(ApiError::InternalError(None)) 128 } 129 } 130 } ··· 137 138 pub async fn update_account_password( 139 State(state): State<AppState>, 140 + _auth: Auth<Admin>, 141 Json(input): Json<UpdateAccountPasswordInput>, 142 + ) -> Result<Response, ApiError> { 143 let did = &input.did; 144 let password = input.password.trim(); 145 if password.is_empty() { 146 + return Err(ApiError::InvalidRequest("password is required".into())); 147 } 148 + let password_hash = bcrypt::hash(password, bcrypt::DEFAULT_COST).map_err(|e| { 149 + error!("Failed to hash password: {:?}", e); 150 + ApiError::InternalError(None) 151 + })?; 152 + 153 match state 154 .user_repo 155 .admin_update_password(did, &password_hash) 156 .await 157 { 158 + Ok(0) => Err(ApiError::AccountNotFound), 159 + Ok(_) => Ok(EmptyResponse::ok().into_response()), 160 Err(e) => { 161 error!("DB error updating password: {:?}", e); 162 + Err(ApiError::InternalError(None)) 163 } 164 } 165 }
+2 -2
crates/tranquil-pds/src/api/admin/config.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 3 use crate::state::AppState; 4 use axum::{Json, extract::State}; 5 use serde::{Deserialize, Serialize}; ··· 78 79 pub async fn update_server_config( 80 State(state): State<AppState>, 81 - _admin: BearerAuthAdmin, 82 Json(req): Json<UpdateServerConfigRequest>, 83 ) -> Result<Json<UpdateServerConfigResponse>, ApiError> { 84 if let Some(server_name) = req.server_name {
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use axum::{Json, extract::State}; 5 use serde::{Deserialize, Serialize}; ··· 78 79 pub async fn update_server_config( 80 State(state): State<AppState>, 81 + _auth: Auth<Admin>, 82 Json(req): Json<UpdateServerConfigRequest>, 83 ) -> Result<Json<UpdateServerConfigResponse>, ApiError> { 84 if let Some(server_name) = req.server_name {
+32 -35
crates/tranquil-pds/src/api/admin/invite.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAdmin; 4 use crate::state::AppState; 5 use axum::{ 6 Json, ··· 21 22 pub async fn disable_invite_codes( 23 State(state): State<AppState>, 24 - _auth: BearerAuthAdmin, 25 Json(input): Json<DisableInviteCodesInput>, 26 - ) -> Response { 27 if let Some(codes) = &input.codes 28 && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await 29 { ··· 40 error!("DB error disabling invite codes by account: {:?}", e); 41 } 42 } 43 - EmptyResponse::ok().into_response() 44 } 45 46 #[derive(Deserialize)] ··· 78 79 pub async fn get_invite_codes( 80 State(state): State<AppState>, 81 - _auth: BearerAuthAdmin, 82 Query(params): Query<GetInviteCodesParams>, 83 - ) -> Response { 84 let limit = params.limit.unwrap_or(100).clamp(1, 500); 85 let sort_order = match params.sort.as_deref() { 86 Some("usage") => InviteCodeSortOrder::Usage, 87 _ => InviteCodeSortOrder::Recent, 88 }; 89 90 - let codes_rows = match state 91 .infra_repo 92 .list_invite_codes(params.cursor.as_deref(), limit, sort_order) 93 .await 94 - { 95 - Ok(rows) => rows, 96 - Err(e) => { 97 error!("DB error fetching invite codes: {:?}", e); 98 - return ApiError::InternalError(None).into_response(); 99 - } 100 - }; 101 102 let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect(); 103 let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect(); ··· 155 } else { 156 None 157 }; 158 - ( 159 StatusCode::OK, 160 Json(GetInviteCodesOutput { 161 cursor: next_cursor, 162 codes, 163 }), 164 ) 165 - .into_response() 166 } 167 168 #[derive(Deserialize)] ··· 172 173 pub async fn disable_account_invites( 174 State(state): State<AppState>, 175 - _auth: BearerAuthAdmin, 176 Json(input): Json<DisableAccountInvitesInput>, 177 - ) -> Response { 178 let account = input.account.trim(); 179 if account.is_empty() { 180 - return ApiError::InvalidRequest("account is required".into()).into_response(); 181 } 182 - let account_did: tranquil_types::Did = match account.parse() { 183 - Ok(d) => d, 184 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 185 - }; 186 match state 187 .user_repo 188 .set_invites_disabled(&account_did, true) 189 .await 190 { 191 - Ok(true) => EmptyResponse::ok().into_response(), 192 - Ok(false) => ApiError::AccountNotFound.into_response(), 193 Err(e) => { 194 error!("DB error disabling account invites: {:?}", e); 195 - ApiError::InternalError(None).into_response() 196 } 197 } 198 } ··· 204 205 pub async fn enable_account_invites( 206 State(state): State<AppState>, 207 - _auth: BearerAuthAdmin, 208 Json(input): Json<EnableAccountInvitesInput>, 209 - ) -> Response { 210 let account = input.account.trim(); 211 if account.is_empty() { 212 - return ApiError::InvalidRequest("account is required".into()).into_response(); 213 } 214 - let account_did: tranquil_types::Did = match account.parse() { 215 - Ok(d) => d, 216 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 217 - }; 218 match state 219 .user_repo 220 .set_invites_disabled(&account_did, false) 221 .await 222 { 223 - Ok(true) => EmptyResponse::ok().into_response(), 224 - Ok(false) => ApiError::AccountNotFound.into_response(), 225 Err(e) => { 226 error!("DB error enabling account invites: {:?}", e); 227 - ApiError::InternalError(None).into_response() 228 } 229 } 230 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::{Admin, Auth}; 4 use crate::state::AppState; 5 use axum::{ 6 Json, ··· 21 22 pub async fn disable_invite_codes( 23 State(state): State<AppState>, 24 + _auth: Auth<Admin>, 25 Json(input): Json<DisableInviteCodesInput>, 26 + ) -> Result<Response, ApiError> { 27 if let Some(codes) = &input.codes 28 && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await 29 { ··· 40 error!("DB error disabling invite codes by account: {:?}", e); 41 } 42 } 43 + Ok(EmptyResponse::ok().into_response()) 44 } 45 46 #[derive(Deserialize)] ··· 78 79 pub async fn get_invite_codes( 80 State(state): State<AppState>, 81 + _auth: Auth<Admin>, 82 Query(params): Query<GetInviteCodesParams>, 83 + ) -> Result<Response, ApiError> { 84 let limit = params.limit.unwrap_or(100).clamp(1, 500); 85 let sort_order = match params.sort.as_deref() { 86 Some("usage") => InviteCodeSortOrder::Usage, 87 _ => InviteCodeSortOrder::Recent, 88 }; 89 90 + let codes_rows = state 91 .infra_repo 92 .list_invite_codes(params.cursor.as_deref(), limit, sort_order) 93 .await 94 + .map_err(|e| { 95 error!("DB error fetching invite codes: {:?}", e); 96 + ApiError::InternalError(None) 97 + })?; 98 99 let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect(); 100 let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect(); ··· 152 } else { 153 None 154 }; 155 + Ok(( 156 StatusCode::OK, 157 Json(GetInviteCodesOutput { 158 cursor: next_cursor, 159 codes, 160 }), 161 ) 162 + .into_response()) 163 } 164 165 #[derive(Deserialize)] ··· 169 170 pub async fn disable_account_invites( 171 State(state): State<AppState>, 172 + _auth: Auth<Admin>, 173 Json(input): Json<DisableAccountInvitesInput>, 174 + ) -> Result<Response, ApiError> { 175 let account = input.account.trim(); 176 if account.is_empty() { 177 + return Err(ApiError::InvalidRequest("account is required".into())); 178 } 179 + let account_did: tranquil_types::Did = account 180 + .parse() 181 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 182 + 183 match state 184 .user_repo 185 .set_invites_disabled(&account_did, true) 186 .await 187 { 188 + Ok(true) => Ok(EmptyResponse::ok().into_response()), 189 + Ok(false) => Err(ApiError::AccountNotFound), 190 Err(e) => { 191 error!("DB error disabling account invites: {:?}", e); 192 + Err(ApiError::InternalError(None)) 193 } 194 } 195 } ··· 201 202 pub async fn enable_account_invites( 203 State(state): State<AppState>, 204 + _auth: Auth<Admin>, 205 Json(input): Json<EnableAccountInvitesInput>, 206 + ) -> Result<Response, ApiError> { 207 let account = input.account.trim(); 208 if account.is_empty() { 209 + return Err(ApiError::InvalidRequest("account is required".into())); 210 } 211 + let account_did: tranquil_types::Did = account 212 + .parse() 213 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 214 + 215 match state 216 .user_repo 217 .set_invites_disabled(&account_did, false) 218 .await 219 { 220 + Ok(true) => Ok(EmptyResponse::ok().into_response()), 221 + Ok(false) => Err(ApiError::AccountNotFound), 222 Err(e) => { 223 error!("DB error enabling account invites: {:?}", e); 224 + Err(ApiError::InternalError(None)) 225 } 226 } 227 }
+8 -4
crates/tranquil-pds/src/api/admin/server_stats.rs
··· 1 - use crate::auth::BearerAuthAdmin; 2 use crate::state::AppState; 3 use axum::{ 4 Json, ··· 16 pub blob_storage_bytes: i64, 17 } 18 19 - pub async fn get_server_stats(State(state): State<AppState>, _auth: BearerAuthAdmin) -> Response { 20 let user_count = state.user_repo.count_users().await.unwrap_or(0); 21 let repo_count = state.repo_repo.count_repos().await.unwrap_or(0); 22 let record_count = state.repo_repo.count_all_records().await.unwrap_or(0); 23 let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0); 24 25 - Json(ServerStatsResponse { 26 user_count, 27 repo_count, 28 record_count, 29 blob_storage_bytes, 30 }) 31 - .into_response() 32 }
··· 1 + use crate::api::error::ApiError; 2 + use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 17 pub blob_storage_bytes: i64, 18 } 19 20 + pub async fn get_server_stats( 21 + State(state): State<AppState>, 22 + _auth: Auth<Admin>, 23 + ) -> Result<Response, ApiError> { 24 let user_count = state.user_repo.count_users().await.unwrap_or(0); 25 let repo_count = state.repo_repo.count_repos().await.unwrap_or(0); 26 let record_count = state.repo_repo.count_all_records().await.unwrap_or(0); 27 let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0); 28 29 + Ok(Json(ServerStatsResponse { 30 user_count, 31 repo_count, 32 record_count, 33 blob_storage_bytes, 34 }) 35 + .into_response()) 36 }
+73 -94
crates/tranquil-pds/src/api/admin/status.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 3 use crate::state::AppState; 4 use crate::types::{CidLink, Did}; 5 use axum::{ ··· 35 36 pub async fn get_subject_status( 37 State(state): State<AppState>, 38 - _auth: BearerAuthAdmin, 39 Query(params): Query<GetSubjectStatusParams>, 40 - ) -> Response { 41 if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { 42 - return ApiError::InvalidRequest("Must provide did, uri, or blob".into()).into_response(); 43 } 44 if let Some(did_str) = &params.did { 45 - let did: Did = match did_str.parse() { 46 - Ok(d) => d, 47 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 48 - }; 49 match state.user_repo.get_status_by_did(&did).await { 50 Ok(Some(status)) => { 51 let deactivated = status.deactivated_at.map(|_| StatusAttr { ··· 56 applied: true, 57 r#ref: Some(r.clone()), 58 }); 59 - return ( 60 StatusCode::OK, 61 Json(SubjectStatus { 62 subject: json!({ ··· 67 deactivated, 68 }), 69 ) 70 - .into_response(); 71 } 72 Ok(None) => { 73 - return ApiError::SubjectNotFound.into_response(); 74 } 75 Err(e) => { 76 error!("DB error in get_subject_status: {:?}", e); 77 - return ApiError::InternalError(None).into_response(); 78 } 79 } 80 } 81 if let Some(uri_str) = &params.uri { 82 - let cid: CidLink = match uri_str.parse() { 83 - Ok(c) => c, 84 - Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(), 85 - }; 86 match state.repo_repo.get_record_by_cid(&cid).await { 87 Ok(Some(record)) => { 88 let takedown = record.takedown_ref.as_ref().map(|r| StatusAttr { 89 applied: true, 90 r#ref: Some(r.clone()), 91 }); 92 - return ( 93 StatusCode::OK, 94 Json(SubjectStatus { 95 subject: json!({ ··· 101 deactivated: None, 102 }), 103 ) 104 - .into_response(); 105 } 106 Ok(None) => { 107 - return ApiError::RecordNotFound.into_response(); 108 } 109 Err(e) => { 110 error!("DB error in get_subject_status: {:?}", e); 111 - return ApiError::InternalError(None).into_response(); 112 } 113 } 114 } 115 if let Some(blob_cid_str) = &params.blob { 116 - let blob_cid: CidLink = match blob_cid_str.parse() { 117 - Ok(c) => c, 118 - Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(), 119 - }; 120 - let did = match &params.did { 121 - Some(d) => d, 122 - None => { 123 - return ApiError::InvalidRequest("Must provide a did to request blob state".into()) 124 - .into_response(); 125 - } 126 - }; 127 match state.blob_repo.get_blob_with_takedown(&blob_cid).await { 128 Ok(Some(blob)) => { 129 let takedown = blob.takedown_ref.as_ref().map(|r| StatusAttr { 130 applied: true, 131 r#ref: Some(r.clone()), 132 }); 133 - return ( 134 StatusCode::OK, 135 Json(SubjectStatus { 136 subject: json!({ ··· 142 deactivated: None, 143 }), 144 ) 145 - .into_response(); 146 } 147 Ok(None) => { 148 - return ApiError::BlobNotFound(None).into_response(); 149 } 150 Err(e) => { 151 error!("DB error in get_subject_status: {:?}", e); 152 - return ApiError::InternalError(None).into_response(); 153 } 154 } 155 } 156 - ApiError::InvalidRequest("Invalid subject type".into()).into_response() 157 } 158 159 #[derive(Deserialize)] ··· 172 173 pub async fn update_subject_status( 174 State(state): State<AppState>, 175 - _auth: BearerAuthAdmin, 176 Json(input): Json<UpdateSubjectStatusInput>, 177 - ) -> Response { 178 let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 179 match subject_type { 180 Some("com.atproto.admin.defs#repoRef") => { ··· 187 } else { 188 None 189 }; 190 - if let Err(e) = state.user_repo.set_user_takedown(&did, takedown_ref).await { 191 - error!("Failed to update user takedown status for {}: {:?}", did, e); 192 - return ApiError::InternalError(Some( 193 - "Failed to update takedown status".into(), 194 - )) 195 - .into_response(); 196 - } 197 } 198 if let Some(deactivated) = &input.deactivated { 199 let result = if deactivated.applied { ··· 201 } else { 202 state.user_repo.activate_account(&did).await 203 }; 204 - if let Err(e) = result { 205 error!( 206 "Failed to update user deactivation status for {}: {:?}", 207 did, e 208 ); 209 - return ApiError::InternalError(Some( 210 - "Failed to update deactivation status".into(), 211 - )) 212 - .into_response(); 213 - } 214 } 215 if let Some(takedown) = &input.takedown { 216 let status = if takedown.applied { ··· 249 if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await { 250 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 251 } 252 - return ( 253 StatusCode::OK, 254 Json(json!({ 255 "subject": input.subject, ··· 262 })) 263 })), 264 ) 265 - .into_response(); 266 } 267 } 268 Some("com.atproto.repo.strongRef") => { 269 let uri_str = input.subject.get("uri").and_then(|u| u.as_str()); 270 if let Some(uri_str) = uri_str { 271 - let cid: CidLink = match uri_str.parse() { 272 - Ok(c) => c, 273 - Err(_) => { 274 - return ApiError::InvalidRequest("Invalid CID format".into()) 275 - .into_response(); 276 - } 277 - }; 278 if let Some(takedown) = &input.takedown { 279 let takedown_ref = if takedown.applied { 280 takedown.r#ref.as_deref() 281 } else { 282 None 283 }; 284 - if let Err(e) = state 285 .repo_repo 286 .set_record_takedown(&cid, takedown_ref) 287 .await 288 - { 289 - error!( 290 - "Failed to update record takedown status for {}: {:?}", 291 - uri_str, e 292 - ); 293 - return ApiError::InternalError(Some( 294 - "Failed to update takedown status".into(), 295 - )) 296 - .into_response(); 297 - } 298 } 299 - return ( 300 StatusCode::OK, 301 Json(json!({ 302 "subject": input.subject, ··· 306 })) 307 })), 308 ) 309 - .into_response(); 310 } 311 } 312 Some("com.atproto.admin.defs#repoBlobRef") => { 313 let cid_str = input.subject.get("cid").and_then(|c| c.as_str()); 314 if let Some(cid_str) = cid_str { 315 - let cid: CidLink = match cid_str.parse() { 316 - Ok(c) => c, 317 - Err(_) => { 318 - return ApiError::InvalidRequest("Invalid CID format".into()) 319 - .into_response(); 320 - } 321 - }; 322 if let Some(takedown) = &input.takedown { 323 let takedown_ref = if takedown.applied { 324 takedown.r#ref.as_deref() 325 } else { 326 None 327 }; 328 - if let Err(e) = state 329 .blob_repo 330 .update_blob_takedown(&cid, takedown_ref) 331 .await 332 - { 333 - error!( 334 - "Failed to update blob takedown status for {}: {:?}", 335 - cid_str, e 336 - ); 337 - return ApiError::InternalError(Some( 338 - "Failed to update takedown status".into(), 339 - )) 340 - .into_response(); 341 - } 342 } 343 - return ( 344 StatusCode::OK, 345 Json(json!({ 346 "subject": input.subject, ··· 350 })) 351 })), 352 ) 353 - .into_response(); 354 } 355 } 356 _ => {} 357 } 358 - ApiError::InvalidRequest("Invalid subject type".into()).into_response() 359 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::{Admin, Auth}; 3 use crate::state::AppState; 4 use crate::types::{CidLink, Did}; 5 use axum::{ ··· 35 36 pub async fn get_subject_status( 37 State(state): State<AppState>, 38 + _auth: Auth<Admin>, 39 Query(params): Query<GetSubjectStatusParams>, 40 + ) -> Result<Response, ApiError> { 41 if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { 42 + return Err(ApiError::InvalidRequest( 43 + "Must provide did, uri, or blob".into(), 44 + )); 45 } 46 if let Some(did_str) = &params.did { 47 + let did: Did = did_str 48 + .parse() 49 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 50 match state.user_repo.get_status_by_did(&did).await { 51 Ok(Some(status)) => { 52 let deactivated = status.deactivated_at.map(|_| StatusAttr { ··· 57 applied: true, 58 r#ref: Some(r.clone()), 59 }); 60 + return Ok(( 61 StatusCode::OK, 62 Json(SubjectStatus { 63 subject: json!({ ··· 68 deactivated, 69 }), 70 ) 71 + .into_response()); 72 } 73 Ok(None) => { 74 + return Err(ApiError::SubjectNotFound); 75 } 76 Err(e) => { 77 error!("DB error in get_subject_status: {:?}", e); 78 + return Err(ApiError::InternalError(None)); 79 } 80 } 81 } 82 if let Some(uri_str) = &params.uri { 83 + let cid: CidLink = uri_str 84 + .parse() 85 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 86 match state.repo_repo.get_record_by_cid(&cid).await { 87 Ok(Some(record)) => { 88 let takedown = record.takedown_ref.as_ref().map(|r| StatusAttr { 89 applied: true, 90 r#ref: Some(r.clone()), 91 }); 92 + return Ok(( 93 StatusCode::OK, 94 Json(SubjectStatus { 95 subject: json!({ ··· 101 deactivated: None, 102 }), 103 ) 104 + .into_response()); 105 } 106 Ok(None) => { 107 + return Err(ApiError::RecordNotFound); 108 } 109 Err(e) => { 110 error!("DB error in get_subject_status: {:?}", e); 111 + return Err(ApiError::InternalError(None)); 112 } 113 } 114 } 115 if let Some(blob_cid_str) = &params.blob { 116 + let blob_cid: CidLink = blob_cid_str 117 + .parse() 118 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 119 + let did = params.did.as_ref().ok_or_else(|| { 120 + ApiError::InvalidRequest("Must provide a did to request blob state".into()) 121 + })?; 122 match state.blob_repo.get_blob_with_takedown(&blob_cid).await { 123 Ok(Some(blob)) => { 124 let takedown = blob.takedown_ref.as_ref().map(|r| StatusAttr { 125 applied: true, 126 r#ref: Some(r.clone()), 127 }); 128 + return Ok(( 129 StatusCode::OK, 130 Json(SubjectStatus { 131 subject: json!({ ··· 137 deactivated: None, 138 }), 139 ) 140 + .into_response()); 141 } 142 Ok(None) => { 143 + return Err(ApiError::BlobNotFound(None)); 144 } 145 Err(e) => { 146 error!("DB error in get_subject_status: {:?}", e); 147 + return Err(ApiError::InternalError(None)); 148 } 149 } 150 } 151 + Err(ApiError::InvalidRequest("Invalid subject type".into())) 152 } 153 154 #[derive(Deserialize)] ··· 167 168 pub async fn update_subject_status( 169 State(state): State<AppState>, 170 + _auth: Auth<Admin>, 171 Json(input): Json<UpdateSubjectStatusInput>, 172 + ) -> Result<Response, ApiError> { 173 let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 174 match subject_type { 175 Some("com.atproto.admin.defs#repoRef") => { ··· 182 } else { 183 None 184 }; 185 + state 186 + .user_repo 187 + .set_user_takedown(&did, takedown_ref) 188 + .await 189 + .map_err(|e| { 190 + error!("Failed to update user takedown status for {}: {:?}", did, e); 191 + ApiError::InternalError(Some("Failed to update takedown status".into())) 192 + })?; 193 } 194 if let Some(deactivated) = &input.deactivated { 195 let result = if deactivated.applied { ··· 197 } else { 198 state.user_repo.activate_account(&did).await 199 }; 200 + result.map_err(|e| { 201 error!( 202 "Failed to update user deactivation status for {}: {:?}", 203 did, e 204 ); 205 + ApiError::InternalError(Some("Failed to update deactivation status".into())) 206 + })?; 207 } 208 if let Some(takedown) = &input.takedown { 209 let status = if takedown.applied { ··· 242 if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await { 243 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 244 } 245 + return Ok(( 246 StatusCode::OK, 247 Json(json!({ 248 "subject": input.subject, ··· 255 })) 256 })), 257 ) 258 + .into_response()); 259 } 260 } 261 Some("com.atproto.repo.strongRef") => { 262 let uri_str = input.subject.get("uri").and_then(|u| u.as_str()); 263 if let Some(uri_str) = uri_str { 264 + let cid: CidLink = uri_str 265 + .parse() 266 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 267 if let Some(takedown) = &input.takedown { 268 let takedown_ref = if takedown.applied { 269 takedown.r#ref.as_deref() 270 } else { 271 None 272 }; 273 + state 274 .repo_repo 275 .set_record_takedown(&cid, takedown_ref) 276 .await 277 + .map_err(|e| { 278 + error!( 279 + "Failed to update record takedown status for {}: {:?}", 280 + uri_str, e 281 + ); 282 + ApiError::InternalError(Some("Failed to update takedown status".into())) 283 + })?; 284 } 285 + return Ok(( 286 StatusCode::OK, 287 Json(json!({ 288 "subject": input.subject, ··· 292 })) 293 })), 294 ) 295 + .into_response()); 296 } 297 } 298 Some("com.atproto.admin.defs#repoBlobRef") => { 299 let cid_str = input.subject.get("cid").and_then(|c| c.as_str()); 300 if let Some(cid_str) = cid_str { 301 + let cid: CidLink = cid_str 302 + .parse() 303 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 304 if let Some(takedown) = &input.takedown { 305 let takedown_ref = if takedown.applied { 306 takedown.r#ref.as_deref() 307 } else { 308 None 309 }; 310 + state 311 .blob_repo 312 .update_blob_takedown(&cid, takedown_ref) 313 .await 314 + .map_err(|e| { 315 + error!( 316 + "Failed to update blob takedown status for {}: {:?}", 317 + cid_str, e 318 + ); 319 + ApiError::InternalError(Some("Failed to update takedown status".into())) 320 + })?; 321 } 322 + return Ok(( 323 StatusCode::OK, 324 Json(json!({ 325 "subject": input.subject, ··· 329 })) 330 })), 331 ) 332 + .into_response()); 333 } 334 } 335 _ => {} 336 } 337 + Err(ApiError::InvalidRequest("Invalid subject type".into())) 338 }
+93 -73
crates/tranquil-pds/src/api/backup.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, EnabledResponse}; 3 - use crate::auth::BearerAuth; 4 use crate::scheduled::generate_full_backup; 5 use crate::state::AppState; 6 use crate::storage::{BackupStorage, backup_retention_count}; ··· 35 pub backup_enabled: bool, 36 } 37 38 - pub async fn list_backups(State(state): State<AppState>, auth: BearerAuth) -> Response { 39 - let (user_id, backup_enabled) = 40 - match state.backup_repo.get_user_backup_status(&auth.0.did).await { 41 - Ok(Some(status)) => status, 42 - Ok(None) => { 43 - return ApiError::AccountNotFound.into_response(); 44 - } 45 - Err(e) => { 46 - error!("DB error fetching user: {:?}", e); 47 - return ApiError::InternalError(None).into_response(); 48 - } 49 - }; 50 51 let backups = match state.backup_repo.list_backups_for_user(user_id).await { 52 Ok(rows) => rows, 53 Err(e) => { 54 error!("DB error fetching backups: {:?}", e); 55 - return ApiError::InternalError(None).into_response(); 56 } 57 }; 58 ··· 68 }) 69 .collect(); 70 71 - ( 72 StatusCode::OK, 73 Json(ListBackupsOutput { 74 backups: backup_list, 75 backup_enabled, 76 }), 77 ) 78 - .into_response() 79 } 80 81 #[derive(Deserialize)] ··· 85 86 pub async fn get_backup( 87 State(state): State<AppState>, 88 - auth: BearerAuth, 89 Query(query): Query<GetBackupQuery>, 90 - ) -> Response { 91 let backup_id = match uuid::Uuid::parse_str(&query.id) { 92 Ok(id) => id, 93 Err(_) => { 94 - return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); 95 } 96 }; 97 98 let backup_info = match state 99 .backup_repo 100 - .get_backup_storage_info(backup_id, &auth.0.did) 101 .await 102 { 103 Ok(Some(b)) => b, 104 Ok(None) => { 105 - return ApiError::BackupNotFound.into_response(); 106 } 107 Err(e) => { 108 error!("DB error fetching backup: {:?}", e); 109 - return ApiError::InternalError(None).into_response(); 110 } 111 }; 112 113 let backup_storage = match state.backup_storage.as_ref() { 114 Some(storage) => storage, 115 None => { 116 - return ApiError::BackupsDisabled.into_response(); 117 } 118 }; 119 ··· 121 Ok(bytes) => bytes, 122 Err(e) => { 123 error!("Failed to fetch backup from storage: {:?}", e); 124 - return ApiError::InternalError(Some("Failed to retrieve backup".into())) 125 - .into_response(); 126 } 127 }; 128 129 - ( 130 StatusCode::OK, 131 [ 132 (axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car"), ··· 137 ], 138 car_bytes, 139 ) 140 - .into_response() 141 } 142 143 #[derive(Serialize)] ··· 149 pub block_count: i32, 150 } 151 152 - pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> Response { 153 let backup_storage = match state.backup_storage.as_ref() { 154 Some(storage) => storage, 155 None => { 156 - return ApiError::BackupsDisabled.into_response(); 157 } 158 }; 159 160 - let user = match state.backup_repo.get_user_for_backup(&auth.0.did).await { 161 Ok(Some(u)) => u, 162 Ok(None) => { 163 - return ApiError::AccountNotFound.into_response(); 164 } 165 Err(e) => { 166 error!("DB error fetching user: {:?}", e); 167 - return ApiError::InternalError(None).into_response(); 168 } 169 }; 170 171 if user.deactivated_at.is_some() { 172 - return ApiError::AccountDeactivated.into_response(); 173 } 174 175 let repo_rev = match &user.repo_rev { 176 Some(rev) => rev.clone(), 177 None => { 178 - return ApiError::RepoNotReady.into_response(); 179 } 180 }; 181 182 let head_cid = match Cid::from_str(&user.repo_root_cid) { 183 Ok(c) => c, 184 Err(_) => { 185 - return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(); 186 } 187 }; 188 ··· 197 Ok(bytes) => bytes, 198 Err(e) => { 199 error!("Failed to generate CAR: {:?}", e); 200 - return ApiError::InternalError(Some("Failed to generate backup".into())) 201 - .into_response(); 202 } 203 }; 204 ··· 212 Ok(key) => key, 213 Err(e) => { 214 error!("Failed to upload backup: {:?}", e); 215 - return ApiError::InternalError(Some("Failed to store backup".into())).into_response(); 216 } 217 }; 218 ··· 238 "Failed to rollback orphaned backup from S3" 239 ); 240 } 241 - return ApiError::InternalError(Some("Failed to record backup".into())).into_response(); 242 } 243 }; 244 ··· 261 warn!(did = %user.did, error = %e, "Failed to cleanup old backups after manual backup"); 262 } 263 264 - ( 265 StatusCode::OK, 266 Json(CreateBackupOutput { 267 id: backup_id.to_string(), ··· 270 block_count, 271 }), 272 ) 273 - .into_response() 274 } 275 276 async fn cleanup_old_backups( ··· 310 311 pub async fn delete_backup( 312 State(state): State<AppState>, 313 - auth: BearerAuth, 314 Query(query): Query<DeleteBackupQuery>, 315 - ) -> Response { 316 let backup_id = match uuid::Uuid::parse_str(&query.id) { 317 Ok(id) => id, 318 Err(_) => { 319 - return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); 320 } 321 }; 322 323 let backup = match state 324 .backup_repo 325 - .get_backup_for_deletion(backup_id, &auth.0.did) 326 .await 327 { 328 Ok(Some(b)) => b, 329 Ok(None) => { 330 - return ApiError::BackupNotFound.into_response(); 331 } 332 Err(e) => { 333 error!("DB error fetching backup: {:?}", e); 334 - return ApiError::InternalError(None).into_response(); 335 } 336 }; 337 338 if backup.deactivated_at.is_some() { 339 - return ApiError::AccountDeactivated.into_response(); 340 } 341 342 if let Some(backup_storage) = state.backup_storage.as_ref() ··· 351 352 if let Err(e) = state.backup_repo.delete_backup(backup.id).await { 353 error!("DB error deleting backup: {:?}", e); 354 - return ApiError::InternalError(Some("Failed to delete backup".into())).into_response(); 355 } 356 357 - info!(did = %auth.0.did, backup_id = %backup_id, "Deleted backup"); 358 359 - EmptyResponse::ok().into_response() 360 } 361 362 #[derive(Deserialize)] ··· 367 368 pub async fn set_backup_enabled( 369 State(state): State<AppState>, 370 - auth: BearerAuth, 371 Json(input): Json<SetBackupEnabledInput>, 372 - ) -> Response { 373 let deactivated_at = match state 374 .backup_repo 375 - .get_user_deactivated_status(&auth.0.did) 376 .await 377 { 378 Ok(Some(status)) => status, 379 Ok(None) => { 380 - return ApiError::AccountNotFound.into_response(); 381 } 382 Err(e) => { 383 error!("DB error fetching user: {:?}", e); 384 - return ApiError::InternalError(None).into_response(); 385 } 386 }; 387 388 if deactivated_at.is_some() { 389 - return ApiError::AccountDeactivated.into_response(); 390 } 391 392 if let Err(e) = state 393 .backup_repo 394 - .update_backup_enabled(&auth.0.did, input.enabled) 395 .await 396 { 397 error!("DB error updating backup_enabled: {:?}", e); 398 - return ApiError::InternalError(Some("Failed to update setting".into())).into_response(); 399 } 400 401 - info!(did = %auth.0.did, enabled = input.enabled, "Updated backup_enabled setting"); 402 403 - EnabledResponse::response(input.enabled).into_response() 404 } 405 406 - pub async fn export_blobs(State(state): State<AppState>, auth: BearerAuth) -> Response { 407 - let user_id = match state.backup_repo.get_user_id_by_did(&auth.0.did).await { 408 Ok(Some(id)) => id, 409 Ok(None) => { 410 - return ApiError::AccountNotFound.into_response(); 411 } 412 Err(e) => { 413 error!("DB error fetching user: {:?}", e); 414 - return ApiError::InternalError(None).into_response(); 415 } 416 }; 417 ··· 419 Ok(rows) => rows, 420 Err(e) => { 421 error!("DB error fetching blobs: {:?}", e); 422 - return ApiError::InternalError(None).into_response(); 423 } 424 }; 425 426 if blobs.is_empty() { 427 - return ( 428 StatusCode::OK, 429 [ 430 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 435 ], 436 Vec::<u8>::new(), 437 ) 438 - .into_response(); 439 } 440 441 let mut zip_buffer = std::io::Cursor::new(Vec::new()); ··· 513 514 if let Err(e) = zip.finish() { 515 error!("Failed to finish zip: {:?}", e); 516 - return ApiError::InternalError(Some("Failed to create zip file".into())) 517 - .into_response(); 518 } 519 } 520 521 let zip_bytes = zip_buffer.into_inner(); 522 523 - info!(did = %auth.0.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); 524 525 - ( 526 StatusCode::OK, 527 [ 528 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 533 ], 534 zip_bytes, 535 ) 536 - .into_response() 537 } 538 539 fn mime_to_extension(mime_type: &str) -> &'static str {
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, EnabledResponse}; 3 + use crate::auth::{Active, Auth}; 4 use crate::scheduled::generate_full_backup; 5 use crate::state::AppState; 6 use crate::storage::{BackupStorage, backup_retention_count}; ··· 35 pub backup_enabled: bool, 36 } 37 38 + pub async fn list_backups( 39 + State(state): State<AppState>, 40 + auth: Auth<Active>, 41 + ) -> Result<Response, crate::api::error::ApiError> { 42 + let (user_id, backup_enabled) = match state.backup_repo.get_user_backup_status(&auth.did).await 43 + { 44 + Ok(Some(status)) => status, 45 + Ok(None) => { 46 + return Ok(ApiError::AccountNotFound.into_response()); 47 + } 48 + Err(e) => { 49 + error!("DB error fetching user: {:?}", e); 50 + return Ok(ApiError::InternalError(None).into_response()); 51 + } 52 + }; 53 54 let backups = match state.backup_repo.list_backups_for_user(user_id).await { 55 Ok(rows) => rows, 56 Err(e) => { 57 error!("DB error fetching backups: {:?}", e); 58 + return Ok(ApiError::InternalError(None).into_response()); 59 } 60 }; 61 ··· 71 }) 72 .collect(); 73 74 + Ok(( 75 StatusCode::OK, 76 Json(ListBackupsOutput { 77 backups: backup_list, 78 backup_enabled, 79 }), 80 ) 81 + .into_response()) 82 } 83 84 #[derive(Deserialize)] ··· 88 89 pub async fn get_backup( 90 State(state): State<AppState>, 91 + auth: Auth<Active>, 92 Query(query): Query<GetBackupQuery>, 93 + ) -> Result<Response, crate::api::error::ApiError> { 94 let backup_id = match uuid::Uuid::parse_str(&query.id) { 95 Ok(id) => id, 96 Err(_) => { 97 + return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response()); 98 } 99 }; 100 101 let backup_info = match state 102 .backup_repo 103 + .get_backup_storage_info(backup_id, &auth.did) 104 .await 105 { 106 Ok(Some(b)) => b, 107 Ok(None) => { 108 + return Ok(ApiError::BackupNotFound.into_response()); 109 } 110 Err(e) => { 111 error!("DB error fetching backup: {:?}", e); 112 + return Ok(ApiError::InternalError(None).into_response()); 113 } 114 }; 115 116 let backup_storage = match state.backup_storage.as_ref() { 117 Some(storage) => storage, 118 None => { 119 + return Ok(ApiError::BackupsDisabled.into_response()); 120 } 121 }; 122 ··· 124 Ok(bytes) => bytes, 125 Err(e) => { 126 error!("Failed to fetch backup from storage: {:?}", e); 127 + return Ok( 128 + ApiError::InternalError(Some("Failed to retrieve backup".into())).into_response(), 129 + ); 130 } 131 }; 132 133 + Ok(( 134 StatusCode::OK, 135 [ 136 (axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car"), ··· 141 ], 142 car_bytes, 143 ) 144 + .into_response()) 145 } 146 147 #[derive(Serialize)] ··· 153 pub block_count: i32, 154 } 155 156 + pub async fn create_backup( 157 + State(state): State<AppState>, 158 + auth: Auth<Active>, 159 + ) -> Result<Response, crate::api::error::ApiError> { 160 let backup_storage = match state.backup_storage.as_ref() { 161 Some(storage) => storage, 162 None => { 163 + return Ok(ApiError::BackupsDisabled.into_response()); 164 } 165 }; 166 167 + let user = match state.backup_repo.get_user_for_backup(&auth.did).await { 168 Ok(Some(u)) => u, 169 Ok(None) => { 170 + return Ok(ApiError::AccountNotFound.into_response()); 171 } 172 Err(e) => { 173 error!("DB error fetching user: {:?}", e); 174 + return Ok(ApiError::InternalError(None).into_response()); 175 } 176 }; 177 178 if user.deactivated_at.is_some() { 179 + return Ok(ApiError::AccountDeactivated.into_response()); 180 } 181 182 let repo_rev = match &user.repo_rev { 183 Some(rev) => rev.clone(), 184 None => { 185 + return Ok(ApiError::RepoNotReady.into_response()); 186 } 187 }; 188 189 let head_cid = match Cid::from_str(&user.repo_root_cid) { 190 Ok(c) => c, 191 Err(_) => { 192 + return Ok( 193 + ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(), 194 + ); 195 } 196 }; 197 ··· 206 Ok(bytes) => bytes, 207 Err(e) => { 208 error!("Failed to generate CAR: {:?}", e); 209 + return Ok( 210 + ApiError::InternalError(Some("Failed to generate backup".into())).into_response(), 211 + ); 212 } 213 }; 214 ··· 222 Ok(key) => key, 223 Err(e) => { 224 error!("Failed to upload backup: {:?}", e); 225 + return Ok( 226 + ApiError::InternalError(Some("Failed to store backup".into())).into_response(), 227 + ); 228 } 229 }; 230 ··· 250 "Failed to rollback orphaned backup from S3" 251 ); 252 } 253 + return Ok( 254 + ApiError::InternalError(Some("Failed to record backup".into())).into_response(), 255 + ); 256 } 257 }; 258 ··· 275 warn!(did = %user.did, error = %e, "Failed to cleanup old backups after manual backup"); 276 } 277 278 + Ok(( 279 StatusCode::OK, 280 Json(CreateBackupOutput { 281 id: backup_id.to_string(), ··· 284 block_count, 285 }), 286 ) 287 + .into_response()) 288 } 289 290 async fn cleanup_old_backups( ··· 324 325 pub async fn delete_backup( 326 State(state): State<AppState>, 327 + auth: Auth<Active>, 328 Query(query): Query<DeleteBackupQuery>, 329 + ) -> Result<Response, crate::api::error::ApiError> { 330 let backup_id = match uuid::Uuid::parse_str(&query.id) { 331 Ok(id) => id, 332 Err(_) => { 333 + return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response()); 334 } 335 }; 336 337 let backup = match state 338 .backup_repo 339 + .get_backup_for_deletion(backup_id, &auth.did) 340 .await 341 { 342 Ok(Some(b)) => b, 343 Ok(None) => { 344 + return Ok(ApiError::BackupNotFound.into_response()); 345 } 346 Err(e) => { 347 error!("DB error fetching backup: {:?}", e); 348 + return Ok(ApiError::InternalError(None).into_response()); 349 } 350 }; 351 352 if backup.deactivated_at.is_some() { 353 + return Ok(ApiError::AccountDeactivated.into_response()); 354 } 355 356 if let Some(backup_storage) = state.backup_storage.as_ref() ··· 365 366 if let Err(e) = state.backup_repo.delete_backup(backup.id).await { 367 error!("DB error deleting backup: {:?}", e); 368 + return Ok(ApiError::InternalError(Some("Failed to delete backup".into())).into_response()); 369 } 370 371 + info!(did = %auth.did, backup_id = %backup_id, "Deleted backup"); 372 373 + Ok(EmptyResponse::ok().into_response()) 374 } 375 376 #[derive(Deserialize)] ··· 381 382 pub async fn set_backup_enabled( 383 State(state): State<AppState>, 384 + auth: Auth<Active>, 385 Json(input): Json<SetBackupEnabledInput>, 386 + ) -> Result<Response, crate::api::error::ApiError> { 387 let deactivated_at = match state 388 .backup_repo 389 + .get_user_deactivated_status(&auth.did) 390 .await 391 { 392 Ok(Some(status)) => status, 393 Ok(None) => { 394 + return Ok(ApiError::AccountNotFound.into_response()); 395 } 396 Err(e) => { 397 error!("DB error fetching user: {:?}", e); 398 + return Ok(ApiError::InternalError(None).into_response()); 399 } 400 }; 401 402 if deactivated_at.is_some() { 403 + return Ok(ApiError::AccountDeactivated.into_response()); 404 } 405 406 if let Err(e) = state 407 .backup_repo 408 + .update_backup_enabled(&auth.did, input.enabled) 409 .await 410 { 411 error!("DB error updating backup_enabled: {:?}", e); 412 + return Ok( 413 + ApiError::InternalError(Some("Failed to update setting".into())).into_response(), 414 + ); 415 } 416 417 + info!(did = %auth.did, enabled = input.enabled, "Updated backup_enabled setting"); 418 419 + Ok(EnabledResponse::response(input.enabled).into_response()) 420 } 421 422 + pub async fn export_blobs( 423 + State(state): State<AppState>, 424 + auth: Auth<Active>, 425 + ) -> Result<Response, crate::api::error::ApiError> { 426 + let user_id = match state.backup_repo.get_user_id_by_did(&auth.did).await { 427 Ok(Some(id)) => id, 428 Ok(None) => { 429 + return Ok(ApiError::AccountNotFound.into_response()); 430 } 431 Err(e) => { 432 error!("DB error fetching user: {:?}", e); 433 + return Ok(ApiError::InternalError(None).into_response()); 434 } 435 }; 436 ··· 438 Ok(rows) => rows, 439 Err(e) => { 440 error!("DB error fetching blobs: {:?}", e); 441 + return Ok(ApiError::InternalError(None).into_response()); 442 } 443 }; 444 445 if blobs.is_empty() { 446 + return Ok(( 447 StatusCode::OK, 448 [ 449 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 454 ], 455 Vec::<u8>::new(), 456 ) 457 + .into_response()); 458 } 459 460 let mut zip_buffer = std::io::Cursor::new(Vec::new()); ··· 532 533 if let Err(e) = zip.finish() { 534 error!("Failed to finish zip: {:?}", e); 535 + return Ok( 536 + ApiError::InternalError(Some("Failed to create zip file".into())).into_response(), 537 + ); 538 } 539 } 540 541 let zip_bytes = zip_buffer.into_inner(); 542 543 + info!(did = %auth.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); 544 545 + Ok(( 546 StatusCode::OK, 547 [ 548 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 553 ], 554 zip_bytes, 555 ) 556 + .into_response()) 557 } 558 559 fn mime_to_extension(mime_type: &str) -> &'static str {
+115 -98
crates/tranquil-pds/src/api/delegation.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::create_signed_commit; 3 - use crate::auth::BearerAuth; 4 use crate::delegation::{DelegationActionType, SCOPE_PRESETS, scopes}; 5 use crate::state::{AppState, RateLimitKind}; 6 use crate::types::{Did, Handle, Nsid, Rkey}; ··· 33 pub controllers: Vec<ControllerInfo>, 34 } 35 36 - pub async fn list_controllers(State(state): State<AppState>, auth: BearerAuth) -> Response { 37 let controllers = match state 38 .delegation_repo 39 - .get_delegations_for_account(&auth.0.did) 40 .await 41 { 42 Ok(c) => c, 43 Err(e) => { 44 tracing::error!("Failed to list controllers: {:?}", e); 45 - return ApiError::InternalError(Some("Failed to list controllers".into())) 46 - .into_response(); 47 } 48 }; 49 50 - Json(ListControllersResponse { 51 controllers: controllers 52 .into_iter() 53 .map(|c| ControllerInfo { ··· 59 }) 60 .collect(), 61 }) 62 - .into_response() 63 } 64 65 #[derive(Debug, Deserialize)] ··· 70 71 pub async fn add_controller( 72 State(state): State<AppState>, 73 - auth: BearerAuth, 74 Json(input): Json<AddControllerInput>, 75 - ) -> Response { 76 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 77 - return ApiError::InvalidScopes(e).into_response(); 78 } 79 80 let controller_exists = state ··· 86 .is_some(); 87 88 if !controller_exists { 89 - return ApiError::ControllerNotFound.into_response(); 90 } 91 92 - match state 93 - .delegation_repo 94 - .controls_any_accounts(&auth.0.did) 95 - .await 96 - { 97 Ok(true) => { 98 - return ApiError::InvalidDelegation( 99 "Cannot add controllers to an account that controls other accounts".into(), 100 ) 101 - .into_response(); 102 } 103 Err(e) => { 104 tracing::error!("Failed to check delegation status: {:?}", e); 105 - return ApiError::InternalError(Some("Failed to verify delegation status".into())) 106 - .into_response(); 107 } 108 Ok(false) => {} 109 } ··· 114 .await 115 { 116 Ok(true) => { 117 - return ApiError::InvalidDelegation( 118 "Cannot add a controlled account as a controller".into(), 119 ) 120 - .into_response(); 121 } 122 Err(e) => { 123 tracing::error!("Failed to check controller status: {:?}", e); 124 - return ApiError::InternalError(Some("Failed to verify controller status".into())) 125 - .into_response(); 126 } 127 Ok(false) => {} 128 } ··· 130 match state 131 .delegation_repo 132 .create_delegation( 133 - &auth.0.did, 134 &input.controller_did, 135 &input.granted_scopes, 136 - &auth.0.did, 137 ) 138 .await 139 { ··· 141 let _ = state 142 .delegation_repo 143 .log_delegation_action( 144 - &auth.0.did, 145 - &auth.0.did, 146 Some(&input.controller_did), 147 DelegationActionType::GrantCreated, 148 Some(serde_json::json!({ ··· 153 ) 154 .await; 155 156 - ( 157 StatusCode::OK, 158 Json(serde_json::json!({ 159 "success": true 160 })), 161 ) 162 - .into_response() 163 } 164 Err(e) => { 165 tracing::error!("Failed to add controller: {:?}", e); 166 - ApiError::InternalError(Some("Failed to add controller".into())).into_response() 167 } 168 } 169 } ··· 175 176 pub async fn remove_controller( 177 State(state): State<AppState>, 178 - auth: BearerAuth, 179 Json(input): Json<RemoveControllerInput>, 180 - ) -> Response { 181 match state 182 .delegation_repo 183 - .revoke_delegation(&auth.0.did, &input.controller_did, &auth.0.did) 184 .await 185 { 186 Ok(true) => { 187 let revoked_app_passwords = state 188 .session_repo 189 - .delete_app_passwords_by_controller(&auth.0.did, &input.controller_did) 190 .await 191 .unwrap_or(0) as usize; 192 193 let revoked_oauth_tokens = state 194 .oauth_repo 195 - .revoke_tokens_for_controller(&auth.0.did, &input.controller_did) 196 .await 197 .unwrap_or(0); 198 199 let _ = state 200 .delegation_repo 201 .log_delegation_action( 202 - &auth.0.did, 203 - &auth.0.did, 204 Some(&input.controller_did), 205 DelegationActionType::GrantRevoked, 206 Some(serde_json::json!({ ··· 212 ) 213 .await; 214 215 - ( 216 StatusCode::OK, 217 Json(serde_json::json!({ 218 "success": true 219 })), 220 ) 221 - .into_response() 222 } 223 - Ok(false) => ApiError::DelegationNotFound.into_response(), 224 Err(e) => { 225 tracing::error!("Failed to remove controller: {:?}", e); 226 - ApiError::InternalError(Some("Failed to remove controller".into())).into_response() 227 } 228 } 229 } ··· 236 237 pub async fn update_controller_scopes( 238 State(state): State<AppState>, 239 - auth: BearerAuth, 240 Json(input): Json<UpdateControllerScopesInput>, 241 - ) -> Response { 242 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 243 - return ApiError::InvalidScopes(e).into_response(); 244 } 245 246 match state 247 .delegation_repo 248 - .update_delegation_scopes(&auth.0.did, &input.controller_did, &input.granted_scopes) 249 .await 250 { 251 Ok(true) => { 252 let _ = state 253 .delegation_repo 254 .log_delegation_action( 255 - &auth.0.did, 256 - &auth.0.did, 257 Some(&input.controller_did), 258 DelegationActionType::ScopesModified, 259 Some(serde_json::json!({ ··· 264 ) 265 .await; 266 267 - ( 268 StatusCode::OK, 269 Json(serde_json::json!({ 270 "success": true 271 })), 272 ) 273 - .into_response() 274 } 275 - Ok(false) => ApiError::DelegationNotFound.into_response(), 276 Err(e) => { 277 tracing::error!("Failed to update controller scopes: {:?}", e); 278 - ApiError::InternalError(Some("Failed to update controller scopes".into())) 279 - .into_response() 280 } 281 } 282 } ··· 295 pub accounts: Vec<DelegatedAccountInfo>, 296 } 297 298 - pub async fn list_controlled_accounts(State(state): State<AppState>, auth: BearerAuth) -> Response { 299 let accounts = match state 300 .delegation_repo 301 - .get_accounts_controlled_by(&auth.0.did) 302 .await 303 { 304 Ok(a) => a, 305 Err(e) => { 306 tracing::error!("Failed to list controlled accounts: {:?}", e); 307 - return ApiError::InternalError(Some("Failed to list controlled accounts".into())) 308 - .into_response(); 309 } 310 }; 311 312 - Json(ListControlledAccountsResponse { 313 accounts: accounts 314 .into_iter() 315 .map(|a| DelegatedAccountInfo { ··· 320 }) 321 .collect(), 322 }) 323 - .into_response() 324 } 325 326 #[derive(Debug, Deserialize)] ··· 355 356 pub async fn get_audit_log( 357 State(state): State<AppState>, 358 - auth: BearerAuth, 359 Query(params): Query<AuditLogParams>, 360 - ) -> Response { 361 let limit = params.limit.clamp(1, 100); 362 let offset = params.offset.max(0); 363 364 let entries = match state 365 .delegation_repo 366 - .get_audit_log_for_account(&auth.0.did, limit, offset) 367 .await 368 { 369 Ok(e) => e, 370 Err(e) => { 371 tracing::error!("Failed to get audit log: {:?}", e); 372 - return ApiError::InternalError(Some("Failed to get audit log".into())).into_response(); 373 } 374 }; 375 376 let total = state 377 .delegation_repo 378 - .count_audit_log_entries(&auth.0.did) 379 .await 380 .unwrap_or_default(); 381 382 - Json(GetAuditLogResponse { 383 entries: entries 384 .into_iter() 385 .map(|e| AuditLogEntry { ··· 394 .collect(), 395 total, 396 }) 397 - .into_response() 398 } 399 400 #[derive(Debug, Serialize)] ··· 444 pub async fn create_delegated_account( 445 State(state): State<AppState>, 446 headers: HeaderMap, 447 - auth: BearerAuth, 448 Json(input): Json<CreateDelegatedAccountInput>, 449 - ) -> Response { 450 let client_ip = extract_client_ip(&headers); 451 if !state 452 .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 453 .await 454 { 455 warn!(ip = %client_ip, "Delegated account creation rate limit exceeded"); 456 - return ApiError::RateLimitExceeded(Some( 457 "Too many account creation attempts. Please try again later.".into(), 458 )) 459 - .into_response(); 460 } 461 462 if let Err(e) = scopes::validate_delegation_scopes(&input.controller_scopes) { 463 - return ApiError::InvalidScopes(e).into_response(); 464 } 465 466 - match state.delegation_repo.has_any_controllers(&auth.0.did).await { 467 Ok(true) => { 468 - return ApiError::InvalidDelegation( 469 "Cannot create delegated accounts from a controlled account".into(), 470 ) 471 - .into_response(); 472 } 473 Err(e) => { 474 tracing::error!("Failed to check controller status: {:?}", e); 475 - return ApiError::InternalError(Some("Failed to verify controller status".into())) 476 - .into_response(); 477 } 478 Ok(false) => {} 479 } ··· 494 match crate::api::validation::validate_short_handle(handle_to_validate) { 495 Ok(h) => format!("{}.{}", h, hostname_for_handles), 496 Err(e) => { 497 - return ApiError::InvalidRequest(e.to_string()).into_response(); 498 } 499 } 500 } else { ··· 509 if let Some(ref email) = email 510 && !crate::api::validation::is_valid_email(email) 511 { 512 - return ApiError::InvalidEmail.into_response(); 513 } 514 515 if let Some(ref code) = input.invite_code { ··· 520 .unwrap_or(false); 521 522 if !valid { 523 - return ApiError::InvalidInviteCode.into_response(); 524 } 525 } else { 526 let invite_required = std::env::var("INVITE_CODE_REQUIRED") 527 .map(|v| v == "true" || v == "1") 528 .unwrap_or(false); 529 if invite_required { 530 - return ApiError::InviteCodeRequired.into_response(); 531 } 532 } 533 ··· 542 Ok(k) => k, 543 Err(e) => { 544 error!("Error creating signing key: {:?}", e); 545 - return ApiError::InternalError(None).into_response(); 546 } 547 }; 548 ··· 558 Ok(r) => r, 559 Err(e) => { 560 error!("Error creating PLC genesis operation: {:?}", e); 561 - return ApiError::InternalError(Some("Failed to create PLC operation".into())) 562 - .into_response(); 563 } 564 }; 565 ··· 569 .await 570 { 571 error!("Failed to submit PLC genesis operation: {:?}", e); 572 - return ApiError::UpstreamErrorMsg(format!( 573 "Failed to register DID with PLC directory: {}", 574 e 575 )) 576 - .into_response(); 577 } 578 579 let did = Did::new_unchecked(&genesis_result.did); 580 let handle = Handle::new_unchecked(&handle); 581 - info!(did = %did, handle = %handle, controller = %&auth.0.did, "Created DID for delegated account"); 582 583 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 584 Ok(bytes) => bytes, 585 Err(e) => { 586 error!("Error encrypting signing key: {:?}", e); 587 - return ApiError::InternalError(None).into_response(); 588 } 589 }; 590 ··· 593 Ok(c) => c, 594 Err(e) => { 595 error!("Error persisting MST: {:?}", e); 596 - return ApiError::InternalError(None).into_response(); 597 } 598 }; 599 let rev = Tid::now(LimitedU32::MIN); ··· 602 Ok(result) => result, 603 Err(e) => { 604 error!("Error creating genesis commit: {:?}", e); 605 - return ApiError::InternalError(None).into_response(); 606 } 607 }; 608 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 609 Ok(c) => c, 610 Err(e) => { 611 error!("Error saving genesis commit: {:?}", e); 612 - return ApiError::InternalError(None).into_response(); 613 } 614 }; 615 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; ··· 618 handle: handle.clone(), 619 email: email.clone(), 620 did: did.clone(), 621 - controller_did: auth.0.did.clone(), 622 controller_scopes: input.controller_scopes.clone(), 623 encrypted_key_bytes, 624 encryption_version: crate::config::ENCRYPTION_VERSION, ··· 635 { 636 Ok(id) => id, 637 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { 638 - return ApiError::HandleNotAvailable(None).into_response(); 639 } 640 Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { 641 - return ApiError::EmailTaken.into_response(); 642 } 643 Err(e) => { 644 error!("Error creating delegated account: {:?}", e); 645 - return ApiError::InternalError(None).into_response(); 646 } 647 }; 648 ··· 678 .delegation_repo 679 .log_delegation_action( 680 &did, 681 - &auth.0.did, 682 - Some(&auth.0.did), 683 DelegationActionType::GrantCreated, 684 Some(json!({ 685 "account_created": true, ··· 690 ) 691 .await; 692 693 - info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created"); 694 695 - Json(CreateDelegatedAccountResponse { did, handle }).into_response() 696 }
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::create_signed_commit; 3 + use crate::auth::{Active, Auth}; 4 use crate::delegation::{DelegationActionType, SCOPE_PRESETS, scopes}; 5 use crate::state::{AppState, RateLimitKind}; 6 use crate::types::{Did, Handle, Nsid, Rkey}; ··· 33 pub controllers: Vec<ControllerInfo>, 34 } 35 36 + pub async fn list_controllers( 37 + State(state): State<AppState>, 38 + auth: Auth<Active>, 39 + ) -> Result<Response, ApiError> { 40 let controllers = match state 41 .delegation_repo 42 + .get_delegations_for_account(&auth.did) 43 .await 44 { 45 Ok(c) => c, 46 Err(e) => { 47 tracing::error!("Failed to list controllers: {:?}", e); 48 + return Ok( 49 + ApiError::InternalError(Some("Failed to list controllers".into())).into_response(), 50 + ); 51 } 52 }; 53 54 + Ok(Json(ListControllersResponse { 55 controllers: controllers 56 .into_iter() 57 .map(|c| ControllerInfo { ··· 63 }) 64 .collect(), 65 }) 66 + .into_response()) 67 } 68 69 #[derive(Debug, Deserialize)] ··· 74 75 pub async fn add_controller( 76 State(state): State<AppState>, 77 + auth: Auth<Active>, 78 Json(input): Json<AddControllerInput>, 79 + ) -> Result<Response, ApiError> { 80 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 81 + return Ok(ApiError::InvalidScopes(e).into_response()); 82 } 83 84 let controller_exists = state ··· 90 .is_some(); 91 92 if !controller_exists { 93 + return Ok(ApiError::ControllerNotFound.into_response()); 94 } 95 96 + match state.delegation_repo.controls_any_accounts(&auth.did).await { 97 Ok(true) => { 98 + return Ok(ApiError::InvalidDelegation( 99 "Cannot add controllers to an account that controls other accounts".into(), 100 ) 101 + .into_response()); 102 } 103 Err(e) => { 104 tracing::error!("Failed to check delegation status: {:?}", e); 105 + return Ok( 106 + ApiError::InternalError(Some("Failed to verify delegation status".into())) 107 + .into_response(), 108 + ); 109 } 110 Ok(false) => {} 111 } ··· 116 .await 117 { 118 Ok(true) => { 119 + return Ok(ApiError::InvalidDelegation( 120 "Cannot add a controlled account as a controller".into(), 121 ) 122 + .into_response()); 123 } 124 Err(e) => { 125 tracing::error!("Failed to check controller status: {:?}", e); 126 + return Ok( 127 + ApiError::InternalError(Some("Failed to verify controller status".into())) 128 + .into_response(), 129 + ); 130 } 131 Ok(false) => {} 132 } ··· 134 match state 135 .delegation_repo 136 .create_delegation( 137 + &auth.did, 138 &input.controller_did, 139 &input.granted_scopes, 140 + &auth.did, 141 ) 142 .await 143 { ··· 145 let _ = state 146 .delegation_repo 147 .log_delegation_action( 148 + &auth.did, 149 + &auth.did, 150 Some(&input.controller_did), 151 DelegationActionType::GrantCreated, 152 Some(serde_json::json!({ ··· 157 ) 158 .await; 159 160 + Ok(( 161 StatusCode::OK, 162 Json(serde_json::json!({ 163 "success": true 164 })), 165 ) 166 + .into_response()) 167 } 168 Err(e) => { 169 tracing::error!("Failed to add controller: {:?}", e); 170 + Ok(ApiError::InternalError(Some("Failed to add controller".into())).into_response()) 171 } 172 } 173 } ··· 179 180 pub async fn remove_controller( 181 State(state): State<AppState>, 182 + auth: Auth<Active>, 183 Json(input): Json<RemoveControllerInput>, 184 + ) -> Result<Response, ApiError> { 185 match state 186 .delegation_repo 187 + .revoke_delegation(&auth.did, &input.controller_did, &auth.did) 188 .await 189 { 190 Ok(true) => { 191 let revoked_app_passwords = state 192 .session_repo 193 + .delete_app_passwords_by_controller(&auth.did, &input.controller_did) 194 .await 195 .unwrap_or(0) as usize; 196 197 let revoked_oauth_tokens = state 198 .oauth_repo 199 + .revoke_tokens_for_controller(&auth.did, &input.controller_did) 200 .await 201 .unwrap_or(0); 202 203 let _ = state 204 .delegation_repo 205 .log_delegation_action( 206 + &auth.did, 207 + &auth.did, 208 Some(&input.controller_did), 209 DelegationActionType::GrantRevoked, 210 Some(serde_json::json!({ ··· 216 ) 217 .await; 218 219 + Ok(( 220 StatusCode::OK, 221 Json(serde_json::json!({ 222 "success": true 223 })), 224 ) 225 + .into_response()) 226 } 227 + Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), 228 Err(e) => { 229 tracing::error!("Failed to remove controller: {:?}", e); 230 + Ok(ApiError::InternalError(Some("Failed to remove controller".into())).into_response()) 231 } 232 } 233 } ··· 240 241 pub async fn update_controller_scopes( 242 State(state): State<AppState>, 243 + auth: Auth<Active>, 244 Json(input): Json<UpdateControllerScopesInput>, 245 + ) -> Result<Response, ApiError> { 246 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 247 + return Ok(ApiError::InvalidScopes(e).into_response()); 248 } 249 250 match state 251 .delegation_repo 252 + .update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes) 253 .await 254 { 255 Ok(true) => { 256 let _ = state 257 .delegation_repo 258 .log_delegation_action( 259 + &auth.did, 260 + &auth.did, 261 Some(&input.controller_did), 262 DelegationActionType::ScopesModified, 263 Some(serde_json::json!({ ··· 268 ) 269 .await; 270 271 + Ok(( 272 StatusCode::OK, 273 Json(serde_json::json!({ 274 "success": true 275 })), 276 ) 277 + .into_response()) 278 } 279 + Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), 280 Err(e) => { 281 tracing::error!("Failed to update controller scopes: {:?}", e); 282 + Ok( 283 + ApiError::InternalError(Some("Failed to update controller scopes".into())) 284 + .into_response(), 285 + ) 286 } 287 } 288 } ··· 301 pub accounts: Vec<DelegatedAccountInfo>, 302 } 303 304 + pub async fn list_controlled_accounts( 305 + State(state): State<AppState>, 306 + auth: Auth<Active>, 307 + ) -> Result<Response, ApiError> { 308 let accounts = match state 309 .delegation_repo 310 + .get_accounts_controlled_by(&auth.did) 311 .await 312 { 313 Ok(a) => a, 314 Err(e) => { 315 tracing::error!("Failed to list controlled accounts: {:?}", e); 316 + return Ok( 317 + ApiError::InternalError(Some("Failed to list controlled accounts".into())) 318 + .into_response(), 319 + ); 320 } 321 }; 322 323 + Ok(Json(ListControlledAccountsResponse { 324 accounts: accounts 325 .into_iter() 326 .map(|a| DelegatedAccountInfo { ··· 331 }) 332 .collect(), 333 }) 334 + .into_response()) 335 } 336 337 #[derive(Debug, Deserialize)] ··· 366 367 pub async fn get_audit_log( 368 State(state): State<AppState>, 369 + auth: Auth<Active>, 370 Query(params): Query<AuditLogParams>, 371 + ) -> Result<Response, ApiError> { 372 let limit = params.limit.clamp(1, 100); 373 let offset = params.offset.max(0); 374 375 let entries = match state 376 .delegation_repo 377 + .get_audit_log_for_account(&auth.did, limit, offset) 378 .await 379 { 380 Ok(e) => e, 381 Err(e) => { 382 tracing::error!("Failed to get audit log: {:?}", e); 383 + return Ok( 384 + ApiError::InternalError(Some("Failed to get audit log".into())).into_response(), 385 + ); 386 } 387 }; 388 389 let total = state 390 .delegation_repo 391 + .count_audit_log_entries(&auth.did) 392 .await 393 .unwrap_or_default(); 394 395 + Ok(Json(GetAuditLogResponse { 396 entries: entries 397 .into_iter() 398 .map(|e| AuditLogEntry { ··· 407 .collect(), 408 total, 409 }) 410 + .into_response()) 411 } 412 413 #[derive(Debug, Serialize)] ··· 457 pub async fn create_delegated_account( 458 State(state): State<AppState>, 459 headers: HeaderMap, 460 + auth: Auth<Active>, 461 Json(input): Json<CreateDelegatedAccountInput>, 462 + ) -> Result<Response, ApiError> { 463 let client_ip = extract_client_ip(&headers); 464 if !state 465 .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 466 .await 467 { 468 warn!(ip = %client_ip, "Delegated account creation rate limit exceeded"); 469 + return Ok(ApiError::RateLimitExceeded(Some( 470 "Too many account creation attempts. Please try again later.".into(), 471 )) 472 + .into_response()); 473 } 474 475 if let Err(e) = scopes::validate_delegation_scopes(&input.controller_scopes) { 476 + return Ok(ApiError::InvalidScopes(e).into_response()); 477 } 478 479 + match state.delegation_repo.has_any_controllers(&auth.did).await { 480 Ok(true) => { 481 + return Ok(ApiError::InvalidDelegation( 482 "Cannot create delegated accounts from a controlled account".into(), 483 ) 484 + .into_response()); 485 } 486 Err(e) => { 487 tracing::error!("Failed to check controller status: {:?}", e); 488 + return Ok( 489 + ApiError::InternalError(Some("Failed to verify controller status".into())) 490 + .into_response(), 491 + ); 492 } 493 Ok(false) => {} 494 } ··· 509 match crate::api::validation::validate_short_handle(handle_to_validate) { 510 Ok(h) => format!("{}.{}", h, hostname_for_handles), 511 Err(e) => { 512 + return Ok(ApiError::InvalidRequest(e.to_string()).into_response()); 513 } 514 } 515 } else { ··· 524 if let Some(ref email) = email 525 && !crate::api::validation::is_valid_email(email) 526 { 527 + return Ok(ApiError::InvalidEmail.into_response()); 528 } 529 530 if let Some(ref code) = input.invite_code { ··· 535 .unwrap_or(false); 536 537 if !valid { 538 + return Ok(ApiError::InvalidInviteCode.into_response()); 539 } 540 } else { 541 let invite_required = std::env::var("INVITE_CODE_REQUIRED") 542 .map(|v| v == "true" || v == "1") 543 .unwrap_or(false); 544 if invite_required { 545 + return Ok(ApiError::InviteCodeRequired.into_response()); 546 } 547 } 548 ··· 557 Ok(k) => k, 558 Err(e) => { 559 error!("Error creating signing key: {:?}", e); 560 + return Ok(ApiError::InternalError(None).into_response()); 561 } 562 }; 563 ··· 573 Ok(r) => r, 574 Err(e) => { 575 error!("Error creating PLC genesis operation: {:?}", e); 576 + return Ok( 577 + ApiError::InternalError(Some("Failed to create PLC operation".into())) 578 + .into_response(), 579 + ); 580 } 581 }; 582 ··· 586 .await 587 { 588 error!("Failed to submit PLC genesis operation: {:?}", e); 589 + return Ok(ApiError::UpstreamErrorMsg(format!( 590 "Failed to register DID with PLC directory: {}", 591 e 592 )) 593 + .into_response()); 594 } 595 596 let did = Did::new_unchecked(&genesis_result.did); 597 let handle = Handle::new_unchecked(&handle); 598 + info!(did = %did, handle = %handle, controller = %&auth.did, "Created DID for delegated account"); 599 600 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 601 Ok(bytes) => bytes, 602 Err(e) => { 603 error!("Error encrypting signing key: {:?}", e); 604 + return Ok(ApiError::InternalError(None).into_response()); 605 } 606 }; 607 ··· 610 Ok(c) => c, 611 Err(e) => { 612 error!("Error persisting MST: {:?}", e); 613 + return Ok(ApiError::InternalError(None).into_response()); 614 } 615 }; 616 let rev = Tid::now(LimitedU32::MIN); ··· 619 Ok(result) => result, 620 Err(e) => { 621 error!("Error creating genesis commit: {:?}", e); 622 + return Ok(ApiError::InternalError(None).into_response()); 623 } 624 }; 625 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 626 Ok(c) => c, 627 Err(e) => { 628 error!("Error saving genesis commit: {:?}", e); 629 + return Ok(ApiError::InternalError(None).into_response()); 630 } 631 }; 632 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; ··· 635 handle: handle.clone(), 636 email: email.clone(), 637 did: did.clone(), 638 + controller_did: auth.did.clone(), 639 controller_scopes: input.controller_scopes.clone(), 640 encrypted_key_bytes, 641 encryption_version: crate::config::ENCRYPTION_VERSION, ··· 652 { 653 Ok(id) => id, 654 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { 655 + return Ok(ApiError::HandleNotAvailable(None).into_response()); 656 } 657 Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { 658 + return Ok(ApiError::EmailTaken.into_response()); 659 } 660 Err(e) => { 661 error!("Error creating delegated account: {:?}", e); 662 + return Ok(ApiError::InternalError(None).into_response()); 663 } 664 }; 665 ··· 695 .delegation_repo 696 .log_delegation_action( 697 &did, 698 + &auth.did, 699 + Some(&auth.did), 700 DelegationActionType::GrantCreated, 701 Some(json!({ 702 "account_created": true, ··· 707 ) 708 .await; 709 710 + info!(did = %did, handle = %handle, controller = %&auth.did, "Delegated account created"); 711 712 + Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response()) 713 }
+7
crates/tranquil-pds/src/api/error.rs
··· 543 crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated, 544 crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown, 545 crate::auth::extractor::AuthError::AdminRequired => Self::AdminRequired, 546 crate::auth::extractor::AuthError::OAuthExpiredToken(msg) => { 547 Self::OAuthExpiredToken(Some(msg)) 548 }
··· 543 crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated, 544 crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown, 545 crate::auth::extractor::AuthError::AdminRequired => Self::AdminRequired, 546 + crate::auth::extractor::AuthError::ServiceAuthNotAllowed => Self::AuthenticationFailed( 547 + Some("Service authentication not allowed for this endpoint".to_string()), 548 + ), 549 + crate::auth::extractor::AuthError::SigningKeyRequired => Self::InvalidSigningKey, 550 + crate::auth::extractor::AuthError::InsufficientScope(msg) => { 551 + Self::InsufficientScope(Some(msg)) 552 + } 553 crate::auth::extractor::AuthError::OAuthExpiredToken(msg) => { 554 Self::OAuthExpiredToken(Some(msg)) 555 }
+88 -102
crates/tranquil-pds/src/api/identity/did.rs
··· 1 use crate::api::{ApiError, DidResponse, EmptyResponse}; 2 - use crate::auth::BearerAuthAllowDeactivated; 3 use crate::plc::signing_key_to_did_key; 4 use crate::state::AppState; 5 use crate::types::Handle; ··· 518 519 pub async fn get_recommended_did_credentials( 520 State(state): State<AppState>, 521 - auth: BearerAuthAllowDeactivated, 522 - ) -> Response { 523 - let auth_user = auth.0; 524 - let handle = match state.user_repo.get_handle_by_did(&auth_user.did).await { 525 - Ok(Some(h)) => h, 526 - Ok(None) => return ApiError::InternalError(None).into_response(), 527 - Err(_) => return ApiError::InternalError(None).into_response(), 528 - }; 529 - let key_bytes = match auth_user.key_bytes { 530 - Some(kb) => kb, 531 - None => { 532 - return ApiError::AuthenticationFailed(Some( 533 - "OAuth tokens cannot get DID credentials".into(), 534 - )) 535 - .into_response(); 536 - } 537 - }; 538 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 539 let pds_endpoint = format!("https://{}", hostname); 540 - let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) { 541 - Ok(k) => k, 542 - Err(_) => return ApiError::InternalError(None).into_response(), 543 - }; 544 let did_key = signing_key_to_did_key(&signing_key); 545 - let rotation_keys = if auth_user.did.starts_with("did:web:") { 546 vec![] 547 } else { 548 let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") { ··· 556 }; 557 vec![server_rotation_key] 558 }; 559 - ( 560 StatusCode::OK, 561 Json(GetRecommendedDidCredentialsOutput { 562 rotation_keys, ··· 570 }, 571 }), 572 ) 573 - .into_response() 574 } 575 576 #[derive(Deserialize)] ··· 580 581 pub async fn update_handle( 582 State(state): State<AppState>, 583 - auth: BearerAuthAllowDeactivated, 584 Json(input): Json<UpdateHandleInput>, 585 - ) -> Response { 586 - let auth_user = auth.0; 587 if let Err(e) = crate::auth::scope_check::check_identity_scope( 588 - auth_user.is_oauth, 589 - auth_user.scope.as_deref(), 590 crate::oauth::scopes::IdentityAttr::Handle, 591 ) { 592 - return e; 593 } 594 - let did = auth_user.did; 595 if !state 596 .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 597 .await 598 { 599 - return ApiError::RateLimitExceeded(Some( 600 "Too many handle updates. Try again later.".into(), 601 - )) 602 - .into_response(); 603 } 604 if !state 605 .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 606 .await 607 { 608 - return ApiError::RateLimitExceeded(Some("Daily handle update limit exceeded.".into())) 609 - .into_response(); 610 } 611 - let user_row = match state.user_repo.get_id_and_handle_by_did(&did).await { 612 - Ok(Some(row)) => row, 613 - Ok(None) => return ApiError::InternalError(None).into_response(), 614 - Err(_) => return ApiError::InternalError(None).into_response(), 615 - }; 616 let user_id = user_row.id; 617 let current_handle = user_row.handle; 618 let new_handle = input.handle.trim().to_ascii_lowercase(); 619 if new_handle.is_empty() { 620 - return ApiError::InvalidRequest("handle is required".into()).into_response(); 621 } 622 if !new_handle 623 .chars() 624 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 625 { 626 - return ApiError::InvalidHandle(Some("Handle contains invalid characters".into())) 627 - .into_response(); 628 } 629 if new_handle.split('.').any(|segment| segment.is_empty()) { 630 - return ApiError::InvalidHandle(Some("Handle contains empty segment".into())) 631 - .into_response(); 632 } 633 if new_handle 634 .split('.') 635 .any(|segment| segment.starts_with('-') || segment.ends_with('-')) 636 { 637 - return ApiError::InvalidHandle(Some( 638 "Handle segment cannot start or end with hyphen".into(), 639 - )) 640 - .into_response(); 641 } 642 if crate::moderation::has_explicit_slur(&new_handle) { 643 - return ApiError::InvalidHandle(Some("Inappropriate language in handle".into())) 644 - .into_response(); 645 } 646 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 647 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 667 { 668 warn!("Failed to sequence identity event for handle update: {}", e); 669 } 670 - return EmptyResponse::ok().into_response(); 671 } 672 if short_part.contains('.') { 673 - return ApiError::InvalidHandle(Some( 674 "Nested subdomains are not allowed. Use a simple handle without dots.".into(), 675 - )) 676 - .into_response(); 677 } 678 if short_part.len() < 3 { 679 - return ApiError::InvalidHandle(Some("Handle too short".into())).into_response(); 680 } 681 if short_part.len() > 18 { 682 - return ApiError::InvalidHandle(Some("Handle too long".into())).into_response(); 683 } 684 full_handle 685 } else { ··· 691 { 692 warn!("Failed to sequence identity event for handle update: {}", e); 693 } 694 - return EmptyResponse::ok().into_response(); 695 } 696 match crate::handle::verify_handle_ownership(&new_handle, &did).await { 697 Ok(()) => {} 698 Err(crate::handle::HandleResolutionError::NotFound) => { 699 - return ApiError::HandleNotAvailable(None).into_response(); 700 } 701 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { 702 - return ApiError::HandleNotAvailable(Some(format!( 703 "Handle points to different DID. Expected {}, got {}", 704 expected, actual 705 - ))) 706 - .into_response(); 707 } 708 Err(e) => { 709 warn!("Handle verification failed: {}", e); 710 - return ApiError::HandleNotAvailable(Some(format!( 711 "Handle verification failed: {}", 712 e 713 - ))) 714 - .into_response(); 715 } 716 } 717 new_handle.clone() 718 }; 719 - let handle_typed: Handle = match handle.parse() { 720 - Ok(h) => h, 721 - Err(_) => { 722 - return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(); 723 - } 724 - }; 725 - let handle_exists = match state 726 .user_repo 727 .check_handle_exists(&handle_typed, user_id) 728 .await 729 - { 730 - Ok(exists) => exists, 731 - Err(_) => return ApiError::InternalError(None).into_response(), 732 - }; 733 if handle_exists { 734 - return ApiError::HandleTaken.into_response(); 735 } 736 - let result = state.user_repo.update_handle(user_id, &handle_typed).await; 737 - match result { 738 - Ok(_) => { 739 - if !current_handle.is_empty() { 740 - let _ = state 741 - .cache 742 - .delete(&format!("handle:{}", current_handle)) 743 - .await; 744 - } 745 - let _ = state.cache.delete(&format!("handle:{}", handle)).await; 746 - if let Err(e) = 747 - crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)) 748 - .await 749 - { 750 - warn!("Failed to sequence identity event for handle update: {}", e); 751 - } 752 - if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await { 753 - warn!("Failed to update PLC handle: {}", e); 754 - } 755 - EmptyResponse::ok().into_response() 756 - } 757 - Err(e) => { 758 error!("DB error updating handle: {:?}", e); 759 - ApiError::InternalError(None).into_response() 760 - } 761 } 762 } 763 764 pub async fn update_plc_handle(
··· 1 use crate::api::{ApiError, DidResponse, EmptyResponse}; 2 + use crate::auth::{Auth, NotTakendown}; 3 use crate::plc::signing_key_to_did_key; 4 use crate::state::AppState; 5 use crate::types::Handle; ··· 518 519 pub async fn get_recommended_did_credentials( 520 State(state): State<AppState>, 521 + auth: Auth<NotTakendown>, 522 + ) -> Result<Response, ApiError> { 523 + let handle = state 524 + .user_repo 525 + .get_handle_by_did(&auth.did) 526 + .await 527 + .map_err(|_| ApiError::InternalError(None))? 528 + .ok_or(ApiError::InternalError(None))?; 529 + 530 + let key_bytes = auth.key_bytes.clone().ok_or_else(|| { 531 + ApiError::AuthenticationFailed(Some("OAuth tokens cannot get DID credentials".into())) 532 + })?; 533 + 534 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 535 let pds_endpoint = format!("https://{}", hostname); 536 + let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes) 537 + .map_err(|_| ApiError::InternalError(None))?; 538 let did_key = signing_key_to_did_key(&signing_key); 539 + let rotation_keys = if auth.did.starts_with("did:web:") { 540 vec![] 541 } else { 542 let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") { ··· 550 }; 551 vec![server_rotation_key] 552 }; 553 + Ok(( 554 StatusCode::OK, 555 Json(GetRecommendedDidCredentialsOutput { 556 rotation_keys, ··· 564 }, 565 }), 566 ) 567 + .into_response()) 568 } 569 570 #[derive(Deserialize)] ··· 574 575 pub async fn update_handle( 576 State(state): State<AppState>, 577 + auth: Auth<NotTakendown>, 578 Json(input): Json<UpdateHandleInput>, 579 + ) -> Result<Response, ApiError> { 580 if let Err(e) = crate::auth::scope_check::check_identity_scope( 581 + auth.is_oauth(), 582 + auth.scope.as_deref(), 583 crate::oauth::scopes::IdentityAttr::Handle, 584 ) { 585 + return Ok(e); 586 } 587 + let did = auth.did.clone(); 588 if !state 589 .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 590 .await 591 { 592 + return Err(ApiError::RateLimitExceeded(Some( 593 "Too many handle updates. Try again later.".into(), 594 + ))); 595 } 596 if !state 597 .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 598 .await 599 { 600 + return Err(ApiError::RateLimitExceeded(Some( 601 + "Daily handle update limit exceeded.".into(), 602 + ))); 603 } 604 + let user_row = state 605 + .user_repo 606 + .get_id_and_handle_by_did(&did) 607 + .await 608 + .map_err(|_| ApiError::InternalError(None))? 609 + .ok_or(ApiError::InternalError(None))?; 610 let user_id = user_row.id; 611 let current_handle = user_row.handle; 612 let new_handle = input.handle.trim().to_ascii_lowercase(); 613 if new_handle.is_empty() { 614 + return Err(ApiError::InvalidRequest("handle is required".into())); 615 } 616 if !new_handle 617 .chars() 618 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 619 { 620 + return Err(ApiError::InvalidHandle(Some( 621 + "Handle contains invalid characters".into(), 622 + ))); 623 } 624 if new_handle.split('.').any(|segment| segment.is_empty()) { 625 + return Err(ApiError::InvalidHandle(Some( 626 + "Handle contains empty segment".into(), 627 + ))); 628 } 629 if new_handle 630 .split('.') 631 .any(|segment| segment.starts_with('-') || segment.ends_with('-')) 632 { 633 + return Err(ApiError::InvalidHandle(Some( 634 "Handle segment cannot start or end with hyphen".into(), 635 + ))); 636 } 637 if crate::moderation::has_explicit_slur(&new_handle) { 638 + return Err(ApiError::InvalidHandle(Some( 639 + "Inappropriate language in handle".into(), 640 + ))); 641 } 642 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 643 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 663 { 664 warn!("Failed to sequence identity event for handle update: {}", e); 665 } 666 + return Ok(EmptyResponse::ok().into_response()); 667 } 668 if short_part.contains('.') { 669 + return Err(ApiError::InvalidHandle(Some( 670 "Nested subdomains are not allowed. Use a simple handle without dots.".into(), 671 + ))); 672 } 673 if short_part.len() < 3 { 674 + return Err(ApiError::InvalidHandle(Some("Handle too short".into()))); 675 } 676 if short_part.len() > 18 { 677 + return Err(ApiError::InvalidHandle(Some("Handle too long".into()))); 678 } 679 full_handle 680 } else { ··· 686 { 687 warn!("Failed to sequence identity event for handle update: {}", e); 688 } 689 + return Ok(EmptyResponse::ok().into_response()); 690 } 691 match crate::handle::verify_handle_ownership(&new_handle, &did).await { 692 Ok(()) => {} 693 Err(crate::handle::HandleResolutionError::NotFound) => { 694 + return Err(ApiError::HandleNotAvailable(None)); 695 } 696 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { 697 + return Err(ApiError::HandleNotAvailable(Some(format!( 698 "Handle points to different DID. Expected {}, got {}", 699 expected, actual 700 + )))); 701 } 702 Err(e) => { 703 warn!("Handle verification failed: {}", e); 704 + return Err(ApiError::HandleNotAvailable(Some(format!( 705 "Handle verification failed: {}", 706 e 707 + )))); 708 } 709 } 710 new_handle.clone() 711 }; 712 + let handle_typed: Handle = handle 713 + .parse() 714 + .map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?; 715 + let handle_exists = state 716 .user_repo 717 .check_handle_exists(&handle_typed, user_id) 718 .await 719 + .map_err(|_| ApiError::InternalError(None))?; 720 if handle_exists { 721 + return Err(ApiError::HandleTaken); 722 } 723 + state 724 + .user_repo 725 + .update_handle(user_id, &handle_typed) 726 + .await 727 + .map_err(|e| { 728 error!("DB error updating handle: {:?}", e); 729 + ApiError::InternalError(None) 730 + })?; 731 + 732 + if !current_handle.is_empty() { 733 + let _ = state 734 + .cache 735 + .delete(&format!("handle:{}", current_handle)) 736 + .await; 737 + } 738 + let _ = state.cache.delete(&format!("handle:{}", handle)).await; 739 + if let Err(e) = 740 + crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)).await 741 + { 742 + warn!("Failed to sequence identity event for handle update: {}", e); 743 + } 744 + if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await { 745 + warn!("Failed to update PLC handle: {}", e); 746 } 747 + Ok(EmptyResponse::ok().into_response()) 748 } 749 750 pub async fn update_plc_handle(
+23 -24
crates/tranquil-pds/src/api/identity/plc/request.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAllowDeactivated; 4 use crate::state::AppState; 5 use axum::{ 6 extract::State, ··· 15 16 pub async fn request_plc_operation_signature( 17 State(state): State<AppState>, 18 - auth: BearerAuthAllowDeactivated, 19 - ) -> Response { 20 - let auth_user = auth.0; 21 if let Err(e) = crate::auth::scope_check::check_identity_scope( 22 - auth_user.is_oauth, 23 - auth_user.scope.as_deref(), 24 crate::oauth::scopes::IdentityAttr::Wildcard, 25 ) { 26 - return e; 27 } 28 - let user_id = match state.user_repo.get_id_by_did(&auth_user.did).await { 29 - Ok(Some(id)) => id, 30 - Ok(None) => return ApiError::AccountNotFound.into_response(), 31 - Err(e) => { 32 error!("DB error: {:?}", e); 33 - return ApiError::InternalError(None).into_response(); 34 - } 35 - }; 36 let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await; 37 let plc_token = generate_plc_token(); 38 let expires_at = Utc::now() + Duration::minutes(10); 39 - if let Err(e) = state 40 .infra_repo 41 .insert_plc_token(user_id, &plc_token, expires_at) 42 .await 43 - { 44 - error!("Failed to create PLC token: {:?}", e); 45 - return ApiError::InternalError(None).into_response(); 46 - } 47 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 48 if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation( 49 state.user_repo.as_ref(), ··· 56 { 57 warn!("Failed to enqueue PLC operation notification: {:?}", e); 58 } 59 - info!( 60 - "PLC operation signature requested for user {}", 61 - auth_user.did 62 - ); 63 - EmptyResponse::ok().into_response() 64 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::{Auth, NotTakendown}; 4 use crate::state::AppState; 5 use axum::{ 6 extract::State, ··· 15 16 pub async fn request_plc_operation_signature( 17 State(state): State<AppState>, 18 + auth: Auth<NotTakendown>, 19 + ) -> Result<Response, ApiError> { 20 if let Err(e) = crate::auth::scope_check::check_identity_scope( 21 + auth.is_oauth(), 22 + auth.scope.as_deref(), 23 crate::oauth::scopes::IdentityAttr::Wildcard, 24 ) { 25 + return Ok(e); 26 } 27 + let user_id = state 28 + .user_repo 29 + .get_id_by_did(&auth.did) 30 + .await 31 + .map_err(|e| { 32 error!("DB error: {:?}", e); 33 + ApiError::InternalError(None) 34 + })? 35 + .ok_or(ApiError::AccountNotFound)?; 36 + 37 let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await; 38 let plc_token = generate_plc_token(); 39 let expires_at = Utc::now() + Duration::minutes(10); 40 + state 41 .infra_repo 42 .insert_plc_token(user_id, &plc_token, expires_at) 43 .await 44 + .map_err(|e| { 45 + error!("Failed to create PLC token: {:?}", e); 46 + ApiError::InternalError(None) 47 + })?; 48 + 49 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 50 if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation( 51 state.user_repo.as_ref(), ··· 58 { 59 warn!("Failed to enqueue PLC operation notification: {:?}", e); 60 } 61 + info!("PLC operation signature requested for user {}", auth.did); 62 + Ok(EmptyResponse::ok().into_response()) 63 }
+70 -85
crates/tranquil-pds/src/api/identity/plc/sign.rs
··· 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuthAllowDeactivated; 3 use crate::circuit_breaker::with_circuit_breaker; 4 use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation}; 5 use crate::state::AppState; ··· 40 41 pub async fn sign_plc_operation( 42 State(state): State<AppState>, 43 - auth: BearerAuthAllowDeactivated, 44 Json(input): Json<SignPlcOperationInput>, 45 - ) -> Response { 46 - let auth_user = auth.0; 47 if let Err(e) = crate::auth::scope_check::check_identity_scope( 48 - auth_user.is_oauth, 49 - auth_user.scope.as_deref(), 50 crate::oauth::scopes::IdentityAttr::Wildcard, 51 ) { 52 - return e; 53 } 54 - let did = &auth_user.did; 55 if did.starts_with("did:web:") { 56 - return ApiError::InvalidRequest( 57 "PLC operations are only valid for did:plc identities".into(), 58 - ) 59 - .into_response(); 60 } 61 - let token = match &input.token { 62 - Some(t) => t, 63 - None => { 64 - return ApiError::InvalidRequest( 65 - "Email confirmation token required to sign PLC operations".into(), 66 - ) 67 - .into_response(); 68 - } 69 - }; 70 - let user_id = match state.user_repo.get_id_by_did(did).await { 71 - Ok(Some(id)) => id, 72 - Ok(None) => return ApiError::AccountNotFound.into_response(), 73 - Err(e) => { 74 error!("DB error: {:?}", e); 75 - return ApiError::InternalError(None).into_response(); 76 - } 77 - }; 78 - let token_expiry = match state.infra_repo.get_plc_token_expiry(user_id, token).await { 79 - Ok(Some(expiry)) => expiry, 80 - Ok(None) => { 81 - return ApiError::InvalidToken(Some("Invalid or expired token".into())).into_response(); 82 - } 83 - Err(e) => { 84 error!("DB error: {:?}", e); 85 - return ApiError::InternalError(None).into_response(); 86 - } 87 - }; 88 if Utc::now() > token_expiry { 89 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 90 - return ApiError::ExpiredToken(Some("Token has expired".into())).into_response(); 91 } 92 - let key_row = match state.user_repo.get_user_key_by_id(user_id).await { 93 - Ok(Some(row)) => row, 94 - Ok(None) => { 95 - return ApiError::InternalError(Some("User signing key not found".into())) 96 - .into_response(); 97 - } 98 - Err(e) => { 99 error!("DB error: {:?}", e); 100 - return ApiError::InternalError(None).into_response(); 101 - } 102 - }; 103 - let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 104 - { 105 - Ok(k) => k, 106 - Err(e) => { 107 error!("Failed to decrypt user key: {}", e); 108 - return ApiError::InternalError(None).into_response(); 109 - } 110 - }; 111 - let signing_key = match SigningKey::from_slice(&key_bytes) { 112 - Ok(k) => k, 113 - Err(e) => { 114 - error!("Failed to create signing key: {:?}", e); 115 - return ApiError::InternalError(None).into_response(); 116 - } 117 - }; 118 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 119 let did_clone = did.clone(); 120 - let last_op = match with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 121 plc_client.get_last_op(&did_clone).await 122 }) 123 .await 124 - { 125 - Ok(op) => op, 126 - Err(e) => return ApiError::from(e).into_response(), 127 - }; 128 if last_op.is_tombstone() { 129 - return ApiError::from(PlcError::Tombstoned).into_response(); 130 } 131 let services = input.services.map(|s| { 132 s.into_iter() ··· 141 }) 142 .collect() 143 }); 144 - let unsigned_op = match create_update_op( 145 &last_op, 146 input.rotation_keys, 147 input.verification_methods, 148 input.also_known_as, 149 services, 150 - ) { 151 - Ok(op) => op, 152 - Err(PlcError::Tombstoned) => { 153 - return ApiError::InvalidRequest("Cannot update tombstoned DID".into()).into_response(); 154 - } 155 - Err(e) => { 156 error!("Failed to create PLC operation: {:?}", e); 157 - return ApiError::InternalError(None).into_response(); 158 - } 159 - }; 160 - let signed_op = match sign_operation(&unsigned_op, &signing_key) { 161 - Ok(op) => op, 162 - Err(e) => { 163 - error!("Failed to sign PLC operation: {:?}", e); 164 - return ApiError::InternalError(None).into_response(); 165 } 166 - }; 167 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 168 info!("Signed PLC operation for user {}", did); 169 - ( 170 StatusCode::OK, 171 Json(SignPlcOperationOutput { 172 operation: signed_op, 173 }), 174 ) 175 - .into_response() 176 }
··· 1 use crate::api::ApiError; 2 + use crate::auth::{Auth, NotTakendown}; 3 use crate::circuit_breaker::with_circuit_breaker; 4 use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation}; 5 use crate::state::AppState; ··· 40 41 pub async fn sign_plc_operation( 42 State(state): State<AppState>, 43 + auth: Auth<NotTakendown>, 44 Json(input): Json<SignPlcOperationInput>, 45 + ) -> Result<Response, ApiError> { 46 if let Err(e) = crate::auth::scope_check::check_identity_scope( 47 + auth.is_oauth(), 48 + auth.scope.as_deref(), 49 crate::oauth::scopes::IdentityAttr::Wildcard, 50 ) { 51 + return Ok(e); 52 } 53 + let did = &auth.did; 54 if did.starts_with("did:web:") { 55 + return Err(ApiError::InvalidRequest( 56 "PLC operations are only valid for did:plc identities".into(), 57 + )); 58 } 59 + let token = input.token.as_ref().ok_or_else(|| { 60 + ApiError::InvalidRequest("Email confirmation token required to sign PLC operations".into()) 61 + })?; 62 + 63 + let user_id = state 64 + .user_repo 65 + .get_id_by_did(did) 66 + .await 67 + .map_err(|e| { 68 error!("DB error: {:?}", e); 69 + ApiError::InternalError(None) 70 + })? 71 + .ok_or(ApiError::AccountNotFound)?; 72 + 73 + let token_expiry = state 74 + .infra_repo 75 + .get_plc_token_expiry(user_id, token) 76 + .await 77 + .map_err(|e| { 78 error!("DB error: {:?}", e); 79 + ApiError::InternalError(None) 80 + })? 81 + .ok_or_else(|| ApiError::InvalidToken(Some("Invalid or expired token".into())))?; 82 + 83 if Utc::now() > token_expiry { 84 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 85 + return Err(ApiError::ExpiredToken(Some("Token has expired".into()))); 86 } 87 + let key_row = state 88 + .user_repo 89 + .get_user_key_by_id(user_id) 90 + .await 91 + .map_err(|e| { 92 error!("DB error: {:?}", e); 93 + ApiError::InternalError(None) 94 + })? 95 + .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; 96 + 97 + let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 98 + .map_err(|e| { 99 error!("Failed to decrypt user key: {}", e); 100 + ApiError::InternalError(None) 101 + })?; 102 + 103 + let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { 104 + error!("Failed to create signing key: {:?}", e); 105 + ApiError::InternalError(None) 106 + })?; 107 + 108 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 109 let did_clone = did.clone(); 110 + let last_op = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 111 plc_client.get_last_op(&did_clone).await 112 }) 113 .await 114 + .map_err(ApiError::from)?; 115 + 116 if last_op.is_tombstone() { 117 + return Err(ApiError::from(PlcError::Tombstoned)); 118 } 119 let services = input.services.map(|s| { 120 s.into_iter() ··· 129 }) 130 .collect() 131 }); 132 + let unsigned_op = create_update_op( 133 &last_op, 134 input.rotation_keys, 135 input.verification_methods, 136 input.also_known_as, 137 services, 138 + ) 139 + .map_err(|e| match e { 140 + PlcError::Tombstoned => ApiError::InvalidRequest("Cannot update tombstoned DID".into()), 141 + _ => { 142 error!("Failed to create PLC operation: {:?}", e); 143 + ApiError::InternalError(None) 144 } 145 + })?; 146 + 147 + let signed_op = sign_operation(&unsigned_op, &signing_key).map_err(|e| { 148 + error!("Failed to sign PLC operation: {:?}", e); 149 + ApiError::InternalError(None) 150 + })?; 151 + 152 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 153 info!("Signed PLC operation for user {}", did); 154 + Ok(( 155 StatusCode::OK, 156 Json(SignPlcOperationOutput { 157 operation: signed_op, 158 }), 159 ) 160 + .into_response()) 161 }
+58 -61
crates/tranquil-pds/src/api/identity/plc/submit.rs
··· 1 use crate::api::{ApiError, EmptyResponse}; 2 - use crate::auth::BearerAuthAllowDeactivated; 3 use crate::circuit_breaker::with_circuit_breaker; 4 use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; 5 use crate::state::AppState; ··· 20 21 pub async fn submit_plc_operation( 22 State(state): State<AppState>, 23 - auth: BearerAuthAllowDeactivated, 24 Json(input): Json<SubmitPlcOperationInput>, 25 - ) -> Response { 26 - let auth_user = auth.0; 27 if let Err(e) = crate::auth::scope_check::check_identity_scope( 28 - auth_user.is_oauth, 29 - auth_user.scope.as_deref(), 30 crate::oauth::scopes::IdentityAttr::Wildcard, 31 ) { 32 - return e; 33 } 34 - let did = &auth_user.did; 35 if did.starts_with("did:web:") { 36 - return ApiError::InvalidRequest( 37 "PLC operations are only valid for did:plc identities".into(), 38 - ) 39 - .into_response(); 40 } 41 - if let Err(e) = validate_plc_operation(&input.operation) { 42 - return ApiError::InvalidRequest(format!("Invalid operation: {}", e)).into_response(); 43 - } 44 let op = &input.operation; 45 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 46 let public_url = format!("https://{}", hostname); 47 - let user = match state.user_repo.get_id_and_handle_by_did(did).await { 48 - Ok(Some(u)) => u, 49 - Ok(None) => return ApiError::AccountNotFound.into_response(), 50 - Err(e) => { 51 error!("DB error: {:?}", e); 52 - return ApiError::InternalError(None).into_response(); 53 - } 54 - }; 55 - let key_row = match state.user_repo.get_user_key_by_id(user.id).await { 56 - Ok(Some(row)) => row, 57 - Ok(None) => { 58 - return ApiError::InternalError(Some("User signing key not found".into())) 59 - .into_response(); 60 - } 61 - Err(e) => { 62 error!("DB error: {:?}", e); 63 - return ApiError::InternalError(None).into_response(); 64 - } 65 - }; 66 - let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 67 - { 68 - Ok(k) => k, 69 - Err(e) => { 70 error!("Failed to decrypt user key: {}", e); 71 - return ApiError::InternalError(None).into_response(); 72 - } 73 - }; 74 - let signing_key = match SigningKey::from_slice(&key_bytes) { 75 - Ok(k) => k, 76 - Err(e) => { 77 - error!("Failed to create signing key: {:?}", e); 78 - return ApiError::InternalError(None).into_response(); 79 - } 80 - }; 81 let user_did_key = signing_key_to_did_key(&signing_key); 82 let server_rotation_key = 83 std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); ··· 86 .iter() 87 .any(|k| k.as_str() == Some(&server_rotation_key)); 88 if !has_server_key { 89 - return ApiError::InvalidRequest( 90 "Rotation keys do not include server's rotation key".into(), 91 - ) 92 - .into_response(); 93 } 94 } 95 if let Some(services) = op.get("services").and_then(|v| v.as_object()) ··· 98 let service_type = pds.get("type").and_then(|v| v.as_str()); 99 let endpoint = pds.get("endpoint").and_then(|v| v.as_str()); 100 if service_type != Some("AtprotoPersonalDataServer") { 101 - return ApiError::InvalidRequest("Incorrect type on atproto_pds service".into()) 102 - .into_response(); 103 } 104 if endpoint != Some(&public_url) { 105 - return ApiError::InvalidRequest("Incorrect endpoint on atproto_pds service".into()) 106 - .into_response(); 107 } 108 } 109 if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object()) 110 && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 111 && atproto_key != user_did_key 112 { 113 - return ApiError::InvalidRequest("Incorrect signing key in verificationMethods".into()) 114 - .into_response(); 115 } 116 if let Some(also_known_as) = (!user.handle.is_empty()) 117 .then(|| op.get("alsoKnownAs").and_then(|v| v.as_array())) ··· 120 let expected_handle = format!("at://{}", user.handle); 121 let first_aka = also_known_as.first().and_then(|v| v.as_str()); 122 if first_aka != Some(&expected_handle) { 123 - return ApiError::InvalidRequest("Incorrect handle in alsoKnownAs".into()) 124 - .into_response(); 125 } 126 } 127 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 128 let operation_clone = input.operation.clone(); 129 let did_clone = did.clone(); 130 - if let Err(e) = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 131 plc_client 132 .send_operation(&did_clone, &operation_clone) 133 .await 134 }) 135 .await 136 - { 137 - return ApiError::from(e).into_response(); 138 - } 139 match state 140 .repo_repo 141 .insert_identity_event(did, Some(&user.handle)) ··· 157 warn!(did = %did, "Failed to refresh DID cache after PLC update"); 158 } 159 info!(did = %did, "PLC operation submitted successfully"); 160 - EmptyResponse::ok().into_response() 161 }
··· 1 use crate::api::{ApiError, EmptyResponse}; 2 + use crate::auth::{Auth, NotTakendown}; 3 use crate::circuit_breaker::with_circuit_breaker; 4 use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; 5 use crate::state::AppState; ··· 20 21 pub async fn submit_plc_operation( 22 State(state): State<AppState>, 23 + auth: Auth<NotTakendown>, 24 Json(input): Json<SubmitPlcOperationInput>, 25 + ) -> Result<Response, ApiError> { 26 if let Err(e) = crate::auth::scope_check::check_identity_scope( 27 + auth.is_oauth(), 28 + auth.scope.as_deref(), 29 crate::oauth::scopes::IdentityAttr::Wildcard, 30 ) { 31 + return Ok(e); 32 } 33 + let did = &auth.did; 34 if did.starts_with("did:web:") { 35 + return Err(ApiError::InvalidRequest( 36 "PLC operations are only valid for did:plc identities".into(), 37 + )); 38 } 39 + validate_plc_operation(&input.operation) 40 + .map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?; 41 + 42 let op = &input.operation; 43 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 44 let public_url = format!("https://{}", hostname); 45 + let user = state 46 + .user_repo 47 + .get_id_and_handle_by_did(did) 48 + .await 49 + .map_err(|e| { 50 error!("DB error: {:?}", e); 51 + ApiError::InternalError(None) 52 + })? 53 + .ok_or(ApiError::AccountNotFound)?; 54 + 55 + let key_row = state 56 + .user_repo 57 + .get_user_key_by_id(user.id) 58 + .await 59 + .map_err(|e| { 60 error!("DB error: {:?}", e); 61 + ApiError::InternalError(None) 62 + })? 63 + .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; 64 + 65 + let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 66 + .map_err(|e| { 67 error!("Failed to decrypt user key: {}", e); 68 + ApiError::InternalError(None) 69 + })?; 70 + 71 + let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { 72 + error!("Failed to create signing key: {:?}", e); 73 + ApiError::InternalError(None) 74 + })?; 75 + 76 let user_did_key = signing_key_to_did_key(&signing_key); 77 let server_rotation_key = 78 std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); ··· 81 .iter() 82 .any(|k| k.as_str() == Some(&server_rotation_key)); 83 if !has_server_key { 84 + return Err(ApiError::InvalidRequest( 85 "Rotation keys do not include server's rotation key".into(), 86 + )); 87 } 88 } 89 if let Some(services) = op.get("services").and_then(|v| v.as_object()) ··· 92 let service_type = pds.get("type").and_then(|v| v.as_str()); 93 let endpoint = pds.get("endpoint").and_then(|v| v.as_str()); 94 if service_type != Some("AtprotoPersonalDataServer") { 95 + return Err(ApiError::InvalidRequest( 96 + "Incorrect type on atproto_pds service".into(), 97 + )); 98 } 99 if endpoint != Some(&public_url) { 100 + return Err(ApiError::InvalidRequest( 101 + "Incorrect endpoint on atproto_pds service".into(), 102 + )); 103 } 104 } 105 if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object()) 106 && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 107 && atproto_key != user_did_key 108 { 109 + return Err(ApiError::InvalidRequest( 110 + "Incorrect signing key in verificationMethods".into(), 111 + )); 112 } 113 if let Some(also_known_as) = (!user.handle.is_empty()) 114 .then(|| op.get("alsoKnownAs").and_then(|v| v.as_array())) ··· 117 let expected_handle = format!("at://{}", user.handle); 118 let first_aka = also_known_as.first().and_then(|v| v.as_str()); 119 if first_aka != Some(&expected_handle) { 120 + return Err(ApiError::InvalidRequest( 121 + "Incorrect handle in alsoKnownAs".into(), 122 + )); 123 } 124 } 125 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 126 let operation_clone = input.operation.clone(); 127 let did_clone = did.clone(); 128 + with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 129 plc_client 130 .send_operation(&did_clone, &operation_clone) 131 .await 132 }) 133 .await 134 + .map_err(ApiError::from)?; 135 + 136 match state 137 .repo_repo 138 .insert_identity_event(did, Some(&user.handle)) ··· 154 warn!(did = %did, "Failed to refresh DID cache after PLC update"); 155 } 156 info!(did = %did, "PLC operation submitted successfully"); 157 + Ok(EmptyResponse::ok().into_response()) 158 }
+5 -7
crates/tranquil-pds/src/api/moderation/mod.rs
··· 1 use crate::api::ApiError; 2 use crate::api::proxy_client::{is_ssrf_safe, proxy_client}; 3 - use crate::auth::extractor::BearerAuthAllowTakendown; 4 use crate::state::AppState; 5 use axum::{ 6 Json, ··· 42 43 pub async fn create_report( 44 State(state): State<AppState>, 45 - auth: BearerAuthAllowTakendown, 46 Json(input): Json<CreateReportInput>, 47 ) -> Response { 48 - let auth_user = auth.0; 49 - let did = &auth_user.did; 50 51 if let Some((service_url, service_did)) = get_report_service_config() { 52 - return proxy_to_report_service(&state, &auth_user, &service_url, &service_did, &input) 53 - .await; 54 } 55 56 - create_report_locally(&state, did, auth_user.is_takendown(), input).await 57 } 58 59 async fn proxy_to_report_service(
··· 1 use crate::api::ApiError; 2 use crate::api::proxy_client::{is_ssrf_safe, proxy_client}; 3 + use crate::auth::{AnyUser, Auth}; 4 use crate::state::AppState; 5 use axum::{ 6 Json, ··· 42 43 pub async fn create_report( 44 State(state): State<AppState>, 45 + auth: Auth<AnyUser>, 46 Json(input): Json<CreateReportInput>, 47 ) -> Response { 48 + let did = &auth.did; 49 50 if let Some((service_url, service_did)) = get_report_service_config() { 51 + return proxy_to_report_service(&state, &auth, &service_url, &service_did, &input).await; 52 } 53 54 + create_report_locally(&state, did, auth.status.is_takendown(), input).await 55 } 56 57 async fn proxy_to_report_service(
+82 -98
crates/tranquil-pds/src/api/notification_prefs.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuth; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 23 pub signal_verified: bool, 24 } 25 26 - pub async fn get_notification_prefs(State(state): State<AppState>, auth: BearerAuth) -> Response { 27 - let user = auth.0; 28 - let prefs = match state.user_repo.get_notification_prefs(&user.did).await { 29 - Ok(Some(p)) => p, 30 - Ok(None) => return ApiError::AccountNotFound.into_response(), 31 - Err(e) => { 32 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 33 - } 34 - }; 35 - Json(NotificationPrefsResponse { 36 preferred_channel: prefs.preferred_channel, 37 email: prefs.email, 38 discord_id: prefs.discord_id, ··· 42 signal_number: prefs.signal_number, 43 signal_verified: prefs.signal_verified, 44 }) 45 - .into_response() 46 } 47 48 #[derive(Serialize)] ··· 62 pub notifications: Vec<NotificationHistoryEntry>, 63 } 64 65 - pub async fn get_notification_history(State(state): State<AppState>, auth: BearerAuth) -> Response { 66 - let user = auth.0; 67 - 68 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&user.did).await { 69 - Ok(Some(id)) => id, 70 - Ok(None) => return ApiError::AccountNotFound.into_response(), 71 - Err(e) => { 72 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 73 - } 74 - }; 75 76 - let rows = match state.infra_repo.get_notification_history(user_id, 50).await { 77 - Ok(r) => r, 78 - Err(e) => { 79 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 80 - } 81 - }; 82 83 let sensitive_types = [ 84 "email_verification", ··· 111 }) 112 .collect(); 113 114 - Json(GetNotificationHistoryResponse { notifications }).into_response() 115 } 116 117 #[derive(Deserialize)] ··· 184 185 pub async fn update_notification_prefs( 186 State(state): State<AppState>, 187 - auth: BearerAuth, 188 Json(input): Json<UpdateNotificationPrefsInput>, 189 - ) -> Response { 190 - let user = auth.0; 191 - 192 - let user_row = match state.user_repo.get_id_handle_email_by_did(&user.did).await { 193 - Ok(Some(row)) => row, 194 - Ok(None) => return ApiError::AccountNotFound.into_response(), 195 - Err(e) => { 196 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 197 - } 198 - }; 199 200 let user_id = user_row.id; 201 let handle = user_row.handle; ··· 206 if let Some(ref channel) = input.preferred_channel { 207 let valid_channels = ["email", "discord", "telegram", "signal"]; 208 if !valid_channels.contains(&channel.as_str()) { 209 - return ApiError::InvalidRequest( 210 "Invalid channel. Must be one of: email, discord, telegram, signal".into(), 211 - ) 212 - .into_response(); 213 } 214 - if let Err(e) = state 215 .user_repo 216 - .update_preferred_comms_channel(&user.did, channel) 217 .await 218 - { 219 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 220 - } 221 - info!(did = %user.did, channel = %channel, "Updated preferred notification channel"); 222 } 223 224 if let Some(ref new_email) = input.email { 225 let email_clean = new_email.trim().to_lowercase(); 226 if email_clean.is_empty() { 227 - return ApiError::InvalidRequest("Email cannot be empty".into()).into_response(); 228 } 229 230 if !crate::api::validation::is_valid_email(&email_clean) { 231 - return ApiError::InvalidEmail.into_response(); 232 } 233 234 - if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email_clean.clone()) { 235 - info!(did = %user.did, "Email unchanged, skipping"); 236 - } else { 237 - if let Err(e) = request_channel_verification( 238 &state, 239 user_id, 240 - &user.did, 241 "email", 242 &email_clean, 243 Some(&handle), 244 ) 245 .await 246 - { 247 - return ApiError::InternalError(Some(e)).into_response(); 248 - } 249 verification_required.push("email".to_string()); 250 - info!(did = %user.did, "Requested email verification"); 251 } 252 } 253 254 if let Some(ref discord_id) = input.discord_id { 255 if discord_id.is_empty() { 256 - if let Err(e) = state.user_repo.clear_discord(user_id).await { 257 - return ApiError::InternalError(Some(format!("Database error: {}", e))) 258 - .into_response(); 259 - } 260 - info!(did = %user.did, "Cleared Discord ID"); 261 } else { 262 - if let Err(e) = request_channel_verification( 263 - &state, user_id, &user.did, "discord", discord_id, None, 264 - ) 265 - .await 266 - { 267 - return ApiError::InternalError(Some(e)).into_response(); 268 - } 269 verification_required.push("discord".to_string()); 270 - info!(did = %user.did, "Requested Discord verification"); 271 } 272 } 273 274 if let Some(ref telegram) = input.telegram_username { 275 let telegram_clean = telegram.trim_start_matches('@'); 276 if telegram_clean.is_empty() { 277 - if let Err(e) = state.user_repo.clear_telegram(user_id).await { 278 - return ApiError::InternalError(Some(format!("Database error: {}", e))) 279 - .into_response(); 280 - } 281 - info!(did = %user.did, "Cleared Telegram username"); 282 } else { 283 - if let Err(e) = request_channel_verification( 284 &state, 285 user_id, 286 - &user.did, 287 "telegram", 288 telegram_clean, 289 None, 290 ) 291 .await 292 - { 293 - return ApiError::InternalError(Some(e)).into_response(); 294 - } 295 verification_required.push("telegram".to_string()); 296 - info!(did = %user.did, "Requested Telegram verification"); 297 } 298 } 299 300 if let Some(ref signal) = input.signal_number { 301 if signal.is_empty() { 302 - if let Err(e) = state.user_repo.clear_signal(user_id).await { 303 - return ApiError::InternalError(Some(format!("Database error: {}", e))) 304 - .into_response(); 305 - } 306 - info!(did = %user.did, "Cleared Signal number"); 307 } else { 308 - if let Err(e) = 309 - request_channel_verification(&state, user_id, &user.did, "signal", signal, None) 310 - .await 311 - { 312 - return ApiError::InternalError(Some(e)).into_response(); 313 - } 314 verification_required.push("signal".to_string()); 315 - info!(did = %user.did, "Requested Signal verification"); 316 } 317 } 318 319 - Json(UpdateNotificationPrefsResponse { 320 success: true, 321 verification_required, 322 }) 323 - .into_response() 324 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::{Active, Auth}; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 23 pub signal_verified: bool, 24 } 25 26 + pub async fn get_notification_prefs( 27 + State(state): State<AppState>, 28 + auth: Auth<Active>, 29 + ) -> Result<Response, ApiError> { 30 + let prefs = state 31 + .user_repo 32 + .get_notification_prefs(&auth.did) 33 + .await 34 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 35 + .ok_or(ApiError::AccountNotFound)?; 36 + Ok(Json(NotificationPrefsResponse { 37 preferred_channel: prefs.preferred_channel, 38 email: prefs.email, 39 discord_id: prefs.discord_id, ··· 43 signal_number: prefs.signal_number, 44 signal_verified: prefs.signal_verified, 45 }) 46 + .into_response()) 47 } 48 49 #[derive(Serialize)] ··· 63 pub notifications: Vec<NotificationHistoryEntry>, 64 } 65 66 + pub async fn get_notification_history( 67 + State(state): State<AppState>, 68 + auth: Auth<Active>, 69 + ) -> Result<Response, ApiError> { 70 + let user_id = state 71 + .user_repo 72 + .get_id_by_did(&auth.did) 73 + .await 74 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 75 + .ok_or(ApiError::AccountNotFound)?; 76 77 + let rows = state 78 + .infra_repo 79 + .get_notification_history(user_id, 50) 80 + .await 81 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 82 83 let sensitive_types = [ 84 "email_verification", ··· 111 }) 112 .collect(); 113 114 + Ok(Json(GetNotificationHistoryResponse { notifications }).into_response()) 115 } 116 117 #[derive(Deserialize)] ··· 184 185 pub async fn update_notification_prefs( 186 State(state): State<AppState>, 187 + auth: Auth<Active>, 188 Json(input): Json<UpdateNotificationPrefsInput>, 189 + ) -> Result<Response, ApiError> { 190 + let user_row = state 191 + .user_repo 192 + .get_id_handle_email_by_did(&auth.did) 193 + .await 194 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 195 + .ok_or(ApiError::AccountNotFound)?; 196 197 let user_id = user_row.id; 198 let handle = user_row.handle; ··· 203 if let Some(ref channel) = input.preferred_channel { 204 let valid_channels = ["email", "discord", "telegram", "signal"]; 205 if !valid_channels.contains(&channel.as_str()) { 206 + return Err(ApiError::InvalidRequest( 207 "Invalid channel. Must be one of: email, discord, telegram, signal".into(), 208 + )); 209 } 210 + state 211 .user_repo 212 + .update_preferred_comms_channel(&auth.did, channel) 213 .await 214 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 215 + info!(did = %auth.did, channel = %channel, "Updated preferred notification channel"); 216 } 217 218 if let Some(ref new_email) = input.email { 219 let email_clean = new_email.trim().to_lowercase(); 220 if email_clean.is_empty() { 221 + return Err(ApiError::InvalidRequest("Email cannot be empty".into())); 222 } 223 224 if !crate::api::validation::is_valid_email(&email_clean) { 225 + return Err(ApiError::InvalidEmail); 226 } 227 228 + if current_email.as_ref().map(|e| e.to_lowercase()) != Some(email_clean.clone()) { 229 + request_channel_verification( 230 &state, 231 user_id, 232 + &auth.did, 233 "email", 234 &email_clean, 235 Some(&handle), 236 ) 237 .await 238 + .map_err(|e| ApiError::InternalError(Some(e)))?; 239 verification_required.push("email".to_string()); 240 + info!(did = %auth.did, "Requested email verification"); 241 } 242 } 243 244 if let Some(ref discord_id) = input.discord_id { 245 if discord_id.is_empty() { 246 + state 247 + .user_repo 248 + .clear_discord(user_id) 249 + .await 250 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 251 + info!(did = %auth.did, "Cleared Discord ID"); 252 } else { 253 + request_channel_verification(&state, user_id, &auth.did, "discord", discord_id, None) 254 + .await 255 + .map_err(|e| ApiError::InternalError(Some(e)))?; 256 verification_required.push("discord".to_string()); 257 + info!(did = %auth.did, "Requested Discord verification"); 258 } 259 } 260 261 if let Some(ref telegram) = input.telegram_username { 262 let telegram_clean = telegram.trim_start_matches('@'); 263 if telegram_clean.is_empty() { 264 + state 265 + .user_repo 266 + .clear_telegram(user_id) 267 + .await 268 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 269 + info!(did = %auth.did, "Cleared Telegram username"); 270 } else { 271 + request_channel_verification( 272 &state, 273 user_id, 274 + &auth.did, 275 "telegram", 276 telegram_clean, 277 None, 278 ) 279 .await 280 + .map_err(|e| ApiError::InternalError(Some(e)))?; 281 verification_required.push("telegram".to_string()); 282 + info!(did = %auth.did, "Requested Telegram verification"); 283 } 284 } 285 286 if let Some(ref signal) = input.signal_number { 287 if signal.is_empty() { 288 + state 289 + .user_repo 290 + .clear_signal(user_id) 291 + .await 292 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 293 + info!(did = %auth.did, "Cleared Signal number"); 294 } else { 295 + request_channel_verification(&state, user_id, &auth.did, "signal", signal, None) 296 + .await 297 + .map_err(|e| ApiError::InternalError(Some(e)))?; 298 verification_required.push("signal".to_string()); 299 + info!(did = %auth.did, "Requested Signal verification"); 300 } 301 } 302 303 + Ok(Json(UpdateNotificationPrefsResponse { 304 success: true, 305 verification_required, 306 }) 307 + .into_response()) 308 }
+1 -1
crates/tranquil-pds/src/api/proxy.rs
··· 238 { 239 Ok(auth_user) => { 240 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 241 - auth_user.is_oauth, 242 auth_user.scope.as_deref(), 243 &resolved.did, 244 method,
··· 238 { 239 Ok(auth_user) => { 240 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 241 + auth_user.is_oauth(), 242 auth_user.scope.as_deref(), 243 &resolved.did, 244 method,
+57 -52
crates/tranquil-pds/src/api/repo/blob.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::{BearerAuthAllowDeactivated, BlobAuth, BlobAuthResult}; 3 use crate::delegation::DelegationActionType; 4 use crate::state::AppState; 5 use crate::types::{CidLink, Did}; ··· 44 pub async fn upload_blob( 45 State(state): State<AppState>, 46 headers: axum::http::HeaderMap, 47 - auth: BlobAuth, 48 body: Body, 49 - ) -> Response { 50 - let (did, controller_did): (Did, Option<Did>) = match auth.0 { 51 - BlobAuthResult::Service { did } => (did, None), 52 - BlobAuthResult::User(auth_user) => { 53 let mime_type_for_check = headers 54 .get("content-type") 55 .and_then(|h| h.to_str().ok()) 56 .unwrap_or("application/octet-stream"); 57 if let Err(e) = crate::auth::scope_check::check_blob_scope( 58 - auth_user.is_oauth, 59 - auth_user.scope.as_deref(), 60 mime_type_for_check, 61 ) { 62 - return e; 63 } 64 - let ctrl_did = auth_user.controller_did.clone(); 65 - (auth_user.did, ctrl_did) 66 } 67 }; 68 ··· 72 .await 73 .unwrap_or(false) 74 { 75 - return ApiError::Forbidden.into_response(); 76 } 77 78 let client_mime_hint = headers ··· 80 .and_then(|h| h.to_str().ok()) 81 .unwrap_or("application/octet-stream"); 82 83 - let user_id = match state.user_repo.get_id_by_did(&did).await { 84 - Ok(Some(id)) => id, 85 - _ => { 86 - return ApiError::InternalError(None).into_response(); 87 - } 88 - }; 89 90 let temp_key = format!("temp/{}", uuid::Uuid::new_v4()); 91 let max_size = get_max_blob_size() as u64; ··· 98 99 info!("Starting streaming blob upload to temp key: {}", temp_key); 100 101 - let upload_result = match state.blob_store.put_stream(&temp_key, pinned_stream).await { 102 - Ok(result) => result, 103 - Err(e) => { 104 error!("Failed to stream blob to storage: {:?}", e); 105 - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 106 - } 107 - }; 108 109 let size = upload_result.size; 110 if size > max_size { 111 let _ = state.blob_store.delete(&temp_key).await; 112 - return ApiError::InvalidRequest(format!( 113 "Blob size {} exceeds maximum of {} bytes", 114 size, max_size 115 - )) 116 - .into_response(); 117 } 118 119 let mime_type = match state.blob_store.get_head(&temp_key, 8192).await { ··· 129 Err(e) => { 130 let _ = state.blob_store.delete(&temp_key).await; 131 error!("Failed to create multihash for blob: {:?}", e); 132 - return ApiError::InternalError(Some("Failed to hash blob".into())).into_response(); 133 } 134 }; 135 let cid = Cid::new_v1(0x55, multihash); ··· 152 Err(e) => { 153 let _ = state.blob_store.delete(&temp_key).await; 154 error!("Failed to insert blob record: {:?}", e); 155 - return ApiError::InternalError(None).into_response(); 156 } 157 }; 158 159 if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await { 160 let _ = state.blob_store.delete(&temp_key).await; 161 error!("Failed to copy blob to final location: {:?}", e); 162 - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 163 } 164 165 let _ = state.blob_store.delete(&temp_key).await; ··· 183 .await; 184 } 185 186 - Json(json!({ 187 "blob": { 188 "$type": "blob", 189 "ref": { ··· 193 "size": size 194 } 195 })) 196 - .into_response() 197 } 198 199 #[derive(Deserialize)] ··· 218 219 pub async fn list_missing_blobs( 220 State(state): State<AppState>, 221 - auth: BearerAuthAllowDeactivated, 222 Query(params): Query<ListMissingBlobsParams>, 223 - ) -> Response { 224 - let auth_user = auth.0; 225 - let did = &auth_user.did; 226 - let user = match state.user_repo.get_by_did(did).await { 227 - Ok(Some(u)) => u, 228 - Ok(None) => return ApiError::InternalError(None).into_response(), 229 - Err(e) => { 230 error!("DB error fetching user: {:?}", e); 231 - return ApiError::InternalError(None).into_response(); 232 - } 233 - }; 234 let limit = params.limit.unwrap_or(500).clamp(1, 1000); 235 let cursor = params.cursor.as_deref(); 236 - let missing = match state 237 .blob_repo 238 .list_missing_blobs(user.id, cursor, limit + 1) 239 .await 240 - { 241 - Ok(m) => m, 242 - Err(e) => { 243 error!("DB error fetching missing blobs: {:?}", e); 244 - return ApiError::InternalError(None).into_response(); 245 - } 246 - }; 247 let has_more = missing.len() > limit as usize; 248 let blobs: Vec<RecordBlob> = missing 249 .into_iter() ··· 258 } else { 259 None 260 }; 261 - ( 262 StatusCode::OK, 263 Json(ListMissingBlobsOutput { 264 cursor: next_cursor, 265 blobs, 266 }), 267 ) 268 - .into_response() 269 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::{Auth, AuthAny, NotTakendown, Permissive}; 3 use crate::delegation::DelegationActionType; 4 use crate::state::AppState; 5 use crate::types::{CidLink, Did}; ··· 44 pub async fn upload_blob( 45 State(state): State<AppState>, 46 headers: axum::http::HeaderMap, 47 + auth: AuthAny<Permissive>, 48 body: Body, 49 + ) -> Result<Response, ApiError> { 50 + let (did, controller_did): (Did, Option<Did>) = match &auth { 51 + AuthAny::Service(service) => { 52 + service.require_lxm("com.atproto.repo.uploadBlob")?; 53 + (service.did.clone(), None) 54 + } 55 + AuthAny::User(user) => { 56 + if user.status.is_takendown() { 57 + return Err(ApiError::AccountTakedown); 58 + } 59 let mime_type_for_check = headers 60 .get("content-type") 61 .and_then(|h| h.to_str().ok()) 62 .unwrap_or("application/octet-stream"); 63 if let Err(e) = crate::auth::scope_check::check_blob_scope( 64 + user.is_oauth(), 65 + user.scope.as_deref(), 66 mime_type_for_check, 67 ) { 68 + return Ok(e); 69 } 70 + (user.did.clone(), user.controller_did.clone()) 71 } 72 }; 73 ··· 77 .await 78 .unwrap_or(false) 79 { 80 + return Err(ApiError::Forbidden); 81 } 82 83 let client_mime_hint = headers ··· 85 .and_then(|h| h.to_str().ok()) 86 .unwrap_or("application/octet-stream"); 87 88 + let user_id = state 89 + .user_repo 90 + .get_id_by_did(&did) 91 + .await 92 + .ok() 93 + .flatten() 94 + .ok_or(ApiError::InternalError(None))?; 95 96 let temp_key = format!("temp/{}", uuid::Uuid::new_v4()); 97 let max_size = get_max_blob_size() as u64; ··· 104 105 info!("Starting streaming blob upload to temp key: {}", temp_key); 106 107 + let upload_result = state 108 + .blob_store 109 + .put_stream(&temp_key, pinned_stream) 110 + .await 111 + .map_err(|e| { 112 error!("Failed to stream blob to storage: {:?}", e); 113 + ApiError::InternalError(Some("Failed to store blob".into())) 114 + })?; 115 116 let size = upload_result.size; 117 if size > max_size { 118 let _ = state.blob_store.delete(&temp_key).await; 119 + return Err(ApiError::InvalidRequest(format!( 120 "Blob size {} exceeds maximum of {} bytes", 121 size, max_size 122 + ))); 123 } 124 125 let mime_type = match state.blob_store.get_head(&temp_key, 8192).await { ··· 135 Err(e) => { 136 let _ = state.blob_store.delete(&temp_key).await; 137 error!("Failed to create multihash for blob: {:?}", e); 138 + return Err(ApiError::InternalError(Some("Failed to hash blob".into()))); 139 } 140 }; 141 let cid = Cid::new_v1(0x55, multihash); ··· 158 Err(e) => { 159 let _ = state.blob_store.delete(&temp_key).await; 160 error!("Failed to insert blob record: {:?}", e); 161 + return Err(ApiError::InternalError(None)); 162 } 163 }; 164 165 if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await { 166 let _ = state.blob_store.delete(&temp_key).await; 167 error!("Failed to copy blob to final location: {:?}", e); 168 + return Err(ApiError::InternalError(Some("Failed to store blob".into()))); 169 } 170 171 let _ = state.blob_store.delete(&temp_key).await; ··· 189 .await; 190 } 191 192 + Ok(Json(json!({ 193 "blob": { 194 "$type": "blob", 195 "ref": { ··· 199 "size": size 200 } 201 })) 202 + .into_response()) 203 } 204 205 #[derive(Deserialize)] ··· 224 225 pub async fn list_missing_blobs( 226 State(state): State<AppState>, 227 + auth: Auth<NotTakendown>, 228 Query(params): Query<ListMissingBlobsParams>, 229 + ) -> Result<Response, ApiError> { 230 + let did = &auth.did; 231 + let user = state 232 + .user_repo 233 + .get_by_did(did) 234 + .await 235 + .map_err(|e| { 236 error!("DB error fetching user: {:?}", e); 237 + ApiError::InternalError(None) 238 + })? 239 + .ok_or(ApiError::InternalError(None))?; 240 + 241 let limit = params.limit.unwrap_or(500).clamp(1, 1000); 242 let cursor = params.cursor.as_deref(); 243 + let missing = state 244 .blob_repo 245 .list_missing_blobs(user.id, cursor, limit + 1) 246 .await 247 + .map_err(|e| { 248 error!("DB error fetching missing blobs: {:?}", e); 249 + ApiError::InternalError(None) 250 + })?; 251 + 252 let has_more = missing.len() > limit as usize; 253 let blobs: Vec<RecordBlob> = missing 254 .into_iter() ··· 263 } else { 264 None 265 }; 266 + Ok(( 267 StatusCode::OK, 268 Json(ListMissingBlobsOutput { 269 cursor: next_cursor, 270 blobs, 271 }), 272 ) 273 + .into_response()) 274 }
+129 -131
crates/tranquil-pds/src/api/repo/import.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 use crate::api::repo::record::create_signed_commit; 4 - use crate::auth::BearerAuthAllowDeactivated; 5 use crate::state::AppState; 6 use crate::sync::import::{ImportError, apply_import, parse_car}; 7 use crate::sync::verify::CarVerifier; ··· 23 24 pub async fn import_repo( 25 State(state): State<AppState>, 26 - auth: BearerAuthAllowDeactivated, 27 body: Bytes, 28 - ) -> Response { 29 let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS") 30 .map(|v| v != "false" && v != "0") 31 .unwrap_or(true); 32 if !accepting_imports { 33 - return ApiError::InvalidRequest("Service is not accepting repo imports".into()) 34 - .into_response(); 35 } 36 let max_size: usize = std::env::var("MAX_IMPORT_SIZE") 37 .ok() 38 .and_then(|s| s.parse().ok()) 39 .unwrap_or(DEFAULT_MAX_IMPORT_SIZE); 40 if body.len() > max_size { 41 - return ApiError::PayloadTooLarge(format!( 42 "Import size exceeds limit of {} bytes", 43 max_size 44 - )) 45 - .into_response(); 46 } 47 - let auth_user = auth.0; 48 - let did = &auth_user.did; 49 - let user = match state.user_repo.get_by_did(did).await { 50 - Ok(Some(row)) => row, 51 - Ok(None) => { 52 - return ApiError::AccountNotFound.into_response(); 53 - } 54 - Err(e) => { 55 error!("DB error fetching user: {:?}", e); 56 - return ApiError::InternalError(None).into_response(); 57 - } 58 - }; 59 if user.takedown_ref.is_some() { 60 - return ApiError::AccountTakedown.into_response(); 61 } 62 let user_id = user.id; 63 let (root, blocks) = match parse_car(&body).await { 64 Ok((r, b)) => (r, b), 65 Err(ImportError::InvalidRootCount) => { 66 - return ApiError::InvalidRequest("Expected exactly one root in CAR file".into()) 67 - .into_response(); 68 } 69 Err(ImportError::CarParse(msg)) => { 70 - return ApiError::InvalidRequest(format!("Failed to parse CAR file: {}", msg)) 71 - .into_response(); 72 } 73 Err(e) => { 74 error!("CAR parsing error: {:?}", e); 75 - return ApiError::InvalidRequest(format!("Invalid CAR file: {}", e)).into_response(); 76 } 77 }; 78 info!( ··· 82 root 83 ); 84 let Some(root_block) = blocks.get(&root) else { 85 - return ApiError::InvalidRequest("Root block not found in CAR file".into()).into_response(); 86 }; 87 let commit_did = match jacquard_repo::commit::Commit::from_cbor(root_block) { 88 Ok(commit) => commit.did().to_string(), 89 Err(e) => { 90 - return ApiError::InvalidRequest(format!("Invalid commit: {}", e)).into_response(); 91 } 92 }; 93 if commit_did != *did { 94 - return ApiError::InvalidRepo(format!( 95 "CAR file is for DID {} but you are authenticated as {}", 96 commit_did, did 97 - )) 98 - .into_response(); 99 } 100 let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION") 101 .map(|v| v == "true" || v == "1") ··· 117 commit_did, 118 expected_did, 119 }) => { 120 - return ApiError::InvalidRepo(format!( 121 "CAR file is for DID {} but you are authenticated as {}", 122 commit_did, expected_did 123 - )) 124 - .into_response(); 125 } 126 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 127 - return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 128 - .into_response(); 129 } 130 Err(e) => { 131 error!("CAR structure verification error: {:?}", e); 132 - return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) 133 - .into_response(); 134 } 135 } 136 } else { ··· 147 commit_did, 148 expected_did, 149 }) => { 150 - return ApiError::InvalidRepo(format!( 151 "CAR file is for DID {} but you are authenticated as {}", 152 commit_did, expected_did 153 - )) 154 - .into_response(); 155 } 156 Err(crate::sync::verify::VerifyError::InvalidSignature) => { 157 - return ApiError::InvalidRequest( 158 "CAR file commit signature verification failed".into(), 159 - ) 160 - .into_response(); 161 } 162 Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => { 163 warn!("DID resolution failed during import verification: {}", msg); 164 - return ApiError::InvalidRequest(format!("Failed to verify DID: {}", msg)) 165 - .into_response(); 166 } 167 Err(crate::sync::verify::VerifyError::NoSigningKey) => { 168 - return ApiError::InvalidRequest( 169 "DID document does not contain a signing key".into(), 170 - ) 171 - .into_response(); 172 } 173 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 174 - return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 175 - .into_response(); 176 } 177 Err(e) => { 178 error!("CAR verification error: {:?}", e); 179 - return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) 180 - .into_response(); 181 } 182 } 183 } ··· 227 } 228 } 229 } 230 - let key_row = match state.user_repo.get_user_with_key_by_did(did).await { 231 - Ok(Some(row)) => row, 232 - Ok(None) => { 233 - error!("No signing key found for user {}", did); 234 - return ApiError::InternalError(Some("Signing key not found".into())) 235 - .into_response(); 236 - } 237 - Err(e) => { 238 error!("DB error fetching signing key: {:?}", e); 239 - return ApiError::InternalError(None).into_response(); 240 - } 241 - }; 242 let key_bytes = 243 - match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) { 244 - Ok(k) => k, 245 - Err(e) => { 246 error!("Failed to decrypt signing key: {}", e); 247 - return ApiError::InternalError(None).into_response(); 248 - } 249 - }; 250 - let signing_key = match SigningKey::from_slice(&key_bytes) { 251 - Ok(k) => k, 252 - Err(e) => { 253 - error!("Invalid signing key: {:?}", e); 254 - return ApiError::InternalError(None).into_response(); 255 - } 256 - }; 257 let new_rev = Tid::now(LimitedU32::MIN); 258 let new_rev_str = new_rev.to_string(); 259 - let (commit_bytes, _sig) = match create_signed_commit( 260 did, 261 import_result.data_cid, 262 &new_rev_str, 263 None, 264 &signing_key, 265 - ) { 266 - Ok(result) => result, 267 - Err(e) => { 268 - error!("Failed to create new commit: {}", e); 269 - return ApiError::InternalError(None).into_response(); 270 - } 271 - }; 272 - let new_root_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 273 - Ok(cid) => cid, 274 - Err(e) => { 275 error!("Failed to store new commit block: {:?}", e); 276 - return ApiError::InternalError(None).into_response(); 277 - } 278 - }; 279 let new_root_cid_link = CidLink::new_unchecked(new_root_cid.to_string()); 280 - if let Err(e) = state 281 .repo_repo 282 .update_repo_root(user_id, &new_root_cid_link, &new_rev_str) 283 .await 284 - { 285 - error!("Failed to update repo root: {:?}", e); 286 - return ApiError::InternalError(None).into_response(); 287 - } 288 let mut all_block_cids: Vec<Vec<u8>> = blocks.keys().map(|c| c.to_bytes()).collect(); 289 all_block_cids.push(new_root_cid.to_bytes()); 290 - if let Err(e) = state 291 .repo_repo 292 .insert_user_blocks(user_id, &all_block_cids, &new_rev_str) 293 .await 294 - { 295 - error!("Failed to insert user_blocks: {:?}", e); 296 - return ApiError::InternalError(None).into_response(); 297 - } 298 let new_root_str = new_root_cid.to_string(); 299 info!( 300 "Created new commit for imported repo: cid={}, rev={}", ··· 324 ); 325 } 326 } 327 - EmptyResponse::ok().into_response() 328 } 329 - Err(ImportError::SizeLimitExceeded) => { 330 - ApiError::PayloadTooLarge(format!("Import exceeds block limit of {}", max_blocks)) 331 - .into_response() 332 - } 333 - Err(ImportError::RepoNotFound) => { 334 - ApiError::RepoNotFound(Some("Repository not initialized for this account".into())) 335 - .into_response() 336 - } 337 - Err(ImportError::InvalidCbor(msg)) => { 338 - ApiError::InvalidRequest(format!("Invalid CBOR data: {}", msg)).into_response() 339 - } 340 - Err(ImportError::InvalidCommit(msg)) => { 341 - ApiError::InvalidRequest(format!("Invalid commit structure: {}", msg)).into_response() 342 - } 343 - Err(ImportError::BlockNotFound(cid)) => { 344 - ApiError::InvalidRequest(format!("Referenced block not found in CAR: {}", cid)) 345 - .into_response() 346 - } 347 - Err(ImportError::ConcurrentModification) => ApiError::InvalidSwap(Some( 348 "Repository is being modified by another operation, please retry".into(), 349 - )) 350 - .into_response(), 351 - Err(ImportError::VerificationFailed(ve)) => { 352 - ApiError::InvalidRequest(format!("CAR verification failed: {}", ve)).into_response() 353 - } 354 - Err(ImportError::DidMismatch { car_did, auth_did }) => ApiError::InvalidRequest(format!( 355 - "CAR is for {} but authenticated as {}", 356 - car_did, auth_did 357 - )) 358 - .into_response(), 359 Err(e) => { 360 error!("Import error: {:?}", e); 361 - ApiError::InternalError(None).into_response() 362 } 363 } 364 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 use crate::api::repo::record::create_signed_commit; 4 + use crate::auth::{Auth, NotTakendown}; 5 use crate::state::AppState; 6 use crate::sync::import::{ImportError, apply_import, parse_car}; 7 use crate::sync::verify::CarVerifier; ··· 23 24 pub async fn import_repo( 25 State(state): State<AppState>, 26 + auth: Auth<NotTakendown>, 27 body: Bytes, 28 + ) -> Result<Response, ApiError> { 29 let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS") 30 .map(|v| v != "false" && v != "0") 31 .unwrap_or(true); 32 if !accepting_imports { 33 + return Err(ApiError::InvalidRequest( 34 + "Service is not accepting repo imports".into(), 35 + )); 36 } 37 let max_size: usize = std::env::var("MAX_IMPORT_SIZE") 38 .ok() 39 .and_then(|s| s.parse().ok()) 40 .unwrap_or(DEFAULT_MAX_IMPORT_SIZE); 41 if body.len() > max_size { 42 + return Err(ApiError::PayloadTooLarge(format!( 43 "Import size exceeds limit of {} bytes", 44 max_size 45 + ))); 46 } 47 + let did = &auth.did; 48 + let user = state 49 + .user_repo 50 + .get_by_did(did) 51 + .await 52 + .map_err(|e| { 53 error!("DB error fetching user: {:?}", e); 54 + ApiError::InternalError(None) 55 + })? 56 + .ok_or(ApiError::AccountNotFound)?; 57 if user.takedown_ref.is_some() { 58 + return Err(ApiError::AccountTakedown); 59 } 60 let user_id = user.id; 61 let (root, blocks) = match parse_car(&body).await { 62 Ok((r, b)) => (r, b), 63 Err(ImportError::InvalidRootCount) => { 64 + return Err(ApiError::InvalidRequest( 65 + "Expected exactly one root in CAR file".into(), 66 + )); 67 } 68 Err(ImportError::CarParse(msg)) => { 69 + return Err(ApiError::InvalidRequest(format!( 70 + "Failed to parse CAR file: {}", 71 + msg 72 + ))); 73 } 74 Err(e) => { 75 error!("CAR parsing error: {:?}", e); 76 + return Err(ApiError::InvalidRequest(format!("Invalid CAR file: {}", e))); 77 } 78 }; 79 info!( ··· 83 root 84 ); 85 let Some(root_block) = blocks.get(&root) else { 86 + return Err(ApiError::InvalidRequest( 87 + "Root block not found in CAR file".into(), 88 + )); 89 }; 90 let commit_did = match jacquard_repo::commit::Commit::from_cbor(root_block) { 91 Ok(commit) => commit.did().to_string(), 92 Err(e) => { 93 + return Err(ApiError::InvalidRequest(format!("Invalid commit: {}", e))); 94 } 95 }; 96 if commit_did != *did { 97 + return Err(ApiError::InvalidRepo(format!( 98 "CAR file is for DID {} but you are authenticated as {}", 99 commit_did, did 100 + ))); 101 } 102 let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION") 103 .map(|v| v == "true" || v == "1") ··· 119 commit_did, 120 expected_did, 121 }) => { 122 + return Err(ApiError::InvalidRepo(format!( 123 "CAR file is for DID {} but you are authenticated as {}", 124 commit_did, expected_did 125 + ))); 126 } 127 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 128 + return Err(ApiError::InvalidRequest(format!( 129 + "MST validation failed: {}", 130 + msg 131 + ))); 132 } 133 Err(e) => { 134 error!("CAR structure verification error: {:?}", e); 135 + return Err(ApiError::InvalidRequest(format!( 136 + "CAR verification failed: {}", 137 + e 138 + ))); 139 } 140 } 141 } else { ··· 152 commit_did, 153 expected_did, 154 }) => { 155 + return Err(ApiError::InvalidRepo(format!( 156 "CAR file is for DID {} but you are authenticated as {}", 157 commit_did, expected_did 158 + ))); 159 } 160 Err(crate::sync::verify::VerifyError::InvalidSignature) => { 161 + return Err(ApiError::InvalidRequest( 162 "CAR file commit signature verification failed".into(), 163 + )); 164 } 165 Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => { 166 warn!("DID resolution failed during import verification: {}", msg); 167 + return Err(ApiError::InvalidRequest(format!( 168 + "Failed to verify DID: {}", 169 + msg 170 + ))); 171 } 172 Err(crate::sync::verify::VerifyError::NoSigningKey) => { 173 + return Err(ApiError::InvalidRequest( 174 "DID document does not contain a signing key".into(), 175 + )); 176 } 177 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 178 + return Err(ApiError::InvalidRequest(format!( 179 + "MST validation failed: {}", 180 + msg 181 + ))); 182 } 183 Err(e) => { 184 error!("CAR verification error: {:?}", e); 185 + return Err(ApiError::InvalidRequest(format!( 186 + "CAR verification failed: {}", 187 + e 188 + ))); 189 } 190 } 191 } ··· 235 } 236 } 237 } 238 + let key_row = state 239 + .user_repo 240 + .get_user_with_key_by_did(did) 241 + .await 242 + .map_err(|e| { 243 error!("DB error fetching signing key: {:?}", e); 244 + ApiError::InternalError(None) 245 + })? 246 + .ok_or_else(|| { 247 + error!("No signing key found for user {}", did); 248 + ApiError::InternalError(Some("Signing key not found".into())) 249 + })?; 250 let key_bytes = 251 + crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 252 + .map_err(|e| { 253 error!("Failed to decrypt signing key: {}", e); 254 + ApiError::InternalError(None) 255 + })?; 256 + let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { 257 + error!("Invalid signing key: {:?}", e); 258 + ApiError::InternalError(None) 259 + })?; 260 let new_rev = Tid::now(LimitedU32::MIN); 261 let new_rev_str = new_rev.to_string(); 262 + let (commit_bytes, _sig) = create_signed_commit( 263 did, 264 import_result.data_cid, 265 &new_rev_str, 266 None, 267 &signing_key, 268 + ) 269 + .map_err(|e| { 270 + error!("Failed to create new commit: {}", e); 271 + ApiError::InternalError(None) 272 + })?; 273 + let new_root_cid: cid::Cid = 274 + state.block_store.put(&commit_bytes).await.map_err(|e| { 275 error!("Failed to store new commit block: {:?}", e); 276 + ApiError::InternalError(None) 277 + })?; 278 let new_root_cid_link = CidLink::new_unchecked(new_root_cid.to_string()); 279 + state 280 .repo_repo 281 .update_repo_root(user_id, &new_root_cid_link, &new_rev_str) 282 .await 283 + .map_err(|e| { 284 + error!("Failed to update repo root: {:?}", e); 285 + ApiError::InternalError(None) 286 + })?; 287 let mut all_block_cids: Vec<Vec<u8>> = blocks.keys().map(|c| c.to_bytes()).collect(); 288 all_block_cids.push(new_root_cid.to_bytes()); 289 + state 290 .repo_repo 291 .insert_user_blocks(user_id, &all_block_cids, &new_rev_str) 292 .await 293 + .map_err(|e| { 294 + error!("Failed to insert user_blocks: {:?}", e); 295 + ApiError::InternalError(None) 296 + })?; 297 let new_root_str = new_root_cid.to_string(); 298 info!( 299 "Created new commit for imported repo: cid={}, rev={}", ··· 323 ); 324 } 325 } 326 + Ok(EmptyResponse::ok().into_response()) 327 } 328 + Err(ImportError::SizeLimitExceeded) => Err(ApiError::PayloadTooLarge(format!( 329 + "Import exceeds block limit of {}", 330 + max_blocks 331 + ))), 332 + Err(ImportError::RepoNotFound) => Err(ApiError::RepoNotFound(Some( 333 + "Repository not initialized for this account".into(), 334 + ))), 335 + Err(ImportError::InvalidCbor(msg)) => Err(ApiError::InvalidRequest(format!( 336 + "Invalid CBOR data: {}", 337 + msg 338 + ))), 339 + Err(ImportError::InvalidCommit(msg)) => Err(ApiError::InvalidRequest(format!( 340 + "Invalid commit structure: {}", 341 + msg 342 + ))), 343 + Err(ImportError::BlockNotFound(cid)) => Err(ApiError::InvalidRequest(format!( 344 + "Referenced block not found in CAR: {}", 345 + cid 346 + ))), 347 + Err(ImportError::ConcurrentModification) => Err(ApiError::InvalidSwap(Some( 348 "Repository is being modified by another operation, please retry".into(), 349 + ))), 350 + Err(ImportError::VerificationFailed(ve)) => Err(ApiError::InvalidRequest(format!( 351 + "CAR verification failed: {}", 352 + ve 353 + ))), 354 + Err(ImportError::DidMismatch { car_did, auth_did }) => Err(ApiError::InvalidRequest( 355 + format!("CAR is for {} but authenticated as {}", car_did, auth_did), 356 + )), 357 Err(e) => { 358 error!("Import error: {:?}", e); 359 + Err(ApiError::InternalError(None)) 360 } 361 } 362 }
+63 -65
crates/tranquil-pds/src/api/repo/record/batch.rs
··· 1 use super::validation::validate_record_with_status; 2 use crate::api::error::ApiError; 3 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; 4 - use crate::auth::BearerAuth; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 262 263 pub async fn apply_writes( 264 State(state): State<AppState>, 265 - auth: BearerAuth, 266 Json(input): Json<ApplyWritesInput>, 267 - ) -> Response { 268 info!( 269 "apply_writes called: repo={}, writes={}", 270 input.repo, 271 input.writes.len() 272 ); 273 - let auth_user = auth.0; 274 - let did = auth_user.did.clone(); 275 - let is_oauth = auth_user.is_oauth; 276 - let scope = auth_user.scope; 277 - let controller_did = auth_user.controller_did.clone(); 278 if input.repo.as_str() != did { 279 - return ApiError::InvalidRepo("Repo does not match authenticated user".into()) 280 - .into_response(); 281 } 282 if state 283 .user_repo ··· 285 .await 286 .unwrap_or(false) 287 { 288 - return ApiError::AccountMigrated.into_response(); 289 } 290 let is_verified = state 291 .user_repo ··· 298 .await 299 .unwrap_or(false); 300 if !is_verified && !is_delegated { 301 - return ApiError::AccountNotVerified.into_response(); 302 } 303 if input.writes.is_empty() { 304 - return ApiError::InvalidRequest("writes array is empty".into()).into_response(); 305 } 306 if input.writes.len() > MAX_BATCH_WRITES { 307 - return ApiError::InvalidRequest(format!("Too many writes (max {})", MAX_BATCH_WRITES)) 308 - .into_response(); 309 } 310 311 let has_custom_scope = scope ··· 374 }) 375 .next() 376 { 377 - return err; 378 } 379 } 380 381 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&did).await { 382 - Ok(Some(id)) => id, 383 - _ => return ApiError::InternalError(Some("User not found".into())).into_response(), 384 - }; 385 - let root_cid_str = match state.repo_repo.get_repo_root_cid_by_user_id(user_id).await { 386 - Ok(Some(cid_str)) => cid_str, 387 - _ => return ApiError::InternalError(Some("Repo root not found".into())).into_response(), 388 - }; 389 - let current_root_cid = match Cid::from_str(&root_cid_str) { 390 - Ok(c) => c, 391 - Err(_) => { 392 - return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(); 393 - } 394 - }; 395 if let Some(swap_commit) = &input.swap_commit 396 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 397 { 398 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 399 } 400 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 401 - let commit_bytes = match tracking_store.get(&current_root_cid).await { 402 - Ok(Some(b)) => b, 403 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 404 - }; 405 - let commit = match Commit::from_cbor(&commit_bytes) { 406 - Ok(c) => c, 407 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 408 - }; 409 let original_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 410 let initial_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 411 let WriteAccumulator { ··· 424 .await 425 { 426 Ok(acc) => acc, 427 - Err(response) => return response, 428 - }; 429 - let new_mst_root = match mst.persist().await { 430 - Ok(c) => c, 431 - Err(_) => { 432 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 433 - } 434 }; 435 let (new_mst_blocks, old_mst_blocks) = { 436 let mut new_blocks = std::collections::BTreeMap::new(); 437 let mut old_blocks = std::collections::BTreeMap::new(); 438 for key in &modified_keys { 439 - if mst.blocks_for_path(key, &mut new_blocks).await.is_err() { 440 - return ApiError::InternalError(Some( 441 - "Failed to get new MST blocks for path".into(), 442 - )) 443 - .into_response(); 444 - } 445 - if original_mst 446 .blocks_for_path(key, &mut old_blocks) 447 .await 448 - .is_err() 449 - { 450 - return ApiError::InternalError(Some( 451 - "Failed to get old MST blocks for path".into(), 452 - )) 453 - .into_response(); 454 - } 455 } 456 (new_blocks, old_blocks) 457 }; ··· 503 { 504 Ok(res) => res, 505 Err(e) if e.contains("ConcurrentModification") => { 506 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 507 } 508 Err(e) => { 509 error!("Commit failed: {}", e); 510 - return ApiError::InternalError(Some("Failed to commit changes".into())) 511 - .into_response(); 512 } 513 }; 514 ··· 557 .await; 558 } 559 560 - ( 561 StatusCode::OK, 562 Json(ApplyWritesOutput { 563 commit: CommitInfo { ··· 567 results, 568 }), 569 ) 570 - .into_response() 571 }
··· 1 use super::validation::validate_record_with_status; 2 use crate::api::error::ApiError; 3 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; 4 + use crate::auth::{Active, Auth}; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 262 263 pub async fn apply_writes( 264 State(state): State<AppState>, 265 + auth: Auth<Active>, 266 Json(input): Json<ApplyWritesInput>, 267 + ) -> Result<Response, ApiError> { 268 info!( 269 "apply_writes called: repo={}, writes={}", 270 input.repo, 271 input.writes.len() 272 ); 273 + let did = auth.did.clone(); 274 + let is_oauth = auth.is_oauth(); 275 + let scope = auth.scope.clone(); 276 + let controller_did = auth.controller_did.clone(); 277 if input.repo.as_str() != did { 278 + return Err(ApiError::InvalidRepo( 279 + "Repo does not match authenticated user".into(), 280 + )); 281 } 282 if state 283 .user_repo ··· 285 .await 286 .unwrap_or(false) 287 { 288 + return Err(ApiError::AccountMigrated); 289 } 290 let is_verified = state 291 .user_repo ··· 298 .await 299 .unwrap_or(false); 300 if !is_verified && !is_delegated { 301 + return Err(ApiError::AccountNotVerified); 302 } 303 if input.writes.is_empty() { 304 + return Err(ApiError::InvalidRequest("writes array is empty".into())); 305 } 306 if input.writes.len() > MAX_BATCH_WRITES { 307 + return Err(ApiError::InvalidRequest(format!( 308 + "Too many writes (max {})", 309 + MAX_BATCH_WRITES 310 + ))); 311 } 312 313 let has_custom_scope = scope ··· 376 }) 377 .next() 378 { 379 + return Ok(err); 380 } 381 } 382 383 + let user_id: uuid::Uuid = state 384 + .user_repo 385 + .get_id_by_did(&did) 386 + .await 387 + .ok() 388 + .flatten() 389 + .ok_or_else(|| ApiError::InternalError(Some("User not found".into())))?; 390 + let root_cid_str = state 391 + .repo_repo 392 + .get_repo_root_cid_by_user_id(user_id) 393 + .await 394 + .ok() 395 + .flatten() 396 + .ok_or_else(|| ApiError::InternalError(Some("Repo root not found".into())))?; 397 + let current_root_cid = Cid::from_str(&root_cid_str) 398 + .map_err(|_| ApiError::InternalError(Some("Invalid repo root CID".into())))?; 399 if let Some(swap_commit) = &input.swap_commit 400 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 401 { 402 + return Err(ApiError::InvalidSwap(Some("Repo has been modified".into()))); 403 } 404 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 405 + let commit_bytes = tracking_store 406 + .get(&current_root_cid) 407 + .await 408 + .ok() 409 + .flatten() 410 + .ok_or_else(|| ApiError::InternalError(Some("Commit block not found".into())))?; 411 + let commit = Commit::from_cbor(&commit_bytes) 412 + .map_err(|_| ApiError::InternalError(Some("Failed to parse commit".into())))?; 413 let original_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 414 let initial_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 415 let WriteAccumulator { ··· 428 .await 429 { 430 Ok(acc) => acc, 431 + Err(response) => return Ok(response), 432 }; 433 + let new_mst_root = mst 434 + .persist() 435 + .await 436 + .map_err(|_| ApiError::InternalError(Some("Failed to persist MST".into())))?; 437 let (new_mst_blocks, old_mst_blocks) = { 438 let mut new_blocks = std::collections::BTreeMap::new(); 439 let mut old_blocks = std::collections::BTreeMap::new(); 440 for key in &modified_keys { 441 + mst.blocks_for_path(key, &mut new_blocks) 442 + .await 443 + .map_err(|_| { 444 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 445 + })?; 446 + original_mst 447 .blocks_for_path(key, &mut old_blocks) 448 .await 449 + .map_err(|_| { 450 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 451 + })?; 452 } 453 (new_blocks, old_blocks) 454 }; ··· 500 { 501 Ok(res) => res, 502 Err(e) if e.contains("ConcurrentModification") => { 503 + return Err(ApiError::InvalidSwap(Some("Repo has been modified".into()))); 504 } 505 Err(e) => { 506 error!("Commit failed: {}", e); 507 + return Err(ApiError::InternalError(Some( 508 + "Failed to commit changes".into(), 509 + ))); 510 } 511 }; 512 ··· 555 .await; 556 } 557 558 + Ok(( 559 StatusCode::OK, 560 Json(ApplyWritesOutput { 561 commit: CommitInfo { ··· 565 results, 566 }), 567 ) 568 + .into_response()) 569 }
+46 -29
crates/tranquil-pds/src/api/repo/record/delete.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log}; 3 use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; 4 - use crate::auth::BearerAuth; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 40 41 pub async fn delete_record( 42 State(state): State<AppState>, 43 - auth: BearerAuth, 44 Json(input): Json<DeleteRecordInput>, 45 - ) -> Response { 46 - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { 47 Ok(res) => res, 48 - Err(err_res) => return err_res, 49 }; 50 51 if let Err(e) = crate::auth::scope_check::check_repo_scope( 52 - auth.is_oauth, 53 - auth.scope.as_deref(), 54 crate::oauth::RepoAction::Delete, 55 &input.collection, 56 ) { 57 - return e; 58 } 59 60 - let did = auth.did; 61 - let user_id = auth.user_id; 62 - let current_root_cid = auth.current_root_cid; 63 - let controller_did = auth.controller_did; 64 65 if let Some(swap_commit) = &input.swap_commit 66 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 67 { 68 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 69 } 70 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 71 let commit_bytes = match tracking_store.get(&current_root_cid).await { 72 Ok(Some(b)) => b, 73 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 74 }; 75 let commit = match Commit::from_cbor(&commit_bytes) { 76 Ok(c) => c, 77 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 78 }; 79 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 80 let key = format!("{}/{}", input.collection, input.rkey); ··· 82 let expected_cid = Cid::from_str(swap_record_str).ok(); 83 let actual_cid = mst.get(&key).await.ok().flatten(); 84 if expected_cid != actual_cid { 85 - return ApiError::InvalidSwap(Some( 86 "Record has been modified or does not exist".into(), 87 )) 88 - .into_response(); 89 } 90 } 91 let prev_record_cid = mst.get(&key).await.ok().flatten(); 92 if prev_record_cid.is_none() { 93 - return (StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response(); 94 } 95 let new_mst = match mst.delete(&key).await { 96 Ok(m) => m, 97 Err(e) => { 98 error!("Failed to delete from MST: {:?}", e); 99 - return ApiError::InternalError(Some(format!("Failed to delete from MST: {:?}", e))) 100 - .into_response(); 101 } 102 }; 103 let new_mst_root = match new_mst.persist().await { 104 Ok(c) => c, 105 Err(e) => { 106 error!("Failed to persist MST: {:?}", e); 107 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 108 } 109 }; 110 let collection_for_audit = input.collection.to_string(); ··· 121 .await 122 .is_err() 123 { 124 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 125 - .into_response(); 126 } 127 if mst 128 .blocks_for_path(&key, &mut old_mst_blocks) 129 .await 130 .is_err() 131 { 132 - return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 133 - .into_response(); 134 } 135 let mut relevant_blocks = new_mst_blocks.clone(); 136 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 169 { 170 Ok(res) => res, 171 Err(e) if e.contains("ConcurrentModification") => { 172 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 173 } 174 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 175 }; 176 177 if let Some(ref controller) = controller_did { ··· 202 error!("Failed to remove backlinks for {}: {}", deleted_uri, e); 203 } 204 205 - ( 206 StatusCode::OK, 207 Json(DeleteRecordOutput { 208 commit: Some(CommitInfo { ··· 211 }), 212 }), 213 ) 214 - .into_response() 215 } 216 217 use crate::types::Did;
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log}; 3 use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; 4 + use crate::auth::{Active, Auth}; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 40 41 pub async fn delete_record( 42 State(state): State<AppState>, 43 + auth: Auth<Active>, 44 Json(input): Json<DeleteRecordInput>, 45 + ) -> Result<Response, crate::api::error::ApiError> { 46 + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 47 Ok(res) => res, 48 + Err(err_res) => return Ok(err_res), 49 }; 50 51 if let Err(e) = crate::auth::scope_check::check_repo_scope( 52 + repo_auth.is_oauth, 53 + repo_auth.scope.as_deref(), 54 crate::oauth::RepoAction::Delete, 55 &input.collection, 56 ) { 57 + return Ok(e); 58 } 59 60 + let did = repo_auth.did; 61 + let user_id = repo_auth.user_id; 62 + let current_root_cid = repo_auth.current_root_cid; 63 + let controller_did = repo_auth.controller_did; 64 65 if let Some(swap_commit) = &input.swap_commit 66 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 67 { 68 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 69 } 70 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 71 let commit_bytes = match tracking_store.get(&current_root_cid).await { 72 Ok(Some(b)) => b, 73 + _ => { 74 + return Ok( 75 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 76 + ); 77 + } 78 }; 79 let commit = match Commit::from_cbor(&commit_bytes) { 80 Ok(c) => c, 81 + _ => { 82 + return Ok( 83 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 84 + ); 85 + } 86 }; 87 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 88 let key = format!("{}/{}", input.collection, input.rkey); ··· 90 let expected_cid = Cid::from_str(swap_record_str).ok(); 91 let actual_cid = mst.get(&key).await.ok().flatten(); 92 if expected_cid != actual_cid { 93 + return Ok(ApiError::InvalidSwap(Some( 94 "Record has been modified or does not exist".into(), 95 )) 96 + .into_response()); 97 } 98 } 99 let prev_record_cid = mst.get(&key).await.ok().flatten(); 100 if prev_record_cid.is_none() { 101 + return Ok((StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response()); 102 } 103 let new_mst = match mst.delete(&key).await { 104 Ok(m) => m, 105 Err(e) => { 106 error!("Failed to delete from MST: {:?}", e); 107 + return Ok(ApiError::InternalError(Some(format!( 108 + "Failed to delete from MST: {:?}", 109 + e 110 + ))) 111 + .into_response()); 112 } 113 }; 114 let new_mst_root = match new_mst.persist().await { 115 Ok(c) => c, 116 Err(e) => { 117 error!("Failed to persist MST: {:?}", e); 118 + return Ok( 119 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 120 + ); 121 } 122 }; 123 let collection_for_audit = input.collection.to_string(); ··· 134 .await 135 .is_err() 136 { 137 + return Ok( 138 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 139 + .into_response(), 140 + ); 141 } 142 if mst 143 .blocks_for_path(&key, &mut old_mst_blocks) 144 .await 145 .is_err() 146 { 147 + return Ok( 148 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 149 + .into_response(), 150 + ); 151 } 152 let mut relevant_blocks = new_mst_blocks.clone(); 153 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 186 { 187 Ok(res) => res, 188 Err(e) if e.contains("ConcurrentModification") => { 189 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 190 } 191 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 192 }; 193 194 if let Some(ref controller) = controller_did { ··· 219 error!("Failed to remove backlinks for {}: {}", deleted_uri, e); 220 } 221 222 + Ok(( 223 StatusCode::OK, 224 Json(DeleteRecordOutput { 225 commit: Some(CommitInfo { ··· 228 }), 229 }), 230 ) 231 + .into_response()) 232 } 233 234 use crate::types::Did;
+113 -80
crates/tranquil-pds/src/api/repo/record/write.rs
··· 3 use crate::api::repo::record::utils::{ 4 CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, 5 }; 6 - use crate::auth::{AuthenticatedUser, BearerAuth}; 7 use crate::delegation::DelegationActionType; 8 use crate::repo::tracking::TrackingBlockStore; 9 use crate::state::AppState; ··· 34 35 pub async fn prepare_repo_write( 36 state: &AppState, 37 - auth_user: AuthenticatedUser, 38 repo: &AtIdentifier, 39 ) -> Result<RepoWriteAuth, Response> { 40 if repo.as_str() != auth_user.did.as_str() { ··· 90 did: auth_user.did.clone(), 91 user_id, 92 current_root_cid, 93 - is_oauth: auth_user.is_oauth, 94 - scope: auth_user.scope, 95 controller_did: auth_user.controller_did.clone(), 96 }) 97 } ··· 124 } 125 pub async fn create_record( 126 State(state): State<AppState>, 127 - auth: BearerAuth, 128 Json(input): Json<CreateRecordInput>, 129 - ) -> Response { 130 - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { 131 Ok(res) => res, 132 - Err(err_res) => return err_res, 133 }; 134 135 if let Err(e) = crate::auth::scope_check::check_repo_scope( 136 - auth.is_oauth, 137 - auth.scope.as_deref(), 138 crate::oauth::RepoAction::Create, 139 &input.collection, 140 ) { 141 - return e; 142 } 143 144 - let did = auth.did; 145 - let user_id = auth.user_id; 146 - let current_root_cid = auth.current_root_cid; 147 - let controller_did = auth.controller_did; 148 149 if let Some(swap_commit) = &input.swap_commit 150 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 151 { 152 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 153 } 154 155 let validation_status = if input.validate == Some(false) { ··· 163 require_lexicon, 164 ) { 165 Ok(status) => Some(status), 166 - Err(err_response) => return *err_response, 167 } 168 }; 169 let rkey = input.rkey.unwrap_or_else(Rkey::generate); ··· 171 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 172 let commit_bytes = match tracking_store.get(&current_root_cid).await { 173 Ok(Some(b)) => b, 174 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 175 }; 176 let commit = match Commit::from_cbor(&commit_bytes) { 177 Ok(c) => c, 178 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 179 }; 180 let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 181 let initial_mst_root = commit.data; ··· 197 Ok(c) => c, 198 Err(e) => { 199 error!("Failed to check backlink conflicts: {}", e); 200 - return ApiError::InternalError(None).into_response(); 201 } 202 }; 203 ··· 250 let record_ipld = crate::util::json_to_ipld(&input.record); 251 let mut record_bytes = Vec::new(); 252 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 253 - return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); 254 } 255 let record_cid = match tracking_store.put(&record_bytes).await { 256 Ok(c) => c, 257 _ => { 258 - return ApiError::InternalError(Some("Failed to save record block".into())) 259 - .into_response(); 260 } 261 }; 262 let key = format!("{}/{}", input.collection, rkey); ··· 271 272 let new_mst = match mst.add(&key, record_cid).await { 273 Ok(m) => m, 274 - _ => return ApiError::InternalError(Some("Failed to add to MST".into())).into_response(), 275 }; 276 let new_mst_root = match new_mst.persist().await { 277 Ok(c) => c, 278 - _ => return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 279 }; 280 281 ops.push(RecordOp::Create { ··· 290 .await 291 .is_err() 292 { 293 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 294 - .into_response(); 295 } 296 297 let mut relevant_blocks = new_mst_blocks.clone(); ··· 333 { 334 Ok(res) => res, 335 Err(e) if e.contains("ConcurrentModification") => { 336 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 337 } 338 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 339 }; 340 341 for conflict_uri in conflict_uris_to_cleanup { ··· 375 error!("Failed to add backlinks for {}: {}", created_uri, e); 376 } 377 378 - ( 379 StatusCode::OK, 380 Json(CreateRecordOutput { 381 uri: created_uri, ··· 387 validation_status: validation_status.map(|s| s.to_string()), 388 }), 389 ) 390 - .into_response() 391 } 392 #[derive(Deserialize)] 393 #[allow(dead_code)] ··· 414 } 415 pub async fn put_record( 416 State(state): State<AppState>, 417 - auth: BearerAuth, 418 Json(input): Json<PutRecordInput>, 419 - ) -> Response { 420 - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { 421 Ok(res) => res, 422 - Err(err_res) => return err_res, 423 }; 424 425 if let Err(e) = crate::auth::scope_check::check_repo_scope( 426 - auth.is_oauth, 427 - auth.scope.as_deref(), 428 crate::oauth::RepoAction::Create, 429 &input.collection, 430 ) { 431 - return e; 432 } 433 if let Err(e) = crate::auth::scope_check::check_repo_scope( 434 - auth.is_oauth, 435 - auth.scope.as_deref(), 436 crate::oauth::RepoAction::Update, 437 &input.collection, 438 ) { 439 - return e; 440 } 441 442 - let did = auth.did; 443 - let user_id = auth.user_id; 444 - let current_root_cid = auth.current_root_cid; 445 - let controller_did = auth.controller_did; 446 447 if let Some(swap_commit) = &input.swap_commit 448 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 449 { 450 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 451 } 452 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 453 let commit_bytes = match tracking_store.get(&current_root_cid).await { 454 Ok(Some(b)) => b, 455 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 456 }; 457 let commit = match Commit::from_cbor(&commit_bytes) { 458 Ok(c) => c, 459 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 460 }; 461 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 462 let key = format!("{}/{}", input.collection, input.rkey); ··· 471 require_lexicon, 472 ) { 473 Ok(status) => Some(status), 474 - Err(err_response) => return *err_response, 475 } 476 }; 477 if let Some(swap_record_str) = &input.swap_record { 478 let expected_cid = Cid::from_str(swap_record_str).ok(); 479 let actual_cid = mst.get(&key).await.ok().flatten(); 480 if expected_cid != actual_cid { 481 - return ApiError::InvalidSwap(Some( 482 "Record has been modified or does not exist".into(), 483 )) 484 - .into_response(); 485 } 486 } 487 let existing_cid = mst.get(&key).await.ok().flatten(); 488 let record_ipld = crate::util::json_to_ipld(&input.record); 489 let mut record_bytes = Vec::new(); 490 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 491 - return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); 492 } 493 let record_cid = match tracking_store.put(&record_bytes).await { 494 Ok(c) => c, 495 _ => { 496 - return ApiError::InternalError(Some("Failed to save record block".into())) 497 - .into_response(); 498 } 499 }; 500 if existing_cid == Some(record_cid) { 501 - return ( 502 StatusCode::OK, 503 Json(PutRecordOutput { 504 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 507 validation_status: validation_status.map(|s| s.to_string()), 508 }), 509 ) 510 - .into_response(); 511 } 512 - let new_mst = if existing_cid.is_some() { 513 - match mst.update(&key, record_cid).await { 514 - Ok(m) => m, 515 - Err(_) => { 516 - return ApiError::InternalError(Some("Failed to update MST".into())) 517 - .into_response(); 518 } 519 - } 520 - } else { 521 - match mst.add(&key, record_cid).await { 522 - Ok(m) => m, 523 - Err(_) => { 524 - return ApiError::InternalError(Some("Failed to add to MST".into())) 525 - .into_response(); 526 } 527 - } 528 - }; 529 let new_mst_root = match new_mst.persist().await { 530 Ok(c) => c, 531 Err(_) => { 532 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 533 } 534 }; 535 let op = if existing_cid.is_some() { ··· 553 .await 554 .is_err() 555 { 556 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 557 - .into_response(); 558 } 559 if mst 560 .blocks_for_path(&key, &mut old_mst_blocks) 561 .await 562 .is_err() 563 { 564 - return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 565 - .into_response(); 566 } 567 let mut relevant_blocks = new_mst_blocks.clone(); 568 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 604 { 605 Ok(res) => res, 606 Err(e) if e.contains("ConcurrentModification") => { 607 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 608 } 609 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 610 }; 611 612 if let Some(ref controller) = controller_did { ··· 628 .await; 629 } 630 631 - ( 632 StatusCode::OK, 633 Json(PutRecordOutput { 634 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 640 validation_status: validation_status.map(|s| s.to_string()), 641 }), 642 ) 643 - .into_response() 644 }
··· 3 use crate::api::repo::record::utils::{ 4 CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, 5 }; 6 + use crate::auth::{Active, Auth}; 7 use crate::delegation::DelegationActionType; 8 use crate::repo::tracking::TrackingBlockStore; 9 use crate::state::AppState; ··· 34 35 pub async fn prepare_repo_write( 36 state: &AppState, 37 + auth_user: &crate::auth::AuthenticatedUser, 38 repo: &AtIdentifier, 39 ) -> Result<RepoWriteAuth, Response> { 40 if repo.as_str() != auth_user.did.as_str() { ··· 90 did: auth_user.did.clone(), 91 user_id, 92 current_root_cid, 93 + is_oauth: auth_user.is_oauth(), 94 + scope: auth_user.scope.clone(), 95 controller_did: auth_user.controller_did.clone(), 96 }) 97 } ··· 124 } 125 pub async fn create_record( 126 State(state): State<AppState>, 127 + auth: Auth<Active>, 128 Json(input): Json<CreateRecordInput>, 129 + ) -> Result<Response, crate::api::error::ApiError> { 130 + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 131 Ok(res) => res, 132 + Err(err_res) => return Ok(err_res), 133 }; 134 135 if let Err(e) = crate::auth::scope_check::check_repo_scope( 136 + repo_auth.is_oauth, 137 + repo_auth.scope.as_deref(), 138 crate::oauth::RepoAction::Create, 139 &input.collection, 140 ) { 141 + return Ok(e); 142 } 143 144 + let did = repo_auth.did; 145 + let user_id = repo_auth.user_id; 146 + let current_root_cid = repo_auth.current_root_cid; 147 + let controller_did = repo_auth.controller_did; 148 149 if let Some(swap_commit) = &input.swap_commit 150 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 151 { 152 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 153 } 154 155 let validation_status = if input.validate == Some(false) { ··· 163 require_lexicon, 164 ) { 165 Ok(status) => Some(status), 166 + Err(err_response) => return Ok(*err_response), 167 } 168 }; 169 let rkey = input.rkey.unwrap_or_else(Rkey::generate); ··· 171 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 172 let commit_bytes = match tracking_store.get(&current_root_cid).await { 173 Ok(Some(b)) => b, 174 + _ => { 175 + return Ok( 176 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 177 + ); 178 + } 179 }; 180 let commit = match Commit::from_cbor(&commit_bytes) { 181 Ok(c) => c, 182 + _ => { 183 + return Ok( 184 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 185 + ); 186 + } 187 }; 188 let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 189 let initial_mst_root = commit.data; ··· 205 Ok(c) => c, 206 Err(e) => { 207 error!("Failed to check backlink conflicts: {}", e); 208 + return Ok(ApiError::InternalError(None).into_response()); 209 } 210 }; 211 ··· 258 let record_ipld = crate::util::json_to_ipld(&input.record); 259 let mut record_bytes = Vec::new(); 260 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 261 + return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); 262 } 263 let record_cid = match tracking_store.put(&record_bytes).await { 264 Ok(c) => c, 265 _ => { 266 + return Ok( 267 + ApiError::InternalError(Some("Failed to save record block".into())).into_response(), 268 + ); 269 } 270 }; 271 let key = format!("{}/{}", input.collection, rkey); ··· 280 281 let new_mst = match mst.add(&key, record_cid).await { 282 Ok(m) => m, 283 + _ => { 284 + return Ok(ApiError::InternalError(Some("Failed to add to MST".into())).into_response()); 285 + } 286 }; 287 let new_mst_root = match new_mst.persist().await { 288 Ok(c) => c, 289 + _ => { 290 + return Ok( 291 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 292 + ); 293 + } 294 }; 295 296 ops.push(RecordOp::Create { ··· 305 .await 306 .is_err() 307 { 308 + return Ok( 309 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 310 + .into_response(), 311 + ); 312 } 313 314 let mut relevant_blocks = new_mst_blocks.clone(); ··· 350 { 351 Ok(res) => res, 352 Err(e) if e.contains("ConcurrentModification") => { 353 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 354 } 355 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 356 }; 357 358 for conflict_uri in conflict_uris_to_cleanup { ··· 392 error!("Failed to add backlinks for {}: {}", created_uri, e); 393 } 394 395 + Ok(( 396 StatusCode::OK, 397 Json(CreateRecordOutput { 398 uri: created_uri, ··· 404 validation_status: validation_status.map(|s| s.to_string()), 405 }), 406 ) 407 + .into_response()) 408 } 409 #[derive(Deserialize)] 410 #[allow(dead_code)] ··· 431 } 432 pub async fn put_record( 433 State(state): State<AppState>, 434 + auth: Auth<Active>, 435 Json(input): Json<PutRecordInput>, 436 + ) -> Result<Response, crate::api::error::ApiError> { 437 + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 438 Ok(res) => res, 439 + Err(err_res) => return Ok(err_res), 440 }; 441 442 if let Err(e) = crate::auth::scope_check::check_repo_scope( 443 + repo_auth.is_oauth, 444 + repo_auth.scope.as_deref(), 445 crate::oauth::RepoAction::Create, 446 &input.collection, 447 ) { 448 + return Ok(e); 449 } 450 if let Err(e) = crate::auth::scope_check::check_repo_scope( 451 + repo_auth.is_oauth, 452 + repo_auth.scope.as_deref(), 453 crate::oauth::RepoAction::Update, 454 &input.collection, 455 ) { 456 + return Ok(e); 457 } 458 459 + let did = repo_auth.did; 460 + let user_id = repo_auth.user_id; 461 + let current_root_cid = repo_auth.current_root_cid; 462 + let controller_did = repo_auth.controller_did; 463 464 if let Some(swap_commit) = &input.swap_commit 465 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 466 { 467 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 468 } 469 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 470 let commit_bytes = match tracking_store.get(&current_root_cid).await { 471 Ok(Some(b)) => b, 472 + _ => { 473 + return Ok( 474 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 475 + ); 476 + } 477 }; 478 let commit = match Commit::from_cbor(&commit_bytes) { 479 Ok(c) => c, 480 + _ => { 481 + return Ok( 482 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 483 + ); 484 + } 485 }; 486 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 487 let key = format!("{}/{}", input.collection, input.rkey); ··· 496 require_lexicon, 497 ) { 498 Ok(status) => Some(status), 499 + Err(err_response) => return Ok(*err_response), 500 } 501 }; 502 if let Some(swap_record_str) = &input.swap_record { 503 let expected_cid = Cid::from_str(swap_record_str).ok(); 504 let actual_cid = mst.get(&key).await.ok().flatten(); 505 if expected_cid != actual_cid { 506 + return Ok(ApiError::InvalidSwap(Some( 507 "Record has been modified or does not exist".into(), 508 )) 509 + .into_response()); 510 } 511 } 512 let existing_cid = mst.get(&key).await.ok().flatten(); 513 let record_ipld = crate::util::json_to_ipld(&input.record); 514 let mut record_bytes = Vec::new(); 515 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 516 + return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); 517 } 518 let record_cid = match tracking_store.put(&record_bytes).await { 519 Ok(c) => c, 520 _ => { 521 + return Ok( 522 + ApiError::InternalError(Some("Failed to save record block".into())).into_response(), 523 + ); 524 } 525 }; 526 if existing_cid == Some(record_cid) { 527 + return Ok(( 528 StatusCode::OK, 529 Json(PutRecordOutput { 530 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 533 validation_status: validation_status.map(|s| s.to_string()), 534 }), 535 ) 536 + .into_response()); 537 } 538 + let new_mst = 539 + if existing_cid.is_some() { 540 + match mst.update(&key, record_cid).await { 541 + Ok(m) => m, 542 + Err(_) => { 543 + return Ok(ApiError::InternalError(Some("Failed to update MST".into())) 544 + .into_response()); 545 + } 546 } 547 + } else { 548 + match mst.add(&key, record_cid).await { 549 + Ok(m) => m, 550 + Err(_) => { 551 + return Ok(ApiError::InternalError(Some("Failed to add to MST".into())) 552 + .into_response()); 553 + } 554 } 555 + }; 556 let new_mst_root = match new_mst.persist().await { 557 Ok(c) => c, 558 Err(_) => { 559 + return Ok( 560 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 561 + ); 562 } 563 }; 564 let op = if existing_cid.is_some() { ··· 582 .await 583 .is_err() 584 { 585 + return Ok( 586 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 587 + .into_response(), 588 + ); 589 } 590 if mst 591 .blocks_for_path(&key, &mut old_mst_blocks) 592 .await 593 .is_err() 594 { 595 + return Ok( 596 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 597 + .into_response(), 598 + ); 599 } 600 let mut relevant_blocks = new_mst_blocks.clone(); 601 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 637 { 638 Ok(res) => res, 639 Err(e) if e.contains("ConcurrentModification") => { 640 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 641 } 642 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 643 }; 644 645 if let Some(ref controller) = controller_did { ··· 661 .await; 662 } 663 664 + Ok(( 665 StatusCode::OK, 666 Json(PutRecordOutput { 667 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 673 validation_status: validation_status.map(|s| s.to_string()), 674 }), 675 ) 676 + .into_response()) 677 }
+51 -52
crates/tranquil-pds/src/api/server/account_status.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 use crate::cache::Cache; 4 use crate::plc::PlcClient; 5 use crate::state::AppState; ··· 40 41 pub async fn check_account_status( 42 State(state): State<AppState>, 43 - auth: crate::auth::BearerAuthAllowDeactivated, 44 - ) -> Response { 45 - let did = auth.0.did; 46 - let user_id = match state.user_repo.get_id_by_did(&did).await { 47 - Ok(Some(id)) => id, 48 - _ => { 49 - return ApiError::InternalError(None).into_response(); 50 - } 51 - }; 52 let is_active = state 53 .user_repo 54 - .is_account_active_by_did(&did) 55 .await 56 .ok() 57 .flatten() ··· 95 .await 96 .unwrap_or(0); 97 let valid_did = 98 - is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), &did).await; 99 - ( 100 StatusCode::OK, 101 Json(CheckAccountStatusOutput { 102 activated: is_active, ··· 110 imported_blobs, 111 }), 112 ) 113 - .into_response() 114 } 115 116 async fn is_valid_did_for_service( ··· 305 306 pub async fn activate_account( 307 State(state): State<AppState>, 308 - auth: crate::auth::BearerAuthAllowDeactivated, 309 - ) -> Response { 310 info!("[MIGRATION] activateAccount called"); 311 - let auth_user = auth.0; 312 info!( 313 "[MIGRATION] activateAccount: Authenticated user did={}", 314 - auth_user.did 315 ); 316 317 if let Err(e) = crate::auth::scope_check::check_account_scope( 318 - auth_user.is_oauth, 319 - auth_user.scope.as_deref(), 320 crate::oauth::scopes::AccountAttr::Repo, 321 crate::oauth::scopes::AccountAction::Manage, 322 ) { 323 info!("[MIGRATION] activateAccount: Scope check failed"); 324 - return e; 325 } 326 327 - let did = auth_user.did; 328 329 info!( 330 "[MIGRATION] activateAccount: Validating DID document for did={}", ··· 344 did, 345 did_validation_start.elapsed() 346 ); 347 - return e.into_response(); 348 } 349 info!( 350 "[MIGRATION] activateAccount: DID document validation SUCCESS for {} (took {:?})", ··· 450 ); 451 } 452 info!("[MIGRATION] activateAccount: SUCCESS for did={}", did); 453 - EmptyResponse::ok().into_response() 454 } 455 Err(e) => { 456 error!( 457 "[MIGRATION] activateAccount: DB error activating account: {:?}", 458 e 459 ); 460 - ApiError::InternalError(None).into_response() 461 } 462 } 463 } ··· 470 471 pub async fn deactivate_account( 472 State(state): State<AppState>, 473 - auth: crate::auth::BearerAuth, 474 Json(input): Json<DeactivateAccountInput>, 475 - ) -> Response { 476 - let auth_user = auth.0; 477 - 478 if let Err(e) = crate::auth::scope_check::check_account_scope( 479 - auth_user.is_oauth, 480 - auth_user.scope.as_deref(), 481 crate::oauth::scopes::AccountAttr::Repo, 482 crate::oauth::scopes::AccountAction::Manage, 483 ) { 484 - return e; 485 } 486 487 let delete_after: Option<chrono::DateTime<chrono::Utc>> = input ··· 490 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) 491 .map(|dt| dt.with_timezone(&chrono::Utc)); 492 493 - let did = auth_user.did; 494 495 let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten(); 496 ··· 511 { 512 warn!("Failed to sequence account deactivated event: {}", e); 513 } 514 - EmptyResponse::ok().into_response() 515 } 516 - Ok(false) => EmptyResponse::ok().into_response(), 517 Err(e) => { 518 error!("DB error deactivating account: {:?}", e); 519 - ApiError::InternalError(None).into_response() 520 } 521 } 522 } 523 524 pub async fn request_account_delete( 525 State(state): State<AppState>, 526 - auth: crate::auth::BearerAuthAllowDeactivated, 527 - ) -> Response { 528 - let did = &auth.0.did; 529 530 if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await { 531 - return crate::api::server::reauth::legacy_mfa_required_response( 532 &*state.user_repo, 533 &*state.session_repo, 534 did, 535 ) 536 - .await; 537 } 538 539 - let user_id = match state.user_repo.get_id_by_did(did).await { 540 - Ok(Some(id)) => id, 541 - _ => { 542 - return ApiError::InternalError(None).into_response(); 543 - } 544 - }; 545 let confirmation_token = Uuid::new_v4().to_string(); 546 let expires_at = Utc::now() + Duration::minutes(15); 547 - if let Err(e) = state 548 .infra_repo 549 .create_deletion_request(&confirmation_token, did, expires_at) 550 .await 551 - { 552 - error!("DB error creating deletion token: {:?}", e); 553 - return ApiError::InternalError(None).into_response(); 554 - } 555 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 556 if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion( 557 state.user_repo.as_ref(), ··· 565 warn!("Failed to enqueue account deletion notification: {:?}", e); 566 } 567 info!("Account deletion requested for user {}", did); 568 - EmptyResponse::ok().into_response() 569 } 570 571 #[derive(Deserialize)]
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::{Active, Auth, NotTakendown}; 4 use crate::cache::Cache; 5 use crate::plc::PlcClient; 6 use crate::state::AppState; ··· 41 42 pub async fn check_account_status( 43 State(state): State<AppState>, 44 + auth: Auth<NotTakendown>, 45 + ) -> Result<Response, ApiError> { 46 + let did = &auth.did; 47 + let user_id = state 48 + .user_repo 49 + .get_id_by_did(did) 50 + .await 51 + .map_err(|_| ApiError::InternalError(None))? 52 + .ok_or(ApiError::InternalError(None))?; 53 let is_active = state 54 .user_repo 55 + .is_account_active_by_did(did) 56 .await 57 .ok() 58 .flatten() ··· 96 .await 97 .unwrap_or(0); 98 let valid_did = 99 + is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), did).await; 100 + Ok(( 101 StatusCode::OK, 102 Json(CheckAccountStatusOutput { 103 activated: is_active, ··· 111 imported_blobs, 112 }), 113 ) 114 + .into_response()) 115 } 116 117 async fn is_valid_did_for_service( ··· 306 307 pub async fn activate_account( 308 State(state): State<AppState>, 309 + auth: Auth<NotTakendown>, 310 + ) -> Result<Response, ApiError> { 311 info!("[MIGRATION] activateAccount called"); 312 info!( 313 "[MIGRATION] activateAccount: Authenticated user did={}", 314 + auth.did 315 ); 316 317 if let Err(e) = crate::auth::scope_check::check_account_scope( 318 + auth.is_oauth(), 319 + auth.scope.as_deref(), 320 crate::oauth::scopes::AccountAttr::Repo, 321 crate::oauth::scopes::AccountAction::Manage, 322 ) { 323 info!("[MIGRATION] activateAccount: Scope check failed"); 324 + return Ok(e); 325 } 326 327 + let did = auth.did.clone(); 328 329 info!( 330 "[MIGRATION] activateAccount: Validating DID document for did={}", ··· 344 did, 345 did_validation_start.elapsed() 346 ); 347 + return Err(e); 348 } 349 info!( 350 "[MIGRATION] activateAccount: DID document validation SUCCESS for {} (took {:?})", ··· 450 ); 451 } 452 info!("[MIGRATION] activateAccount: SUCCESS for did={}", did); 453 + Ok(EmptyResponse::ok().into_response()) 454 } 455 Err(e) => { 456 error!( 457 "[MIGRATION] activateAccount: DB error activating account: {:?}", 458 e 459 ); 460 + Err(ApiError::InternalError(None)) 461 } 462 } 463 } ··· 470 471 pub async fn deactivate_account( 472 State(state): State<AppState>, 473 + auth: Auth<Active>, 474 Json(input): Json<DeactivateAccountInput>, 475 + ) -> Result<Response, ApiError> { 476 if let Err(e) = crate::auth::scope_check::check_account_scope( 477 + auth.is_oauth(), 478 + auth.scope.as_deref(), 479 crate::oauth::scopes::AccountAttr::Repo, 480 crate::oauth::scopes::AccountAction::Manage, 481 ) { 482 + return Ok(e); 483 } 484 485 let delete_after: Option<chrono::DateTime<chrono::Utc>> = input ··· 488 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) 489 .map(|dt| dt.with_timezone(&chrono::Utc)); 490 491 + let did = auth.did.clone(); 492 493 let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten(); 494 ··· 509 { 510 warn!("Failed to sequence account deactivated event: {}", e); 511 } 512 + Ok(EmptyResponse::ok().into_response()) 513 } 514 + Ok(false) => Ok(EmptyResponse::ok().into_response()), 515 Err(e) => { 516 error!("DB error deactivating account: {:?}", e); 517 + Err(ApiError::InternalError(None)) 518 } 519 } 520 } 521 522 pub async fn request_account_delete( 523 State(state): State<AppState>, 524 + auth: Auth<NotTakendown>, 525 + ) -> Result<Response, ApiError> { 526 + let did = &auth.did; 527 528 if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await { 529 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 530 &*state.user_repo, 531 &*state.session_repo, 532 did, 533 ) 534 + .await); 535 } 536 537 + let user_id = state 538 + .user_repo 539 + .get_id_by_did(did) 540 + .await 541 + .ok() 542 + .flatten() 543 + .ok_or(ApiError::InternalError(None))?; 544 let confirmation_token = Uuid::new_v4().to_string(); 545 let expires_at = Utc::now() + Duration::minutes(15); 546 + state 547 .infra_repo 548 .create_deletion_request(&confirmation_token, did, expires_at) 549 .await 550 + .map_err(|e| { 551 + error!("DB error creating deletion token: {:?}", e); 552 + ApiError::InternalError(None) 553 + })?; 554 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 555 if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion( 556 state.user_repo.as_ref(), ··· 564 warn!("Failed to enqueue account deletion notification: {:?}", e); 565 } 566 info!("Account deletion requested for user {}", did); 567 + Ok(EmptyResponse::ok().into_response()) 568 } 569 570 #[derive(Deserialize)]
+126 -122
crates/tranquil-pds/src/api/server/app_password.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::{BearerAuth, generate_app_password}; 4 use crate::delegation::{DelegationActionType, intersect_scopes}; 5 use crate::state::{AppState, RateLimitKind}; 6 use axum::{ ··· 33 34 pub async fn list_app_passwords( 35 State(state): State<AppState>, 36 - BearerAuth(auth_user): BearerAuth, 37 - ) -> Response { 38 - let user = match state.user_repo.get_by_did(&auth_user.did).await { 39 - Ok(Some(u)) => u, 40 - Ok(None) => return ApiError::AccountNotFound.into_response(), 41 - Err(e) => { 42 error!("DB error getting user: {:?}", e); 43 - return ApiError::InternalError(None).into_response(); 44 - } 45 - }; 46 47 - match state.session_repo.list_app_passwords(user.id).await { 48 - Ok(rows) => { 49 - let passwords: Vec<AppPassword> = rows 50 - .iter() 51 - .map(|row| AppPassword { 52 - name: row.name.clone(), 53 - created_at: row.created_at.to_rfc3339(), 54 - privileged: row.privileged, 55 - scopes: row.scopes.clone(), 56 - created_by_controller: row 57 - .created_by_controller_did 58 - .as_ref() 59 - .map(|d| d.to_string()), 60 - }) 61 - .collect(); 62 - Json(ListAppPasswordsOutput { passwords }).into_response() 63 - } 64 - Err(e) => { 65 error!("DB error listing app passwords: {:?}", e); 66 - ApiError::InternalError(None).into_response() 67 - } 68 - } 69 } 70 71 #[derive(Deserialize)] ··· 89 pub async fn create_app_password( 90 State(state): State<AppState>, 91 headers: HeaderMap, 92 - BearerAuth(auth_user): BearerAuth, 93 Json(input): Json<CreateAppPasswordInput>, 94 - ) -> Response { 95 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 96 if !state 97 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 98 .await 99 { 100 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 101 - return ApiError::RateLimitExceeded(None).into_response(); 102 } 103 104 - let user = match state.user_repo.get_by_did(&auth_user.did).await { 105 - Ok(Some(u)) => u, 106 - Ok(None) => return ApiError::AccountNotFound.into_response(), 107 - Err(e) => { 108 error!("DB error getting user: {:?}", e); 109 - return ApiError::InternalError(None).into_response(); 110 - } 111 - }; 112 113 let name = input.name.trim(); 114 if name.is_empty() { 115 - return ApiError::InvalidRequest("name is required".into()).into_response(); 116 } 117 118 - match state 119 .session_repo 120 .get_app_password_by_name(user.id, name) 121 .await 122 - { 123 - Ok(Some(_)) => return ApiError::DuplicateAppPassword.into_response(), 124 - Err(e) => { 125 error!("DB error checking app password: {:?}", e); 126 - return ApiError::InternalError(None).into_response(); 127 - } 128 - Ok(None) => {} 129 } 130 131 - let (final_scopes, controller_did) = if let Some(ref controller) = auth_user.controller_did { 132 let grant = state 133 .delegation_repo 134 - .get_delegation(&auth_user.did, controller) 135 .await 136 .ok() 137 .flatten(); ··· 141 let intersected = intersect_scopes(requested, &granted_scopes); 142 143 if intersected.is_empty() && !granted_scopes.is_empty() { 144 - return ApiError::InsufficientScope(None).into_response(); 145 } 146 147 let scope_result = if intersected.is_empty() { ··· 157 let password = generate_app_password(); 158 159 let password_clone = password.clone(); 160 - let password_hash = match tokio::task::spawn_blocking(move || { 161 - bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST) 162 - }) 163 - .await 164 - { 165 - Ok(Ok(h)) => h, 166 - Ok(Err(e)) => { 167 - error!("Failed to hash password: {:?}", e); 168 - return ApiError::InternalError(None).into_response(); 169 - } 170 - Err(e) => { 171 - error!("Failed to spawn blocking task: {:?}", e); 172 - return ApiError::InternalError(None).into_response(); 173 - } 174 - }; 175 176 let privileged = input.privileged.unwrap_or(false); 177 let created_at = chrono::Utc::now(); ··· 185 created_by_controller_did: controller_did.clone(), 186 }; 187 188 - match state.session_repo.create_app_password(&create_data).await { 189 - Ok(_) => { 190 - if let Some(ref controller) = controller_did { 191 - let _ = state 192 - .delegation_repo 193 - .log_delegation_action( 194 - &auth_user.did, 195 - controller, 196 - Some(controller), 197 - DelegationActionType::AccountAction, 198 - Some(json!({ 199 - "action": "create_app_password", 200 - "name": name, 201 - "scopes": final_scopes 202 - })), 203 - None, 204 - None, 205 - ) 206 - .await; 207 - } 208 - Json(CreateAppPasswordOutput { 209 - name: name.to_string(), 210 - password, 211 - created_at: created_at.to_rfc3339(), 212 - privileged, 213 - scopes: final_scopes, 214 - }) 215 - .into_response() 216 - } 217 - Err(e) => { 218 error!("DB error creating app password: {:?}", e); 219 - ApiError::InternalError(None).into_response() 220 - } 221 } 222 } 223 224 #[derive(Deserialize)] ··· 228 229 pub async fn revoke_app_password( 230 State(state): State<AppState>, 231 - BearerAuth(auth_user): BearerAuth, 232 Json(input): Json<RevokeAppPasswordInput>, 233 - ) -> Response { 234 - let user = match state.user_repo.get_by_did(&auth_user.did).await { 235 - Ok(Some(u)) => u, 236 - Ok(None) => return ApiError::AccountNotFound.into_response(), 237 - Err(e) => { 238 error!("DB error getting user: {:?}", e); 239 - return ApiError::InternalError(None).into_response(); 240 - } 241 - }; 242 243 let name = input.name.trim(); 244 if name.is_empty() { 245 - return ApiError::InvalidRequest("name is required".into()).into_response(); 246 } 247 248 let sessions_to_invalidate = state 249 .session_repo 250 - .get_session_jtis_by_app_password(&auth_user.did, name) 251 .await 252 .unwrap_or_default(); 253 254 - if let Err(e) = state 255 .session_repo 256 - .delete_sessions_by_app_password(&auth_user.did, name) 257 .await 258 - { 259 - error!("DB error revoking sessions for app password: {:?}", e); 260 - return ApiError::InternalError(None).into_response(); 261 - } 262 263 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 264 - let cache_key = format!("auth:session:{}:{}", &auth_user.did, jti); 265 let cache = state.cache.clone(); 266 async move { 267 let _ = cache.delete(&cache_key).await; ··· 269 })) 270 .await; 271 272 - if let Err(e) = state.session_repo.delete_app_password(user.id, name).await { 273 - error!("DB error revoking app password: {:?}", e); 274 - return ApiError::InternalError(None).into_response(); 275 - } 276 277 - EmptyResponse::ok().into_response() 278 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::{Active, Auth, generate_app_password}; 4 use crate::delegation::{DelegationActionType, intersect_scopes}; 5 use crate::state::{AppState, RateLimitKind}; 6 use axum::{ ··· 33 34 pub async fn list_app_passwords( 35 State(state): State<AppState>, 36 + auth: Auth<Active>, 37 + ) -> Result<Response, ApiError> { 38 + let user = state 39 + .user_repo 40 + .get_by_did(&auth.did) 41 + .await 42 + .map_err(|e| { 43 error!("DB error getting user: {:?}", e); 44 + ApiError::InternalError(None) 45 + })? 46 + .ok_or(ApiError::AccountNotFound)?; 47 48 + let rows = state 49 + .session_repo 50 + .list_app_passwords(user.id) 51 + .await 52 + .map_err(|e| { 53 error!("DB error listing app passwords: {:?}", e); 54 + ApiError::InternalError(None) 55 + })?; 56 + let passwords: Vec<AppPassword> = rows 57 + .iter() 58 + .map(|row| AppPassword { 59 + name: row.name.clone(), 60 + created_at: row.created_at.to_rfc3339(), 61 + privileged: row.privileged, 62 + scopes: row.scopes.clone(), 63 + created_by_controller: row 64 + .created_by_controller_did 65 + .as_ref() 66 + .map(|d| d.to_string()), 67 + }) 68 + .collect(); 69 + Ok(Json(ListAppPasswordsOutput { passwords }).into_response()) 70 } 71 72 #[derive(Deserialize)] ··· 90 pub async fn create_app_password( 91 State(state): State<AppState>, 92 headers: HeaderMap, 93 + auth: Auth<Active>, 94 Json(input): Json<CreateAppPasswordInput>, 95 + ) -> Result<Response, ApiError> { 96 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 97 if !state 98 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 99 .await 100 { 101 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 102 + return Err(ApiError::RateLimitExceeded(None)); 103 } 104 105 + let user = state 106 + .user_repo 107 + .get_by_did(&auth.did) 108 + .await 109 + .map_err(|e| { 110 error!("DB error getting user: {:?}", e); 111 + ApiError::InternalError(None) 112 + })? 113 + .ok_or(ApiError::AccountNotFound)?; 114 115 let name = input.name.trim(); 116 if name.is_empty() { 117 + return Err(ApiError::InvalidRequest("name is required".into())); 118 } 119 120 + if state 121 .session_repo 122 .get_app_password_by_name(user.id, name) 123 .await 124 + .map_err(|e| { 125 error!("DB error checking app password: {:?}", e); 126 + ApiError::InternalError(None) 127 + })? 128 + .is_some() 129 + { 130 + return Err(ApiError::DuplicateAppPassword); 131 } 132 133 + let (final_scopes, controller_did) = if let Some(ref controller) = auth.controller_did { 134 let grant = state 135 .delegation_repo 136 + .get_delegation(&auth.did, controller) 137 .await 138 .ok() 139 .flatten(); ··· 143 let intersected = intersect_scopes(requested, &granted_scopes); 144 145 if intersected.is_empty() && !granted_scopes.is_empty() { 146 + return Err(ApiError::InsufficientScope(None)); 147 } 148 149 let scope_result = if intersected.is_empty() { ··· 159 let password = generate_app_password(); 160 161 let password_clone = password.clone(); 162 + let password_hash = 163 + tokio::task::spawn_blocking(move || bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST)) 164 + .await 165 + .map_err(|e| { 166 + error!("Failed to spawn blocking task: {:?}", e); 167 + ApiError::InternalError(None) 168 + })? 169 + .map_err(|e| { 170 + error!("Failed to hash password: {:?}", e); 171 + ApiError::InternalError(None) 172 + })?; 173 174 let privileged = input.privileged.unwrap_or(false); 175 let created_at = chrono::Utc::now(); ··· 183 created_by_controller_did: controller_did.clone(), 184 }; 185 186 + state 187 + .session_repo 188 + .create_app_password(&create_data) 189 + .await 190 + .map_err(|e| { 191 error!("DB error creating app password: {:?}", e); 192 + ApiError::InternalError(None) 193 + })?; 194 + 195 + if let Some(ref controller) = controller_did { 196 + let _ = state 197 + .delegation_repo 198 + .log_delegation_action( 199 + &auth.did, 200 + controller, 201 + Some(controller), 202 + DelegationActionType::AccountAction, 203 + Some(json!({ 204 + "action": "create_app_password", 205 + "name": name, 206 + "scopes": final_scopes 207 + })), 208 + None, 209 + None, 210 + ) 211 + .await; 212 } 213 + Ok(Json(CreateAppPasswordOutput { 214 + name: name.to_string(), 215 + password, 216 + created_at: created_at.to_rfc3339(), 217 + privileged, 218 + scopes: final_scopes, 219 + }) 220 + .into_response()) 221 } 222 223 #[derive(Deserialize)] ··· 227 228 pub async fn revoke_app_password( 229 State(state): State<AppState>, 230 + auth: Auth<Active>, 231 Json(input): Json<RevokeAppPasswordInput>, 232 + ) -> Result<Response, ApiError> { 233 + let user = state 234 + .user_repo 235 + .get_by_did(&auth.did) 236 + .await 237 + .map_err(|e| { 238 error!("DB error getting user: {:?}", e); 239 + ApiError::InternalError(None) 240 + })? 241 + .ok_or(ApiError::AccountNotFound)?; 242 243 let name = input.name.trim(); 244 if name.is_empty() { 245 + return Err(ApiError::InvalidRequest("name is required".into())); 246 } 247 248 let sessions_to_invalidate = state 249 .session_repo 250 + .get_session_jtis_by_app_password(&auth.did, name) 251 .await 252 .unwrap_or_default(); 253 254 + state 255 .session_repo 256 + .delete_sessions_by_app_password(&auth.did, name) 257 .await 258 + .map_err(|e| { 259 + error!("DB error revoking sessions for app password: {:?}", e); 260 + ApiError::InternalError(None) 261 + })?; 262 263 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 264 + let cache_key = format!("auth:session:{}:{}", &auth.did, jti); 265 let cache = state.cache.clone(); 266 async move { 267 let _ = cache.delete(&cache_key).await; ··· 269 })) 270 .await; 271 272 + state 273 + .session_repo 274 + .delete_app_password(user.id, name) 275 + .await 276 + .map_err(|e| { 277 + error!("DB error revoking app password: {:?}", e); 278 + ApiError::InternalError(None) 279 + })?; 280 281 + Ok(EmptyResponse::ok().into_response()) 282 }
+92 -89
crates/tranquil-pds/src/api/server/email.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 3 - use crate::auth::BearerAuth; 4 use crate::state::{AppState, RateLimitKind}; 5 use axum::{ 6 Json, ··· 45 pub async fn request_email_update( 46 State(state): State<AppState>, 47 headers: axum::http::HeaderMap, 48 - auth: BearerAuth, 49 input: Option<Json<RequestEmailUpdateInput>>, 50 - ) -> Response { 51 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 52 if !state 53 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 54 .await 55 { 56 warn!(ip = %client_ip, "Email update rate limit exceeded"); 57 - return ApiError::RateLimitExceeded(None).into_response(); 58 } 59 60 if let Err(e) = crate::auth::scope_check::check_account_scope( 61 - auth.0.is_oauth, 62 - auth.0.scope.as_deref(), 63 crate::oauth::scopes::AccountAttr::Email, 64 crate::oauth::scopes::AccountAction::Manage, 65 ) { 66 - return e; 67 } 68 69 - let user = match state.user_repo.get_email_info_by_did(&auth.0.did).await { 70 - Ok(Some(row)) => row, 71 - Ok(None) => { 72 - return ApiError::AccountNotFound.into_response(); 73 - } 74 - Err(e) => { 75 error!("DB error: {:?}", e); 76 - return ApiError::InternalError(None).into_response(); 77 - } 78 - }; 79 80 let Some(current_email) = user.email else { 81 - return ApiError::InvalidRequest("account does not have an email address".into()) 82 - .into_response(); 83 }; 84 85 let token_required = user.email_verified; 86 87 if token_required { 88 let code = crate::auth::verification_token::generate_channel_update_token( 89 - &auth.0.did, 90 "email_update", 91 &current_email.to_lowercase(), 92 ); ··· 103 authorized: false, 104 }; 105 if let Ok(json) = serde_json::to_string(&pending) { 106 - let cache_key = email_update_cache_key(&auth.0.did); 107 if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await { 108 warn!("Failed to cache pending email update: {:?}", e); 109 } ··· 127 } 128 129 info!("Email update requested for user {}", user.id); 130 - TokenRequiredResponse::response(token_required).into_response() 131 } 132 133 #[derive(Deserialize)] ··· 140 pub async fn confirm_email( 141 State(state): State<AppState>, 142 headers: axum::http::HeaderMap, 143 - auth: BearerAuth, 144 Json(input): Json<ConfirmEmailInput>, 145 - ) -> Response { 146 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 147 if !state 148 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 149 .await 150 { 151 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 152 - return ApiError::RateLimitExceeded(None).into_response(); 153 } 154 155 if let Err(e) = crate::auth::scope_check::check_account_scope( 156 - auth.0.is_oauth, 157 - auth.0.scope.as_deref(), 158 crate::oauth::scopes::AccountAttr::Email, 159 crate::oauth::scopes::AccountAction::Manage, 160 ) { 161 - return e; 162 } 163 164 - let did = &auth.0.did; 165 - let user = match state.user_repo.get_email_info_by_did(did).await { 166 - Ok(Some(row)) => row, 167 - Ok(None) => { 168 - return ApiError::AccountNotFound.into_response(); 169 - } 170 - Err(e) => { 171 error!("DB error: {:?}", e); 172 - return ApiError::InternalError(None).into_response(); 173 - } 174 - }; 175 176 let Some(ref email) = user.email else { 177 - return ApiError::InvalidEmail.into_response(); 178 }; 179 let current_email = email.to_lowercase(); 180 181 let provided_email = input.email.trim().to_lowercase(); 182 if provided_email != current_email { 183 - return ApiError::InvalidEmail.into_response(); 184 } 185 186 if user.email_verified { 187 - return EmptyResponse::ok().into_response(); 188 } 189 190 let confirmation_code = ··· 199 match verified { 200 Ok(token_data) => { 201 if token_data.did != did.as_str() { 202 - return ApiError::InvalidToken(None).into_response(); 203 } 204 } 205 Err(crate::auth::verification_token::VerifyError::Expired) => { 206 - return ApiError::ExpiredToken(None).into_response(); 207 } 208 Err(_) => { 209 - return ApiError::InvalidToken(None).into_response(); 210 } 211 } 212 213 - if let Err(e) = state.user_repo.set_email_verified(user.id, true).await { 214 - error!("DB error confirming email: {:?}", e); 215 - return ApiError::InternalError(None).into_response(); 216 - } 217 218 info!("Email confirmed for user {}", user.id); 219 - EmptyResponse::ok().into_response() 220 } 221 222 #[derive(Deserialize)] ··· 230 231 pub async fn update_email( 232 State(state): State<AppState>, 233 - auth: BearerAuth, 234 Json(input): Json<UpdateEmailInput>, 235 - ) -> Response { 236 - let auth_user = auth.0; 237 - 238 if let Err(e) = crate::auth::scope_check::check_account_scope( 239 - auth_user.is_oauth, 240 - auth_user.scope.as_deref(), 241 crate::oauth::scopes::AccountAttr::Email, 242 crate::oauth::scopes::AccountAction::Manage, 243 ) { 244 - return e; 245 } 246 247 - let did = &auth_user.did; 248 - let user = match state.user_repo.get_email_info_by_did(did).await { 249 - Ok(Some(row)) => row, 250 - Ok(None) => { 251 - return ApiError::AccountNotFound.into_response(); 252 - } 253 - Err(e) => { 254 error!("DB error: {:?}", e); 255 - return ApiError::InternalError(None).into_response(); 256 - } 257 - }; 258 259 let user_id = user.id; 260 let current_email = user.email.clone(); ··· 262 let new_email = input.email.trim().to_lowercase(); 263 264 if !crate::api::validation::is_valid_email(&new_email) { 265 - return ApiError::InvalidRequest( 266 "This email address is not supported, please use a different email.".into(), 267 - ) 268 - .into_response(); 269 } 270 271 if let Some(ref current) = current_email 272 && new_email == current.to_lowercase() 273 { 274 - return EmptyResponse::ok().into_response(); 275 } 276 277 if email_verified { ··· 290 291 if !authorized_via_link { 292 let Some(ref t) = input.token else { 293 - return ApiError::TokenRequired.into_response(); 294 }; 295 let confirmation_token = 296 crate::auth::verification_token::normalize_token_input(t.trim()); ··· 309 match verified { 310 Ok(token_data) => { 311 if token_data.did != did.as_str() { 312 - return ApiError::InvalidToken(None).into_response(); 313 } 314 } 315 Err(crate::auth::verification_token::VerifyError::Expired) => { 316 - return ApiError::ExpiredToken(None).into_response(); 317 } 318 Err(_) => { 319 - return ApiError::InvalidToken(None).into_response(); 320 } 321 } 322 } 323 } 324 325 - if let Err(e) = state.user_repo.update_email(user_id, &new_email).await { 326 - error!("DB error updating email: {:?}", e); 327 - return ApiError::InternalError(None).into_response(); 328 - } 329 330 let verification_token = 331 crate::auth::verification_token::generate_signup_token(did, "email", &new_email); ··· 358 } 359 360 info!("Email updated for user {}", user_id); 361 - EmptyResponse::ok().into_response() 362 } 363 364 #[derive(Deserialize)] ··· 497 pub async fn check_email_update_status( 498 State(state): State<AppState>, 499 headers: axum::http::HeaderMap, 500 - auth: BearerAuth, 501 - ) -> Response { 502 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 503 if !state 504 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 505 .await 506 { 507 - return ApiError::RateLimitExceeded(None).into_response(); 508 } 509 510 if let Err(e) = crate::auth::scope_check::check_account_scope( 511 - auth.0.is_oauth, 512 - auth.0.scope.as_deref(), 513 crate::oauth::scopes::AccountAttr::Email, 514 crate::oauth::scopes::AccountAction::Read, 515 ) { 516 - return e; 517 } 518 519 - let cache_key = email_update_cache_key(&auth.0.did); 520 let pending_json = match state.cache.get(&cache_key).await { 521 Some(json) => json, 522 None => { 523 - return Json(json!({ "pending": false, "authorized": false })).into_response(); 524 } 525 }; 526 527 let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) { 528 Ok(p) => p, 529 Err(_) => { 530 - return Json(json!({ "pending": false, "authorized": false })).into_response(); 531 } 532 }; 533 534 - Json(json!({ 535 "pending": true, 536 "authorized": pending.authorized, 537 "newEmail": pending.new_email, 538 })) 539 - .into_response() 540 } 541 542 #[derive(Deserialize)]
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 3 + use crate::auth::{Active, Auth}; 4 use crate::state::{AppState, RateLimitKind}; 5 use axum::{ 6 Json, ··· 45 pub async fn request_email_update( 46 State(state): State<AppState>, 47 headers: axum::http::HeaderMap, 48 + auth: Auth<Active>, 49 input: Option<Json<RequestEmailUpdateInput>>, 50 + ) -> Result<Response, ApiError> { 51 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 52 if !state 53 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 54 .await 55 { 56 warn!(ip = %client_ip, "Email update rate limit exceeded"); 57 + return Err(ApiError::RateLimitExceeded(None)); 58 } 59 60 if let Err(e) = crate::auth::scope_check::check_account_scope( 61 + auth.is_oauth(), 62 + auth.scope.as_deref(), 63 crate::oauth::scopes::AccountAttr::Email, 64 crate::oauth::scopes::AccountAction::Manage, 65 ) { 66 + return Ok(e); 67 } 68 69 + let user = state 70 + .user_repo 71 + .get_email_info_by_did(&auth.did) 72 + .await 73 + .map_err(|e| { 74 error!("DB error: {:?}", e); 75 + ApiError::InternalError(None) 76 + })? 77 + .ok_or(ApiError::AccountNotFound)?; 78 79 let Some(current_email) = user.email else { 80 + return Err(ApiError::InvalidRequest( 81 + "account does not have an email address".into(), 82 + )); 83 }; 84 85 let token_required = user.email_verified; 86 87 if token_required { 88 let code = crate::auth::verification_token::generate_channel_update_token( 89 + &auth.did, 90 "email_update", 91 &current_email.to_lowercase(), 92 ); ··· 103 authorized: false, 104 }; 105 if let Ok(json) = serde_json::to_string(&pending) { 106 + let cache_key = email_update_cache_key(&auth.did); 107 if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await { 108 warn!("Failed to cache pending email update: {:?}", e); 109 } ··· 127 } 128 129 info!("Email update requested for user {}", user.id); 130 + Ok(TokenRequiredResponse::response(token_required).into_response()) 131 } 132 133 #[derive(Deserialize)] ··· 140 pub async fn confirm_email( 141 State(state): State<AppState>, 142 headers: axum::http::HeaderMap, 143 + auth: Auth<Active>, 144 Json(input): Json<ConfirmEmailInput>, 145 + ) -> Result<Response, ApiError> { 146 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 147 if !state 148 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 149 .await 150 { 151 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 152 + return Err(ApiError::RateLimitExceeded(None)); 153 } 154 155 if let Err(e) = crate::auth::scope_check::check_account_scope( 156 + auth.is_oauth(), 157 + auth.scope.as_deref(), 158 crate::oauth::scopes::AccountAttr::Email, 159 crate::oauth::scopes::AccountAction::Manage, 160 ) { 161 + return Ok(e); 162 } 163 164 + let did = &auth.did; 165 + let user = state 166 + .user_repo 167 + .get_email_info_by_did(did) 168 + .await 169 + .map_err(|e| { 170 error!("DB error: {:?}", e); 171 + ApiError::InternalError(None) 172 + })? 173 + .ok_or(ApiError::AccountNotFound)?; 174 175 let Some(ref email) = user.email else { 176 + return Err(ApiError::InvalidEmail); 177 }; 178 let current_email = email.to_lowercase(); 179 180 let provided_email = input.email.trim().to_lowercase(); 181 if provided_email != current_email { 182 + return Err(ApiError::InvalidEmail); 183 } 184 185 if user.email_verified { 186 + return Ok(EmptyResponse::ok().into_response()); 187 } 188 189 let confirmation_code = ··· 198 match verified { 199 Ok(token_data) => { 200 if token_data.did != did.as_str() { 201 + return Err(ApiError::InvalidToken(None)); 202 } 203 } 204 Err(crate::auth::verification_token::VerifyError::Expired) => { 205 + return Err(ApiError::ExpiredToken(None)); 206 } 207 Err(_) => { 208 + return Err(ApiError::InvalidToken(None)); 209 } 210 } 211 212 + state 213 + .user_repo 214 + .set_email_verified(user.id, true) 215 + .await 216 + .map_err(|e| { 217 + error!("DB error confirming email: {:?}", e); 218 + ApiError::InternalError(None) 219 + })?; 220 221 info!("Email confirmed for user {}", user.id); 222 + Ok(EmptyResponse::ok().into_response()) 223 } 224 225 #[derive(Deserialize)] ··· 233 234 pub async fn update_email( 235 State(state): State<AppState>, 236 + auth: Auth<Active>, 237 Json(input): Json<UpdateEmailInput>, 238 + ) -> Result<Response, ApiError> { 239 if let Err(e) = crate::auth::scope_check::check_account_scope( 240 + auth.is_oauth(), 241 + auth.scope.as_deref(), 242 crate::oauth::scopes::AccountAttr::Email, 243 crate::oauth::scopes::AccountAction::Manage, 244 ) { 245 + return Ok(e); 246 } 247 248 + let did = &auth.did; 249 + let user = state 250 + .user_repo 251 + .get_email_info_by_did(did) 252 + .await 253 + .map_err(|e| { 254 error!("DB error: {:?}", e); 255 + ApiError::InternalError(None) 256 + })? 257 + .ok_or(ApiError::AccountNotFound)?; 258 259 let user_id = user.id; 260 let current_email = user.email.clone(); ··· 262 let new_email = input.email.trim().to_lowercase(); 263 264 if !crate::api::validation::is_valid_email(&new_email) { 265 + return Err(ApiError::InvalidRequest( 266 "This email address is not supported, please use a different email.".into(), 267 + )); 268 } 269 270 if let Some(ref current) = current_email 271 && new_email == current.to_lowercase() 272 { 273 + return Ok(EmptyResponse::ok().into_response()); 274 } 275 276 if email_verified { ··· 289 290 if !authorized_via_link { 291 let Some(ref t) = input.token else { 292 + return Err(ApiError::TokenRequired); 293 }; 294 let confirmation_token = 295 crate::auth::verification_token::normalize_token_input(t.trim()); ··· 308 match verified { 309 Ok(token_data) => { 310 if token_data.did != did.as_str() { 311 + return Err(ApiError::InvalidToken(None)); 312 } 313 } 314 Err(crate::auth::verification_token::VerifyError::Expired) => { 315 + return Err(ApiError::ExpiredToken(None)); 316 } 317 Err(_) => { 318 + return Err(ApiError::InvalidToken(None)); 319 } 320 } 321 } 322 } 323 324 + state 325 + .user_repo 326 + .update_email(user_id, &new_email) 327 + .await 328 + .map_err(|e| { 329 + error!("DB error updating email: {:?}", e); 330 + ApiError::InternalError(None) 331 + })?; 332 333 let verification_token = 334 crate::auth::verification_token::generate_signup_token(did, "email", &new_email); ··· 361 } 362 363 info!("Email updated for user {}", user_id); 364 + Ok(EmptyResponse::ok().into_response()) 365 } 366 367 #[derive(Deserialize)] ··· 500 pub async fn check_email_update_status( 501 State(state): State<AppState>, 502 headers: axum::http::HeaderMap, 503 + auth: Auth<Active>, 504 + ) -> Result<Response, ApiError> { 505 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 506 if !state 507 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 508 .await 509 { 510 + return Err(ApiError::RateLimitExceeded(None)); 511 } 512 513 if let Err(e) = crate::auth::scope_check::check_account_scope( 514 + auth.is_oauth(), 515 + auth.scope.as_deref(), 516 crate::oauth::scopes::AccountAttr::Email, 517 crate::oauth::scopes::AccountAction::Read, 518 ) { 519 + return Ok(e); 520 } 521 522 + let cache_key = email_update_cache_key(&auth.did); 523 let pending_json = match state.cache.get(&cache_key).await { 524 Some(json) => json, 525 None => { 526 + return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); 527 } 528 }; 529 530 let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) { 531 Ok(p) => p, 532 Err(_) => { 533 + return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); 534 } 535 }; 536 537 + Ok(Json(json!({ 538 "pending": true, 539 "authorized": pending.authorized, 540 "newEmail": pending.new_email, 541 })) 542 + .into_response()) 543 } 544 545 #[derive(Deserialize)]
+46 -48
crates/tranquil-pds/src/api/server/invite.rs
··· 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuth; 3 - use crate::auth::extractor::BearerAuthAdmin; 4 use crate::state::AppState; 5 use crate::types::Did; 6 use axum::{ ··· 44 45 pub async fn create_invite_code( 46 State(state): State<AppState>, 47 - BearerAuthAdmin(auth_user): BearerAuthAdmin, 48 Json(input): Json<CreateInviteCodeInput>, 49 - ) -> Response { 50 if input.use_count < 1 { 51 - return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 52 } 53 54 let for_account: Did = match &input.for_account { 55 - Some(acct) => match acct.parse() { 56 - Ok(d) => d, 57 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 58 - }, 59 - None => auth_user.did.clone(), 60 }; 61 let code = gen_invite_code(); 62 ··· 65 .create_invite_code(&code, input.use_count, Some(&for_account)) 66 .await 67 { 68 - Ok(true) => Json(CreateInviteCodeOutput { code }).into_response(), 69 Ok(false) => { 70 error!("No admin user found to create invite code"); 71 - ApiError::InternalError(None).into_response() 72 } 73 Err(e) => { 74 error!("DB error creating invite code: {:?}", e); 75 - ApiError::InternalError(None).into_response() 76 } 77 } 78 } ··· 98 99 pub async fn create_invite_codes( 100 State(state): State<AppState>, 101 - BearerAuthAdmin(auth_user): BearerAuthAdmin, 102 Json(input): Json<CreateInviteCodesInput>, 103 - ) -> Response { 104 if input.use_count < 1 { 105 - return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 106 } 107 108 let code_count = input.code_count.unwrap_or(1).max(1); 109 let for_accounts: Vec<Did> = match &input.for_accounts { 110 - Some(accounts) if !accounts.is_empty() => { 111 - let parsed: Result<Vec<Did>, _> = accounts.iter().map(|a| a.parse()).collect(); 112 - match parsed { 113 - Ok(dids) => dids, 114 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 115 - } 116 - } 117 - _ => vec![auth_user.did.clone()], 118 }; 119 120 - let admin_user_id = match state.user_repo.get_any_admin_user_id().await { 121 - Ok(Some(id)) => id, 122 - Ok(None) => { 123 - error!("No admin user found to create invite codes"); 124 - return ApiError::InternalError(None).into_response(); 125 - } 126 - Err(e) => { 127 error!("DB error looking up admin user: {:?}", e); 128 - return ApiError::InternalError(None).into_response(); 129 - } 130 - }; 131 132 let result = futures::future::try_join_all(for_accounts.into_iter().map(|account| { 133 let infra_repo = state.infra_repo.clone(); ··· 146 .await; 147 148 match result { 149 - Ok(result_codes) => Json(CreateInviteCodesOutput { 150 codes: result_codes, 151 }) 152 - .into_response(), 153 Err(e) => { 154 error!("DB error creating invite codes: {:?}", e); 155 - ApiError::InternalError(None).into_response() 156 } 157 } 158 } ··· 192 193 pub async fn get_account_invite_codes( 194 State(state): State<AppState>, 195 - BearerAuth(auth_user): BearerAuth, 196 axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>, 197 - ) -> Response { 198 let include_used = params.include_used.unwrap_or(true); 199 200 - let codes_info = match state 201 .infra_repo 202 - .get_invite_codes_for_account(&auth_user.did) 203 .await 204 - { 205 - Ok(info) => info, 206 - Err(e) => { 207 error!("DB error fetching invite codes: {:?}", e); 208 - return ApiError::InternalError(None).into_response(); 209 - } 210 - }; 211 212 let filtered_codes: Vec<_> = codes_info 213 .into_iter() ··· 254 .await; 255 256 let codes: Vec<InviteCode> = codes.into_iter().flatten().collect(); 257 - Json(GetAccountInviteCodesOutput { codes }).into_response() 258 }
··· 1 use crate::api::ApiError; 2 + use crate::auth::{Active, Admin, Auth}; 3 use crate::state::AppState; 4 use crate::types::Did; 5 use axum::{ ··· 43 44 pub async fn create_invite_code( 45 State(state): State<AppState>, 46 + auth: Auth<Admin>, 47 Json(input): Json<CreateInviteCodeInput>, 48 + ) -> Result<Response, ApiError> { 49 if input.use_count < 1 { 50 + return Err(ApiError::InvalidRequest( 51 + "useCount must be at least 1".into(), 52 + )); 53 } 54 55 let for_account: Did = match &input.for_account { 56 + Some(acct) => acct 57 + .parse() 58 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?, 59 + None => auth.did.clone(), 60 }; 61 let code = gen_invite_code(); 62 ··· 65 .create_invite_code(&code, input.use_count, Some(&for_account)) 66 .await 67 { 68 + Ok(true) => Ok(Json(CreateInviteCodeOutput { code }).into_response()), 69 Ok(false) => { 70 error!("No admin user found to create invite code"); 71 + Err(ApiError::InternalError(None)) 72 } 73 Err(e) => { 74 error!("DB error creating invite code: {:?}", e); 75 + Err(ApiError::InternalError(None)) 76 } 77 } 78 } ··· 98 99 pub async fn create_invite_codes( 100 State(state): State<AppState>, 101 + auth: Auth<Admin>, 102 Json(input): Json<CreateInviteCodesInput>, 103 + ) -> Result<Response, ApiError> { 104 if input.use_count < 1 { 105 + return Err(ApiError::InvalidRequest( 106 + "useCount must be at least 1".into(), 107 + )); 108 } 109 110 let code_count = input.code_count.unwrap_or(1).max(1); 111 let for_accounts: Vec<Did> = match &input.for_accounts { 112 + Some(accounts) if !accounts.is_empty() => accounts 113 + .iter() 114 + .map(|a| a.parse()) 115 + .collect::<Result<Vec<Did>, _>>() 116 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?, 117 + _ => vec![auth.did.clone()], 118 }; 119 120 + let admin_user_id = state 121 + .user_repo 122 + .get_any_admin_user_id() 123 + .await 124 + .map_err(|e| { 125 error!("DB error looking up admin user: {:?}", e); 126 + ApiError::InternalError(None) 127 + })? 128 + .ok_or_else(|| { 129 + error!("No admin user found to create invite codes"); 130 + ApiError::InternalError(None) 131 + })?; 132 133 let result = futures::future::try_join_all(for_accounts.into_iter().map(|account| { 134 let infra_repo = state.infra_repo.clone(); ··· 147 .await; 148 149 match result { 150 + Ok(result_codes) => Ok(Json(CreateInviteCodesOutput { 151 codes: result_codes, 152 }) 153 + .into_response()), 154 Err(e) => { 155 error!("DB error creating invite codes: {:?}", e); 156 + Err(ApiError::InternalError(None)) 157 } 158 } 159 } ··· 193 194 pub async fn get_account_invite_codes( 195 State(state): State<AppState>, 196 + auth: Auth<Active>, 197 axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>, 198 + ) -> Result<Response, ApiError> { 199 let include_used = params.include_used.unwrap_or(true); 200 201 + let codes_info = state 202 .infra_repo 203 + .get_invite_codes_for_account(&auth.did) 204 .await 205 + .map_err(|e| { 206 error!("DB error fetching invite codes: {:?}", e); 207 + ApiError::InternalError(None) 208 + })?; 209 210 let filtered_codes: Vec<_> = codes_info 211 .into_iter() ··· 252 .await; 253 254 let codes: Vec<InviteCode> = codes.into_iter().flatten().collect(); 255 + Ok(Json(GetAccountInviteCodesOutput { codes }).into_response()) 256 }
+48 -51
crates/tranquil-pds/src/api/server/migration.rs
··· 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuth; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 36 37 pub async fn update_did_document( 38 State(state): State<AppState>, 39 - auth: BearerAuth, 40 Json(input): Json<UpdateDidDocumentInput>, 41 - ) -> Response { 42 - let auth_user = auth.0; 43 - 44 - if !auth_user.did.starts_with("did:web:") { 45 - return ApiError::InvalidRequest( 46 "DID document updates are only available for did:web accounts".into(), 47 - ) 48 - .into_response(); 49 } 50 51 - let user = match state.user_repo.get_user_for_did_doc(&auth_user.did).await { 52 - Ok(Some(u)) => u, 53 - Ok(None) => return ApiError::AccountNotFound.into_response(), 54 - Err(e) => { 55 tracing::error!("DB error getting user: {:?}", e); 56 - return ApiError::InternalError(None).into_response(); 57 - } 58 - }; 59 - 60 - if user.deactivated_at.is_some() { 61 - return ApiError::AccountDeactivated.into_response(); 62 - } 63 64 if let Some(ref methods) = input.verification_methods { 65 if methods.is_empty() { 66 - return ApiError::InvalidRequest("verification_methods cannot be empty".into()) 67 - .into_response(); 68 } 69 let validation_error = methods.iter().find_map(|method| { 70 if method.id.is_empty() { ··· 80 } 81 }); 82 if let Some(err) = validation_error { 83 - return ApiError::InvalidRequest(err.into()).into_response(); 84 } 85 } 86 87 if let Some(ref handles) = input.also_known_as 88 && handles.iter().any(|h| !h.starts_with("at://")) 89 { 90 - return ApiError::InvalidRequest("alsoKnownAs entries must be at:// URIs".into()) 91 - .into_response(); 92 } 93 94 if let Some(ref endpoint) = input.service_endpoint { 95 let endpoint = endpoint.trim(); 96 if !endpoint.starts_with("https://") { 97 - return ApiError::InvalidRequest("serviceEndpoint must start with https://".into()) 98 - .into_response(); 99 } 100 } 101 ··· 106 107 let also_known_as: Option<Vec<String>> = input.also_known_as.clone(); 108 109 - if let Err(e) = state 110 .user_repo 111 .upsert_did_web_overrides(user.id, verification_methods_json, also_known_as) 112 .await 113 - { 114 - tracing::error!("DB error upserting did_web_overrides: {:?}", e); 115 - return ApiError::InternalError(None).into_response(); 116 - } 117 118 if let Some(ref endpoint) = input.service_endpoint { 119 let endpoint_clean = endpoint.trim().trim_end_matches('/'); 120 - if let Err(e) = state 121 .user_repo 122 - .update_migrated_to_pds(&auth_user.did, endpoint_clean) 123 .await 124 - { 125 - tracing::error!("DB error updating service endpoint: {:?}", e); 126 - return ApiError::InternalError(None).into_response(); 127 - } 128 } 129 130 - let did_doc = build_did_document(&state, &auth_user.did).await; 131 132 - tracing::info!("Updated DID document for {}", &auth_user.did); 133 134 - ( 135 StatusCode::OK, 136 Json(UpdateDidDocumentOutput { 137 success: true, 138 did_document: did_doc, 139 }), 140 ) 141 - .into_response() 142 } 143 144 - pub async fn get_did_document(State(state): State<AppState>, auth: BearerAuth) -> Response { 145 - let auth_user = auth.0; 146 - 147 - if !auth_user.did.starts_with("did:web:") { 148 - return ApiError::InvalidRequest( 149 "This endpoint is only available for did:web accounts".into(), 150 - ) 151 - .into_response(); 152 } 153 154 - let did_doc = build_did_document(&state, &auth_user.did).await; 155 156 - (StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response() 157 } 158 159 async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value {
··· 1 use crate::api::ApiError; 2 + use crate::auth::{Active, Auth}; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 36 37 pub async fn update_did_document( 38 State(state): State<AppState>, 39 + auth: Auth<Active>, 40 Json(input): Json<UpdateDidDocumentInput>, 41 + ) -> Result<Response, ApiError> { 42 + if !auth.did.starts_with("did:web:") { 43 + return Err(ApiError::InvalidRequest( 44 "DID document updates are only available for did:web accounts".into(), 45 + )); 46 } 47 48 + let user = state 49 + .user_repo 50 + .get_user_for_did_doc(&auth.did) 51 + .await 52 + .map_err(|e| { 53 tracing::error!("DB error getting user: {:?}", e); 54 + ApiError::InternalError(None) 55 + })? 56 + .ok_or(ApiError::AccountNotFound)?; 57 58 if let Some(ref methods) = input.verification_methods { 59 if methods.is_empty() { 60 + return Err(ApiError::InvalidRequest( 61 + "verification_methods cannot be empty".into(), 62 + )); 63 } 64 let validation_error = methods.iter().find_map(|method| { 65 if method.id.is_empty() { ··· 75 } 76 }); 77 if let Some(err) = validation_error { 78 + return Err(ApiError::InvalidRequest(err.into())); 79 } 80 } 81 82 if let Some(ref handles) = input.also_known_as 83 && handles.iter().any(|h| !h.starts_with("at://")) 84 { 85 + return Err(ApiError::InvalidRequest( 86 + "alsoKnownAs entries must be at:// URIs".into(), 87 + )); 88 } 89 90 if let Some(ref endpoint) = input.service_endpoint { 91 let endpoint = endpoint.trim(); 92 if !endpoint.starts_with("https://") { 93 + return Err(ApiError::InvalidRequest( 94 + "serviceEndpoint must start with https://".into(), 95 + )); 96 } 97 } 98 ··· 103 104 let also_known_as: Option<Vec<String>> = input.also_known_as.clone(); 105 106 + state 107 .user_repo 108 .upsert_did_web_overrides(user.id, verification_methods_json, also_known_as) 109 .await 110 + .map_err(|e| { 111 + tracing::error!("DB error upserting did_web_overrides: {:?}", e); 112 + ApiError::InternalError(None) 113 + })?; 114 115 if let Some(ref endpoint) = input.service_endpoint { 116 let endpoint_clean = endpoint.trim().trim_end_matches('/'); 117 + state 118 .user_repo 119 + .update_migrated_to_pds(&auth.did, endpoint_clean) 120 .await 121 + .map_err(|e| { 122 + tracing::error!("DB error updating service endpoint: {:?}", e); 123 + ApiError::InternalError(None) 124 + })?; 125 } 126 127 + let did_doc = build_did_document(&state, &auth.did).await; 128 129 + tracing::info!("Updated DID document for {}", &auth.did); 130 131 + Ok(( 132 StatusCode::OK, 133 Json(UpdateDidDocumentOutput { 134 success: true, 135 did_document: did_doc, 136 }), 137 ) 138 + .into_response()) 139 } 140 141 + pub async fn get_did_document( 142 + State(state): State<AppState>, 143 + auth: Auth<Active>, 144 + ) -> Result<Response, ApiError> { 145 + if !auth.did.starts_with("did:web:") { 146 + return Err(ApiError::InvalidRequest( 147 "This endpoint is only available for did:web accounts".into(), 148 + )); 149 } 150 151 + let did_doc = build_did_document(&state, &auth.did).await; 152 153 + Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response()) 154 } 155 156 async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value {
+108 -147
crates/tranquil-pds/src/api/server/passkeys.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuth; 4 use crate::auth::webauthn::WebAuthnConfig; 5 use crate::state::AppState; 6 use axum::{ 7 Json, ··· 34 35 pub async fn start_passkey_registration( 36 State(state): State<AppState>, 37 - auth: BearerAuth, 38 Json(input): Json<StartRegistrationInput>, 39 - ) -> Response { 40 - let webauthn = match get_webauthn() { 41 - Ok(w) => w, 42 - Err(e) => return e.into_response(), 43 - }; 44 45 - let handle = match state.user_repo.get_handle_by_did(&auth.0.did).await { 46 - Ok(Some(h)) => h, 47 - Ok(None) => { 48 - return ApiError::AccountNotFound.into_response(); 49 - } 50 - Err(e) => { 51 error!("DB error fetching user: {:?}", e); 52 - return ApiError::InternalError(None).into_response(); 53 - } 54 - }; 55 56 - let existing_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { 57 - Ok(passkeys) => passkeys, 58 - Err(e) => { 59 error!("DB error fetching existing passkeys: {:?}", e); 60 - return ApiError::InternalError(None).into_response(); 61 - } 62 - }; 63 64 let exclude_credentials: Vec<CredentialID> = existing_passkeys 65 .iter() ··· 68 69 let display_name = input.friendly_name.as_deref().unwrap_or(&handle); 70 71 - let (ccr, reg_state) = match webauthn.start_registration( 72 - &auth.0.did, 73 - &handle, 74 - display_name, 75 - exclude_credentials, 76 - ) { 77 - Ok(result) => result, 78 - Err(e) => { 79 error!("Failed to start passkey registration: {}", e); 80 - return ApiError::InternalError(Some("Failed to start registration".into())) 81 - .into_response(); 82 - } 83 - }; 84 85 - let state_json = match serde_json::to_string(&reg_state) { 86 - Ok(s) => s, 87 - Err(e) => { 88 - error!("Failed to serialize registration state: {:?}", e); 89 - return ApiError::InternalError(None).into_response(); 90 - } 91 - }; 92 93 - if let Err(e) = state 94 .user_repo 95 - .save_webauthn_challenge(&auth.0.did, "registration", &state_json) 96 .await 97 - { 98 - error!("Failed to save registration state: {:?}", e); 99 - return ApiError::InternalError(None).into_response(); 100 - } 101 102 let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 103 104 - info!(did = %auth.0.did, "Passkey registration started"); 105 106 - Json(StartRegistrationResponse { options }).into_response() 107 } 108 109 #[derive(Deserialize)] ··· 122 123 pub async fn finish_passkey_registration( 124 State(state): State<AppState>, 125 - auth: BearerAuth, 126 Json(input): Json<FinishRegistrationInput>, 127 - ) -> Response { 128 - let webauthn = match get_webauthn() { 129 - Ok(w) => w, 130 - Err(e) => return e.into_response(), 131 - }; 132 133 - let reg_state_json = match state 134 .user_repo 135 - .load_webauthn_challenge(&auth.0.did, "registration") 136 .await 137 - { 138 - Ok(Some(json)) => json, 139 - Ok(None) => { 140 - return ApiError::NoRegistrationInProgress.into_response(); 141 - } 142 - Err(e) => { 143 error!("DB error loading registration state: {:?}", e); 144 - return ApiError::InternalError(None).into_response(); 145 - } 146 - }; 147 148 - let reg_state: SecurityKeyRegistration = match serde_json::from_str(&reg_state_json) { 149 - Ok(s) => s, 150 - Err(e) => { 151 error!("Failed to deserialize registration state: {:?}", e); 152 - return ApiError::InternalError(None).into_response(); 153 - } 154 - }; 155 156 - let credential: RegisterPublicKeyCredential = match serde_json::from_value(input.credential) { 157 - Ok(c) => c, 158 - Err(e) => { 159 warn!("Failed to parse credential: {:?}", e); 160 - return ApiError::InvalidCredential.into_response(); 161 - } 162 - }; 163 164 - let passkey = match webauthn.finish_registration(&credential, &reg_state) { 165 - Ok(pk) => pk, 166 - Err(e) => { 167 warn!("Failed to finish passkey registration: {}", e); 168 - return ApiError::RegistrationFailed.into_response(); 169 - } 170 - }; 171 172 - let public_key = match serde_json::to_vec(&passkey) { 173 - Ok(pk) => pk, 174 - Err(e) => { 175 - error!("Failed to serialize passkey: {:?}", e); 176 - return ApiError::InternalError(None).into_response(); 177 - } 178 - }; 179 180 - let passkey_id = match state 181 .user_repo 182 .save_passkey( 183 - &auth.0.did, 184 passkey.cred_id(), 185 &public_key, 186 input.friendly_name.as_deref(), 187 ) 188 .await 189 - { 190 - Ok(id) => id, 191 - Err(e) => { 192 error!("Failed to save passkey: {:?}", e); 193 - return ApiError::InternalError(None).into_response(); 194 - } 195 - }; 196 197 if let Err(e) = state 198 .user_repo 199 - .delete_webauthn_challenge(&auth.0.did, "registration") 200 .await 201 { 202 warn!("Failed to delete registration state: {:?}", e); ··· 207 passkey.cred_id(), 208 ); 209 210 - info!(did = %auth.0.did, passkey_id = %passkey_id, "Passkey registered"); 211 212 - Json(FinishRegistrationResponse { 213 id: passkey_id.to_string(), 214 credential_id: credential_id_base64, 215 }) 216 - .into_response() 217 } 218 219 #[derive(Serialize)] ··· 232 pub passkeys: Vec<PasskeyInfo>, 233 } 234 235 - pub async fn list_passkeys(State(state): State<AppState>, auth: BearerAuth) -> Response { 236 - let passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { 237 - Ok(pks) => pks, 238 - Err(e) => { 239 error!("DB error fetching passkeys: {:?}", e); 240 - return ApiError::InternalError(None).into_response(); 241 - } 242 - }; 243 244 let passkey_infos: Vec<PasskeyInfo> = passkeys 245 .into_iter() ··· 252 }) 253 .collect(); 254 255 - Json(ListPasskeysResponse { 256 passkeys: passkey_infos, 257 }) 258 - .into_response() 259 } 260 261 #[derive(Deserialize)] ··· 266 267 pub async fn delete_passkey( 268 State(state): State<AppState>, 269 - auth: BearerAuth, 270 Json(input): Json<DeletePasskeyInput>, 271 - ) -> Response { 272 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 273 - .await 274 { 275 - return crate::api::server::reauth::legacy_mfa_required_response( 276 &*state.user_repo, 277 &*state.session_repo, 278 - &auth.0.did, 279 ) 280 - .await; 281 } 282 283 - if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await { 284 - return crate::api::server::reauth::reauth_required_response( 285 &*state.user_repo, 286 &*state.session_repo, 287 - &auth.0.did, 288 ) 289 - .await; 290 } 291 292 - let id: uuid::Uuid = match input.id.parse() { 293 - Ok(id) => id, 294 - Err(_) => { 295 - return ApiError::InvalidId.into_response(); 296 - } 297 - }; 298 299 - match state.user_repo.delete_passkey(id, &auth.0.did).await { 300 Ok(true) => { 301 - info!(did = %auth.0.did, passkey_id = %id, "Passkey deleted"); 302 - EmptyResponse::ok().into_response() 303 } 304 - Ok(false) => ApiError::PasskeyNotFound.into_response(), 305 Err(e) => { 306 error!("DB error deleting passkey: {:?}", e); 307 - ApiError::InternalError(None).into_response() 308 } 309 } 310 } ··· 318 319 pub async fn update_passkey( 320 State(state): State<AppState>, 321 - auth: BearerAuth, 322 Json(input): Json<UpdatePasskeyInput>, 323 - ) -> Response { 324 - let id: uuid::Uuid = match input.id.parse() { 325 - Ok(id) => id, 326 - Err(_) => { 327 - return ApiError::InvalidId.into_response(); 328 - } 329 - }; 330 331 match state 332 .user_repo 333 - .update_passkey_name(id, &auth.0.did, &input.friendly_name) 334 .await 335 { 336 Ok(true) => { 337 - info!(did = %auth.0.did, passkey_id = %id, "Passkey renamed"); 338 - EmptyResponse::ok().into_response() 339 } 340 - Ok(false) => ApiError::PasskeyNotFound.into_response(), 341 Err(e) => { 342 error!("DB error updating passkey: {:?}", e); 343 - ApiError::InternalError(None).into_response() 344 } 345 } 346 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 use crate::auth::webauthn::WebAuthnConfig; 4 + use crate::auth::{Active, Auth}; 5 use crate::state::AppState; 6 use axum::{ 7 Json, ··· 34 35 pub async fn start_passkey_registration( 36 State(state): State<AppState>, 37 + auth: Auth<Active>, 38 Json(input): Json<StartRegistrationInput>, 39 + ) -> Result<Response, ApiError> { 40 + let webauthn = get_webauthn()?; 41 42 + let handle = state 43 + .user_repo 44 + .get_handle_by_did(&auth.did) 45 + .await 46 + .map_err(|e| { 47 error!("DB error fetching user: {:?}", e); 48 + ApiError::InternalError(None) 49 + })? 50 + .ok_or(ApiError::AccountNotFound)?; 51 52 + let existing_passkeys = state 53 + .user_repo 54 + .get_passkeys_for_user(&auth.did) 55 + .await 56 + .map_err(|e| { 57 error!("DB error fetching existing passkeys: {:?}", e); 58 + ApiError::InternalError(None) 59 + })?; 60 61 let exclude_credentials: Vec<CredentialID> = existing_passkeys 62 .iter() ··· 65 66 let display_name = input.friendly_name.as_deref().unwrap_or(&handle); 67 68 + let (ccr, reg_state) = webauthn 69 + .start_registration(&auth.did, &handle, display_name, exclude_credentials) 70 + .map_err(|e| { 71 error!("Failed to start passkey registration: {}", e); 72 + ApiError::InternalError(Some("Failed to start registration".into())) 73 + })?; 74 75 + let state_json = serde_json::to_string(&reg_state).map_err(|e| { 76 + error!("Failed to serialize registration state: {:?}", e); 77 + ApiError::InternalError(None) 78 + })?; 79 80 + state 81 .user_repo 82 + .save_webauthn_challenge(&auth.did, "registration", &state_json) 83 .await 84 + .map_err(|e| { 85 + error!("Failed to save registration state: {:?}", e); 86 + ApiError::InternalError(None) 87 + })?; 88 89 let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 90 91 + info!(did = %auth.did, "Passkey registration started"); 92 93 + Ok(Json(StartRegistrationResponse { options }).into_response()) 94 } 95 96 #[derive(Deserialize)] ··· 109 110 pub async fn finish_passkey_registration( 111 State(state): State<AppState>, 112 + auth: Auth<Active>, 113 Json(input): Json<FinishRegistrationInput>, 114 + ) -> Result<Response, ApiError> { 115 + let webauthn = get_webauthn()?; 116 117 + let reg_state_json = state 118 .user_repo 119 + .load_webauthn_challenge(&auth.did, "registration") 120 .await 121 + .map_err(|e| { 122 error!("DB error loading registration state: {:?}", e); 123 + ApiError::InternalError(None) 124 + })? 125 + .ok_or(ApiError::NoRegistrationInProgress)?; 126 127 + let reg_state: SecurityKeyRegistration = 128 + serde_json::from_str(&reg_state_json).map_err(|e| { 129 error!("Failed to deserialize registration state: {:?}", e); 130 + ApiError::InternalError(None) 131 + })?; 132 133 + let credential: RegisterPublicKeyCredential = serde_json::from_value(input.credential) 134 + .map_err(|e| { 135 warn!("Failed to parse credential: {:?}", e); 136 + ApiError::InvalidCredential 137 + })?; 138 139 + let passkey = webauthn 140 + .finish_registration(&credential, &reg_state) 141 + .map_err(|e| { 142 warn!("Failed to finish passkey registration: {}", e); 143 + ApiError::RegistrationFailed 144 + })?; 145 146 + let public_key = serde_json::to_vec(&passkey).map_err(|e| { 147 + error!("Failed to serialize passkey: {:?}", e); 148 + ApiError::InternalError(None) 149 + })?; 150 151 + let passkey_id = state 152 .user_repo 153 .save_passkey( 154 + &auth.did, 155 passkey.cred_id(), 156 &public_key, 157 input.friendly_name.as_deref(), 158 ) 159 .await 160 + .map_err(|e| { 161 error!("Failed to save passkey: {:?}", e); 162 + ApiError::InternalError(None) 163 + })?; 164 165 if let Err(e) = state 166 .user_repo 167 + .delete_webauthn_challenge(&auth.did, "registration") 168 .await 169 { 170 warn!("Failed to delete registration state: {:?}", e); ··· 175 passkey.cred_id(), 176 ); 177 178 + info!(did = %auth.did, passkey_id = %passkey_id, "Passkey registered"); 179 180 + Ok(Json(FinishRegistrationResponse { 181 id: passkey_id.to_string(), 182 credential_id: credential_id_base64, 183 }) 184 + .into_response()) 185 } 186 187 #[derive(Serialize)] ··· 200 pub passkeys: Vec<PasskeyInfo>, 201 } 202 203 + pub async fn list_passkeys( 204 + State(state): State<AppState>, 205 + auth: Auth<Active>, 206 + ) -> Result<Response, ApiError> { 207 + let passkeys = state 208 + .user_repo 209 + .get_passkeys_for_user(&auth.did) 210 + .await 211 + .map_err(|e| { 212 error!("DB error fetching passkeys: {:?}", e); 213 + ApiError::InternalError(None) 214 + })?; 215 216 let passkey_infos: Vec<PasskeyInfo> = passkeys 217 .into_iter() ··· 224 }) 225 .collect(); 226 227 + Ok(Json(ListPasskeysResponse { 228 passkeys: passkey_infos, 229 }) 230 + .into_response()) 231 } 232 233 #[derive(Deserialize)] ··· 238 239 pub async fn delete_passkey( 240 State(state): State<AppState>, 241 + auth: Auth<Active>, 242 Json(input): Json<DeletePasskeyInput>, 243 + ) -> Result<Response, ApiError> { 244 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 245 { 246 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 247 &*state.user_repo, 248 &*state.session_repo, 249 + &auth.did, 250 ) 251 + .await); 252 } 253 254 + if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.did).await { 255 + return Ok(crate::api::server::reauth::reauth_required_response( 256 &*state.user_repo, 257 &*state.session_repo, 258 + &auth.did, 259 ) 260 + .await); 261 } 262 263 + let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 264 265 + match state.user_repo.delete_passkey(id, &auth.did).await { 266 Ok(true) => { 267 + info!(did = %auth.did, passkey_id = %id, "Passkey deleted"); 268 + Ok(EmptyResponse::ok().into_response()) 269 } 270 + Ok(false) => Err(ApiError::PasskeyNotFound), 271 Err(e) => { 272 error!("DB error deleting passkey: {:?}", e); 273 + Err(ApiError::InternalError(None)) 274 } 275 } 276 } ··· 284 285 pub async fn update_passkey( 286 State(state): State<AppState>, 287 + auth: Auth<Active>, 288 Json(input): Json<UpdatePasskeyInput>, 289 + ) -> Result<Response, ApiError> { 290 + let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 291 292 match state 293 .user_repo 294 + .update_passkey_name(id, &auth.did, &input.friendly_name) 295 .await 296 { 297 Ok(true) => { 298 + info!(did = %auth.did, passkey_id = %id, "Passkey renamed"); 299 + Ok(EmptyResponse::ok().into_response()) 300 } 301 + Ok(false) => Err(ApiError::PasskeyNotFound), 302 Err(e) => { 303 error!("DB error updating passkey: {:?}", e); 304 + Err(ApiError::InternalError(None)) 305 } 306 } 307 }
+127 -126
crates/tranquil-pds/src/api/server/password.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; 3 - use crate::auth::BearerAuth; 4 use crate::state::{AppState, RateLimitKind}; 5 use crate::types::PlainPassword; 6 use crate::validation::validate_password; ··· 227 228 pub async fn change_password( 229 State(state): State<AppState>, 230 - auth: BearerAuth, 231 Json(input): Json<ChangePasswordInput>, 232 - ) -> Response { 233 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 234 - .await 235 { 236 - return crate::api::server::reauth::legacy_mfa_required_response( 237 &*state.user_repo, 238 &*state.session_repo, 239 - &auth.0.did, 240 ) 241 - .await; 242 } 243 244 let current_password = &input.current_password; 245 let new_password = &input.new_password; 246 if current_password.is_empty() { 247 - return ApiError::InvalidRequest("currentPassword is required".into()).into_response(); 248 } 249 if new_password.is_empty() { 250 - return ApiError::InvalidRequest("newPassword is required".into()).into_response(); 251 } 252 if let Err(e) = validate_password(new_password) { 253 - return ApiError::InvalidRequest(e.to_string()).into_response(); 254 } 255 - let user = match state 256 .user_repo 257 - .get_id_and_password_hash_by_did(&auth.0.did) 258 .await 259 - { 260 - Ok(Some(u)) => u, 261 - Ok(None) => { 262 - return ApiError::AccountNotFound.into_response(); 263 - } 264 - Err(e) => { 265 error!("DB error in change_password: {:?}", e); 266 - return ApiError::InternalError(None).into_response(); 267 - } 268 - }; 269 let (user_id, password_hash) = (user.id, user.password_hash); 270 - let valid = match verify(current_password, &password_hash) { 271 - Ok(v) => v, 272 - Err(e) => { 273 - error!("Password verification error: {:?}", e); 274 - return ApiError::InternalError(None).into_response(); 275 - } 276 - }; 277 if !valid { 278 - return ApiError::InvalidPassword("Current password is incorrect".into()).into_response(); 279 } 280 let new_password_clone = new_password.to_string(); 281 - let new_hash = 282 - match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await { 283 - Ok(Ok(h)) => h, 284 - Ok(Err(e)) => { 285 - error!("Failed to hash password: {:?}", e); 286 - return ApiError::InternalError(None).into_response(); 287 - } 288 - Err(e) => { 289 - error!("Failed to spawn blocking task: {:?}", e); 290 - return ApiError::InternalError(None).into_response(); 291 - } 292 - }; 293 - if let Err(e) = state 294 .user_repo 295 .update_password_hash(user_id, &new_hash) 296 .await 297 - { 298 - error!("DB error updating password: {:?}", e); 299 - return ApiError::InternalError(None).into_response(); 300 - } 301 - info!(did = %&auth.0.did, "Password changed successfully"); 302 - EmptyResponse::ok().into_response() 303 } 304 305 - pub async fn get_password_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 306 - match state.user_repo.has_password_by_did(&auth.0.did).await { 307 - Ok(Some(has)) => HasPasswordResponse::response(has).into_response(), 308 - Ok(None) => ApiError::AccountNotFound.into_response(), 309 Err(e) => { 310 error!("DB error: {:?}", e); 311 - ApiError::InternalError(None).into_response() 312 } 313 } 314 } 315 316 - pub async fn remove_password(State(state): State<AppState>, auth: BearerAuth) -> Response { 317 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 318 - .await 319 { 320 - return crate::api::server::reauth::legacy_mfa_required_response( 321 &*state.user_repo, 322 &*state.session_repo, 323 - &auth.0.did, 324 ) 325 - .await; 326 } 327 328 if crate::api::server::reauth::check_reauth_required_cached( 329 &*state.session_repo, 330 &state.cache, 331 - &auth.0.did, 332 ) 333 .await 334 { 335 - return crate::api::server::reauth::reauth_required_response( 336 &*state.user_repo, 337 &*state.session_repo, 338 - &auth.0.did, 339 ) 340 - .await; 341 } 342 343 let has_passkeys = state 344 .user_repo 345 - .has_passkeys(&auth.0.did) 346 .await 347 .unwrap_or(false); 348 if !has_passkeys { 349 - return ApiError::InvalidRequest( 350 "You must have at least one passkey registered before removing your password".into(), 351 - ) 352 - .into_response(); 353 } 354 355 - let user = match state.user_repo.get_password_info_by_did(&auth.0.did).await { 356 - Ok(Some(u)) => u, 357 - Ok(None) => { 358 - return ApiError::AccountNotFound.into_response(); 359 - } 360 - Err(e) => { 361 error!("DB error: {:?}", e); 362 - return ApiError::InternalError(None).into_response(); 363 - } 364 - }; 365 366 if user.password_hash.is_none() { 367 - return ApiError::InvalidRequest("Account already has no password".into()).into_response(); 368 } 369 370 - if let Err(e) = state.user_repo.remove_user_password(user.id).await { 371 - error!("DB error removing password: {:?}", e); 372 - return ApiError::InternalError(None).into_response(); 373 - } 374 375 - info!(did = %&auth.0.did, "Password removed - account is now passkey-only"); 376 - SuccessResponse::ok().into_response() 377 } 378 379 #[derive(Deserialize)] ··· 384 385 pub async fn set_password( 386 State(state): State<AppState>, 387 - auth: BearerAuth, 388 Json(input): Json<SetPasswordInput>, 389 - ) -> Response { 390 let has_password = state 391 .user_repo 392 - .has_password_by_did(&auth.0.did) 393 .await 394 .ok() 395 .flatten() 396 .unwrap_or(false); 397 let has_passkeys = state 398 .user_repo 399 - .has_passkeys(&auth.0.did) 400 .await 401 .unwrap_or(false); 402 let has_totp = state 403 .user_repo 404 - .has_totp_enabled(&auth.0.did) 405 .await 406 .unwrap_or(false); 407 ··· 411 && crate::api::server::reauth::check_reauth_required_cached( 412 &*state.session_repo, 413 &state.cache, 414 - &auth.0.did, 415 ) 416 .await 417 { 418 - return crate::api::server::reauth::reauth_required_response( 419 &*state.user_repo, 420 &*state.session_repo, 421 - &auth.0.did, 422 ) 423 - .await; 424 } 425 426 let new_password = &input.new_password; 427 if new_password.is_empty() { 428 - return ApiError::InvalidRequest("newPassword is required".into()).into_response(); 429 } 430 if let Err(e) = validate_password(new_password) { 431 - return ApiError::InvalidRequest(e.to_string()).into_response(); 432 } 433 434 - let user = match state.user_repo.get_password_info_by_did(&auth.0.did).await { 435 - Ok(Some(u)) => u, 436 - Ok(None) => { 437 - return ApiError::AccountNotFound.into_response(); 438 - } 439 - Err(e) => { 440 error!("DB error: {:?}", e); 441 - return ApiError::InternalError(None).into_response(); 442 - } 443 - }; 444 445 if user.password_hash.is_some() { 446 - return ApiError::InvalidRequest( 447 "Account already has a password. Use changePassword instead.".into(), 448 - ) 449 - .into_response(); 450 } 451 452 let new_password_clone = new_password.to_string(); 453 - let new_hash = 454 - match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await { 455 - Ok(Ok(h)) => h, 456 - Ok(Err(e)) => { 457 - error!("Failed to hash password: {:?}", e); 458 - return ApiError::InternalError(None).into_response(); 459 - } 460 - Err(e) => { 461 - error!("Failed to spawn blocking task: {:?}", e); 462 - return ApiError::InternalError(None).into_response(); 463 - } 464 - }; 465 466 - if let Err(e) = state 467 .user_repo 468 .set_new_user_password(user.id, &new_hash) 469 .await 470 - { 471 - error!("DB error setting password: {:?}", e); 472 - return ApiError::InternalError(None).into_response(); 473 - } 474 475 - info!(did = %&auth.0.did, "Password set for passkey-only account"); 476 - SuccessResponse::ok().into_response() 477 }
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; 3 + use crate::auth::{Active, Auth}; 4 use crate::state::{AppState, RateLimitKind}; 5 use crate::types::PlainPassword; 6 use crate::validation::validate_password; ··· 227 228 pub async fn change_password( 229 State(state): State<AppState>, 230 + auth: Auth<Active>, 231 Json(input): Json<ChangePasswordInput>, 232 + ) -> Result<Response, ApiError> { 233 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 234 { 235 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 236 &*state.user_repo, 237 &*state.session_repo, 238 + &auth.did, 239 ) 240 + .await); 241 } 242 243 let current_password = &input.current_password; 244 let new_password = &input.new_password; 245 if current_password.is_empty() { 246 + return Err(ApiError::InvalidRequest( 247 + "currentPassword is required".into(), 248 + )); 249 } 250 if new_password.is_empty() { 251 + return Err(ApiError::InvalidRequest("newPassword is required".into())); 252 } 253 if let Err(e) = validate_password(new_password) { 254 + return Err(ApiError::InvalidRequest(e.to_string())); 255 } 256 + let user = state 257 .user_repo 258 + .get_id_and_password_hash_by_did(&auth.did) 259 .await 260 + .map_err(|e| { 261 error!("DB error in change_password: {:?}", e); 262 + ApiError::InternalError(None) 263 + })? 264 + .ok_or(ApiError::AccountNotFound)?; 265 + 266 let (user_id, password_hash) = (user.id, user.password_hash); 267 + let valid = verify(current_password, &password_hash).map_err(|e| { 268 + error!("Password verification error: {:?}", e); 269 + ApiError::InternalError(None) 270 + })?; 271 if !valid { 272 + return Err(ApiError::InvalidPassword( 273 + "Current password is incorrect".into(), 274 + )); 275 } 276 let new_password_clone = new_password.to_string(); 277 + let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)) 278 + .await 279 + .map_err(|e| { 280 + error!("Failed to spawn blocking task: {:?}", e); 281 + ApiError::InternalError(None) 282 + })? 283 + .map_err(|e| { 284 + error!("Failed to hash password: {:?}", e); 285 + ApiError::InternalError(None) 286 + })?; 287 + 288 + state 289 .user_repo 290 .update_password_hash(user_id, &new_hash) 291 .await 292 + .map_err(|e| { 293 + error!("DB error updating password: {:?}", e); 294 + ApiError::InternalError(None) 295 + })?; 296 + 297 + info!(did = %&auth.did, "Password changed successfully"); 298 + Ok(EmptyResponse::ok().into_response()) 299 } 300 301 + pub async fn get_password_status( 302 + State(state): State<AppState>, 303 + auth: Auth<Active>, 304 + ) -> Result<Response, ApiError> { 305 + match state.user_repo.has_password_by_did(&auth.did).await { 306 + Ok(Some(has)) => Ok(HasPasswordResponse::response(has).into_response()), 307 + Ok(None) => Err(ApiError::AccountNotFound), 308 Err(e) => { 309 error!("DB error: {:?}", e); 310 + Err(ApiError::InternalError(None)) 311 } 312 } 313 } 314 315 + pub async fn remove_password( 316 + State(state): State<AppState>, 317 + auth: Auth<Active>, 318 + ) -> Result<Response, ApiError> { 319 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 320 { 321 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 322 &*state.user_repo, 323 &*state.session_repo, 324 + &auth.did, 325 ) 326 + .await); 327 } 328 329 if crate::api::server::reauth::check_reauth_required_cached( 330 &*state.session_repo, 331 &state.cache, 332 + &auth.did, 333 ) 334 .await 335 { 336 + return Ok(crate::api::server::reauth::reauth_required_response( 337 &*state.user_repo, 338 &*state.session_repo, 339 + &auth.did, 340 ) 341 + .await); 342 } 343 344 let has_passkeys = state 345 .user_repo 346 + .has_passkeys(&auth.did) 347 .await 348 .unwrap_or(false); 349 if !has_passkeys { 350 + return Err(ApiError::InvalidRequest( 351 "You must have at least one passkey registered before removing your password".into(), 352 + )); 353 } 354 355 + let user = state 356 + .user_repo 357 + .get_password_info_by_did(&auth.did) 358 + .await 359 + .map_err(|e| { 360 error!("DB error: {:?}", e); 361 + ApiError::InternalError(None) 362 + })? 363 + .ok_or(ApiError::AccountNotFound)?; 364 365 if user.password_hash.is_none() { 366 + return Err(ApiError::InvalidRequest( 367 + "Account already has no password".into(), 368 + )); 369 } 370 371 + state 372 + .user_repo 373 + .remove_user_password(user.id) 374 + .await 375 + .map_err(|e| { 376 + error!("DB error removing password: {:?}", e); 377 + ApiError::InternalError(None) 378 + })?; 379 380 + info!(did = %&auth.did, "Password removed - account is now passkey-only"); 381 + Ok(SuccessResponse::ok().into_response()) 382 } 383 384 #[derive(Deserialize)] ··· 389 390 pub async fn set_password( 391 State(state): State<AppState>, 392 + auth: Auth<Active>, 393 Json(input): Json<SetPasswordInput>, 394 + ) -> Result<Response, ApiError> { 395 let has_password = state 396 .user_repo 397 + .has_password_by_did(&auth.did) 398 .await 399 .ok() 400 .flatten() 401 .unwrap_or(false); 402 let has_passkeys = state 403 .user_repo 404 + .has_passkeys(&auth.did) 405 .await 406 .unwrap_or(false); 407 let has_totp = state 408 .user_repo 409 + .has_totp_enabled(&auth.did) 410 .await 411 .unwrap_or(false); 412 ··· 416 && crate::api::server::reauth::check_reauth_required_cached( 417 &*state.session_repo, 418 &state.cache, 419 + &auth.did, 420 ) 421 .await 422 { 423 + return Ok(crate::api::server::reauth::reauth_required_response( 424 &*state.user_repo, 425 &*state.session_repo, 426 + &auth.did, 427 ) 428 + .await); 429 } 430 431 let new_password = &input.new_password; 432 if new_password.is_empty() { 433 + return Err(ApiError::InvalidRequest("newPassword is required".into())); 434 } 435 if let Err(e) = validate_password(new_password) { 436 + return Err(ApiError::InvalidRequest(e.to_string())); 437 } 438 439 + let user = state 440 + .user_repo 441 + .get_password_info_by_did(&auth.did) 442 + .await 443 + .map_err(|e| { 444 error!("DB error: {:?}", e); 445 + ApiError::InternalError(None) 446 + })? 447 + .ok_or(ApiError::AccountNotFound)?; 448 449 if user.password_hash.is_some() { 450 + return Err(ApiError::InvalidRequest( 451 "Account already has a password. Use changePassword instead.".into(), 452 + )); 453 } 454 455 let new_password_clone = new_password.to_string(); 456 + let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)) 457 + .await 458 + .map_err(|e| { 459 + error!("Failed to spawn blocking task: {:?}", e); 460 + ApiError::InternalError(None) 461 + })? 462 + .map_err(|e| { 463 + error!("Failed to hash password: {:?}", e); 464 + ApiError::InternalError(None) 465 + })?; 466 467 + state 468 .user_repo 469 .set_new_user_password(user.id, &new_hash) 470 .await 471 + .map_err(|e| { 472 + error!("DB error setting password: {:?}", e); 473 + ApiError::InternalError(None) 474 + })?; 475 476 + info!(did = %&auth.did, "Password set for passkey-only account"); 477 + Ok(SuccessResponse::ok().into_response()) 478 }
+127 -145
crates/tranquil-pds/src/api/server/reauth.rs
··· 10 use tracing::{error, info, warn}; 11 use tranquil_db_traits::{SessionRepository, UserRepository}; 12 13 - use crate::auth::BearerAuth; 14 use crate::state::{AppState, RateLimitKind}; 15 use crate::types::PlainPassword; 16 ··· 24 pub available_methods: Vec<String>, 25 } 26 27 - pub async fn get_reauth_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 28 - let last_reauth_at = match state.session_repo.get_last_reauth_at(&auth.0.did).await { 29 - Ok(t) => t, 30 - Err(e) => { 31 error!("DB error: {:?}", e); 32 - return ApiError::InternalError(None).into_response(); 33 - } 34 - }; 35 36 let reauth_required = is_reauth_required(last_reauth_at); 37 let available_methods = 38 - get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.0.did).await; 39 40 - Json(ReauthStatusResponse { 41 last_reauth_at, 42 reauth_required, 43 available_methods, 44 }) 45 - .into_response() 46 } 47 48 #[derive(Deserialize)] ··· 59 60 pub async fn reauth_password( 61 State(state): State<AppState>, 62 - auth: BearerAuth, 63 Json(input): Json<PasswordReauthInput>, 64 - ) -> Response { 65 - let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await { 66 - Ok(Some(hash)) => hash, 67 - Ok(None) => { 68 - return ApiError::AccountNotFound.into_response(); 69 - } 70 - Err(e) => { 71 error!("DB error: {:?}", e); 72 - return ApiError::InternalError(None).into_response(); 73 - } 74 - }; 75 76 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 77 78 if !password_valid { 79 let app_password_hashes = state 80 .session_repo 81 - .get_app_password_hashes_by_did(&auth.0.did) 82 .await 83 .unwrap_or_default(); 84 ··· 87 }); 88 89 if !app_password_valid { 90 - warn!(did = %&auth.0.did, "Re-auth failed: invalid password"); 91 - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 92 } 93 } 94 95 - match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await { 96 - Ok(reauthed_at) => { 97 - info!(did = %&auth.0.did, "Re-auth successful via password"); 98 - Json(ReauthResponse { reauthed_at }).into_response() 99 - } 100 - Err(e) => { 101 error!("DB error updating reauth: {:?}", e); 102 - ApiError::InternalError(None).into_response() 103 - } 104 - } 105 } 106 107 #[derive(Deserialize)] ··· 112 113 pub async fn reauth_totp( 114 State(state): State<AppState>, 115 - auth: BearerAuth, 116 Json(input): Json<TotpReauthInput>, 117 - ) -> Response { 118 if !state 119 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 120 .await 121 { 122 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 123 - return ApiError::RateLimitExceeded(Some( 124 "Too many verification attempts. Please try again in a few minutes.".into(), 125 - )) 126 - .into_response(); 127 } 128 129 let valid = 130 - crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.0.did, &input.code) 131 .await; 132 133 if !valid { 134 - warn!(did = %&auth.0.did, "Re-auth failed: invalid TOTP code"); 135 - return ApiError::InvalidCode(Some("Invalid TOTP or backup code".into())).into_response(); 136 } 137 138 - match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await { 139 - Ok(reauthed_at) => { 140 - info!(did = %&auth.0.did, "Re-auth successful via TOTP"); 141 - Json(ReauthResponse { reauthed_at }).into_response() 142 - } 143 - Err(e) => { 144 error!("DB error updating reauth: {:?}", e); 145 - ApiError::InternalError(None).into_response() 146 - } 147 - } 148 } 149 150 #[derive(Serialize)] ··· 153 pub options: serde_json::Value, 154 } 155 156 - pub async fn reauth_passkey_start(State(state): State<AppState>, auth: BearerAuth) -> Response { 157 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 158 159 - let stored_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { 160 - Ok(pks) => pks, 161 - Err(e) => { 162 error!("Failed to get passkeys: {:?}", e); 163 - return ApiError::InternalError(None).into_response(); 164 - } 165 - }; 166 167 if stored_passkeys.is_empty() { 168 - return ApiError::NoPasskeys.into_response(); 169 } 170 171 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys ··· 174 .collect(); 175 176 if passkeys.is_empty() { 177 - return ApiError::InternalError(Some("Failed to load passkeys".into())).into_response(); 178 } 179 180 - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 181 - Ok(w) => w, 182 - Err(e) => { 183 - error!("Failed to create WebAuthn config: {:?}", e); 184 - return ApiError::InternalError(None).into_response(); 185 - } 186 - }; 187 188 - let (rcr, auth_state) = match webauthn.start_authentication(passkeys) { 189 - Ok(result) => result, 190 - Err(e) => { 191 - error!("Failed to start passkey authentication: {:?}", e); 192 - return ApiError::InternalError(None).into_response(); 193 - } 194 - }; 195 196 - let state_json = match serde_json::to_string(&auth_state) { 197 - Ok(s) => s, 198 - Err(e) => { 199 - error!("Failed to serialize authentication state: {:?}", e); 200 - return ApiError::InternalError(None).into_response(); 201 - } 202 - }; 203 204 - if let Err(e) = state 205 .user_repo 206 - .save_webauthn_challenge(&auth.0.did, "authentication", &state_json) 207 .await 208 - { 209 - error!("Failed to save authentication state: {:?}", e); 210 - return ApiError::InternalError(None).into_response(); 211 - } 212 213 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 214 - Json(PasskeyReauthStartResponse { options }).into_response() 215 } 216 217 #[derive(Deserialize)] ··· 222 223 pub async fn reauth_passkey_finish( 224 State(state): State<AppState>, 225 - auth: BearerAuth, 226 Json(input): Json<PasskeyReauthFinishInput>, 227 - ) -> Response { 228 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 229 230 - let auth_state_json = match state 231 .user_repo 232 - .load_webauthn_challenge(&auth.0.did, "authentication") 233 .await 234 - { 235 - Ok(Some(json)) => json, 236 - Ok(None) => { 237 - return ApiError::NoChallengeInProgress.into_response(); 238 - } 239 - Err(e) => { 240 error!("Failed to load authentication state: {:?}", e); 241 - return ApiError::InternalError(None).into_response(); 242 - } 243 - }; 244 245 let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = 246 - match serde_json::from_str(&auth_state_json) { 247 - Ok(s) => s, 248 - Err(e) => { 249 - error!("Failed to deserialize authentication state: {:?}", e); 250 - return ApiError::InternalError(None).into_response(); 251 - } 252 - }; 253 254 let credential: webauthn_rs::prelude::PublicKeyCredential = 255 - match serde_json::from_value(input.credential) { 256 - Ok(c) => c, 257 - Err(e) => { 258 - warn!("Failed to parse credential: {:?}", e); 259 - return ApiError::InvalidCredential.into_response(); 260 - } 261 - }; 262 263 - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 264 - Ok(w) => w, 265 - Err(e) => { 266 - error!("Failed to create WebAuthn config: {:?}", e); 267 - return ApiError::InternalError(None).into_response(); 268 - } 269 - }; 270 271 - let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { 272 - Ok(r) => r, 273 - Err(e) => { 274 - warn!(did = %&auth.0.did, "Passkey re-auth failed: {:?}", e); 275 - return ApiError::AuthenticationFailed(Some("Passkey authentication failed".into())) 276 - .into_response(); 277 - } 278 - }; 279 280 let cred_id_bytes = auth_result.cred_id().as_ref(); 281 match state ··· 284 .await 285 { 286 Ok(false) => { 287 - warn!(did = %&auth.0.did, "Passkey counter anomaly detected - possible cloned key"); 288 let _ = state 289 .user_repo 290 - .delete_webauthn_challenge(&auth.0.did, "authentication") 291 .await; 292 - return ApiError::PasskeyCounterAnomaly.into_response(); 293 } 294 Err(e) => { 295 error!("Failed to update passkey counter: {:?}", e); ··· 299 300 let _ = state 301 .user_repo 302 - .delete_webauthn_challenge(&auth.0.did, "authentication") 303 .await; 304 305 - match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await { 306 - Ok(reauthed_at) => { 307 - info!(did = %&auth.0.did, "Re-auth successful via passkey"); 308 - Json(ReauthResponse { reauthed_at }).into_response() 309 - } 310 - Err(e) => { 311 error!("DB error updating reauth: {:?}", e); 312 - ApiError::InternalError(None).into_response() 313 - } 314 - } 315 } 316 317 pub async fn update_last_reauth_cached(
··· 10 use tracing::{error, info, warn}; 11 use tranquil_db_traits::{SessionRepository, UserRepository}; 12 13 + use crate::auth::{Active, Auth}; 14 use crate::state::{AppState, RateLimitKind}; 15 use crate::types::PlainPassword; 16 ··· 24 pub available_methods: Vec<String>, 25 } 26 27 + pub async fn get_reauth_status( 28 + State(state): State<AppState>, 29 + auth: Auth<Active>, 30 + ) -> Result<Response, ApiError> { 31 + let last_reauth_at = state 32 + .session_repo 33 + .get_last_reauth_at(&auth.did) 34 + .await 35 + .map_err(|e| { 36 error!("DB error: {:?}", e); 37 + ApiError::InternalError(None) 38 + })?; 39 40 let reauth_required = is_reauth_required(last_reauth_at); 41 let available_methods = 42 + get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.did).await; 43 44 + Ok(Json(ReauthStatusResponse { 45 last_reauth_at, 46 reauth_required, 47 available_methods, 48 }) 49 + .into_response()) 50 } 51 52 #[derive(Deserialize)] ··· 63 64 pub async fn reauth_password( 65 State(state): State<AppState>, 66 + auth: Auth<Active>, 67 Json(input): Json<PasswordReauthInput>, 68 + ) -> Result<Response, ApiError> { 69 + let password_hash = state 70 + .user_repo 71 + .get_password_hash_by_did(&auth.did) 72 + .await 73 + .map_err(|e| { 74 error!("DB error: {:?}", e); 75 + ApiError::InternalError(None) 76 + })? 77 + .ok_or(ApiError::AccountNotFound)?; 78 79 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 80 81 if !password_valid { 82 let app_password_hashes = state 83 .session_repo 84 + .get_app_password_hashes_by_did(&auth.did) 85 .await 86 .unwrap_or_default(); 87 ··· 90 }); 91 92 if !app_password_valid { 93 + warn!(did = %&auth.did, "Re-auth failed: invalid password"); 94 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 95 } 96 } 97 98 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 99 + .await 100 + .map_err(|e| { 101 error!("DB error updating reauth: {:?}", e); 102 + ApiError::InternalError(None) 103 + })?; 104 + 105 + info!(did = %&auth.did, "Re-auth successful via password"); 106 + Ok(Json(ReauthResponse { reauthed_at }).into_response()) 107 } 108 109 #[derive(Deserialize)] ··· 114 115 pub async fn reauth_totp( 116 State(state): State<AppState>, 117 + auth: Auth<Active>, 118 Json(input): Json<TotpReauthInput>, 119 + ) -> Result<Response, ApiError> { 120 if !state 121 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 122 .await 123 { 124 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 125 + return Err(ApiError::RateLimitExceeded(Some( 126 "Too many verification attempts. Please try again in a few minutes.".into(), 127 + ))); 128 } 129 130 let valid = 131 + crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code) 132 .await; 133 134 if !valid { 135 + warn!(did = %&auth.did, "Re-auth failed: invalid TOTP code"); 136 + return Err(ApiError::InvalidCode(Some( 137 + "Invalid TOTP or backup code".into(), 138 + ))); 139 } 140 141 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 142 + .await 143 + .map_err(|e| { 144 error!("DB error updating reauth: {:?}", e); 145 + ApiError::InternalError(None) 146 + })?; 147 + 148 + info!(did = %&auth.did, "Re-auth successful via TOTP"); 149 + Ok(Json(ReauthResponse { reauthed_at }).into_response()) 150 } 151 152 #[derive(Serialize)] ··· 155 pub options: serde_json::Value, 156 } 157 158 + pub async fn reauth_passkey_start( 159 + State(state): State<AppState>, 160 + auth: Auth<Active>, 161 + ) -> Result<Response, ApiError> { 162 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 163 164 + let stored_passkeys = state 165 + .user_repo 166 + .get_passkeys_for_user(&auth.did) 167 + .await 168 + .map_err(|e| { 169 error!("Failed to get passkeys: {:?}", e); 170 + ApiError::InternalError(None) 171 + })?; 172 173 if stored_passkeys.is_empty() { 174 + return Err(ApiError::NoPasskeys); 175 } 176 177 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys ··· 180 .collect(); 181 182 if passkeys.is_empty() { 183 + return Err(ApiError::InternalError(Some( 184 + "Failed to load passkeys".into(), 185 + ))); 186 } 187 188 + let webauthn = crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname).map_err(|e| { 189 + error!("Failed to create WebAuthn config: {:?}", e); 190 + ApiError::InternalError(None) 191 + })?; 192 193 + let (rcr, auth_state) = webauthn.start_authentication(passkeys).map_err(|e| { 194 + error!("Failed to start passkey authentication: {:?}", e); 195 + ApiError::InternalError(None) 196 + })?; 197 198 + let state_json = serde_json::to_string(&auth_state).map_err(|e| { 199 + error!("Failed to serialize authentication state: {:?}", e); 200 + ApiError::InternalError(None) 201 + })?; 202 203 + state 204 .user_repo 205 + .save_webauthn_challenge(&auth.did, "authentication", &state_json) 206 .await 207 + .map_err(|e| { 208 + error!("Failed to save authentication state: {:?}", e); 209 + ApiError::InternalError(None) 210 + })?; 211 212 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 213 + Ok(Json(PasskeyReauthStartResponse { options }).into_response()) 214 } 215 216 #[derive(Deserialize)] ··· 221 222 pub async fn reauth_passkey_finish( 223 State(state): State<AppState>, 224 + auth: Auth<Active>, 225 Json(input): Json<PasskeyReauthFinishInput>, 226 + ) -> Result<Response, ApiError> { 227 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 228 229 + let auth_state_json = state 230 .user_repo 231 + .load_webauthn_challenge(&auth.did, "authentication") 232 .await 233 + .map_err(|e| { 234 error!("Failed to load authentication state: {:?}", e); 235 + ApiError::InternalError(None) 236 + })? 237 + .ok_or(ApiError::NoChallengeInProgress)?; 238 239 let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = 240 + serde_json::from_str(&auth_state_json).map_err(|e| { 241 + error!("Failed to deserialize authentication state: {:?}", e); 242 + ApiError::InternalError(None) 243 + })?; 244 245 let credential: webauthn_rs::prelude::PublicKeyCredential = 246 + serde_json::from_value(input.credential).map_err(|e| { 247 + warn!("Failed to parse credential: {:?}", e); 248 + ApiError::InvalidCredential 249 + })?; 250 251 + let webauthn = crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname).map_err(|e| { 252 + error!("Failed to create WebAuthn config: {:?}", e); 253 + ApiError::InternalError(None) 254 + })?; 255 256 + let auth_result = webauthn 257 + .finish_authentication(&credential, &auth_state) 258 + .map_err(|e| { 259 + warn!(did = %&auth.did, "Passkey re-auth failed: {:?}", e); 260 + ApiError::AuthenticationFailed(Some("Passkey authentication failed".into())) 261 + })?; 262 263 let cred_id_bytes = auth_result.cred_id().as_ref(); 264 match state ··· 267 .await 268 { 269 Ok(false) => { 270 + warn!(did = %&auth.did, "Passkey counter anomaly detected - possible cloned key"); 271 let _ = state 272 .user_repo 273 + .delete_webauthn_challenge(&auth.did, "authentication") 274 .await; 275 + return Err(ApiError::PasskeyCounterAnomaly); 276 } 277 Err(e) => { 278 error!("Failed to update passkey counter: {:?}", e); ··· 282 283 let _ = state 284 .user_repo 285 + .delete_webauthn_challenge(&auth.did, "authentication") 286 .await; 287 288 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 289 + .await 290 + .map_err(|e| { 291 error!("DB error updating reauth: {:?}", e); 292 + ApiError::InternalError(None) 293 + })?; 294 + 295 + info!(did = %&auth.did, "Re-auth successful via passkey"); 296 + Ok(Json(ReauthResponse { reauthed_at }).into_response()) 297 } 298 299 pub async fn update_last_reauth_cached(
+4 -4
crates/tranquil-pds/src/api/server/service_auth.rs
··· 95 { 96 Ok(result) => crate::auth::AuthenticatedUser { 97 did: Did::new_unchecked(result.did), 98 - is_oauth: true, 99 is_admin: false, 100 status: AccountStatus::Active, 101 scope: result.scope, 102 key_bytes: None, 103 controller_did: None, 104 }, 105 Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => { 106 return ( ··· 131 }; 132 info!( 133 did = %&auth_user.did, 134 - is_oauth = auth_user.is_oauth, 135 has_key = auth_user.key_bytes.is_some(), 136 "getServiceAuth auth validated" 137 ); ··· 180 181 if let Some(method) = lxm { 182 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 183 - auth_user.is_oauth, 184 auth_user.scope.as_deref(), 185 &params.aud, 186 method, 187 ) { 188 return e; 189 } 190 - } else if auth_user.is_oauth { 191 let permissions = auth_user.permissions(); 192 if !permissions.has_full_access() { 193 return ApiError::InvalidRequest(
··· 95 { 96 Ok(result) => crate::auth::AuthenticatedUser { 97 did: Did::new_unchecked(result.did), 98 is_admin: false, 99 status: AccountStatus::Active, 100 scope: result.scope, 101 key_bytes: None, 102 controller_did: None, 103 + auth_source: crate::auth::AuthSource::OAuth, 104 }, 105 Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => { 106 return ( ··· 131 }; 132 info!( 133 did = %&auth_user.did, 134 + is_oauth = auth_user.is_oauth(), 135 has_key = auth_user.key_bytes.is_some(), 136 "getServiceAuth auth validated" 137 ); ··· 180 181 if let Some(method) = lxm { 182 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 183 + auth_user.is_oauth(), 184 auth_user.scope.as_deref(), 185 &params.aud, 186 method, 187 ) { 188 return e; 189 } 190 + } else if auth_user.is_oauth() { 191 let permissions = auth_user.permissions(); 192 if !permissions.has_full_access() { 193 return ApiError::InvalidRequest(
+169 -174
crates/tranquil-pds/src/api/server/session.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, SuccessResponse}; 3 - use crate::auth::{BearerAuth, BearerAuthAllowDeactivated}; 4 use crate::state::{AppState, RateLimitKind}; 5 use crate::types::{AccountState, Did, Handle, PlainPassword}; 6 use axum::{ ··· 279 280 pub async fn get_session( 281 State(state): State<AppState>, 282 - BearerAuthAllowDeactivated(auth_user): BearerAuthAllowDeactivated, 283 - ) -> Response { 284 - let permissions = auth_user.permissions(); 285 let can_read_email = permissions.allows_email_read(); 286 287 - let did_for_doc = auth_user.did.clone(); 288 let did_resolver = state.did_resolver.clone(); 289 let (db_result, did_doc) = tokio::join!( 290 - state.user_repo.get_session_info_by_did(&auth_user.did), 291 did_resolver.resolve_did_document(&did_for_doc) 292 ); 293 match db_result { ··· 316 let email_confirmed_value = can_read_email && row.email_verified; 317 let mut response = json!({ 318 "handle": handle, 319 - "did": &auth_user.did, 320 "active": account_state.is_active(), 321 "preferredChannel": preferred_channel, 322 "preferredChannelVerified": preferred_channel_verified, ··· 337 if let Some(doc) = did_doc { 338 response["didDoc"] = doc; 339 } 340 - Json(response).into_response() 341 } 342 - Ok(None) => ApiError::AuthenticationFailed(None).into_response(), 343 Err(e) => { 344 error!("Database error in get_session: {:?}", e); 345 - ApiError::InternalError(None).into_response() 346 } 347 } 348 } ··· 350 pub async fn delete_session( 351 State(state): State<AppState>, 352 headers: axum::http::HeaderMap, 353 - _auth: BearerAuth, 354 - ) -> Response { 355 - let extracted = match crate::auth::extract_auth_token_from_header( 356 headers.get("Authorization").and_then(|h| h.to_str().ok()), 357 - ) { 358 - Some(t) => t, 359 - None => return ApiError::AuthenticationRequired.into_response(), 360 - }; 361 - let jti = match crate::auth::get_jti_from_token(&extracted.token) { 362 - Ok(jti) => jti, 363 - Err(_) => return ApiError::AuthenticationFailed(None).into_response(), 364 - }; 365 let did = crate::auth::get_did_from_token(&extracted.token).ok(); 366 match state.session_repo.delete_session_by_access_jti(&jti).await { 367 Ok(rows) if rows > 0 => { ··· 369 let session_cache_key = format!("auth:session:{}:{}", did, jti); 370 let _ = state.cache.delete(&session_cache_key).await; 371 } 372 - EmptyResponse::ok().into_response() 373 } 374 - Ok(_) => ApiError::AuthenticationFailed(None).into_response(), 375 - Err(_) => ApiError::AuthenticationFailed(None).into_response(), 376 } 377 } 378 ··· 796 pub async fn list_sessions( 797 State(state): State<AppState>, 798 headers: HeaderMap, 799 - auth: BearerAuth, 800 - ) -> Response { 801 let current_jti = headers 802 .get("authorization") 803 .and_then(|v| v.to_str().ok()) 804 .and_then(|v| v.strip_prefix("Bearer ")) 805 .and_then(|token| crate::auth::get_jti_from_token(token).ok()); 806 807 - let jwt_rows = match state.session_repo.list_sessions_by_did(&auth.0.did).await { 808 - Ok(rows) => rows, 809 - Err(e) => { 810 error!("DB error fetching JWT sessions: {:?}", e); 811 - return ApiError::InternalError(None).into_response(); 812 - } 813 - }; 814 815 - let oauth_rows = match state.oauth_repo.list_sessions_by_did(&auth.0.did).await { 816 - Ok(rows) => rows, 817 - Err(e) => { 818 error!("DB error fetching OAuth sessions: {:?}", e); 819 - return ApiError::InternalError(None).into_response(); 820 - } 821 - }; 822 823 let jwt_sessions = jwt_rows.into_iter().map(|row| SessionInfo { 824 id: format!("jwt:{}", row.id), ··· 829 is_current: current_jti.as_ref() == Some(&row.access_jti), 830 }); 831 832 - let is_oauth = auth.0.is_oauth; 833 let oauth_sessions = oauth_rows.into_iter().map(|row| { 834 let client_name = extract_client_name(&row.client_id); 835 let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str()); ··· 846 let mut sessions: Vec<SessionInfo> = jwt_sessions.chain(oauth_sessions).collect(); 847 sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); 848 849 - (StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response() 850 } 851 852 fn extract_client_name(client_id: &str) -> String { ··· 867 868 pub async fn revoke_session( 869 State(state): State<AppState>, 870 - auth: BearerAuth, 871 Json(input): Json<RevokeSessionInput>, 872 - ) -> Response { 873 if let Some(jwt_id) = input.session_id.strip_prefix("jwt:") { 874 - let Ok(session_id) = jwt_id.parse::<i32>() else { 875 - return ApiError::InvalidRequest("Invalid session ID".into()).into_response(); 876 - }; 877 - let access_jti = match state 878 .session_repo 879 - .get_session_access_jti_by_id(session_id, &auth.0.did) 880 .await 881 - { 882 - Ok(Some(jti)) => jti, 883 - Ok(None) => { 884 - return ApiError::SessionNotFound.into_response(); 885 - } 886 - Err(e) => { 887 error!("DB error in revoke_session: {:?}", e); 888 - return ApiError::InternalError(None).into_response(); 889 - } 890 - }; 891 - if let Err(e) = state.session_repo.delete_session_by_id(session_id).await { 892 - error!("DB error deleting session: {:?}", e); 893 - return ApiError::InternalError(None).into_response(); 894 - } 895 - let cache_key = format!("auth:session:{}:{}", &auth.0.did, access_jti); 896 if let Err(e) = state.cache.delete(&cache_key).await { 897 warn!("Failed to invalidate session cache: {:?}", e); 898 } 899 - info!(did = %&auth.0.did, session_id = %session_id, "JWT session revoked"); 900 } else if let Some(oauth_id) = input.session_id.strip_prefix("oauth:") { 901 - let Ok(session_id) = oauth_id.parse::<i32>() else { 902 - return ApiError::InvalidRequest("Invalid session ID".into()).into_response(); 903 - }; 904 - match state 905 .oauth_repo 906 - .delete_session_by_id(session_id, &auth.0.did) 907 .await 908 - { 909 - Ok(0) => { 910 - return ApiError::SessionNotFound.into_response(); 911 - } 912 - Err(e) => { 913 error!("DB error deleting OAuth session: {:?}", e); 914 - return ApiError::InternalError(None).into_response(); 915 - } 916 - _ => {} 917 } 918 - info!(did = %&auth.0.did, session_id = %session_id, "OAuth session revoked"); 919 } else { 920 - return ApiError::InvalidRequest("Invalid session ID format".into()).into_response(); 921 } 922 - EmptyResponse::ok().into_response() 923 } 924 925 pub async fn revoke_all_sessions( 926 State(state): State<AppState>, 927 headers: HeaderMap, 928 - auth: BearerAuth, 929 - ) -> Response { 930 - let current_jti = crate::auth::extract_auth_token_from_header( 931 headers.get("authorization").and_then(|v| v.to_str().ok()), 932 ) 933 - .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()); 934 935 - let Some(ref jti) = current_jti else { 936 - return ApiError::InvalidToken(None).into_response(); 937 - }; 938 - 939 - if auth.0.is_oauth { 940 - if let Err(e) = state.session_repo.delete_sessions_by_did(&auth.0.did).await { 941 - error!("DB error revoking JWT sessions: {:?}", e); 942 - return ApiError::InternalError(None).into_response(); 943 - } 944 let jti_typed = TokenId::from(jti.clone()); 945 - if let Err(e) = state 946 .oauth_repo 947 - .delete_sessions_by_did_except(&auth.0.did, &jti_typed) 948 .await 949 - { 950 - error!("DB error revoking OAuth sessions: {:?}", e); 951 - return ApiError::InternalError(None).into_response(); 952 - } 953 } else { 954 - if let Err(e) = state 955 .session_repo 956 - .delete_sessions_by_did_except_jti(&auth.0.did, jti) 957 .await 958 - { 959 - error!("DB error revoking JWT sessions: {:?}", e); 960 - return ApiError::InternalError(None).into_response(); 961 - } 962 - if let Err(e) = state.oauth_repo.delete_sessions_by_did(&auth.0.did).await { 963 - error!("DB error revoking OAuth sessions: {:?}", e); 964 - return ApiError::InternalError(None).into_response(); 965 - } 966 } 967 968 - info!(did = %&auth.0.did, "All other sessions revoked"); 969 - SuccessResponse::ok().into_response() 970 } 971 972 #[derive(Serialize)] ··· 978 979 pub async fn get_legacy_login_preference( 980 State(state): State<AppState>, 981 - auth: BearerAuth, 982 - ) -> Response { 983 - match state.user_repo.get_legacy_login_pref(&auth.0.did).await { 984 - Ok(Some(pref)) => Json(LegacyLoginPreferenceOutput { 985 - allow_legacy_login: pref.allow_legacy_login, 986 - has_mfa: pref.has_mfa, 987 - }) 988 - .into_response(), 989 - Ok(None) => ApiError::AccountNotFound.into_response(), 990 - Err(e) => { 991 error!("DB error: {:?}", e); 992 - ApiError::InternalError(None).into_response() 993 - } 994 - } 995 } 996 997 #[derive(Deserialize)] ··· 1002 1003 pub async fn update_legacy_login_preference( 1004 State(state): State<AppState>, 1005 - auth: BearerAuth, 1006 Json(input): Json<UpdateLegacyLoginInput>, 1007 - ) -> Response { 1008 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 1009 - .await 1010 { 1011 - return crate::api::server::reauth::legacy_mfa_required_response( 1012 &*state.user_repo, 1013 &*state.session_repo, 1014 - &auth.0.did, 1015 ) 1016 - .await; 1017 } 1018 1019 - if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await { 1020 - return crate::api::server::reauth::reauth_required_response( 1021 &*state.user_repo, 1022 &*state.session_repo, 1023 - &auth.0.did, 1024 ) 1025 - .await; 1026 } 1027 1028 - match state 1029 .user_repo 1030 - .update_legacy_login(&auth.0.did, input.allow_legacy_login) 1031 .await 1032 - { 1033 - Ok(true) => { 1034 - info!( 1035 - did = %&auth.0.did, 1036 - allow_legacy_login = input.allow_legacy_login, 1037 - "Legacy login preference updated" 1038 - ); 1039 - Json(json!({ 1040 - "allowLegacyLogin": input.allow_legacy_login 1041 - })) 1042 - .into_response() 1043 - } 1044 - Ok(false) => ApiError::AccountNotFound.into_response(), 1045 - Err(e) => { 1046 error!("DB error: {:?}", e); 1047 - ApiError::InternalError(None).into_response() 1048 - } 1049 } 1050 } 1051 1052 use crate::comms::VALID_LOCALES; ··· 1059 1060 pub async fn update_locale( 1061 State(state): State<AppState>, 1062 - auth: BearerAuth, 1063 Json(input): Json<UpdateLocaleInput>, 1064 - ) -> Response { 1065 if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) { 1066 - return ApiError::InvalidRequest(format!( 1067 "Invalid locale. Valid options: {}", 1068 VALID_LOCALES.join(", ") 1069 - )) 1070 - .into_response(); 1071 } 1072 1073 - match state 1074 .user_repo 1075 - .update_locale(&auth.0.did, &input.preferred_locale) 1076 .await 1077 - { 1078 - Ok(true) => { 1079 - info!( 1080 - did = %&auth.0.did, 1081 - locale = %input.preferred_locale, 1082 - "User locale preference updated" 1083 - ); 1084 - Json(json!({ 1085 - "preferredLocale": input.preferred_locale 1086 - })) 1087 - .into_response() 1088 - } 1089 - Ok(false) => ApiError::AccountNotFound.into_response(), 1090 - Err(e) => { 1091 error!("DB error updating locale: {:?}", e); 1092 - ApiError::InternalError(None).into_response() 1093 - } 1094 } 1095 }
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, SuccessResponse}; 3 + use crate::auth::{Active, Auth, NotTakendown}; 4 use crate::state::{AppState, RateLimitKind}; 5 use crate::types::{AccountState, Did, Handle, PlainPassword}; 6 use axum::{ ··· 279 280 pub async fn get_session( 281 State(state): State<AppState>, 282 + auth: Auth<NotTakendown>, 283 + ) -> Result<Response, ApiError> { 284 + let permissions = auth.permissions(); 285 let can_read_email = permissions.allows_email_read(); 286 287 + let did_for_doc = auth.did.clone(); 288 let did_resolver = state.did_resolver.clone(); 289 let (db_result, did_doc) = tokio::join!( 290 + state.user_repo.get_session_info_by_did(&auth.did), 291 did_resolver.resolve_did_document(&did_for_doc) 292 ); 293 match db_result { ··· 316 let email_confirmed_value = can_read_email && row.email_verified; 317 let mut response = json!({ 318 "handle": handle, 319 + "did": &auth.did, 320 "active": account_state.is_active(), 321 "preferredChannel": preferred_channel, 322 "preferredChannelVerified": preferred_channel_verified, ··· 337 if let Some(doc) = did_doc { 338 response["didDoc"] = doc; 339 } 340 + Ok(Json(response).into_response()) 341 } 342 + Ok(None) => Err(ApiError::AuthenticationFailed(None)), 343 Err(e) => { 344 error!("Database error in get_session: {:?}", e); 345 + Err(ApiError::InternalError(None)) 346 } 347 } 348 } ··· 350 pub async fn delete_session( 351 State(state): State<AppState>, 352 headers: axum::http::HeaderMap, 353 + _auth: Auth<Active>, 354 + ) -> Result<Response, ApiError> { 355 + let extracted = crate::auth::extract_auth_token_from_header( 356 headers.get("Authorization").and_then(|h| h.to_str().ok()), 357 + ) 358 + .ok_or(ApiError::AuthenticationRequired)?; 359 + let jti = crate::auth::get_jti_from_token(&extracted.token) 360 + .map_err(|_| ApiError::AuthenticationFailed(None))?; 361 let did = crate::auth::get_did_from_token(&extracted.token).ok(); 362 match state.session_repo.delete_session_by_access_jti(&jti).await { 363 Ok(rows) if rows > 0 => { ··· 365 let session_cache_key = format!("auth:session:{}:{}", did, jti); 366 let _ = state.cache.delete(&session_cache_key).await; 367 } 368 + Ok(EmptyResponse::ok().into_response()) 369 } 370 + Ok(_) => Err(ApiError::AuthenticationFailed(None)), 371 + Err(_) => Err(ApiError::AuthenticationFailed(None)), 372 } 373 } 374 ··· 792 pub async fn list_sessions( 793 State(state): State<AppState>, 794 headers: HeaderMap, 795 + auth: Auth<Active>, 796 + ) -> Result<Response, ApiError> { 797 let current_jti = headers 798 .get("authorization") 799 .and_then(|v| v.to_str().ok()) 800 .and_then(|v| v.strip_prefix("Bearer ")) 801 .and_then(|token| crate::auth::get_jti_from_token(token).ok()); 802 803 + let jwt_rows = state 804 + .session_repo 805 + .list_sessions_by_did(&auth.did) 806 + .await 807 + .map_err(|e| { 808 error!("DB error fetching JWT sessions: {:?}", e); 809 + ApiError::InternalError(None) 810 + })?; 811 812 + let oauth_rows = state 813 + .oauth_repo 814 + .list_sessions_by_did(&auth.did) 815 + .await 816 + .map_err(|e| { 817 error!("DB error fetching OAuth sessions: {:?}", e); 818 + ApiError::InternalError(None) 819 + })?; 820 821 let jwt_sessions = jwt_rows.into_iter().map(|row| SessionInfo { 822 id: format!("jwt:{}", row.id), ··· 827 is_current: current_jti.as_ref() == Some(&row.access_jti), 828 }); 829 830 + let is_oauth = auth.is_oauth(); 831 let oauth_sessions = oauth_rows.into_iter().map(|row| { 832 let client_name = extract_client_name(&row.client_id); 833 let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str()); ··· 844 let mut sessions: Vec<SessionInfo> = jwt_sessions.chain(oauth_sessions).collect(); 845 sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); 846 847 + Ok((StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response()) 848 } 849 850 fn extract_client_name(client_id: &str) -> String { ··· 865 866 pub async fn revoke_session( 867 State(state): State<AppState>, 868 + auth: Auth<Active>, 869 Json(input): Json<RevokeSessionInput>, 870 + ) -> Result<Response, ApiError> { 871 if let Some(jwt_id) = input.session_id.strip_prefix("jwt:") { 872 + let session_id: i32 = jwt_id 873 + .parse() 874 + .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; 875 + let access_jti = state 876 .session_repo 877 + .get_session_access_jti_by_id(session_id, &auth.did) 878 .await 879 + .map_err(|e| { 880 error!("DB error in revoke_session: {:?}", e); 881 + ApiError::InternalError(None) 882 + })? 883 + .ok_or(ApiError::SessionNotFound)?; 884 + state 885 + .session_repo 886 + .delete_session_by_id(session_id) 887 + .await 888 + .map_err(|e| { 889 + error!("DB error deleting session: {:?}", e); 890 + ApiError::InternalError(None) 891 + })?; 892 + let cache_key = format!("auth:session:{}:{}", &auth.did, access_jti); 893 if let Err(e) = state.cache.delete(&cache_key).await { 894 warn!("Failed to invalidate session cache: {:?}", e); 895 } 896 + info!(did = %&auth.did, session_id = %session_id, "JWT session revoked"); 897 } else if let Some(oauth_id) = input.session_id.strip_prefix("oauth:") { 898 + let session_id: i32 = oauth_id 899 + .parse() 900 + .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; 901 + let deleted = state 902 .oauth_repo 903 + .delete_session_by_id(session_id, &auth.did) 904 .await 905 + .map_err(|e| { 906 error!("DB error deleting OAuth session: {:?}", e); 907 + ApiError::InternalError(None) 908 + })?; 909 + if deleted == 0 { 910 + return Err(ApiError::SessionNotFound); 911 } 912 + info!(did = %&auth.did, session_id = %session_id, "OAuth session revoked"); 913 } else { 914 + return Err(ApiError::InvalidRequest("Invalid session ID format".into())); 915 } 916 + Ok(EmptyResponse::ok().into_response()) 917 } 918 919 pub async fn revoke_all_sessions( 920 State(state): State<AppState>, 921 headers: HeaderMap, 922 + auth: Auth<Active>, 923 + ) -> Result<Response, ApiError> { 924 + let jti = crate::auth::extract_auth_token_from_header( 925 headers.get("authorization").and_then(|v| v.to_str().ok()), 926 ) 927 + .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()) 928 + .ok_or(ApiError::InvalidToken(None))?; 929 930 + if auth.is_oauth() { 931 + state 932 + .session_repo 933 + .delete_sessions_by_did(&auth.did) 934 + .await 935 + .map_err(|e| { 936 + error!("DB error revoking JWT sessions: {:?}", e); 937 + ApiError::InternalError(None) 938 + })?; 939 let jti_typed = TokenId::from(jti.clone()); 940 + state 941 .oauth_repo 942 + .delete_sessions_by_did_except(&auth.did, &jti_typed) 943 .await 944 + .map_err(|e| { 945 + error!("DB error revoking OAuth sessions: {:?}", e); 946 + ApiError::InternalError(None) 947 + })?; 948 } else { 949 + state 950 .session_repo 951 + .delete_sessions_by_did_except_jti(&auth.did, &jti) 952 + .await 953 + .map_err(|e| { 954 + error!("DB error revoking JWT sessions: {:?}", e); 955 + ApiError::InternalError(None) 956 + })?; 957 + state 958 + .oauth_repo 959 + .delete_sessions_by_did(&auth.did) 960 .await 961 + .map_err(|e| { 962 + error!("DB error revoking OAuth sessions: {:?}", e); 963 + ApiError::InternalError(None) 964 + })?; 965 } 966 967 + info!(did = %&auth.did, "All other sessions revoked"); 968 + Ok(SuccessResponse::ok().into_response()) 969 } 970 971 #[derive(Serialize)] ··· 977 978 pub async fn get_legacy_login_preference( 979 State(state): State<AppState>, 980 + auth: Auth<Active>, 981 + ) -> Result<Response, ApiError> { 982 + let pref = state 983 + .user_repo 984 + .get_legacy_login_pref(&auth.did) 985 + .await 986 + .map_err(|e| { 987 error!("DB error: {:?}", e); 988 + ApiError::InternalError(None) 989 + })? 990 + .ok_or(ApiError::AccountNotFound)?; 991 + Ok(Json(LegacyLoginPreferenceOutput { 992 + allow_legacy_login: pref.allow_legacy_login, 993 + has_mfa: pref.has_mfa, 994 + }) 995 + .into_response()) 996 } 997 998 #[derive(Deserialize)] ··· 1003 1004 pub async fn update_legacy_login_preference( 1005 State(state): State<AppState>, 1006 + auth: Auth<Active>, 1007 Json(input): Json<UpdateLegacyLoginInput>, 1008 + ) -> Result<Response, ApiError> { 1009 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 1010 { 1011 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 1012 &*state.user_repo, 1013 &*state.session_repo, 1014 + &auth.did, 1015 ) 1016 + .await); 1017 } 1018 1019 + if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.did).await { 1020 + return Ok(crate::api::server::reauth::reauth_required_response( 1021 &*state.user_repo, 1022 &*state.session_repo, 1023 + &auth.did, 1024 ) 1025 + .await); 1026 } 1027 1028 + let updated = state 1029 .user_repo 1030 + .update_legacy_login(&auth.did, input.allow_legacy_login) 1031 .await 1032 + .map_err(|e| { 1033 error!("DB error: {:?}", e); 1034 + ApiError::InternalError(None) 1035 + })?; 1036 + if !updated { 1037 + return Err(ApiError::AccountNotFound); 1038 } 1039 + info!( 1040 + did = %&auth.did, 1041 + allow_legacy_login = input.allow_legacy_login, 1042 + "Legacy login preference updated" 1043 + ); 1044 + Ok(Json(json!({ 1045 + "allowLegacyLogin": input.allow_legacy_login 1046 + })) 1047 + .into_response()) 1048 } 1049 1050 use crate::comms::VALID_LOCALES; ··· 1057 1058 pub async fn update_locale( 1059 State(state): State<AppState>, 1060 + auth: Auth<Active>, 1061 Json(input): Json<UpdateLocaleInput>, 1062 + ) -> Result<Response, ApiError> { 1063 if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) { 1064 + return Err(ApiError::InvalidRequest(format!( 1065 "Invalid locale. Valid options: {}", 1066 VALID_LOCALES.join(", ") 1067 + ))); 1068 } 1069 1070 + let updated = state 1071 .user_repo 1072 + .update_locale(&auth.did, &input.preferred_locale) 1073 .await 1074 + .map_err(|e| { 1075 error!("DB error updating locale: {:?}", e); 1076 + ApiError::InternalError(None) 1077 + })?; 1078 + if !updated { 1079 + return Err(ApiError::AccountNotFound); 1080 } 1081 + info!( 1082 + did = %&auth.did, 1083 + locale = %input.preferred_locale, 1084 + "User locale preference updated" 1085 + ); 1086 + Ok(Json(json!({ 1087 + "preferredLocale": input.preferred_locale 1088 + })) 1089 + .into_response()) 1090 }
+161 -161
crates/tranquil-pds/src/api/server/totp.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuth; 4 use crate::auth::{ 5 decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, 6 generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, ··· 26 pub qr_base64: String, 27 } 28 29 - pub async fn create_totp_secret(State(state): State<AppState>, auth: BearerAuth) -> Response { 30 - match state.user_repo.get_totp_record(&auth.0.did).await { 31 - Ok(Some(record)) if record.verified => return ApiError::TotpAlreadyEnabled.into_response(), 32 Ok(_) => {} 33 Err(e) => { 34 error!("DB error checking TOTP: {:?}", e); 35 - return ApiError::InternalError(None).into_response(); 36 } 37 } 38 39 let secret = generate_totp_secret(); 40 41 - let handle = match state.user_repo.get_handle_by_did(&auth.0.did).await { 42 - Ok(Some(h)) => h, 43 - Ok(None) => return ApiError::AccountNotFound.into_response(), 44 - Err(e) => { 45 error!("DB error fetching handle: {:?}", e); 46 - return ApiError::InternalError(None).into_response(); 47 - } 48 - }; 49 50 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 51 let uri = generate_totp_uri(&secret, &handle, &hostname); 52 53 - let qr_code = match generate_qr_png_base64(&secret, &handle, &hostname) { 54 - Ok(qr) => qr, 55 - Err(e) => { 56 - error!("Failed to generate QR code: {:?}", e); 57 - return ApiError::InternalError(Some("Failed to generate QR code".into())) 58 - .into_response(); 59 - } 60 - }; 61 62 - let encrypted_secret = match encrypt_totp_secret(&secret) { 63 - Ok(enc) => enc, 64 - Err(e) => { 65 - error!("Failed to encrypt TOTP secret: {:?}", e); 66 - return ApiError::InternalError(None).into_response(); 67 - } 68 - }; 69 70 - if let Err(e) = state 71 .user_repo 72 - .upsert_totp_secret(&auth.0.did, &encrypted_secret, ENCRYPTION_VERSION) 73 .await 74 - { 75 - error!("Failed to store TOTP secret: {:?}", e); 76 - return ApiError::InternalError(None).into_response(); 77 - } 78 79 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 80 81 - info!(did = %&auth.0.did, "TOTP secret created (pending verification)"); 82 83 - Json(CreateTotpSecretResponse { 84 secret: secret_base32, 85 uri, 86 qr_base64: qr_code, 87 }) 88 - .into_response() 89 } 90 91 #[derive(Deserialize)] ··· 101 102 pub async fn enable_totp( 103 State(state): State<AppState>, 104 - auth: BearerAuth, 105 Json(input): Json<EnableTotpInput>, 106 - ) -> Response { 107 if !state 108 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 109 .await 110 { 111 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 112 - return ApiError::RateLimitExceeded(None).into_response(); 113 } 114 115 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 116 Ok(Some(row)) => row, 117 - Ok(None) => return ApiError::TotpNotEnabled.into_response(), 118 Err(e) => { 119 error!("DB error fetching TOTP: {:?}", e); 120 - return ApiError::InternalError(None).into_response(); 121 } 122 }; 123 124 if totp_record.verified { 125 - return ApiError::TotpAlreadyEnabled.into_response(); 126 } 127 128 - let secret = match decrypt_totp_secret( 129 &totp_record.secret_encrypted, 130 totp_record.encryption_version, 131 - ) { 132 - Ok(s) => s, 133 - Err(e) => { 134 - error!("Failed to decrypt TOTP secret: {:?}", e); 135 - return ApiError::InternalError(None).into_response(); 136 - } 137 - }; 138 139 let code = input.code.trim(); 140 if !verify_totp_code(&secret, code) { 141 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 142 } 143 144 let backup_codes = generate_backup_codes(); 145 - let backup_hashes: Result<Vec<_>, _> = 146 - backup_codes.iter().map(|c| hash_backup_code(c)).collect(); 147 - let backup_hashes = match backup_hashes { 148 - Ok(hashes) => hashes, 149 - Err(e) => { 150 error!("Failed to hash backup code: {:?}", e); 151 - return ApiError::InternalError(None).into_response(); 152 - } 153 - }; 154 155 - if let Err(e) = state 156 .user_repo 157 - .enable_totp_with_backup_codes(&auth.0.did, &backup_hashes) 158 .await 159 - { 160 - error!("Failed to enable TOTP: {:?}", e); 161 - return ApiError::InternalError(None).into_response(); 162 - } 163 164 - info!(did = %&auth.0.did, "TOTP enabled with {} backup codes", backup_codes.len()); 165 166 - Json(EnableTotpResponse { backup_codes }).into_response() 167 } 168 169 #[derive(Deserialize)] ··· 174 175 pub async fn disable_totp( 176 State(state): State<AppState>, 177 - auth: BearerAuth, 178 Json(input): Json<DisableTotpInput>, 179 - ) -> Response { 180 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 181 - .await 182 { 183 - return crate::api::server::reauth::legacy_mfa_required_response( 184 &*state.user_repo, 185 &*state.session_repo, 186 - &auth.0.did, 187 ) 188 - .await; 189 } 190 191 if !state 192 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 193 .await 194 { 195 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 196 - return ApiError::RateLimitExceeded(None).into_response(); 197 } 198 199 - let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await { 200 - Ok(Some(hash)) => hash, 201 - Ok(None) => return ApiError::AccountNotFound.into_response(), 202 - Err(e) => { 203 error!("DB error fetching user: {:?}", e); 204 - return ApiError::InternalError(None).into_response(); 205 - } 206 - }; 207 208 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 209 if !password_valid { 210 - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 211 } 212 213 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 214 Ok(Some(row)) if row.verified => row, 215 - Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 216 Err(e) => { 217 error!("DB error fetching TOTP: {:?}", e); 218 - return ApiError::InternalError(None).into_response(); 219 } 220 }; 221 222 let code = input.code.trim(); 223 let code_valid = if is_backup_code_format(code) { 224 - verify_backup_code_for_user(&state, &auth.0.did, code).await 225 } else { 226 - let secret = match decrypt_totp_secret( 227 &totp_record.secret_encrypted, 228 totp_record.encryption_version, 229 - ) { 230 - Ok(s) => s, 231 - Err(e) => { 232 - error!("Failed to decrypt TOTP secret: {:?}", e); 233 - return ApiError::InternalError(None).into_response(); 234 - } 235 - }; 236 verify_totp_code(&secret, code) 237 }; 238 239 if !code_valid { 240 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 241 } 242 243 - if let Err(e) = state 244 .user_repo 245 - .delete_totp_and_backup_codes(&auth.0.did) 246 .await 247 - { 248 - error!("Failed to delete TOTP: {:?}", e); 249 - return ApiError::InternalError(None).into_response(); 250 - } 251 252 - info!(did = %&auth.0.did, "TOTP disabled"); 253 254 - EmptyResponse::ok().into_response() 255 } 256 257 #[derive(Serialize)] ··· 262 pub backup_codes_remaining: i64, 263 } 264 265 - pub async fn get_totp_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 266 - let enabled = match state.user_repo.get_totp_record(&auth.0.did).await { 267 Ok(Some(row)) => row.verified, 268 Ok(None) => false, 269 Err(e) => { 270 error!("DB error fetching TOTP status: {:?}", e); 271 - return ApiError::InternalError(None).into_response(); 272 } 273 }; 274 275 - let backup_count = match state.user_repo.count_unused_backup_codes(&auth.0.did).await { 276 - Ok(count) => count, 277 - Err(e) => { 278 error!("DB error counting backup codes: {:?}", e); 279 - return ApiError::InternalError(None).into_response(); 280 - } 281 - }; 282 283 - Json(GetTotpStatusResponse { 284 enabled, 285 has_backup_codes: backup_count > 0, 286 backup_codes_remaining: backup_count, 287 }) 288 - .into_response() 289 } 290 291 #[derive(Deserialize)] ··· 302 303 pub async fn regenerate_backup_codes( 304 State(state): State<AppState>, 305 - auth: BearerAuth, 306 Json(input): Json<RegenerateBackupCodesInput>, 307 - ) -> Response { 308 if !state 309 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 310 .await 311 { 312 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 313 - return ApiError::RateLimitExceeded(None).into_response(); 314 } 315 316 - let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await { 317 - Ok(Some(hash)) => hash, 318 - Ok(None) => return ApiError::AccountNotFound.into_response(), 319 - Err(e) => { 320 error!("DB error fetching user: {:?}", e); 321 - return ApiError::InternalError(None).into_response(); 322 - } 323 - }; 324 325 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 326 if !password_valid { 327 - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 328 } 329 330 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 331 Ok(Some(row)) if row.verified => row, 332 - Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 333 Err(e) => { 334 error!("DB error fetching TOTP: {:?}", e); 335 - return ApiError::InternalError(None).into_response(); 336 } 337 }; 338 339 - let secret = match decrypt_totp_secret( 340 &totp_record.secret_encrypted, 341 totp_record.encryption_version, 342 - ) { 343 - Ok(s) => s, 344 - Err(e) => { 345 - error!("Failed to decrypt TOTP secret: {:?}", e); 346 - return ApiError::InternalError(None).into_response(); 347 - } 348 - }; 349 350 let code = input.code.trim(); 351 if !verify_totp_code(&secret, code) { 352 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 353 } 354 355 let backup_codes = generate_backup_codes(); 356 - let backup_hashes: Result<Vec<_>, _> = 357 - backup_codes.iter().map(|c| hash_backup_code(c)).collect(); 358 - let backup_hashes = match backup_hashes { 359 - Ok(hashes) => hashes, 360 - Err(e) => { 361 error!("Failed to hash backup code: {:?}", e); 362 - return ApiError::InternalError(None).into_response(); 363 - } 364 - }; 365 366 - if let Err(e) = state 367 .user_repo 368 - .replace_backup_codes(&auth.0.did, &backup_hashes) 369 .await 370 - { 371 - error!("Failed to regenerate backup codes: {:?}", e); 372 - return ApiError::InternalError(None).into_response(); 373 - } 374 375 - info!(did = %&auth.0.did, "Backup codes regenerated"); 376 377 - Json(RegenerateBackupCodesResponse { backup_codes }).into_response() 378 } 379 380 async fn verify_backup_code_for_user(
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::{Active, Auth}; 4 use crate::auth::{ 5 decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, 6 generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, ··· 26 pub qr_base64: String, 27 } 28 29 + pub async fn create_totp_secret( 30 + State(state): State<AppState>, 31 + auth: Auth<Active>, 32 + ) -> Result<Response, ApiError> { 33 + match state.user_repo.get_totp_record(&auth.did).await { 34 + Ok(Some(record)) if record.verified => return Err(ApiError::TotpAlreadyEnabled), 35 Ok(_) => {} 36 Err(e) => { 37 error!("DB error checking TOTP: {:?}", e); 38 + return Err(ApiError::InternalError(None)); 39 } 40 } 41 42 let secret = generate_totp_secret(); 43 44 + let handle = state 45 + .user_repo 46 + .get_handle_by_did(&auth.did) 47 + .await 48 + .map_err(|e| { 49 error!("DB error fetching handle: {:?}", e); 50 + ApiError::InternalError(None) 51 + })? 52 + .ok_or(ApiError::AccountNotFound)?; 53 54 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 55 let uri = generate_totp_uri(&secret, &handle, &hostname); 56 57 + let qr_code = generate_qr_png_base64(&secret, &handle, &hostname).map_err(|e| { 58 + error!("Failed to generate QR code: {:?}", e); 59 + ApiError::InternalError(Some("Failed to generate QR code".into())) 60 + })?; 61 62 + let encrypted_secret = encrypt_totp_secret(&secret).map_err(|e| { 63 + error!("Failed to encrypt TOTP secret: {:?}", e); 64 + ApiError::InternalError(None) 65 + })?; 66 67 + state 68 .user_repo 69 + .upsert_totp_secret(&auth.did, &encrypted_secret, ENCRYPTION_VERSION) 70 .await 71 + .map_err(|e| { 72 + error!("Failed to store TOTP secret: {:?}", e); 73 + ApiError::InternalError(None) 74 + })?; 75 76 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 77 78 + info!(did = %&auth.did, "TOTP secret created (pending verification)"); 79 80 + Ok(Json(CreateTotpSecretResponse { 81 secret: secret_base32, 82 uri, 83 qr_base64: qr_code, 84 }) 85 + .into_response()) 86 } 87 88 #[derive(Deserialize)] ··· 98 99 pub async fn enable_totp( 100 State(state): State<AppState>, 101 + auth: Auth<Active>, 102 Json(input): Json<EnableTotpInput>, 103 + ) -> Result<Response, ApiError> { 104 if !state 105 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 106 .await 107 { 108 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 109 + return Err(ApiError::RateLimitExceeded(None)); 110 } 111 112 + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 113 Ok(Some(row)) => row, 114 + Ok(None) => return Err(ApiError::TotpNotEnabled), 115 Err(e) => { 116 error!("DB error fetching TOTP: {:?}", e); 117 + return Err(ApiError::InternalError(None)); 118 } 119 }; 120 121 if totp_record.verified { 122 + return Err(ApiError::TotpAlreadyEnabled); 123 } 124 125 + let secret = decrypt_totp_secret( 126 &totp_record.secret_encrypted, 127 totp_record.encryption_version, 128 + ) 129 + .map_err(|e| { 130 + error!("Failed to decrypt TOTP secret: {:?}", e); 131 + ApiError::InternalError(None) 132 + })?; 133 134 let code = input.code.trim(); 135 if !verify_totp_code(&secret, code) { 136 + return Err(ApiError::InvalidCode(Some( 137 + "Invalid verification code".into(), 138 + ))); 139 } 140 141 let backup_codes = generate_backup_codes(); 142 + let backup_hashes: Vec<_> = backup_codes 143 + .iter() 144 + .map(|c| hash_backup_code(c)) 145 + .collect::<Result<Vec<_>, _>>() 146 + .map_err(|e| { 147 error!("Failed to hash backup code: {:?}", e); 148 + ApiError::InternalError(None) 149 + })?; 150 151 + state 152 .user_repo 153 + .enable_totp_with_backup_codes(&auth.did, &backup_hashes) 154 .await 155 + .map_err(|e| { 156 + error!("Failed to enable TOTP: {:?}", e); 157 + ApiError::InternalError(None) 158 + })?; 159 160 + info!(did = %&auth.did, "TOTP enabled with {} backup codes", backup_codes.len()); 161 162 + Ok(Json(EnableTotpResponse { backup_codes }).into_response()) 163 } 164 165 #[derive(Deserialize)] ··· 170 171 pub async fn disable_totp( 172 State(state): State<AppState>, 173 + auth: Auth<Active>, 174 Json(input): Json<DisableTotpInput>, 175 + ) -> Result<Response, ApiError> { 176 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 177 { 178 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 179 &*state.user_repo, 180 &*state.session_repo, 181 + &auth.did, 182 ) 183 + .await); 184 } 185 186 if !state 187 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 188 .await 189 { 190 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 191 + return Err(ApiError::RateLimitExceeded(None)); 192 } 193 194 + let password_hash = state 195 + .user_repo 196 + .get_password_hash_by_did(&auth.did) 197 + .await 198 + .map_err(|e| { 199 error!("DB error fetching user: {:?}", e); 200 + ApiError::InternalError(None) 201 + })? 202 + .ok_or(ApiError::AccountNotFound)?; 203 204 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 205 if !password_valid { 206 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 207 } 208 209 + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 210 Ok(Some(row)) if row.verified => row, 211 + Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 212 Err(e) => { 213 error!("DB error fetching TOTP: {:?}", e); 214 + return Err(ApiError::InternalError(None)); 215 } 216 }; 217 218 let code = input.code.trim(); 219 let code_valid = if is_backup_code_format(code) { 220 + verify_backup_code_for_user(&state, &auth.did, code).await 221 } else { 222 + let secret = decrypt_totp_secret( 223 &totp_record.secret_encrypted, 224 totp_record.encryption_version, 225 + ) 226 + .map_err(|e| { 227 + error!("Failed to decrypt TOTP secret: {:?}", e); 228 + ApiError::InternalError(None) 229 + })?; 230 verify_totp_code(&secret, code) 231 }; 232 233 if !code_valid { 234 + return Err(ApiError::InvalidCode(Some( 235 + "Invalid verification code".into(), 236 + ))); 237 } 238 239 + state 240 .user_repo 241 + .delete_totp_and_backup_codes(&auth.did) 242 .await 243 + .map_err(|e| { 244 + error!("Failed to delete TOTP: {:?}", e); 245 + ApiError::InternalError(None) 246 + })?; 247 248 + info!(did = %&auth.did, "TOTP disabled"); 249 250 + Ok(EmptyResponse::ok().into_response()) 251 } 252 253 #[derive(Serialize)] ··· 258 pub backup_codes_remaining: i64, 259 } 260 261 + pub async fn get_totp_status( 262 + State(state): State<AppState>, 263 + auth: Auth<Active>, 264 + ) -> Result<Response, ApiError> { 265 + let enabled = match state.user_repo.get_totp_record(&auth.did).await { 266 Ok(Some(row)) => row.verified, 267 Ok(None) => false, 268 Err(e) => { 269 error!("DB error fetching TOTP status: {:?}", e); 270 + return Err(ApiError::InternalError(None)); 271 } 272 }; 273 274 + let backup_count = state 275 + .user_repo 276 + .count_unused_backup_codes(&auth.did) 277 + .await 278 + .map_err(|e| { 279 error!("DB error counting backup codes: {:?}", e); 280 + ApiError::InternalError(None) 281 + })?; 282 283 + Ok(Json(GetTotpStatusResponse { 284 enabled, 285 has_backup_codes: backup_count > 0, 286 backup_codes_remaining: backup_count, 287 }) 288 + .into_response()) 289 } 290 291 #[derive(Deserialize)] ··· 302 303 pub async fn regenerate_backup_codes( 304 State(state): State<AppState>, 305 + auth: Auth<Active>, 306 Json(input): Json<RegenerateBackupCodesInput>, 307 + ) -> Result<Response, ApiError> { 308 if !state 309 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 310 .await 311 { 312 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 313 + return Err(ApiError::RateLimitExceeded(None)); 314 } 315 316 + let password_hash = state 317 + .user_repo 318 + .get_password_hash_by_did(&auth.did) 319 + .await 320 + .map_err(|e| { 321 error!("DB error fetching user: {:?}", e); 322 + ApiError::InternalError(None) 323 + })? 324 + .ok_or(ApiError::AccountNotFound)?; 325 326 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 327 if !password_valid { 328 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 329 } 330 331 + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 332 Ok(Some(row)) if row.verified => row, 333 + Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 334 Err(e) => { 335 error!("DB error fetching TOTP: {:?}", e); 336 + return Err(ApiError::InternalError(None)); 337 } 338 }; 339 340 + let secret = decrypt_totp_secret( 341 &totp_record.secret_encrypted, 342 totp_record.encryption_version, 343 + ) 344 + .map_err(|e| { 345 + error!("Failed to decrypt TOTP secret: {:?}", e); 346 + ApiError::InternalError(None) 347 + })?; 348 349 let code = input.code.trim(); 350 if !verify_totp_code(&secret, code) { 351 + return Err(ApiError::InvalidCode(Some( 352 + "Invalid verification code".into(), 353 + ))); 354 } 355 356 let backup_codes = generate_backup_codes(); 357 + let backup_hashes: Vec<_> = backup_codes 358 + .iter() 359 + .map(|c| hash_backup_code(c)) 360 + .collect::<Result<Vec<_>, _>>() 361 + .map_err(|e| { 362 error!("Failed to hash backup code: {:?}", e); 363 + ApiError::InternalError(None) 364 + })?; 365 366 + state 367 .user_repo 368 + .replace_backup_codes(&auth.did, &backup_hashes) 369 .await 370 + .map_err(|e| { 371 + error!("Failed to regenerate backup codes: {:?}", e); 372 + ApiError::InternalError(None) 373 + })?; 374 375 + info!(did = %&auth.did, "Backup codes regenerated"); 376 377 + Ok(Json(RegenerateBackupCodesResponse { backup_codes }).into_response()) 378 } 379 380 async fn verify_backup_code_for_user(
+57 -55
crates/tranquil-pds/src/api/server/trusted_devices.rs
··· 11 use tranquil_db_traits::OAuthRepository; 12 use tranquil_types::DeviceId; 13 14 - use crate::auth::BearerAuth; 15 use crate::state::AppState; 16 17 const TRUST_DURATION_DAYS: i64 = 30; ··· 71 pub devices: Vec<TrustedDevice>, 72 } 73 74 - pub async fn list_trusted_devices(State(state): State<AppState>, auth: BearerAuth) -> Response { 75 - match state.oauth_repo.list_trusted_devices(&auth.0.did).await { 76 - Ok(rows) => { 77 - let devices = rows 78 - .into_iter() 79 - .map(|row| { 80 - let trust_state = 81 - DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until); 82 - TrustedDevice { 83 - id: row.id, 84 - user_agent: row.user_agent, 85 - friendly_name: row.friendly_name, 86 - trusted_at: row.trusted_at, 87 - trusted_until: row.trusted_until, 88 - last_seen_at: row.last_seen_at, 89 - trust_state, 90 - } 91 - }) 92 - .collect(); 93 - Json(ListTrustedDevicesResponse { devices }).into_response() 94 - } 95 - Err(e) => { 96 error!("DB error: {:?}", e); 97 - ApiError::InternalError(None).into_response() 98 - } 99 - } 100 } 101 102 #[derive(Deserialize)] ··· 107 108 pub async fn revoke_trusted_device( 109 State(state): State<AppState>, 110 - auth: BearerAuth, 111 Json(input): Json<RevokeTrustedDeviceInput>, 112 - ) -> Response { 113 let device_id = DeviceId::from(input.device_id.clone()); 114 match state 115 .oauth_repo 116 - .device_belongs_to_user(&device_id, &auth.0.did) 117 .await 118 { 119 Ok(true) => {} 120 Ok(false) => { 121 - return ApiError::DeviceNotFound.into_response(); 122 } 123 Err(e) => { 124 error!("DB error: {:?}", e); 125 - return ApiError::InternalError(None).into_response(); 126 } 127 } 128 129 - match state.oauth_repo.revoke_device_trust(&device_id).await { 130 - Ok(()) => { 131 - info!(did = %&auth.0.did, device_id = %input.device_id, "Trusted device revoked"); 132 - SuccessResponse::ok().into_response() 133 - } 134 - Err(e) => { 135 error!("DB error: {:?}", e); 136 - ApiError::InternalError(None).into_response() 137 - } 138 - } 139 } 140 141 #[derive(Deserialize)] ··· 147 148 pub async fn update_trusted_device( 149 State(state): State<AppState>, 150 - auth: BearerAuth, 151 Json(input): Json<UpdateTrustedDeviceInput>, 152 - ) -> Response { 153 let device_id = DeviceId::from(input.device_id.clone()); 154 match state 155 .oauth_repo 156 - .device_belongs_to_user(&device_id, &auth.0.did) 157 .await 158 { 159 Ok(true) => {} 160 Ok(false) => { 161 - return ApiError::DeviceNotFound.into_response(); 162 } 163 Err(e) => { 164 error!("DB error: {:?}", e); 165 - return ApiError::InternalError(None).into_response(); 166 } 167 } 168 169 - match state 170 .oauth_repo 171 .update_device_friendly_name(&device_id, input.friendly_name.as_deref()) 172 .await 173 - { 174 - Ok(()) => { 175 - info!(did = %auth.0.did, device_id = %input.device_id, "Trusted device updated"); 176 - SuccessResponse::ok().into_response() 177 - } 178 - Err(e) => { 179 error!("DB error: {:?}", e); 180 - ApiError::InternalError(None).into_response() 181 - } 182 - } 183 } 184 185 pub async fn get_device_trust_state(
··· 11 use tranquil_db_traits::OAuthRepository; 12 use tranquil_types::DeviceId; 13 14 + use crate::auth::{Active, Auth}; 15 use crate::state::AppState; 16 17 const TRUST_DURATION_DAYS: i64 = 30; ··· 71 pub devices: Vec<TrustedDevice>, 72 } 73 74 + pub async fn list_trusted_devices( 75 + State(state): State<AppState>, 76 + auth: Auth<Active>, 77 + ) -> Result<Response, ApiError> { 78 + let rows = state 79 + .oauth_repo 80 + .list_trusted_devices(&auth.did) 81 + .await 82 + .map_err(|e| { 83 error!("DB error: {:?}", e); 84 + ApiError::InternalError(None) 85 + })?; 86 + 87 + let devices = rows 88 + .into_iter() 89 + .map(|row| { 90 + let trust_state = DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until); 91 + TrustedDevice { 92 + id: row.id, 93 + user_agent: row.user_agent, 94 + friendly_name: row.friendly_name, 95 + trusted_at: row.trusted_at, 96 + trusted_until: row.trusted_until, 97 + last_seen_at: row.last_seen_at, 98 + trust_state, 99 + } 100 + }) 101 + .collect(); 102 + 103 + Ok(Json(ListTrustedDevicesResponse { devices }).into_response()) 104 } 105 106 #[derive(Deserialize)] ··· 111 112 pub async fn revoke_trusted_device( 113 State(state): State<AppState>, 114 + auth: Auth<Active>, 115 Json(input): Json<RevokeTrustedDeviceInput>, 116 + ) -> Result<Response, ApiError> { 117 let device_id = DeviceId::from(input.device_id.clone()); 118 match state 119 .oauth_repo 120 + .device_belongs_to_user(&device_id, &auth.did) 121 .await 122 { 123 Ok(true) => {} 124 Ok(false) => { 125 + return Err(ApiError::DeviceNotFound); 126 } 127 Err(e) => { 128 error!("DB error: {:?}", e); 129 + return Err(ApiError::InternalError(None)); 130 } 131 } 132 133 + state 134 + .oauth_repo 135 + .revoke_device_trust(&device_id) 136 + .await 137 + .map_err(|e| { 138 error!("DB error: {:?}", e); 139 + ApiError::InternalError(None) 140 + })?; 141 + 142 + info!(did = %&auth.did, device_id = %input.device_id, "Trusted device revoked"); 143 + Ok(SuccessResponse::ok().into_response()) 144 } 145 146 #[derive(Deserialize)] ··· 152 153 pub async fn update_trusted_device( 154 State(state): State<AppState>, 155 + auth: Auth<Active>, 156 Json(input): Json<UpdateTrustedDeviceInput>, 157 + ) -> Result<Response, ApiError> { 158 let device_id = DeviceId::from(input.device_id.clone()); 159 match state 160 .oauth_repo 161 + .device_belongs_to_user(&device_id, &auth.did) 162 .await 163 { 164 Ok(true) => {} 165 Ok(false) => { 166 + return Err(ApiError::DeviceNotFound); 167 } 168 Err(e) => { 169 error!("DB error: {:?}", e); 170 + return Err(ApiError::InternalError(None)); 171 } 172 } 173 174 + state 175 .oauth_repo 176 .update_device_friendly_name(&device_id, input.friendly_name.as_deref()) 177 .await 178 + .map_err(|e| { 179 error!("DB error: {:?}", e); 180 + ApiError::InternalError(None) 181 + })?; 182 + 183 + info!(did = %auth.did, device_id = %input.device_id, "Trusted device updated"); 184 + Ok(SuccessResponse::ok().into_response()) 185 } 186 187 pub async fn get_device_trust_state(
+8 -10
crates/tranquil-pds/src/api/temp.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::{BearerAuth, OptionalBearerAuth}; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 21 pub estimated_time_ms: Option<i64>, 22 } 23 24 - pub async fn check_signup_queue(auth: OptionalBearerAuth) -> Response { 25 - if let Some(user) = auth.0 26 - && user.is_oauth 27 { 28 return ApiError::Forbidden.into_response(); 29 } ··· 49 50 pub async fn dereference_scope( 51 State(state): State<AppState>, 52 - auth: BearerAuth, 53 Json(input): Json<DereferenceScopeInput>, 54 - ) -> Response { 55 - let _ = auth; 56 - 57 let scope_parts: Vec<&str> = input.scope.split_whitespace().collect(); 58 let mut resolved_scopes: Vec<String> = Vec::new(); 59 ··· 118 } 119 } 120 121 - Json(DereferenceScopeOutput { 122 scope: resolved_scopes.join(" "), 123 }) 124 - .into_response() 125 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::{Active, Auth, Permissive}; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 21 pub estimated_time_ms: Option<i64>, 22 } 23 24 + pub async fn check_signup_queue(auth: Option<Auth<Permissive>>) -> Response { 25 + if let Some(ref user) = auth 26 + && user.is_oauth() 27 { 28 return ApiError::Forbidden.into_response(); 29 } ··· 49 50 pub async fn dereference_scope( 51 State(state): State<AppState>, 52 + _auth: Auth<Active>, 53 Json(input): Json<DereferenceScopeInput>, 54 + ) -> Result<Response, ApiError> { 55 let scope_parts: Vec<&str> = input.scope.split_whitespace().collect(); 56 let mut resolved_scopes: Vec<String> = Vec::new(); 57 ··· 116 } 117 } 118 119 + Ok(Json(DereferenceScopeOutput { 120 scope: resolved_scopes.join(" "), 121 }) 122 + .into_response()) 123 }
+1 -1
crates/tranquil-pds/src/auth/auth_extractor.rs
··· 161 .await 162 .unwrap(); 163 164 - assert_eq!(res.status(), StatusCode::OK, "OAuth token should work with BearerAuth extractor"); 165 let body: Value = res.json().await.unwrap(); 166 assert_eq!(body["did"].as_str().unwrap(), did); 167 }
··· 161 .await 162 .unwrap(); 163 164 + assert_eq!(res.status(), StatusCode::OK, "OAuth token should work with RequiredAuth extractor"); 165 let body: Value = res.json().await.unwrap(); 166 assert_eq!(body["did"].as_str().unwrap(), did); 167 }
+371 -405
crates/tranquil-pds/src/auth/extractor.rs
··· 1 use axum::{ 2 - extract::FromRequestParts, 3 http::{StatusCode, header::AUTHORIZATION, request::Parts}, 4 response::{IntoResponse, Response}, 5 }; 6 use tracing::{debug, error, info}; 7 8 use super::{ 9 - AccountStatus, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, is_service_token, 10 - validate_bearer_token, validate_bearer_token_allow_deactivated, 11 - validate_bearer_token_allow_takendown, 12 }; 13 use crate::api::error::ApiError; 14 use crate::state::AppState; 15 use crate::types::Did; 16 use crate::util::build_full_url; 17 - 18 - pub struct BearerAuth(pub AuthenticatedUser); 19 20 #[derive(Debug)] 21 pub enum AuthError { ··· 26 AccountDeactivated, 27 AccountTakedown, 28 AdminRequired, 29 OAuthExpiredToken(String), 30 UseDpopNonce(String), 31 InvalidDpopProof(String), ··· 56 })), 57 ) 58 .into_response(), 59 other => ApiError::from(other).into_response(), 60 } 61 } 62 } 63 64 - #[cfg(test)] 65 - fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> { 66 - let auth_header = auth_header.trim(); 67 - 68 - if auth_header.len() < 8 { 69 - return Err(AuthError::InvalidFormat); 70 - } 71 - 72 - let prefix = &auth_header[..7]; 73 - if !prefix.eq_ignore_ascii_case("bearer ") { 74 - return Err(AuthError::InvalidFormat); 75 - } 76 - 77 - let token = auth_header[7..].trim(); 78 - if token.is_empty() { 79 - return Err(AuthError::InvalidFormat); 80 - } 81 - 82 - Ok(token) 83 } 84 85 pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option<String> { ··· 100 } 101 102 Some(token.to_string()) 103 - } 104 - 105 - pub struct ExtractedToken { 106 - pub token: String, 107 - pub is_dpop: bool, 108 } 109 110 pub fn extract_auth_token_from_header(auth_header: Option<&str>) -> Option<ExtractedToken> { ··· 136 None 137 } 138 139 - #[derive(Default)] 140 - struct StatusCheckFlags { 141 - allow_deactivated: bool, 142 - allow_takendown: bool, 143 } 144 145 async fn verify_oauth_token_and_build_user( ··· 148 dpop_proof: Option<&str>, 149 method: &str, 150 uri: &str, 151 - flags: StatusCheckFlags, 152 ) -> Result<AuthenticatedUser, AuthError> { 153 match crate::oauth::verify::verify_oauth_access_token( 154 state.oauth_repo.as_ref(), ··· 171 user_info.takedown_ref.as_deref(), 172 user_info.deactivated_at, 173 ); 174 - if !flags.allow_deactivated && status.is_deactivated() { 175 - return Err(AuthError::AccountDeactivated); 176 - } 177 - if !flags.allow_takendown && status.is_takendown() { 178 - return Err(AuthError::AccountTakedown); 179 - } 180 Ok(AuthenticatedUser { 181 did: result.did, 182 key_bytes: user_info.key_bytes.and_then(|kb| { 183 crate::config::decrypt_key(&kb, user_info.encryption_version).ok() 184 }), 185 - is_oauth: true, 186 is_admin: user_info.is_admin, 187 status, 188 scope: result.scope, 189 controller_did: None, 190 }) 191 } 192 Err(crate::oauth::OAuthError::ExpiredToken(msg)) => Err(AuthError::OAuthExpiredToken(msg)), ··· 198 } 199 } 200 201 - impl FromRequestParts<AppState> for BearerAuth { 202 - type Rejection = AuthError; 203 204 - async fn from_request_parts( 205 - parts: &mut Parts, 206 - state: &AppState, 207 - ) -> Result<Self, Self::Rejection> { 208 - let auth_header = parts 209 - .headers 210 - .get(AUTHORIZATION) 211 - .ok_or(AuthError::MissingToken)? 212 - .to_str() 213 - .map_err(|_| AuthError::InvalidFormat)?; 214 215 - let extracted = 216 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 217 218 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 219 - let method = parts.method.as_str(); 220 - let uri = build_full_url(&parts.uri.to_string()); 221 222 - match validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await { 223 - Ok(user) if !user.is_oauth => { 224 - return if user.status.is_deactivated() { 225 - Err(AuthError::AccountDeactivated) 226 - } else if user.status.is_takendown() { 227 - Err(AuthError::AccountTakedown) 228 - } else { 229 - Ok(BearerAuth(user)) 230 - }; 231 - } 232 - Ok(_) => {} 233 - Err(super::TokenValidationError::AccountDeactivated) => { 234 - return Err(AuthError::AccountDeactivated); 235 - } 236 - Err(super::TokenValidationError::AccountTakedown) => { 237 - return Err(AuthError::AccountTakedown); 238 - } 239 - Err(super::TokenValidationError::TokenExpired) => { 240 - info!("JWT access token expired in BearerAuth, returning ExpiredToken"); 241 - return Err(AuthError::TokenExpired); 242 - } 243 - Err(_) => {} 244 } 245 246 - verify_oauth_token_and_build_user( 247 - state, 248 - &extracted.token, 249 - dpop_proof, 250 - method, 251 - &uri, 252 - StatusCheckFlags::default(), 253 - ) 254 - .await 255 - .map(BearerAuth) 256 } 257 } 258 259 - pub struct BearerAuthAllowDeactivated(pub AuthenticatedUser); 260 - 261 - impl FromRequestParts<AppState> for BearerAuthAllowDeactivated { 262 - type Rejection = AuthError; 263 264 - async fn from_request_parts( 265 - parts: &mut Parts, 266 - state: &AppState, 267 - ) -> Result<Self, Self::Rejection> { 268 - let auth_header = parts 269 - .headers 270 - .get(AUTHORIZATION) 271 - .ok_or(AuthError::MissingToken)? 272 - .to_str() 273 - .map_err(|_| AuthError::InvalidFormat)?; 274 275 - let extracted = 276 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 277 278 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 279 - let method = parts.method.as_str(); 280 - let uri = build_full_url(&parts.uri.to_string()); 281 282 - match validate_bearer_token_allow_deactivated(state.user_repo.as_ref(), &extracted.token) 283 - .await 284 - { 285 - Ok(user) if !user.is_oauth => { 286 - return if user.status.is_takendown() { 287 - Err(AuthError::AccountTakedown) 288 - } else { 289 - Ok(BearerAuthAllowDeactivated(user)) 290 - }; 291 - } 292 - Ok(_) => {} 293 - Err(super::TokenValidationError::AccountTakedown) => { 294 - return Err(AuthError::AccountTakedown); 295 - } 296 - Err(super::TokenValidationError::TokenExpired) => { 297 - return Err(AuthError::TokenExpired); 298 - } 299 - Err(_) => {} 300 } 301 302 - verify_oauth_token_and_build_user( 303 - state, 304 - &extracted.token, 305 - dpop_proof, 306 - method, 307 - &uri, 308 - StatusCheckFlags { 309 - allow_deactivated: true, 310 - allow_takendown: false, 311 - }, 312 - ) 313 - .await 314 - .map(BearerAuthAllowDeactivated) 315 } 316 } 317 318 - pub struct BearerAuthAllowTakendown(pub AuthenticatedUser); 319 - 320 - impl FromRequestParts<AppState> for BearerAuthAllowTakendown { 321 type Rejection = AuthError; 322 323 async fn from_request_parts( 324 parts: &mut Parts, 325 state: &AppState, 326 ) -> Result<Self, Self::Rejection> { 327 - let auth_header = parts 328 - .headers 329 - .get(AUTHORIZATION) 330 - .ok_or(AuthError::MissingToken)? 331 - .to_str() 332 - .map_err(|_| AuthError::InvalidFormat)?; 333 - 334 - let extracted = 335 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 336 - 337 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 338 - let method = parts.method.as_str(); 339 - let uri = build_full_url(&parts.uri.to_string()); 340 - 341 - match validate_bearer_token_allow_takendown(state.user_repo.as_ref(), &extracted.token) 342 - .await 343 - { 344 - Ok(user) if !user.is_oauth => { 345 - return if user.status.is_deactivated() { 346 - Err(AuthError::AccountDeactivated) 347 - } else { 348 - Ok(BearerAuthAllowTakendown(user)) 349 - }; 350 - } 351 - Ok(_) => {} 352 - Err(super::TokenValidationError::AccountDeactivated) => { 353 - return Err(AuthError::AccountDeactivated); 354 - } 355 - Err(super::TokenValidationError::TokenExpired) => { 356 - return Err(AuthError::TokenExpired); 357 - } 358 - Err(_) => {} 359 - } 360 - 361 - verify_oauth_token_and_build_user( 362 - state, 363 - &extracted.token, 364 - dpop_proof, 365 - method, 366 - &uri, 367 - StatusCheckFlags { 368 - allow_deactivated: false, 369 - allow_takendown: true, 370 - }, 371 - ) 372 - .await 373 - .map(BearerAuthAllowTakendown) 374 } 375 } 376 377 - pub struct BearerAuthAdmin(pub AuthenticatedUser); 378 - 379 - impl FromRequestParts<AppState> for BearerAuthAdmin { 380 type Rejection = AuthError; 381 382 async fn from_request_parts( 383 parts: &mut Parts, 384 state: &AppState, 385 - ) -> Result<Self, Self::Rejection> { 386 - let auth_header = parts 387 - .headers 388 - .get(AUTHORIZATION) 389 - .ok_or(AuthError::MissingToken)? 390 - .to_str() 391 - .map_err(|_| AuthError::InvalidFormat)?; 392 - 393 - let extracted = 394 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 395 - 396 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 397 - let method = parts.method.as_str(); 398 - let uri = build_full_url(&parts.uri.to_string()); 399 - 400 - match validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await { 401 - Ok(user) if !user.is_oauth => { 402 - if user.status.is_deactivated() { 403 - return Err(AuthError::AccountDeactivated); 404 - } 405 - if user.status.is_takendown() { 406 - return Err(AuthError::AccountTakedown); 407 - } 408 - if !user.is_admin { 409 - return Err(AuthError::AdminRequired); 410 - } 411 - return Ok(BearerAuthAdmin(user)); 412 } 413 - Ok(_) => {} 414 - Err(super::TokenValidationError::AccountDeactivated) => { 415 - return Err(AuthError::AccountDeactivated); 416 - } 417 - Err(super::TokenValidationError::AccountTakedown) => { 418 - return Err(AuthError::AccountTakedown); 419 - } 420 - Err(super::TokenValidationError::TokenExpired) => { 421 - return Err(AuthError::TokenExpired); 422 - } 423 - Err(_) => {} 424 } 425 426 - let user = verify_oauth_token_and_build_user( 427 - state, 428 - &extracted.token, 429 - dpop_proof, 430 - method, 431 - &uri, 432 - StatusCheckFlags::default(), 433 - ) 434 - .await?; 435 436 - if !user.is_admin { 437 - return Err(AuthError::AdminRequired); 438 } 439 - Ok(BearerAuthAdmin(user)) 440 } 441 } 442 443 - pub struct OptionalBearerAuth(pub Option<AuthenticatedUser>); 444 - 445 - impl FromRequestParts<AppState> for OptionalBearerAuth { 446 type Rejection = AuthError; 447 448 async fn from_request_parts( 449 parts: &mut Parts, 450 state: &AppState, 451 ) -> Result<Self, Self::Rejection> { 452 - let auth_header = match parts.headers.get(AUTHORIZATION) { 453 - Some(h) => match h.to_str() { 454 - Ok(s) => s, 455 - Err(_) => return Ok(OptionalBearerAuth(None)), 456 - }, 457 - None => return Ok(OptionalBearerAuth(None)), 458 - }; 459 460 - let extracted = match extract_auth_token_from_header(Some(auth_header)) { 461 - Some(e) => e, 462 - None => return Ok(OptionalBearerAuth(None)), 463 - }; 464 465 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 466 - let method = parts.method.as_str(); 467 - let uri = build_full_url(&parts.uri.to_string()); 468 469 - if let Ok(user) = validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await 470 - && !user.is_oauth 471 - { 472 - return if user.status.is_deactivated() || user.status.is_takendown() { 473 - Ok(OptionalBearerAuth(None)) 474 - } else { 475 - Ok(OptionalBearerAuth(Some(user))) 476 - }; 477 } 478 479 - Ok(OptionalBearerAuth( 480 - verify_oauth_token_and_build_user( 481 - state, 482 - &extracted.token, 483 - dpop_proof, 484 - method, 485 - &uri, 486 - StatusCheckFlags::default(), 487 - ) 488 - .await 489 - .ok(), 490 - )) 491 } 492 - } 493 494 - pub struct ServiceAuth { 495 - pub claims: ServiceTokenClaims, 496 - pub did: Did, 497 } 498 499 - impl FromRequestParts<AppState> for ServiceAuth { 500 type Rejection = AuthError; 501 502 async fn from_request_parts( 503 parts: &mut Parts, 504 - _state: &AppState, 505 ) -> Result<Self, Self::Rejection> { 506 - let auth_header = parts 507 - .headers 508 - .get(AUTHORIZATION) 509 - .ok_or(AuthError::MissingToken)? 510 - .to_str() 511 - .map_err(|_| AuthError::InvalidFormat)?; 512 - 513 - let extracted = 514 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 515 - 516 - if !is_service_token(&extracted.token) { 517 - return Err(AuthError::InvalidFormat); 518 } 519 - 520 - let verifier = ServiceTokenVerifier::new(); 521 - let claims = verifier 522 - .verify_service_token(&extracted.token, None) 523 - .await 524 - .map_err(|e| { 525 - error!("Service token verification failed: {:?}", e); 526 - AuthError::AuthenticationFailed 527 - })?; 528 - 529 - let did: Did = claims 530 - .iss 531 - .parse() 532 - .map_err(|_| AuthError::AuthenticationFailed)?; 533 - 534 - debug!("Service token verified for DID: {}", did); 535 - 536 - Ok(ServiceAuth { claims, did }) 537 } 538 } 539 540 - pub struct OptionalServiceAuth(pub Option<ServiceTokenClaims>); 541 - 542 - impl FromRequestParts<AppState> for OptionalServiceAuth { 543 - type Rejection = std::convert::Infallible; 544 545 async fn from_request_parts( 546 parts: &mut Parts, 547 - _state: &AppState, 548 - ) -> Result<Self, Self::Rejection> { 549 - let auth_header = match parts.headers.get(AUTHORIZATION) { 550 - Some(h) => match h.to_str() { 551 - Ok(s) => s, 552 - Err(_) => return Ok(OptionalServiceAuth(None)), 553 - }, 554 - None => return Ok(OptionalServiceAuth(None)), 555 - }; 556 - 557 - let extracted = match extract_auth_token_from_header(Some(auth_header)) { 558 - Some(e) => e, 559 - None => return Ok(OptionalServiceAuth(None)), 560 - }; 561 - 562 - if !is_service_token(&extracted.token) { 563 - return Ok(OptionalServiceAuth(None)); 564 - } 565 - 566 - let verifier = ServiceTokenVerifier::new(); 567 - match verifier.verify_service_token(&extracted.token, None).await { 568 - Ok(claims) => { 569 - debug!("Service token verified for DID: {}", claims.iss); 570 - Ok(OptionalServiceAuth(Some(claims))) 571 } 572 - Err(e) => { 573 - debug!("Service token verification failed (optional): {:?}", e); 574 - Ok(OptionalServiceAuth(None)) 575 } 576 } 577 } 578 } 579 580 - pub enum BlobAuthResult { 581 - Service { did: Did }, 582 - User(AuthenticatedUser), 583 } 584 585 - pub struct BlobAuth(pub BlobAuthResult); 586 587 - impl FromRequestParts<AppState> for BlobAuth { 588 type Rejection = AuthError; 589 590 async fn from_request_parts( 591 parts: &mut Parts, 592 state: &AppState, 593 ) -> Result<Self, Self::Rejection> { 594 - let auth_header = parts 595 - .headers 596 - .get(AUTHORIZATION) 597 - .ok_or(AuthError::MissingToken)? 598 - .to_str() 599 - .map_err(|_| AuthError::InvalidFormat)?; 600 601 - let extracted = 602 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 603 604 - if is_service_token(&extracted.token) { 605 - debug!("Verifying service token for blob upload"); 606 - let verifier = ServiceTokenVerifier::new(); 607 - let claims = verifier 608 - .verify_service_token(&extracted.token, Some("com.atproto.repo.uploadBlob")) 609 - .await 610 - .map_err(|e| { 611 - error!("Service token verification failed: {:?}", e); 612 - AuthError::AuthenticationFailed 613 - })?; 614 615 - let did: Did = claims 616 - .iss 617 - .parse() 618 - .map_err(|_| AuthError::AuthenticationFailed)?; 619 - 620 - debug!("Service token verified for DID: {}", did); 621 - return Ok(BlobAuth(BlobAuthResult::Service { did })); 622 - } 623 624 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 625 - let uri = build_full_url("/xrpc/com.atproto.repo.uploadBlob"); 626 627 - if let Ok(user) = 628 - validate_bearer_token_allow_deactivated(state.user_repo.as_ref(), &extracted.token) 629 - .await 630 - && !user.is_oauth 631 - { 632 - return if user.status.is_takendown() { 633 - Err(AuthError::AccountTakedown) 634 - } else { 635 - Ok(BlobAuth(BlobAuthResult::User(user))) 636 - }; 637 - } 638 639 - verify_oauth_token_and_build_user( 640 - state, 641 - &extracted.token, 642 - dpop_proof, 643 - "POST", 644 - &uri, 645 - StatusCheckFlags { 646 - allow_deactivated: true, 647 - allow_takendown: false, 648 - }, 649 - ) 650 - .await 651 - .map(|user| BlobAuth(BlobAuthResult::User(user))) 652 } 653 } 654 655 #[cfg(test)]
··· 1 + use std::marker::PhantomData; 2 + 3 use axum::{ 4 + extract::{FromRequestParts, OptionalFromRequestParts}, 5 http::{StatusCode, header::AUTHORIZATION, request::Parts}, 6 response::{IntoResponse, Response}, 7 }; 8 use tracing::{debug, error, info}; 9 10 use super::{ 11 + AccountStatus, AuthSource, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, 12 + is_service_token, validate_bearer_token_for_service_auth, 13 }; 14 use crate::api::error::ApiError; 15 + use crate::oauth::scopes::{RepoAction, ScopePermissions}; 16 use crate::state::AppState; 17 use crate::types::Did; 18 use crate::util::build_full_url; 19 20 #[derive(Debug)] 21 pub enum AuthError { ··· 26 AccountDeactivated, 27 AccountTakedown, 28 AdminRequired, 29 + ServiceAuthNotAllowed, 30 + SigningKeyRequired, 31 + InsufficientScope(String), 32 OAuthExpiredToken(String), 33 UseDpopNonce(String), 34 InvalidDpopProof(String), ··· 59 })), 60 ) 61 .into_response(), 62 + Self::InsufficientScope(msg) => ApiError::InsufficientScope(Some(msg)).into_response(), 63 other => ApiError::from(other).into_response(), 64 } 65 } 66 } 67 68 + pub struct ExtractedToken { 69 + pub token: String, 70 + pub is_dpop: bool, 71 } 72 73 pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option<String> { ··· 88 } 89 90 Some(token.to_string()) 91 } 92 93 pub fn extract_auth_token_from_header(auth_header: Option<&str>) -> Option<ExtractedToken> { ··· 119 None 120 } 121 122 + pub trait AuthPolicy: Send + Sync + 'static { 123 + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError>; 124 + } 125 + 126 + pub struct Permissive; 127 + 128 + impl AuthPolicy for Permissive { 129 + fn validate(_user: &AuthenticatedUser) -> Result<(), AuthError> { 130 + Ok(()) 131 + } 132 + } 133 + 134 + pub struct Active; 135 + 136 + impl AuthPolicy for Active { 137 + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> { 138 + if user.status.is_deactivated() { 139 + return Err(AuthError::AccountDeactivated); 140 + } 141 + if user.status.is_takendown() { 142 + return Err(AuthError::AccountTakedown); 143 + } 144 + Ok(()) 145 + } 146 + } 147 + 148 + pub struct NotTakendown; 149 + 150 + impl AuthPolicy for NotTakendown { 151 + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> { 152 + if user.status.is_takendown() { 153 + return Err(AuthError::AccountTakedown); 154 + } 155 + Ok(()) 156 + } 157 + } 158 + 159 + pub struct AnyUser; 160 + 161 + impl AuthPolicy for AnyUser { 162 + fn validate(_user: &AuthenticatedUser) -> Result<(), AuthError> { 163 + Ok(()) 164 + } 165 + } 166 + 167 + pub struct Admin; 168 + 169 + impl AuthPolicy for Admin { 170 + fn validate(user: &AuthenticatedUser) -> Result<(), AuthError> { 171 + if user.status.is_deactivated() { 172 + return Err(AuthError::AccountDeactivated); 173 + } 174 + if user.status.is_takendown() { 175 + return Err(AuthError::AccountTakedown); 176 + } 177 + if !user.is_admin { 178 + return Err(AuthError::AdminRequired); 179 + } 180 + Ok(()) 181 + } 182 + } 183 + 184 + impl AuthenticatedUser { 185 + pub fn require_active(&self) -> Result<&Self, ApiError> { 186 + if self.status.is_deactivated() { 187 + return Err(ApiError::AccountDeactivated); 188 + } 189 + if self.status.is_takendown() { 190 + return Err(ApiError::AccountTakedown); 191 + } 192 + Ok(self) 193 + } 194 + 195 + pub fn require_not_takendown(&self) -> Result<&Self, ApiError> { 196 + if self.status.is_takendown() { 197 + return Err(ApiError::AccountTakedown); 198 + } 199 + Ok(self) 200 + } 201 + 202 + pub fn require_admin(&self) -> Result<&Self, ApiError> { 203 + if !self.is_admin { 204 + return Err(ApiError::AdminRequired); 205 + } 206 + Ok(self) 207 + } 208 } 209 210 async fn verify_oauth_token_and_build_user( ··· 213 dpop_proof: Option<&str>, 214 method: &str, 215 uri: &str, 216 ) -> Result<AuthenticatedUser, AuthError> { 217 match crate::oauth::verify::verify_oauth_access_token( 218 state.oauth_repo.as_ref(), ··· 235 user_info.takedown_ref.as_deref(), 236 user_info.deactivated_at, 237 ); 238 Ok(AuthenticatedUser { 239 did: result.did, 240 key_bytes: user_info.key_bytes.and_then(|kb| { 241 crate::config::decrypt_key(&kb, user_info.encryption_version).ok() 242 }), 243 is_admin: user_info.is_admin, 244 status, 245 scope: result.scope, 246 controller_did: None, 247 + auth_source: AuthSource::OAuth, 248 }) 249 } 250 Err(crate::oauth::OAuthError::ExpiredToken(msg)) => Err(AuthError::OAuthExpiredToken(msg)), ··· 256 } 257 } 258 259 + async fn verify_service_token(token: &str) -> Result<ServiceTokenClaims, AuthError> { 260 + let verifier = ServiceTokenVerifier::new(); 261 + let claims = verifier 262 + .verify_service_token(token, None) 263 + .await 264 + .map_err(|e| { 265 + error!("Service token verification failed: {:?}", e); 266 + AuthError::AuthenticationFailed 267 + })?; 268 269 + debug!("Service token verified for DID: {}", claims.iss); 270 + Ok(claims) 271 + } 272 273 + enum ExtractedAuth { 274 + User(AuthenticatedUser), 275 + Service(ServiceTokenClaims), 276 + } 277 278 + async fn extract_auth_internal( 279 + parts: &mut Parts, 280 + state: &AppState, 281 + ) -> Result<ExtractedAuth, AuthError> { 282 + let auth_header = parts 283 + .headers 284 + .get(AUTHORIZATION) 285 + .ok_or(AuthError::MissingToken)? 286 + .to_str() 287 + .map_err(|_| AuthError::InvalidFormat)?; 288 289 + let extracted = 290 + extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 291 + 292 + if is_service_token(&extracted.token) { 293 + let claims = verify_service_token(&extracted.token).await?; 294 + return Ok(ExtractedAuth::Service(claims)); 295 + } 296 + 297 + let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 298 + let method = parts.method.as_str(); 299 + let uri = build_full_url(&parts.uri.to_string()); 300 + 301 + match validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &extracted.token).await { 302 + Ok(user) if !user.auth_source.is_oauth() => { 303 + return Ok(ExtractedAuth::User(user)); 304 + } 305 + Ok(_) => {} 306 + Err(super::TokenValidationError::TokenExpired) => { 307 + info!("JWT access token expired, returning ExpiredToken"); 308 + return Err(AuthError::TokenExpired); 309 } 310 + Err(_) => {} 311 + } 312 313 + let user = verify_oauth_token_and_build_user(state, &extracted.token, dpop_proof, method, &uri) 314 + .await?; 315 + Ok(ExtractedAuth::User(user)) 316 + } 317 + 318 + async fn extract_user_auth_internal( 319 + parts: &mut Parts, 320 + state: &AppState, 321 + ) -> Result<AuthenticatedUser, AuthError> { 322 + match extract_auth_internal(parts, state).await? { 323 + ExtractedAuth::User(user) => Ok(user), 324 + ExtractedAuth::Service(_) => Err(AuthError::ServiceAuthNotAllowed), 325 } 326 } 327 328 + pub struct Auth<P: AuthPolicy = Active>(pub AuthenticatedUser, PhantomData<P>); 329 330 + impl<P: AuthPolicy> Auth<P> { 331 + pub fn into_inner(self) -> AuthenticatedUser { 332 + self.0 333 + } 334 335 + pub fn needs_scope_check(&self) -> bool { 336 + self.0.is_oauth() 337 + } 338 339 + pub fn permissions(&self) -> ScopePermissions { 340 + self.0.permissions() 341 + } 342 343 + #[allow(clippy::result_large_err)] 344 + pub fn check_repo_scope(&self, action: RepoAction, collection: &str) -> Result<(), Response> { 345 + if !self.needs_scope_check() { 346 + return Ok(()); 347 } 348 + self.permissions() 349 + .assert_repo(action, collection) 350 + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 351 + } 352 + } 353 354 + impl<P: AuthPolicy> std::ops::Deref for Auth<P> { 355 + type Target = AuthenticatedUser; 356 + 357 + fn deref(&self) -> &Self::Target { 358 + &self.0 359 } 360 } 361 362 + impl<P: AuthPolicy> FromRequestParts<AppState> for Auth<P> { 363 type Rejection = AuthError; 364 365 async fn from_request_parts( 366 parts: &mut Parts, 367 state: &AppState, 368 ) -> Result<Self, Self::Rejection> { 369 + let user = extract_user_auth_internal(parts, state).await?; 370 + P::validate(&user)?; 371 + Ok(Auth(user, PhantomData)) 372 } 373 } 374 375 + impl<P: AuthPolicy> OptionalFromRequestParts<AppState> for Auth<P> { 376 type Rejection = AuthError; 377 378 async fn from_request_parts( 379 parts: &mut Parts, 380 state: &AppState, 381 + ) -> Result<Option<Self>, Self::Rejection> { 382 + match extract_user_auth_internal(parts, state).await { 383 + Ok(user) => { 384 + P::validate(&user)?; 385 + Ok(Some(Auth(user, PhantomData))) 386 } 387 + Err(AuthError::MissingToken) => Ok(None), 388 + Err(e) => Err(e), 389 } 390 + } 391 + } 392 393 + pub struct ServiceAuth { 394 + pub did: Did, 395 + pub claims: ServiceTokenClaims, 396 + } 397 398 + impl ServiceAuth { 399 + pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> { 400 + match &self.claims.lxm { 401 + Some(lxm) if lxm == "*" || lxm == expected_lxm => Ok(()), 402 + Some(lxm) => Err(ApiError::AuthorizationError(format!( 403 + "Token lxm '{}' does not permit '{}'", 404 + lxm, expected_lxm 405 + ))), 406 + None => Err(ApiError::AuthorizationError( 407 + "Token missing lxm claim".to_string(), 408 + )), 409 } 410 } 411 } 412 413 + impl FromRequestParts<AppState> for ServiceAuth { 414 type Rejection = AuthError; 415 416 async fn from_request_parts( 417 parts: &mut Parts, 418 state: &AppState, 419 ) -> Result<Self, Self::Rejection> { 420 + match extract_auth_internal(parts, state).await? { 421 + ExtractedAuth::Service(claims) => { 422 + let did: Did = claims 423 + .iss 424 + .parse() 425 + .map_err(|_| AuthError::AuthenticationFailed)?; 426 + Ok(ServiceAuth { did, claims }) 427 + } 428 + ExtractedAuth::User(_) => Err(AuthError::AuthenticationFailed), 429 + } 430 + } 431 + } 432 433 + pub enum AuthAny<P: AuthPolicy = Active> { 434 + User(Auth<P>), 435 + Service(ServiceAuth), 436 + } 437 438 + impl<P: AuthPolicy> AuthAny<P> { 439 + pub fn did(&self) -> &Did { 440 + match self { 441 + Self::User(auth) => &auth.did, 442 + Self::Service(auth) => &auth.did, 443 + } 444 + } 445 + 446 + pub fn as_user(&self) -> Option<&Auth<P>> { 447 + match self { 448 + Self::User(auth) => Some(auth), 449 + Self::Service(_) => None, 450 + } 451 + } 452 453 + pub fn as_service(&self) -> Option<&ServiceAuth> { 454 + match self { 455 + Self::User(_) => None, 456 + Self::Service(auth) => Some(auth), 457 } 458 + } 459 460 + pub fn is_service(&self) -> bool { 461 + matches!(self, Self::Service(_)) 462 } 463 464 + pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> { 465 + match self { 466 + Self::User(_) => Ok(()), 467 + Self::Service(auth) => auth.require_lxm(expected_lxm), 468 + } 469 + } 470 } 471 472 + impl<P: AuthPolicy> FromRequestParts<AppState> for AuthAny<P> { 473 type Rejection = AuthError; 474 475 async fn from_request_parts( 476 parts: &mut Parts, 477 + state: &AppState, 478 ) -> Result<Self, Self::Rejection> { 479 + match extract_auth_internal(parts, state).await? { 480 + ExtractedAuth::User(user) => { 481 + P::validate(&user)?; 482 + Ok(AuthAny::User(Auth(user, PhantomData))) 483 + } 484 + ExtractedAuth::Service(claims) => { 485 + let did: Did = claims 486 + .iss 487 + .parse() 488 + .map_err(|_| AuthError::AuthenticationFailed)?; 489 + Ok(AuthAny::Service(ServiceAuth { did, claims })) 490 + } 491 } 492 } 493 } 494 495 + impl<P: AuthPolicy> OptionalFromRequestParts<AppState> for AuthAny<P> { 496 + type Rejection = AuthError; 497 498 async fn from_request_parts( 499 parts: &mut Parts, 500 + state: &AppState, 501 + ) -> Result<Option<Self>, Self::Rejection> { 502 + match extract_auth_internal(parts, state).await { 503 + Ok(ExtractedAuth::User(user)) => { 504 + P::validate(&user)?; 505 + Ok(Some(AuthAny::User(Auth(user, PhantomData)))) 506 } 507 + Ok(ExtractedAuth::Service(claims)) => { 508 + let did: Did = claims 509 + .iss 510 + .parse() 511 + .map_err(|_| AuthError::AuthenticationFailed)?; 512 + Ok(Some(AuthAny::Service(ServiceAuth { did, claims }))) 513 } 514 + Err(AuthError::MissingToken) => Ok(None), 515 + Err(e) => Err(e), 516 } 517 } 518 } 519 520 + pub struct SigningAuth<P: AuthPolicy = Active> { 521 + pub did: Did, 522 + pub key_bytes: Vec<u8>, 523 + pub is_admin: bool, 524 + pub status: AccountStatus, 525 + pub scope: Option<String>, 526 + pub controller_did: Option<Did>, 527 + is_oauth: bool, 528 + _policy: PhantomData<P>, 529 } 530 531 + impl<P: AuthPolicy> SigningAuth<P> { 532 + pub fn needs_scope_check(&self) -> bool { 533 + self.is_oauth 534 + } 535 536 + pub fn permissions(&self) -> ScopePermissions { 537 + if let Some(ref scope) = self.scope 538 + && scope != super::SCOPE_ACCESS 539 + { 540 + return ScopePermissions::from_scope_string(Some(scope)); 541 + } 542 + if !self.is_oauth { 543 + return ScopePermissions::from_scope_string(Some("atproto")); 544 + } 545 + ScopePermissions::from_scope_string(self.scope.as_deref()) 546 + } 547 + 548 + #[allow(clippy::result_large_err)] 549 + pub fn check_repo_scope(&self, action: RepoAction, collection: &str) -> Result<(), Response> { 550 + if !self.needs_scope_check() { 551 + return Ok(()); 552 + } 553 + self.permissions() 554 + .assert_repo(action, collection) 555 + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 556 + } 557 + } 558 + 559 + impl<P: AuthPolicy> FromRequestParts<AppState> for SigningAuth<P> { 560 type Rejection = AuthError; 561 562 async fn from_request_parts( 563 parts: &mut Parts, 564 state: &AppState, 565 ) -> Result<Self, Self::Rejection> { 566 + let user = extract_user_auth_internal(parts, state).await?; 567 + P::validate(&user)?; 568 569 + let key_bytes = match user.key_bytes { 570 + Some(kb) => kb, 571 + None => { 572 + let user_with_key = state 573 + .user_repo 574 + .get_with_key_by_did(&user.did) 575 + .await 576 + .ok() 577 + .flatten() 578 + .ok_or(AuthError::SigningKeyRequired)?; 579 + crate::config::decrypt_key( 580 + &user_with_key.key_bytes, 581 + user_with_key.encryption_version, 582 + ) 583 + .map_err(|_| AuthError::SigningKeyRequired)? 584 + } 585 + }; 586 587 + Ok(SigningAuth { 588 + did: user.did, 589 + key_bytes, 590 + is_admin: user.is_admin, 591 + status: user.status, 592 + scope: user.scope, 593 + controller_did: user.controller_did, 594 + is_oauth: user.auth_source.is_oauth(), 595 + _policy: PhantomData, 596 + }) 597 + } 598 + } 599 600 + #[cfg(test)] 601 + fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> { 602 + let auth_header = auth_header.trim(); 603 604 + if auth_header.len() < 8 { 605 + return Err(AuthError::InvalidFormat); 606 + } 607 608 + let prefix = &auth_header[..7]; 609 + if !prefix.eq_ignore_ascii_case("bearer ") { 610 + return Err(AuthError::InvalidFormat); 611 + } 612 613 + let token = auth_header[7..].trim(); 614 + if token.is_empty() { 615 + return Err(AuthError::InvalidFormat); 616 } 617 + 618 + Ok(token) 619 } 620 621 #[cfg(test)]
+75 -8
crates/tranquil-pds/src/auth/mod.rs
··· 3 use std::time::Duration; 4 5 use crate::AccountStatus; 6 use crate::cache::Cache; 7 use crate::oauth::scopes::ScopePermissions; 8 use crate::types::Did; ··· 16 pub mod webauthn; 17 18 pub use extractor::{ 19 - AuthError, BearerAuth, BearerAuthAdmin, BearerAuthAllowDeactivated, BlobAuth, BlobAuthResult, 20 - ExtractedToken, OptionalBearerAuth, OptionalServiceAuth, ServiceAuth, 21 - extract_auth_token_from_header, extract_bearer_token_from_header, 22 }; 23 pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; 24 ··· 94 } 95 } 96 97 pub struct AuthenticatedUser { 98 pub did: Did, 99 pub key_bytes: Option<Vec<u8>>, 100 - pub is_oauth: bool, 101 pub is_admin: bool, 102 pub status: AccountStatus, 103 pub scope: Option<String>, 104 pub controller_did: Option<Did>, 105 } 106 107 impl AuthenticatedUser { ··· 111 { 112 return ScopePermissions::from_scope_string(Some(scope)); 113 } 114 - if !self.is_oauth { 115 return ScopePermissions::from_scope_string(Some("atproto")); 116 } 117 ScopePermissions::from_scope_string(self.scope.as_deref()) ··· 349 return Ok(AuthenticatedUser { 350 did: did.clone(), 351 key_bytes: Some(decrypted_key), 352 - is_oauth: false, 353 is_admin, 354 status, 355 scope: token_data.claims.scope.clone(), 356 controller_did, 357 }); 358 } 359 } ··· 397 return Ok(AuthenticatedUser { 398 did: Did::new_unchecked(oauth_token.did), 399 key_bytes, 400 - is_oauth: true, 401 is_admin: oauth_token.is_admin, 402 status, 403 scope: oauth_info.scope, 404 controller_did: oauth_info.controller_did.map(Did::new_unchecked), 405 }); 406 } else { 407 return Err(TokenValidationError::TokenExpired); ··· 481 Ok(AuthenticatedUser { 482 did: Did::new_unchecked(result.did), 483 key_bytes, 484 - is_oauth: true, 485 is_admin: user_info.is_admin, 486 status, 487 scope: result.scope, 488 controller_did: None, 489 }) 490 } 491 Err(crate::oauth::OAuthError::ExpiredToken(_)) => {
··· 3 use std::time::Duration; 4 5 use crate::AccountStatus; 6 + use crate::api::ApiError; 7 use crate::cache::Cache; 8 use crate::oauth::scopes::ScopePermissions; 9 use crate::types::Did; ··· 17 pub mod webauthn; 18 19 pub use extractor::{ 20 + Active, Admin, AnyUser, Auth, AuthAny, AuthError, AuthPolicy, ExtractedToken, NotTakendown, 21 + Permissive, ServiceAuth, SigningAuth, extract_auth_token_from_header, 22 + extract_bearer_token_from_header, 23 }; 24 pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; 25 ··· 95 } 96 } 97 98 + pub enum AuthSource { 99 + Session, 100 + OAuth, 101 + Service { claims: ServiceTokenClaims }, 102 + } 103 + 104 + impl AuthSource { 105 + pub fn is_oauth(&self) -> bool { 106 + matches!(self, Self::OAuth) 107 + } 108 + 109 + pub fn is_service(&self) -> bool { 110 + matches!(self, Self::Service { .. }) 111 + } 112 + 113 + pub fn service_claims(&self) -> Option<&ServiceTokenClaims> { 114 + match self { 115 + Self::Service { claims } => Some(claims), 116 + _ => None, 117 + } 118 + } 119 + } 120 + 121 pub struct AuthenticatedUser { 122 pub did: Did, 123 pub key_bytes: Option<Vec<u8>>, 124 pub is_admin: bool, 125 pub status: AccountStatus, 126 pub scope: Option<String>, 127 pub controller_did: Option<Did>, 128 + pub auth_source: AuthSource, 129 + } 130 + 131 + impl AuthenticatedUser { 132 + pub fn is_oauth(&self) -> bool { 133 + self.auth_source.is_oauth() 134 + } 135 + 136 + pub fn is_service(&self) -> bool { 137 + self.auth_source.is_service() 138 + } 139 + 140 + pub fn service_claims(&self) -> Option<&ServiceTokenClaims> { 141 + self.auth_source.service_claims() 142 + } 143 + 144 + pub fn require_lxm(&self, expected_lxm: &str) -> Result<(), ApiError> { 145 + match self.auth_source.service_claims() { 146 + Some(claims) => match &claims.lxm { 147 + Some(lxm) if lxm == "*" || lxm == expected_lxm => Ok(()), 148 + Some(lxm) => Err(ApiError::AuthorizationError(format!( 149 + "Token lxm '{}' does not permit '{}'", 150 + lxm, expected_lxm 151 + ))), 152 + None => Err(ApiError::AuthorizationError( 153 + "Token missing lxm claim".to_string(), 154 + )), 155 + }, 156 + None => Ok(()), 157 + } 158 + } 159 + 160 + pub fn require_user(&self) -> Result<&Self, ApiError> { 161 + if self.is_service() { 162 + return Err(ApiError::AuthenticationFailed(Some( 163 + "User authentication required".to_string(), 164 + ))); 165 + } 166 + Ok(self) 167 + } 168 + 169 + pub fn as_user(&self) -> Option<&Self> { 170 + if self.is_service() { None } else { Some(self) } 171 + } 172 } 173 174 impl AuthenticatedUser { ··· 178 { 179 return ScopePermissions::from_scope_string(Some(scope)); 180 } 181 + if !self.is_oauth() { 182 return ScopePermissions::from_scope_string(Some("atproto")); 183 } 184 ScopePermissions::from_scope_string(self.scope.as_deref()) ··· 416 return Ok(AuthenticatedUser { 417 did: did.clone(), 418 key_bytes: Some(decrypted_key), 419 is_admin, 420 status, 421 scope: token_data.claims.scope.clone(), 422 controller_did, 423 + auth_source: AuthSource::Session, 424 }); 425 } 426 } ··· 464 return Ok(AuthenticatedUser { 465 did: Did::new_unchecked(oauth_token.did), 466 key_bytes, 467 is_admin: oauth_token.is_admin, 468 status, 469 scope: oauth_info.scope, 470 controller_did: oauth_info.controller_did.map(Did::new_unchecked), 471 + auth_source: AuthSource::OAuth, 472 }); 473 } else { 474 return Err(TokenValidationError::TokenExpired); ··· 548 Ok(AuthenticatedUser { 549 did: Did::new_unchecked(result.did), 550 key_bytes, 551 is_admin: user_info.is_admin, 552 status, 553 scope: result.scope, 554 controller_did: None, 555 + auth_source: AuthSource::OAuth, 556 }) 557 } 558 Err(crate::oauth::OAuthError::ExpiredToken(_)) => {
+13 -6
crates/tranquil-pds/src/oauth/endpoints/authorize.rs
··· 1 use crate::comms::{channel_display_name, comms_repo::enqueue_2fa_code}; 2 use crate::oauth::{ 3 AuthFlowState, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, SessionId, 4 - db::should_show_consent, 5 - scopes::expand_include_scopes, 6 }; 7 use crate::state::{AppState, RateLimitKind}; 8 use crate::types::{Did, Handle, PlainPassword}; ··· 3645 pub async fn establish_session( 3646 State(state): State<AppState>, 3647 headers: HeaderMap, 3648 - auth: crate::auth::BearerAuth, 3649 ) -> Response { 3650 - let did = &auth.0.did; 3651 3652 let existing_device = extract_device_cookie(&headers); 3653 ··· 3670 }; 3671 let device_typed = DeviceIdType::from(new_id.0.clone()); 3672 3673 - if let Err(e) = state.oauth_repo.create_device(&device_typed, &device_data).await { 3674 tracing::error!(error = ?e, "Failed to create device"); 3675 return ( 3676 StatusCode::INTERNAL_SERVER_ERROR, ··· 3682 .into_response(); 3683 } 3684 3685 - if let Err(e) = state.oauth_repo.upsert_account_device(did, &device_typed).await { 3686 tracing::error!(error = ?e, "Failed to link device to account"); 3687 return ( 3688 StatusCode::INTERNAL_SERVER_ERROR,
··· 1 use crate::comms::{channel_display_name, comms_repo::enqueue_2fa_code}; 2 use crate::oauth::{ 3 AuthFlowState, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, SessionId, 4 + db::should_show_consent, scopes::expand_include_scopes, 5 }; 6 use crate::state::{AppState, RateLimitKind}; 7 use crate::types::{Did, Handle, PlainPassword}; ··· 3644 pub async fn establish_session( 3645 State(state): State<AppState>, 3646 headers: HeaderMap, 3647 + auth: crate::auth::Auth<crate::auth::Active>, 3648 ) -> Response { 3649 + let did = &auth.did; 3650 3651 let existing_device = extract_device_cookie(&headers); 3652 ··· 3669 }; 3670 let device_typed = DeviceIdType::from(new_id.0.clone()); 3671 3672 + if let Err(e) = state 3673 + .oauth_repo 3674 + .create_device(&device_typed, &device_data) 3675 + .await 3676 + { 3677 tracing::error!(error = ?e, "Failed to create device"); 3678 return ( 3679 StatusCode::INTERNAL_SERVER_ERROR, ··· 3685 .into_response(); 3686 } 3687 3688 + if let Err(e) = state 3689 + .oauth_repo 3690 + .upsert_account_device(did, &device_typed) 3691 + .await 3692 + { 3693 tracing::error!(error = ?e, "Failed to link device to account"); 3694 return ( 3695 StatusCode::INTERNAL_SERVER_ERROR,
+7 -7
crates/tranquil-pds/src/oauth/endpoints/delegation.rs
··· 1 - use crate::auth::BearerAuth; 2 use crate::delegation::DelegationActionType; 3 use crate::state::{AppState, RateLimitKind}; 4 use crate::types::PlainPassword; ··· 463 pub async fn delegation_auth_token( 464 State(state): State<AppState>, 465 headers: HeaderMap, 466 - auth: BearerAuth, 467 Json(form): Json<DelegationTokenAuthSubmit>, 468 ) -> Response { 469 - let controller_did = auth.0.did; 470 471 let delegated_did: Did = match form.delegated_did.parse() { 472 Ok(d) => d, ··· 510 511 let grant = match state 512 .delegation_repo 513 - .get_delegation(&delegated_did, &controller_did) 514 .await 515 { 516 Ok(Some(g)) => g, ··· 551 552 if state 553 .oauth_repo 554 - .set_controller_did(&request_id, &controller_did) 555 .await 556 .is_err() 557 { ··· 574 .delegation_repo 575 .log_delegation_action( 576 &delegated_did, 577 - &controller_did, 578 - Some(&controller_did), 579 DelegationActionType::TokenIssued, 580 Some(serde_json::json!({ 581 "client_id": request.client_id,
··· 1 + use crate::auth::{Active, Auth}; 2 use crate::delegation::DelegationActionType; 3 use crate::state::{AppState, RateLimitKind}; 4 use crate::types::PlainPassword; ··· 463 pub async fn delegation_auth_token( 464 State(state): State<AppState>, 465 headers: HeaderMap, 466 + auth: Auth<Active>, 467 Json(form): Json<DelegationTokenAuthSubmit>, 468 ) -> Response { 469 + let controller_did = &auth.did; 470 471 let delegated_did: Did = match form.delegated_did.parse() { 472 Ok(d) => d, ··· 510 511 let grant = match state 512 .delegation_repo 513 + .get_delegation(&delegated_did, controller_did) 514 .await 515 { 516 Ok(Some(g)) => g, ··· 551 552 if state 553 .oauth_repo 554 + .set_controller_did(&request_id, controller_did) 555 .await 556 .is_err() 557 { ··· 574 .delegation_repo 575 .log_delegation_action( 576 &delegated_did, 577 + controller_did, 578 + Some(controller_did), 579 DelegationActionType::TokenIssued, 580 Some(serde_json::json!({ 581 "client_id": request.client_id,
+1 -1
crates/tranquil-pds/src/oauth/verify.rs
··· 396 token: &str, 397 ) -> Result<LegacyAuthResult, ()> { 398 match crate::auth::validate_bearer_token(user_repo, token).await { 399 - Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did }), 400 _ => Err(()), 401 } 402 }
··· 396 token: &str, 397 ) -> Result<LegacyAuthResult, ()> { 398 match crate::auth::validate_bearer_token(user_repo, token).await { 399 + Ok(user) if !user.is_oauth() => Ok(LegacyAuthResult { did: user.did }), 400 _ => Err(()), 401 } 402 }
+2 -2
crates/tranquil-pds/src/sso/endpoints.rs
··· 644 645 pub async fn get_linked_accounts( 646 State(state): State<AppState>, 647 - crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth, 648 ) -> Result<Json<LinkedAccountsResponse>, ApiError> { 649 let identities = state 650 .sso_repo ··· 679 680 pub async fn unlink_account( 681 State(state): State<AppState>, 682 - crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth, 683 Json(input): Json<UnlinkAccountRequest>, 684 ) -> Result<Json<UnlinkAccountResponse>, ApiError> { 685 if !state
··· 644 645 pub async fn get_linked_accounts( 646 State(state): State<AppState>, 647 + auth: crate::auth::Auth<crate::auth::Active>, 648 ) -> Result<Json<LinkedAccountsResponse>, ApiError> { 649 let identities = state 650 .sso_repo ··· 679 680 pub async fn unlink_account( 681 State(state): State<AppState>, 682 + auth: crate::auth::Auth<crate::auth::Active>, 683 Json(input): Json<UnlinkAccountRequest>, 684 ) -> Result<Json<UnlinkAccountResponse>, ApiError> { 685 if !state
+55 -35
crates/tranquil-scopes/src/permission_set.rs
··· 149 return Err(format!("Invalid NSID format: {}", nsid)); 150 } 151 152 - let authority = parts[..2].iter().rev().cloned().collect::<Vec<_>>().join("."); 153 debug!(nsid, authority = %authority, "Resolving lexicon DID authority via DNS"); 154 155 let did = resolve_lexicon_did_authority(&authority).await?; ··· 279 ) -> String { 280 let mut scopes: Vec<String> = Vec::new(); 281 282 - permissions.iter().for_each(|perm| match perm.resource.as_str() { 283 - "repo" => { 284 - if let Some(collections) = &perm.collection { 285 - let actions: Vec<&str> = perm 286 - .action 287 - .as_ref() 288 - .map(|a| a.iter().map(String::as_str).collect()) 289 - .unwrap_or_else(|| DEFAULT_ACTIONS.to_vec()); 290 291 - collections 292 - .iter() 293 - .filter(|coll| is_under_authority(coll, namespace_authority)) 294 - .for_each(|coll| { 295 - actions.iter().for_each(|action| { 296 - scopes.push(format!("repo:{}?action={}", coll, action)); 297 }); 298 - }); 299 } 300 - } 301 - "rpc" => { 302 - if let Some(lxms) = &perm.lxm { 303 - let perm_aud = perm.aud.as_deref().or(default_aud); 304 305 - lxms.iter().for_each(|lxm| { 306 - let scope = match perm_aud { 307 - Some(aud) => format!("rpc:{}?aud={}", lxm, aud), 308 - None => format!("rpc:{}", lxm), 309 - }; 310 - scopes.push(scope); 311 - }); 312 } 313 - } 314 - _ => {} 315 - }); 316 317 scopes.join(" ") 318 } ··· 334 335 #[test] 336 fn test_parse_include_scope_with_multiple_params() { 337 - let (nsid, aud) = parse_include_scope("io.atcr.authFullApp?foo=bar&aud=did:web:example.com&baz=qux"); 338 assert_eq!(nsid, "io.atcr.authFullApp"); 339 assert_eq!(aud, Some("did:web:example.com")); 340 } ··· 443 aud: None, 444 }]; 445 446 - let expanded = build_expanded_scopes(&permissions, Some("did:web:api.example.com"), "io.atcr"); 447 assert!(expanded.contains("rpc:io.atcr.getManifest?aud=did:web:api.example.com")); 448 } 449 ··· 583 cache_key.to_string(), 584 CachedLexicon { 585 expanded_scope: "old_value".to_string(), 586 - cached_at: std::time::Instant::now() - std::time::Duration::from_secs(CACHE_TTL_SECS + 1), 587 }, 588 ); 589 } ··· 601 fn test_nsid_authority_extraction_for_dns() { 602 let nsid = "io.atcr.authFullApp"; 603 let parts: Vec<&str> = nsid.split('.').collect(); 604 - let authority = parts[..2].iter().rev().cloned().collect::<Vec<_>>().join("."); 605 assert_eq!(authority, "atcr.io"); 606 607 let nsid2 = "app.bsky.feed.post"; 608 let parts2: Vec<&str> = nsid2.split('.').collect(); 609 - let authority2 = parts2[..2].iter().rev().cloned().collect::<Vec<_>>().join("."); 610 assert_eq!(authority2, "bsky.app"); 611 } 612 }
··· 149 return Err(format!("Invalid NSID format: {}", nsid)); 150 } 151 152 + let authority = parts[..2] 153 + .iter() 154 + .rev() 155 + .cloned() 156 + .collect::<Vec<_>>() 157 + .join("."); 158 debug!(nsid, authority = %authority, "Resolving lexicon DID authority via DNS"); 159 160 let did = resolve_lexicon_did_authority(&authority).await?; ··· 284 ) -> String { 285 let mut scopes: Vec<String> = Vec::new(); 286 287 + permissions 288 + .iter() 289 + .for_each(|perm| match perm.resource.as_str() { 290 + "repo" => { 291 + if let Some(collections) = &perm.collection { 292 + let actions: Vec<&str> = perm 293 + .action 294 + .as_ref() 295 + .map(|a| a.iter().map(String::as_str).collect()) 296 + .unwrap_or_else(|| DEFAULT_ACTIONS.to_vec()); 297 298 + collections 299 + .iter() 300 + .filter(|coll| is_under_authority(coll, namespace_authority)) 301 + .for_each(|coll| { 302 + actions.iter().for_each(|action| { 303 + scopes.push(format!("repo:{}?action={}", coll, action)); 304 + }); 305 }); 306 + } 307 } 308 + "rpc" => { 309 + if let Some(lxms) = &perm.lxm { 310 + let perm_aud = perm.aud.as_deref().or(default_aud); 311 312 + lxms.iter().for_each(|lxm| { 313 + let scope = match perm_aud { 314 + Some(aud) => format!("rpc:{}?aud={}", lxm, aud), 315 + None => format!("rpc:{}", lxm), 316 + }; 317 + scopes.push(scope); 318 + }); 319 + } 320 } 321 + _ => {} 322 + }); 323 324 scopes.join(" ") 325 } ··· 341 342 #[test] 343 fn test_parse_include_scope_with_multiple_params() { 344 + let (nsid, aud) = 345 + parse_include_scope("io.atcr.authFullApp?foo=bar&aud=did:web:example.com&baz=qux"); 346 assert_eq!(nsid, "io.atcr.authFullApp"); 347 assert_eq!(aud, Some("did:web:example.com")); 348 } ··· 451 aud: None, 452 }]; 453 454 + let expanded = 455 + build_expanded_scopes(&permissions, Some("did:web:api.example.com"), "io.atcr"); 456 assert!(expanded.contains("rpc:io.atcr.getManifest?aud=did:web:api.example.com")); 457 } 458 ··· 592 cache_key.to_string(), 593 CachedLexicon { 594 expanded_scope: "old_value".to_string(), 595 + cached_at: std::time::Instant::now() 596 + - std::time::Duration::from_secs(CACHE_TTL_SECS + 1), 597 }, 598 ); 599 } ··· 611 fn test_nsid_authority_extraction_for_dns() { 612 let nsid = "io.atcr.authFullApp"; 613 let parts: Vec<&str> = nsid.split('.').collect(); 614 + let authority = parts[..2] 615 + .iter() 616 + .rev() 617 + .cloned() 618 + .collect::<Vec<_>>() 619 + .join("."); 620 assert_eq!(authority, "atcr.io"); 621 622 let nsid2 = "app.bsky.feed.post"; 623 let parts2: Vec<&str> = nsid2.split('.').collect(); 624 + let authority2 = parts2[..2] 625 + .iter() 626 + .rev() 627 + .cloned() 628 + .collect::<Vec<_>>() 629 + .join("."); 630 assert_eq!(authority2, "bsky.app"); 631 } 632 }
-1
crates/tranquil-scopes/src/permissions.rs
··· 556 "app.bsky.feed.getAuthorFeed" 557 )); 558 } 559 - 560 }
··· 556 "app.bsky.feed.getAuthorFeed" 557 )); 558 } 559 }