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 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAllowDeactivated; 2 + use crate::auth::{Active, Auth}; 3 3 use crate::state::AppState; 4 4 use axum::{ 5 5 Json, ··· 32 32 pub struct GetPreferencesOutput { 33 33 pub preferences: Vec<Value>, 34 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 { 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 { 42 38 Ok(Some(id)) => id, 43 39 _ => { 44 40 return ApiError::InternalError(Some("User not found".into())).into_response(); ··· 93 89 } 94 90 pub async fn put_preferences( 95 91 State(state): State<AppState>, 96 - auth: BearerAuthAllowDeactivated, 92 + auth: Auth<Active>, 97 93 Json(input): Json<PutPreferencesInput>, 98 94 ) -> 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 { 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 { 102 97 Ok(Some(id)) => id, 103 98 _ => { 104 99 return ApiError::InternalError(Some("User not found".into())).into_response();
+20 -18
crates/tranquil-pds/src/api/admin/account/delete.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAdmin; 3 + use crate::auth::{Admin, Auth}; 4 4 use crate::state::AppState; 5 5 use crate::types::Did; 6 6 use axum::{ ··· 18 18 19 19 pub async fn delete_account( 20 20 State(state): State<AppState>, 21 - _auth: BearerAuthAdmin, 21 + _auth: Auth<Admin>, 22 22 Json(input): Json<DeleteAccountInput>, 23 - ) -> Response { 23 + ) -> Result<Response, ApiError> { 24 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) => { 25 + let (user_id, handle) = state 26 + .user_repo 27 + .get_id_and_handle_by_did(did) 28 + .await 29 + .map_err(|e| { 31 30 error!("DB error in delete_account: {:?}", e); 32 - return ApiError::InternalError(None).into_response(); 33 - } 34 - }; 35 - if let Err(e) = state 31 + ApiError::InternalError(None) 32 + })? 33 + .ok_or(ApiError::AccountNotFound) 34 + .map(|row| (row.id, row.handle))?; 35 + 36 + state 36 37 .user_repo 37 38 .admin_delete_account_complete(user_id, did) 38 39 .await 39 - { 40 - error!("Failed to delete account {}: {:?}", did, e); 41 - return ApiError::InternalError(Some("Failed to delete account".into())).into_response(); 42 - } 40 + .map_err(|e| { 41 + error!("Failed to delete account {}: {:?}", did, e); 42 + ApiError::InternalError(Some("Failed to delete account".into())) 43 + })?; 44 + 43 45 if let Err(e) = 44 46 crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await 45 47 { ··· 49 51 ); 50 52 } 51 53 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 52 - EmptyResponse::ok().into_response() 54 + Ok(EmptyResponse::ok().into_response()) 53 55 }
+16 -21
crates/tranquil-pds/src/api/admin/account/email.rs
··· 1 1 use crate::api::error::{ApiError, AtpJson}; 2 - use crate::auth::BearerAuthAdmin; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use crate::types::Did; 5 5 use axum::{ ··· 28 28 29 29 pub async fn send_email( 30 30 State(state): State<AppState>, 31 - _auth: BearerAuthAdmin, 31 + _auth: Auth<Admin>, 32 32 AtpJson(input): AtpJson<SendEmailInput>, 33 - ) -> Response { 33 + ) -> Result<Response, ApiError> { 34 34 let content = input.content.trim(); 35 35 if content.is_empty() { 36 - return ApiError::InvalidRequest("content is required".into()).into_response(); 36 + return Err(ApiError::InvalidRequest("content is required".into())); 37 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) => { 38 + let user = state 39 + .user_repo 40 + .get_by_did(&input.recipient_did) 41 + .await 42 + .map_err(|e| { 44 43 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 - }; 44 + ApiError::InternalError(None) 45 + })? 46 + .ok_or(ApiError::AccountNotFound)?; 47 + 48 + let email = user.email.ok_or(ApiError::NoEmail)?; 54 49 let (user_id, handle) = (user.id, user.handle); 55 50 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 56 51 let subject = input ··· 76 71 handle, 77 72 input.recipient_did 78 73 ); 79 - (StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response() 74 + Ok((StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()) 80 75 } 81 76 Err(e) => { 82 77 warn!("Failed to enqueue admin email: {:?}", e); 83 - (StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response() 78 + Ok((StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response()) 84 79 } 85 80 } 86 81 }
+18 -24
crates/tranquil-pds/src/api/admin/account/info.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use crate::types::{Did, Handle}; 5 5 use axum::{ ··· 67 67 68 68 pub async fn get_account_info( 69 69 State(state): State<AppState>, 70 - _auth: BearerAuthAdmin, 70 + _auth: Auth<Admin>, 71 71 Query(params): Query<GetAccountInfoParams>, 72 - ) -> Response { 73 - let account = match state 72 + ) -> Result<Response, ApiError> { 73 + let account = state 74 74 .infra_repo 75 75 .get_admin_account_info_by_did(&params.did) 76 76 .await 77 - { 78 - Ok(Some(a)) => a, 79 - Ok(None) => return ApiError::AccountNotFound.into_response(), 80 - Err(e) => { 77 + .map_err(|e| { 81 78 error!("DB error in get_account_info: {:?}", e); 82 - return ApiError::InternalError(None).into_response(); 83 - } 84 - }; 79 + ApiError::InternalError(None) 80 + })? 81 + .ok_or(ApiError::AccountNotFound)?; 85 82 86 83 let invited_by = get_invited_by(&state, account.id).await; 87 84 let invites = get_invites_for_user(&state, account.id).await; 88 85 89 - ( 86 + Ok(( 90 87 StatusCode::OK, 91 88 Json(AccountInfo { 92 89 did: account.did, ··· 105 102 invites, 106 103 }), 107 104 ) 108 - .into_response() 105 + .into_response()) 109 106 } 110 107 111 108 async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> { ··· 200 197 201 198 pub async fn get_account_infos( 202 199 State(state): State<AppState>, 203 - _auth: BearerAuthAdmin, 200 + _auth: Auth<Admin>, 204 201 RawQuery(raw_query): RawQuery, 205 - ) -> Response { 202 + ) -> Result<Response, ApiError> { 206 203 let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids") 207 204 .into_iter() 208 205 .filter(|d| !d.is_empty()) 209 206 .collect(); 210 207 211 208 if dids.is_empty() { 212 - return ApiError::InvalidRequest("dids is required".into()).into_response(); 209 + return Err(ApiError::InvalidRequest("dids is required".into())); 213 210 } 214 211 215 212 let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect(); 216 - let accounts = match state 213 + let accounts = state 217 214 .infra_repo 218 215 .get_admin_account_infos_by_dids(&dids_typed) 219 216 .await 220 - { 221 - Ok(accounts) => accounts, 222 - Err(e) => { 217 + .map_err(|e| { 223 218 error!("Failed to fetch account infos: {:?}", e); 224 - return ApiError::InternalError(None).into_response(); 225 - } 226 - }; 219 + ApiError::InternalError(None) 220 + })?; 227 221 228 222 let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect(); 229 223 ··· 316 310 }) 317 311 .collect(); 318 312 319 - (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response() 313 + Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()) 320 314 }
+39 -42
crates/tranquil-pds/src/api/admin/account/search.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use crate::types::{Did, Handle}; 5 5 use axum::{ ··· 50 50 51 51 pub async fn search_accounts( 52 52 State(state): State<AppState>, 53 - _auth: BearerAuthAdmin, 53 + _auth: Auth<Admin>, 54 54 Query(params): Query<SearchAccountsParams>, 55 - ) -> Response { 55 + ) -> Result<Response, ApiError> { 56 56 let limit = params.limit.clamp(1, 100); 57 57 let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); 58 58 let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); 59 59 let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|c| c.parse().ok()); 60 - let result = state 60 + let rows = state 61 61 .user_repo 62 62 .search_accounts( 63 63 cursor_did.as_ref(), ··· 65 65 handle_filter.as_deref(), 66 66 limit + 1, 67 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()) 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()) 91 85 } else { 92 86 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 - } 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()) 108 105 }
+39 -36
crates/tranquil-pds/src/api/admin/account/update.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAdmin; 3 + use crate::auth::{Admin, Auth}; 4 4 use crate::state::AppState; 5 5 use crate::types::{Did, Handle, PlainPassword}; 6 6 use axum::{ ··· 19 19 20 20 pub async fn update_account_email( 21 21 State(state): State<AppState>, 22 - _auth: BearerAuthAdmin, 22 + _auth: Auth<Admin>, 23 23 Json(input): Json<UpdateAccountEmailInput>, 24 - ) -> Response { 24 + ) -> Result<Response, ApiError> { 25 25 let account = input.account.trim(); 26 26 let email = input.email.trim(); 27 27 if account.is_empty() || email.is_empty() { 28 - return ApiError::InvalidRequest("account and email are required".into()).into_response(); 28 + return Err(ApiError::InvalidRequest( 29 + "account and email are required".into(), 30 + )); 29 31 } 30 - let account_did: Did = match account.parse() { 31 - Ok(d) => d, 32 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 33 - }; 32 + let account_did: Did = account 33 + .parse() 34 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 35 + 34 36 match state 35 37 .user_repo 36 38 .admin_update_email(&account_did, email) 37 39 .await 38 40 { 39 - Ok(0) => ApiError::AccountNotFound.into_response(), 40 - Ok(_) => EmptyResponse::ok().into_response(), 41 + Ok(0) => Err(ApiError::AccountNotFound), 42 + Ok(_) => Ok(EmptyResponse::ok().into_response()), 41 43 Err(e) => { 42 44 error!("DB error updating email: {:?}", e); 43 - ApiError::InternalError(None).into_response() 45 + Err(ApiError::InternalError(None)) 44 46 } 45 47 } 46 48 } ··· 53 55 54 56 pub async fn update_account_handle( 55 57 State(state): State<AppState>, 56 - _auth: BearerAuthAdmin, 58 + _auth: Auth<Admin>, 57 59 Json(input): Json<UpdateAccountHandleInput>, 58 - ) -> Response { 60 + ) -> Result<Response, ApiError> { 59 61 let did = &input.did; 60 62 let input_handle = input.handle.trim(); 61 63 if input_handle.is_empty() { 62 - return ApiError::InvalidRequest("handle is required".into()).into_response(); 64 + return Err(ApiError::InvalidRequest("handle is required".into())); 63 65 } 64 66 if !input_handle 65 67 .chars() 66 68 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 67 69 { 68 - return ApiError::InvalidHandle(None).into_response(); 70 + return Err(ApiError::InvalidHandle(None)); 69 71 } 70 72 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 71 73 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 75 77 input_handle.to_string() 76 78 }; 77 79 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 - }; 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)?; 82 87 let handle_for_check = Handle::new_unchecked(&handle); 83 88 if let Ok(true) = state 84 89 .user_repo 85 90 .check_handle_exists(&handle_for_check, user_id) 86 91 .await 87 92 { 88 - return ApiError::HandleTaken.into_response(); 93 + return Err(ApiError::HandleTaken); 89 94 } 90 95 match state 91 96 .user_repo 92 97 .admin_update_handle(did, &handle_for_check) 93 98 .await 94 99 { 95 - Ok(0) => ApiError::AccountNotFound.into_response(), 100 + Ok(0) => Err(ApiError::AccountNotFound), 96 101 Ok(_) => { 97 102 if let Some(old) = old_handle { 98 103 let _ = state.cache.delete(&format!("handle:{}", old)).await; ··· 115 120 { 116 121 warn!("Failed to update PLC handle for admin handle update: {}", e); 117 122 } 118 - EmptyResponse::ok().into_response() 123 + Ok(EmptyResponse::ok().into_response()) 119 124 } 120 125 Err(e) => { 121 126 error!("DB error updating handle: {:?}", e); 122 - ApiError::InternalError(None).into_response() 127 + Err(ApiError::InternalError(None)) 123 128 } 124 129 } 125 130 } ··· 132 137 133 138 pub async fn update_account_password( 134 139 State(state): State<AppState>, 135 - _auth: BearerAuthAdmin, 140 + _auth: Auth<Admin>, 136 141 Json(input): Json<UpdateAccountPasswordInput>, 137 - ) -> Response { 142 + ) -> Result<Response, ApiError> { 138 143 let did = &input.did; 139 144 let password = input.password.trim(); 140 145 if password.is_empty() { 141 - return ApiError::InvalidRequest("password is required".into()).into_response(); 146 + return Err(ApiError::InvalidRequest("password is required".into())); 142 147 } 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 - }; 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 + 150 153 match state 151 154 .user_repo 152 155 .admin_update_password(did, &password_hash) 153 156 .await 154 157 { 155 - Ok(0) => ApiError::AccountNotFound.into_response(), 156 - Ok(_) => EmptyResponse::ok().into_response(), 158 + Ok(0) => Err(ApiError::AccountNotFound), 159 + Ok(_) => Ok(EmptyResponse::ok().into_response()), 157 160 Err(e) => { 158 161 error!("DB error updating password: {:?}", e); 159 - ApiError::InternalError(None).into_response() 162 + Err(ApiError::InternalError(None)) 160 163 } 161 164 } 162 165 }
+2 -2
crates/tranquil-pds/src/api/admin/config.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use axum::{Json, extract::State}; 5 5 use serde::{Deserialize, Serialize}; ··· 78 78 79 79 pub async fn update_server_config( 80 80 State(state): State<AppState>, 81 - _admin: BearerAuthAdmin, 81 + _auth: Auth<Admin>, 82 82 Json(req): Json<UpdateServerConfigRequest>, 83 83 ) -> Result<Json<UpdateServerConfigResponse>, ApiError> { 84 84 if let Some(server_name) = req.server_name {
+32 -35
crates/tranquil-pds/src/api/admin/invite.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAdmin; 3 + use crate::auth::{Admin, Auth}; 4 4 use crate::state::AppState; 5 5 use axum::{ 6 6 Json, ··· 21 21 22 22 pub async fn disable_invite_codes( 23 23 State(state): State<AppState>, 24 - _auth: BearerAuthAdmin, 24 + _auth: Auth<Admin>, 25 25 Json(input): Json<DisableInviteCodesInput>, 26 - ) -> Response { 26 + ) -> Result<Response, ApiError> { 27 27 if let Some(codes) = &input.codes 28 28 && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await 29 29 { ··· 40 40 error!("DB error disabling invite codes by account: {:?}", e); 41 41 } 42 42 } 43 - EmptyResponse::ok().into_response() 43 + Ok(EmptyResponse::ok().into_response()) 44 44 } 45 45 46 46 #[derive(Deserialize)] ··· 78 78 79 79 pub async fn get_invite_codes( 80 80 State(state): State<AppState>, 81 - _auth: BearerAuthAdmin, 81 + _auth: Auth<Admin>, 82 82 Query(params): Query<GetInviteCodesParams>, 83 - ) -> Response { 83 + ) -> Result<Response, ApiError> { 84 84 let limit = params.limit.unwrap_or(100).clamp(1, 500); 85 85 let sort_order = match params.sort.as_deref() { 86 86 Some("usage") => InviteCodeSortOrder::Usage, 87 87 _ => InviteCodeSortOrder::Recent, 88 88 }; 89 89 90 - let codes_rows = match state 90 + let codes_rows = state 91 91 .infra_repo 92 92 .list_invite_codes(params.cursor.as_deref(), limit, sort_order) 93 93 .await 94 - { 95 - Ok(rows) => rows, 96 - Err(e) => { 94 + .map_err(|e| { 97 95 error!("DB error fetching invite codes: {:?}", e); 98 - return ApiError::InternalError(None).into_response(); 99 - } 100 - }; 96 + ApiError::InternalError(None) 97 + })?; 101 98 102 99 let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect(); 103 100 let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect(); ··· 155 152 } else { 156 153 None 157 154 }; 158 - ( 155 + Ok(( 159 156 StatusCode::OK, 160 157 Json(GetInviteCodesOutput { 161 158 cursor: next_cursor, 162 159 codes, 163 160 }), 164 161 ) 165 - .into_response() 162 + .into_response()) 166 163 } 167 164 168 165 #[derive(Deserialize)] ··· 172 169 173 170 pub async fn disable_account_invites( 174 171 State(state): State<AppState>, 175 - _auth: BearerAuthAdmin, 172 + _auth: Auth<Admin>, 176 173 Json(input): Json<DisableAccountInvitesInput>, 177 - ) -> Response { 174 + ) -> Result<Response, ApiError> { 178 175 let account = input.account.trim(); 179 176 if account.is_empty() { 180 - return ApiError::InvalidRequest("account is required".into()).into_response(); 177 + return Err(ApiError::InvalidRequest("account is required".into())); 181 178 } 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 - }; 179 + let account_did: tranquil_types::Did = account 180 + .parse() 181 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 182 + 186 183 match state 187 184 .user_repo 188 185 .set_invites_disabled(&account_did, true) 189 186 .await 190 187 { 191 - Ok(true) => EmptyResponse::ok().into_response(), 192 - Ok(false) => ApiError::AccountNotFound.into_response(), 188 + Ok(true) => Ok(EmptyResponse::ok().into_response()), 189 + Ok(false) => Err(ApiError::AccountNotFound), 193 190 Err(e) => { 194 191 error!("DB error disabling account invites: {:?}", e); 195 - ApiError::InternalError(None).into_response() 192 + Err(ApiError::InternalError(None)) 196 193 } 197 194 } 198 195 } ··· 204 201 205 202 pub async fn enable_account_invites( 206 203 State(state): State<AppState>, 207 - _auth: BearerAuthAdmin, 204 + _auth: Auth<Admin>, 208 205 Json(input): Json<EnableAccountInvitesInput>, 209 - ) -> Response { 206 + ) -> Result<Response, ApiError> { 210 207 let account = input.account.trim(); 211 208 if account.is_empty() { 212 - return ApiError::InvalidRequest("account is required".into()).into_response(); 209 + return Err(ApiError::InvalidRequest("account is required".into())); 213 210 } 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 - }; 211 + let account_did: tranquil_types::Did = account 212 + .parse() 213 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 214 + 218 215 match state 219 216 .user_repo 220 217 .set_invites_disabled(&account_did, false) 221 218 .await 222 219 { 223 - Ok(true) => EmptyResponse::ok().into_response(), 224 - Ok(false) => ApiError::AccountNotFound.into_response(), 220 + Ok(true) => Ok(EmptyResponse::ok().into_response()), 221 + Ok(false) => Err(ApiError::AccountNotFound), 225 222 Err(e) => { 226 223 error!("DB error enabling account invites: {:?}", e); 227 - ApiError::InternalError(None).into_response() 224 + Err(ApiError::InternalError(None)) 228 225 } 229 226 } 230 227 }
+8 -4
crates/tranquil-pds/src/api/admin/server_stats.rs
··· 1 - use crate::auth::BearerAuthAdmin; 1 + use crate::api::error::ApiError; 2 + use crate::auth::{Admin, Auth}; 2 3 use crate::state::AppState; 3 4 use axum::{ 4 5 Json, ··· 16 17 pub blob_storage_bytes: i64, 17 18 } 18 19 19 - pub async fn get_server_stats(State(state): State<AppState>, _auth: BearerAuthAdmin) -> Response { 20 + pub async fn get_server_stats( 21 + State(state): State<AppState>, 22 + _auth: Auth<Admin>, 23 + ) -> Result<Response, ApiError> { 20 24 let user_count = state.user_repo.count_users().await.unwrap_or(0); 21 25 let repo_count = state.repo_repo.count_repos().await.unwrap_or(0); 22 26 let record_count = state.repo_repo.count_all_records().await.unwrap_or(0); 23 27 let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0); 24 28 25 - Json(ServerStatsResponse { 29 + Ok(Json(ServerStatsResponse { 26 30 user_count, 27 31 repo_count, 28 32 record_count, 29 33 blob_storage_bytes, 30 34 }) 31 - .into_response() 35 + .into_response()) 32 36 }
+73 -94
crates/tranquil-pds/src/api/admin/status.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 2 + use crate::auth::{Admin, Auth}; 3 3 use crate::state::AppState; 4 4 use crate::types::{CidLink, Did}; 5 5 use axum::{ ··· 35 35 36 36 pub async fn get_subject_status( 37 37 State(state): State<AppState>, 38 - _auth: BearerAuthAdmin, 38 + _auth: Auth<Admin>, 39 39 Query(params): Query<GetSubjectStatusParams>, 40 - ) -> Response { 40 + ) -> Result<Response, ApiError> { 41 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(); 42 + return Err(ApiError::InvalidRequest( 43 + "Must provide did, uri, or blob".into(), 44 + )); 43 45 } 44 46 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 - }; 47 + let did: Did = did_str 48 + .parse() 49 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 49 50 match state.user_repo.get_status_by_did(&did).await { 50 51 Ok(Some(status)) => { 51 52 let deactivated = status.deactivated_at.map(|_| StatusAttr { ··· 56 57 applied: true, 57 58 r#ref: Some(r.clone()), 58 59 }); 59 - return ( 60 + return Ok(( 60 61 StatusCode::OK, 61 62 Json(SubjectStatus { 62 63 subject: json!({ ··· 67 68 deactivated, 68 69 }), 69 70 ) 70 - .into_response(); 71 + .into_response()); 71 72 } 72 73 Ok(None) => { 73 - return ApiError::SubjectNotFound.into_response(); 74 + return Err(ApiError::SubjectNotFound); 74 75 } 75 76 Err(e) => { 76 77 error!("DB error in get_subject_status: {:?}", e); 77 - return ApiError::InternalError(None).into_response(); 78 + return Err(ApiError::InternalError(None)); 78 79 } 79 80 } 80 81 } 81 82 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 - }; 83 + let cid: CidLink = uri_str 84 + .parse() 85 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 86 86 match state.repo_repo.get_record_by_cid(&cid).await { 87 87 Ok(Some(record)) => { 88 88 let takedown = record.takedown_ref.as_ref().map(|r| StatusAttr { 89 89 applied: true, 90 90 r#ref: Some(r.clone()), 91 91 }); 92 - return ( 92 + return Ok(( 93 93 StatusCode::OK, 94 94 Json(SubjectStatus { 95 95 subject: json!({ ··· 101 101 deactivated: None, 102 102 }), 103 103 ) 104 - .into_response(); 104 + .into_response()); 105 105 } 106 106 Ok(None) => { 107 - return ApiError::RecordNotFound.into_response(); 107 + return Err(ApiError::RecordNotFound); 108 108 } 109 109 Err(e) => { 110 110 error!("DB error in get_subject_status: {:?}", e); 111 - return ApiError::InternalError(None).into_response(); 111 + return Err(ApiError::InternalError(None)); 112 112 } 113 113 } 114 114 } 115 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 - }; 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 + })?; 127 122 match state.blob_repo.get_blob_with_takedown(&blob_cid).await { 128 123 Ok(Some(blob)) => { 129 124 let takedown = blob.takedown_ref.as_ref().map(|r| StatusAttr { 130 125 applied: true, 131 126 r#ref: Some(r.clone()), 132 127 }); 133 - return ( 128 + return Ok(( 134 129 StatusCode::OK, 135 130 Json(SubjectStatus { 136 131 subject: json!({ ··· 142 137 deactivated: None, 143 138 }), 144 139 ) 145 - .into_response(); 140 + .into_response()); 146 141 } 147 142 Ok(None) => { 148 - return ApiError::BlobNotFound(None).into_response(); 143 + return Err(ApiError::BlobNotFound(None)); 149 144 } 150 145 Err(e) => { 151 146 error!("DB error in get_subject_status: {:?}", e); 152 - return ApiError::InternalError(None).into_response(); 147 + return Err(ApiError::InternalError(None)); 153 148 } 154 149 } 155 150 } 156 - ApiError::InvalidRequest("Invalid subject type".into()).into_response() 151 + Err(ApiError::InvalidRequest("Invalid subject type".into())) 157 152 } 158 153 159 154 #[derive(Deserialize)] ··· 172 167 173 168 pub async fn update_subject_status( 174 169 State(state): State<AppState>, 175 - _auth: BearerAuthAdmin, 170 + _auth: Auth<Admin>, 176 171 Json(input): Json<UpdateSubjectStatusInput>, 177 - ) -> Response { 172 + ) -> Result<Response, ApiError> { 178 173 let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 179 174 match subject_type { 180 175 Some("com.atproto.admin.defs#repoRef") => { ··· 187 182 } else { 188 183 None 189 184 }; 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 - } 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 + })?; 197 193 } 198 194 if let Some(deactivated) = &input.deactivated { 199 195 let result = if deactivated.applied { ··· 201 197 } else { 202 198 state.user_repo.activate_account(&did).await 203 199 }; 204 - if let Err(e) = result { 200 + result.map_err(|e| { 205 201 error!( 206 202 "Failed to update user deactivation status for {}: {:?}", 207 203 did, e 208 204 ); 209 - return ApiError::InternalError(Some( 210 - "Failed to update deactivation status".into(), 211 - )) 212 - .into_response(); 213 - } 205 + ApiError::InternalError(Some("Failed to update deactivation status".into())) 206 + })?; 214 207 } 215 208 if let Some(takedown) = &input.takedown { 216 209 let status = if takedown.applied { ··· 249 242 if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await { 250 243 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 251 244 } 252 - return ( 245 + return Ok(( 253 246 StatusCode::OK, 254 247 Json(json!({ 255 248 "subject": input.subject, ··· 262 255 })) 263 256 })), 264 257 ) 265 - .into_response(); 258 + .into_response()); 266 259 } 267 260 } 268 261 Some("com.atproto.repo.strongRef") => { 269 262 let uri_str = input.subject.get("uri").and_then(|u| u.as_str()); 270 263 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 - }; 264 + let cid: CidLink = uri_str 265 + .parse() 266 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 278 267 if let Some(takedown) = &input.takedown { 279 268 let takedown_ref = if takedown.applied { 280 269 takedown.r#ref.as_deref() 281 270 } else { 282 271 None 283 272 }; 284 - if let Err(e) = state 273 + state 285 274 .repo_repo 286 275 .set_record_takedown(&cid, takedown_ref) 287 276 .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 - } 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 + })?; 298 284 } 299 - return ( 285 + return Ok(( 300 286 StatusCode::OK, 301 287 Json(json!({ 302 288 "subject": input.subject, ··· 306 292 })) 307 293 })), 308 294 ) 309 - .into_response(); 295 + .into_response()); 310 296 } 311 297 } 312 298 Some("com.atproto.admin.defs#repoBlobRef") => { 313 299 let cid_str = input.subject.get("cid").and_then(|c| c.as_str()); 314 300 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 - }; 301 + let cid: CidLink = cid_str 302 + .parse() 303 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 322 304 if let Some(takedown) = &input.takedown { 323 305 let takedown_ref = if takedown.applied { 324 306 takedown.r#ref.as_deref() 325 307 } else { 326 308 None 327 309 }; 328 - if let Err(e) = state 310 + state 329 311 .blob_repo 330 312 .update_blob_takedown(&cid, takedown_ref) 331 313 .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 - } 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 + })?; 342 321 } 343 - return ( 322 + return Ok(( 344 323 StatusCode::OK, 345 324 Json(json!({ 346 325 "subject": input.subject, ··· 350 329 })) 351 330 })), 352 331 ) 353 - .into_response(); 332 + .into_response()); 354 333 } 355 334 } 356 335 _ => {} 357 336 } 358 - ApiError::InvalidRequest("Invalid subject type".into()).into_response() 337 + Err(ApiError::InvalidRequest("Invalid subject type".into())) 359 338 }
+93 -73
crates/tranquil-pds/src/api/backup.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::{EmptyResponse, EnabledResponse}; 3 - use crate::auth::BearerAuth; 3 + use crate::auth::{Active, Auth}; 4 4 use crate::scheduled::generate_full_backup; 5 5 use crate::state::AppState; 6 6 use crate::storage::{BackupStorage, backup_retention_count}; ··· 35 35 pub backup_enabled: bool, 36 36 } 37 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 - }; 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 + }; 50 53 51 54 let backups = match state.backup_repo.list_backups_for_user(user_id).await { 52 55 Ok(rows) => rows, 53 56 Err(e) => { 54 57 error!("DB error fetching backups: {:?}", e); 55 - return ApiError::InternalError(None).into_response(); 58 + return Ok(ApiError::InternalError(None).into_response()); 56 59 } 57 60 }; 58 61 ··· 68 71 }) 69 72 .collect(); 70 73 71 - ( 74 + Ok(( 72 75 StatusCode::OK, 73 76 Json(ListBackupsOutput { 74 77 backups: backup_list, 75 78 backup_enabled, 76 79 }), 77 80 ) 78 - .into_response() 81 + .into_response()) 79 82 } 80 83 81 84 #[derive(Deserialize)] ··· 85 88 86 89 pub async fn get_backup( 87 90 State(state): State<AppState>, 88 - auth: BearerAuth, 91 + auth: Auth<Active>, 89 92 Query(query): Query<GetBackupQuery>, 90 - ) -> Response { 93 + ) -> Result<Response, crate::api::error::ApiError> { 91 94 let backup_id = match uuid::Uuid::parse_str(&query.id) { 92 95 Ok(id) => id, 93 96 Err(_) => { 94 - return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); 97 + return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response()); 95 98 } 96 99 }; 97 100 98 101 let backup_info = match state 99 102 .backup_repo 100 - .get_backup_storage_info(backup_id, &auth.0.did) 103 + .get_backup_storage_info(backup_id, &auth.did) 101 104 .await 102 105 { 103 106 Ok(Some(b)) => b, 104 107 Ok(None) => { 105 - return ApiError::BackupNotFound.into_response(); 108 + return Ok(ApiError::BackupNotFound.into_response()); 106 109 } 107 110 Err(e) => { 108 111 error!("DB error fetching backup: {:?}", e); 109 - return ApiError::InternalError(None).into_response(); 112 + return Ok(ApiError::InternalError(None).into_response()); 110 113 } 111 114 }; 112 115 113 116 let backup_storage = match state.backup_storage.as_ref() { 114 117 Some(storage) => storage, 115 118 None => { 116 - return ApiError::BackupsDisabled.into_response(); 119 + return Ok(ApiError::BackupsDisabled.into_response()); 117 120 } 118 121 }; 119 122 ··· 121 124 Ok(bytes) => bytes, 122 125 Err(e) => { 123 126 error!("Failed to fetch backup from storage: {:?}", e); 124 - return ApiError::InternalError(Some("Failed to retrieve backup".into())) 125 - .into_response(); 127 + return Ok( 128 + ApiError::InternalError(Some("Failed to retrieve backup".into())).into_response(), 129 + ); 126 130 } 127 131 }; 128 132 129 - ( 133 + Ok(( 130 134 StatusCode::OK, 131 135 [ 132 136 (axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car"), ··· 137 141 ], 138 142 car_bytes, 139 143 ) 140 - .into_response() 144 + .into_response()) 141 145 } 142 146 143 147 #[derive(Serialize)] ··· 149 153 pub block_count: i32, 150 154 } 151 155 152 - pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> Response { 156 + pub async fn create_backup( 157 + State(state): State<AppState>, 158 + auth: Auth<Active>, 159 + ) -> Result<Response, crate::api::error::ApiError> { 153 160 let backup_storage = match state.backup_storage.as_ref() { 154 161 Some(storage) => storage, 155 162 None => { 156 - return ApiError::BackupsDisabled.into_response(); 163 + return Ok(ApiError::BackupsDisabled.into_response()); 157 164 } 158 165 }; 159 166 160 - let user = match state.backup_repo.get_user_for_backup(&auth.0.did).await { 167 + let user = match state.backup_repo.get_user_for_backup(&auth.did).await { 161 168 Ok(Some(u)) => u, 162 169 Ok(None) => { 163 - return ApiError::AccountNotFound.into_response(); 170 + return Ok(ApiError::AccountNotFound.into_response()); 164 171 } 165 172 Err(e) => { 166 173 error!("DB error fetching user: {:?}", e); 167 - return ApiError::InternalError(None).into_response(); 174 + return Ok(ApiError::InternalError(None).into_response()); 168 175 } 169 176 }; 170 177 171 178 if user.deactivated_at.is_some() { 172 - return ApiError::AccountDeactivated.into_response(); 179 + return Ok(ApiError::AccountDeactivated.into_response()); 173 180 } 174 181 175 182 let repo_rev = match &user.repo_rev { 176 183 Some(rev) => rev.clone(), 177 184 None => { 178 - return ApiError::RepoNotReady.into_response(); 185 + return Ok(ApiError::RepoNotReady.into_response()); 179 186 } 180 187 }; 181 188 182 189 let head_cid = match Cid::from_str(&user.repo_root_cid) { 183 190 Ok(c) => c, 184 191 Err(_) => { 185 - return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(); 192 + return Ok( 193 + ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(), 194 + ); 186 195 } 187 196 }; 188 197 ··· 197 206 Ok(bytes) => bytes, 198 207 Err(e) => { 199 208 error!("Failed to generate CAR: {:?}", e); 200 - return ApiError::InternalError(Some("Failed to generate backup".into())) 201 - .into_response(); 209 + return Ok( 210 + ApiError::InternalError(Some("Failed to generate backup".into())).into_response(), 211 + ); 202 212 } 203 213 }; 204 214 ··· 212 222 Ok(key) => key, 213 223 Err(e) => { 214 224 error!("Failed to upload backup: {:?}", e); 215 - return ApiError::InternalError(Some("Failed to store backup".into())).into_response(); 225 + return Ok( 226 + ApiError::InternalError(Some("Failed to store backup".into())).into_response(), 227 + ); 216 228 } 217 229 }; 218 230 ··· 238 250 "Failed to rollback orphaned backup from S3" 239 251 ); 240 252 } 241 - return ApiError::InternalError(Some("Failed to record backup".into())).into_response(); 253 + return Ok( 254 + ApiError::InternalError(Some("Failed to record backup".into())).into_response(), 255 + ); 242 256 } 243 257 }; 244 258 ··· 261 275 warn!(did = %user.did, error = %e, "Failed to cleanup old backups after manual backup"); 262 276 } 263 277 264 - ( 278 + Ok(( 265 279 StatusCode::OK, 266 280 Json(CreateBackupOutput { 267 281 id: backup_id.to_string(), ··· 270 284 block_count, 271 285 }), 272 286 ) 273 - .into_response() 287 + .into_response()) 274 288 } 275 289 276 290 async fn cleanup_old_backups( ··· 310 324 311 325 pub async fn delete_backup( 312 326 State(state): State<AppState>, 313 - auth: BearerAuth, 327 + auth: Auth<Active>, 314 328 Query(query): Query<DeleteBackupQuery>, 315 - ) -> Response { 329 + ) -> Result<Response, crate::api::error::ApiError> { 316 330 let backup_id = match uuid::Uuid::parse_str(&query.id) { 317 331 Ok(id) => id, 318 332 Err(_) => { 319 - return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); 333 + return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response()); 320 334 } 321 335 }; 322 336 323 337 let backup = match state 324 338 .backup_repo 325 - .get_backup_for_deletion(backup_id, &auth.0.did) 339 + .get_backup_for_deletion(backup_id, &auth.did) 326 340 .await 327 341 { 328 342 Ok(Some(b)) => b, 329 343 Ok(None) => { 330 - return ApiError::BackupNotFound.into_response(); 344 + return Ok(ApiError::BackupNotFound.into_response()); 331 345 } 332 346 Err(e) => { 333 347 error!("DB error fetching backup: {:?}", e); 334 - return ApiError::InternalError(None).into_response(); 348 + return Ok(ApiError::InternalError(None).into_response()); 335 349 } 336 350 }; 337 351 338 352 if backup.deactivated_at.is_some() { 339 - return ApiError::AccountDeactivated.into_response(); 353 + return Ok(ApiError::AccountDeactivated.into_response()); 340 354 } 341 355 342 356 if let Some(backup_storage) = state.backup_storage.as_ref() ··· 351 365 352 366 if let Err(e) = state.backup_repo.delete_backup(backup.id).await { 353 367 error!("DB error deleting backup: {:?}", e); 354 - return ApiError::InternalError(Some("Failed to delete backup".into())).into_response(); 368 + return Ok(ApiError::InternalError(Some("Failed to delete backup".into())).into_response()); 355 369 } 356 370 357 - info!(did = %auth.0.did, backup_id = %backup_id, "Deleted backup"); 371 + info!(did = %auth.did, backup_id = %backup_id, "Deleted backup"); 358 372 359 - EmptyResponse::ok().into_response() 373 + Ok(EmptyResponse::ok().into_response()) 360 374 } 361 375 362 376 #[derive(Deserialize)] ··· 367 381 368 382 pub async fn set_backup_enabled( 369 383 State(state): State<AppState>, 370 - auth: BearerAuth, 384 + auth: Auth<Active>, 371 385 Json(input): Json<SetBackupEnabledInput>, 372 - ) -> Response { 386 + ) -> Result<Response, crate::api::error::ApiError> { 373 387 let deactivated_at = match state 374 388 .backup_repo 375 - .get_user_deactivated_status(&auth.0.did) 389 + .get_user_deactivated_status(&auth.did) 376 390 .await 377 391 { 378 392 Ok(Some(status)) => status, 379 393 Ok(None) => { 380 - return ApiError::AccountNotFound.into_response(); 394 + return Ok(ApiError::AccountNotFound.into_response()); 381 395 } 382 396 Err(e) => { 383 397 error!("DB error fetching user: {:?}", e); 384 - return ApiError::InternalError(None).into_response(); 398 + return Ok(ApiError::InternalError(None).into_response()); 385 399 } 386 400 }; 387 401 388 402 if deactivated_at.is_some() { 389 - return ApiError::AccountDeactivated.into_response(); 403 + return Ok(ApiError::AccountDeactivated.into_response()); 390 404 } 391 405 392 406 if let Err(e) = state 393 407 .backup_repo 394 - .update_backup_enabled(&auth.0.did, input.enabled) 408 + .update_backup_enabled(&auth.did, input.enabled) 395 409 .await 396 410 { 397 411 error!("DB error updating backup_enabled: {:?}", e); 398 - return ApiError::InternalError(Some("Failed to update setting".into())).into_response(); 412 + return Ok( 413 + ApiError::InternalError(Some("Failed to update setting".into())).into_response(), 414 + ); 399 415 } 400 416 401 - info!(did = %auth.0.did, enabled = input.enabled, "Updated backup_enabled setting"); 417 + info!(did = %auth.did, enabled = input.enabled, "Updated backup_enabled setting"); 402 418 403 - EnabledResponse::response(input.enabled).into_response() 419 + Ok(EnabledResponse::response(input.enabled).into_response()) 404 420 } 405 421 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 { 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 { 408 427 Ok(Some(id)) => id, 409 428 Ok(None) => { 410 - return ApiError::AccountNotFound.into_response(); 429 + return Ok(ApiError::AccountNotFound.into_response()); 411 430 } 412 431 Err(e) => { 413 432 error!("DB error fetching user: {:?}", e); 414 - return ApiError::InternalError(None).into_response(); 433 + return Ok(ApiError::InternalError(None).into_response()); 415 434 } 416 435 }; 417 436 ··· 419 438 Ok(rows) => rows, 420 439 Err(e) => { 421 440 error!("DB error fetching blobs: {:?}", e); 422 - return ApiError::InternalError(None).into_response(); 441 + return Ok(ApiError::InternalError(None).into_response()); 423 442 } 424 443 }; 425 444 426 445 if blobs.is_empty() { 427 - return ( 446 + return Ok(( 428 447 StatusCode::OK, 429 448 [ 430 449 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 435 454 ], 436 455 Vec::<u8>::new(), 437 456 ) 438 - .into_response(); 457 + .into_response()); 439 458 } 440 459 441 460 let mut zip_buffer = std::io::Cursor::new(Vec::new()); ··· 513 532 514 533 if let Err(e) = zip.finish() { 515 534 error!("Failed to finish zip: {:?}", e); 516 - return ApiError::InternalError(Some("Failed to create zip file".into())) 517 - .into_response(); 535 + return Ok( 536 + ApiError::InternalError(Some("Failed to create zip file".into())).into_response(), 537 + ); 518 538 } 519 539 } 520 540 521 541 let zip_bytes = zip_buffer.into_inner(); 522 542 523 - info!(did = %auth.0.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); 543 + info!(did = %auth.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); 524 544 525 - ( 545 + Ok(( 526 546 StatusCode::OK, 527 547 [ 528 548 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 533 553 ], 534 554 zip_bytes, 535 555 ) 536 - .into_response() 556 + .into_response()) 537 557 } 538 558 539 559 fn mime_to_extension(mime_type: &str) -> &'static str {
+115 -98
crates/tranquil-pds/src/api/delegation.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::repo::record::utils::create_signed_commit; 3 - use crate::auth::BearerAuth; 3 + use crate::auth::{Active, Auth}; 4 4 use crate::delegation::{DelegationActionType, SCOPE_PRESETS, scopes}; 5 5 use crate::state::{AppState, RateLimitKind}; 6 6 use crate::types::{Did, Handle, Nsid, Rkey}; ··· 33 33 pub controllers: Vec<ControllerInfo>, 34 34 } 35 35 36 - pub async fn list_controllers(State(state): State<AppState>, auth: BearerAuth) -> Response { 36 + pub async fn list_controllers( 37 + State(state): State<AppState>, 38 + auth: Auth<Active>, 39 + ) -> Result<Response, ApiError> { 37 40 let controllers = match state 38 41 .delegation_repo 39 - .get_delegations_for_account(&auth.0.did) 42 + .get_delegations_for_account(&auth.did) 40 43 .await 41 44 { 42 45 Ok(c) => c, 43 46 Err(e) => { 44 47 tracing::error!("Failed to list controllers: {:?}", e); 45 - return ApiError::InternalError(Some("Failed to list controllers".into())) 46 - .into_response(); 48 + return Ok( 49 + ApiError::InternalError(Some("Failed to list controllers".into())).into_response(), 50 + ); 47 51 } 48 52 }; 49 53 50 - Json(ListControllersResponse { 54 + Ok(Json(ListControllersResponse { 51 55 controllers: controllers 52 56 .into_iter() 53 57 .map(|c| ControllerInfo { ··· 59 63 }) 60 64 .collect(), 61 65 }) 62 - .into_response() 66 + .into_response()) 63 67 } 64 68 65 69 #[derive(Debug, Deserialize)] ··· 70 74 71 75 pub async fn add_controller( 72 76 State(state): State<AppState>, 73 - auth: BearerAuth, 77 + auth: Auth<Active>, 74 78 Json(input): Json<AddControllerInput>, 75 - ) -> Response { 79 + ) -> Result<Response, ApiError> { 76 80 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 77 - return ApiError::InvalidScopes(e).into_response(); 81 + return Ok(ApiError::InvalidScopes(e).into_response()); 78 82 } 79 83 80 84 let controller_exists = state ··· 86 90 .is_some(); 87 91 88 92 if !controller_exists { 89 - return ApiError::ControllerNotFound.into_response(); 93 + return Ok(ApiError::ControllerNotFound.into_response()); 90 94 } 91 95 92 - match state 93 - .delegation_repo 94 - .controls_any_accounts(&auth.0.did) 95 - .await 96 - { 96 + match state.delegation_repo.controls_any_accounts(&auth.did).await { 97 97 Ok(true) => { 98 - return ApiError::InvalidDelegation( 98 + return Ok(ApiError::InvalidDelegation( 99 99 "Cannot add controllers to an account that controls other accounts".into(), 100 100 ) 101 - .into_response(); 101 + .into_response()); 102 102 } 103 103 Err(e) => { 104 104 tracing::error!("Failed to check delegation status: {:?}", e); 105 - return ApiError::InternalError(Some("Failed to verify delegation status".into())) 106 - .into_response(); 105 + return Ok( 106 + ApiError::InternalError(Some("Failed to verify delegation status".into())) 107 + .into_response(), 108 + ); 107 109 } 108 110 Ok(false) => {} 109 111 } ··· 114 116 .await 115 117 { 116 118 Ok(true) => { 117 - return ApiError::InvalidDelegation( 119 + return Ok(ApiError::InvalidDelegation( 118 120 "Cannot add a controlled account as a controller".into(), 119 121 ) 120 - .into_response(); 122 + .into_response()); 121 123 } 122 124 Err(e) => { 123 125 tracing::error!("Failed to check controller status: {:?}", e); 124 - return ApiError::InternalError(Some("Failed to verify controller status".into())) 125 - .into_response(); 126 + return Ok( 127 + ApiError::InternalError(Some("Failed to verify controller status".into())) 128 + .into_response(), 129 + ); 126 130 } 127 131 Ok(false) => {} 128 132 } ··· 130 134 match state 131 135 .delegation_repo 132 136 .create_delegation( 133 - &auth.0.did, 137 + &auth.did, 134 138 &input.controller_did, 135 139 &input.granted_scopes, 136 - &auth.0.did, 140 + &auth.did, 137 141 ) 138 142 .await 139 143 { ··· 141 145 let _ = state 142 146 .delegation_repo 143 147 .log_delegation_action( 144 - &auth.0.did, 145 - &auth.0.did, 148 + &auth.did, 149 + &auth.did, 146 150 Some(&input.controller_did), 147 151 DelegationActionType::GrantCreated, 148 152 Some(serde_json::json!({ ··· 153 157 ) 154 158 .await; 155 159 156 - ( 160 + Ok(( 157 161 StatusCode::OK, 158 162 Json(serde_json::json!({ 159 163 "success": true 160 164 })), 161 165 ) 162 - .into_response() 166 + .into_response()) 163 167 } 164 168 Err(e) => { 165 169 tracing::error!("Failed to add controller: {:?}", e); 166 - ApiError::InternalError(Some("Failed to add controller".into())).into_response() 170 + Ok(ApiError::InternalError(Some("Failed to add controller".into())).into_response()) 167 171 } 168 172 } 169 173 } ··· 175 179 176 180 pub async fn remove_controller( 177 181 State(state): State<AppState>, 178 - auth: BearerAuth, 182 + auth: Auth<Active>, 179 183 Json(input): Json<RemoveControllerInput>, 180 - ) -> Response { 184 + ) -> Result<Response, ApiError> { 181 185 match state 182 186 .delegation_repo 183 - .revoke_delegation(&auth.0.did, &input.controller_did, &auth.0.did) 187 + .revoke_delegation(&auth.did, &input.controller_did, &auth.did) 184 188 .await 185 189 { 186 190 Ok(true) => { 187 191 let revoked_app_passwords = state 188 192 .session_repo 189 - .delete_app_passwords_by_controller(&auth.0.did, &input.controller_did) 193 + .delete_app_passwords_by_controller(&auth.did, &input.controller_did) 190 194 .await 191 195 .unwrap_or(0) as usize; 192 196 193 197 let revoked_oauth_tokens = state 194 198 .oauth_repo 195 - .revoke_tokens_for_controller(&auth.0.did, &input.controller_did) 199 + .revoke_tokens_for_controller(&auth.did, &input.controller_did) 196 200 .await 197 201 .unwrap_or(0); 198 202 199 203 let _ = state 200 204 .delegation_repo 201 205 .log_delegation_action( 202 - &auth.0.did, 203 - &auth.0.did, 206 + &auth.did, 207 + &auth.did, 204 208 Some(&input.controller_did), 205 209 DelegationActionType::GrantRevoked, 206 210 Some(serde_json::json!({ ··· 212 216 ) 213 217 .await; 214 218 215 - ( 219 + Ok(( 216 220 StatusCode::OK, 217 221 Json(serde_json::json!({ 218 222 "success": true 219 223 })), 220 224 ) 221 - .into_response() 225 + .into_response()) 222 226 } 223 - Ok(false) => ApiError::DelegationNotFound.into_response(), 227 + Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), 224 228 Err(e) => { 225 229 tracing::error!("Failed to remove controller: {:?}", e); 226 - ApiError::InternalError(Some("Failed to remove controller".into())).into_response() 230 + Ok(ApiError::InternalError(Some("Failed to remove controller".into())).into_response()) 227 231 } 228 232 } 229 233 } ··· 236 240 237 241 pub async fn update_controller_scopes( 238 242 State(state): State<AppState>, 239 - auth: BearerAuth, 243 + auth: Auth<Active>, 240 244 Json(input): Json<UpdateControllerScopesInput>, 241 - ) -> Response { 245 + ) -> Result<Response, ApiError> { 242 246 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 243 - return ApiError::InvalidScopes(e).into_response(); 247 + return Ok(ApiError::InvalidScopes(e).into_response()); 244 248 } 245 249 246 250 match state 247 251 .delegation_repo 248 - .update_delegation_scopes(&auth.0.did, &input.controller_did, &input.granted_scopes) 252 + .update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes) 249 253 .await 250 254 { 251 255 Ok(true) => { 252 256 let _ = state 253 257 .delegation_repo 254 258 .log_delegation_action( 255 - &auth.0.did, 256 - &auth.0.did, 259 + &auth.did, 260 + &auth.did, 257 261 Some(&input.controller_did), 258 262 DelegationActionType::ScopesModified, 259 263 Some(serde_json::json!({ ··· 264 268 ) 265 269 .await; 266 270 267 - ( 271 + Ok(( 268 272 StatusCode::OK, 269 273 Json(serde_json::json!({ 270 274 "success": true 271 275 })), 272 276 ) 273 - .into_response() 277 + .into_response()) 274 278 } 275 - Ok(false) => ApiError::DelegationNotFound.into_response(), 279 + Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), 276 280 Err(e) => { 277 281 tracing::error!("Failed to update controller scopes: {:?}", e); 278 - ApiError::InternalError(Some("Failed to update controller scopes".into())) 279 - .into_response() 282 + Ok( 283 + ApiError::InternalError(Some("Failed to update controller scopes".into())) 284 + .into_response(), 285 + ) 280 286 } 281 287 } 282 288 } ··· 295 301 pub accounts: Vec<DelegatedAccountInfo>, 296 302 } 297 303 298 - pub async fn list_controlled_accounts(State(state): State<AppState>, auth: BearerAuth) -> Response { 304 + pub async fn list_controlled_accounts( 305 + State(state): State<AppState>, 306 + auth: Auth<Active>, 307 + ) -> Result<Response, ApiError> { 299 308 let accounts = match state 300 309 .delegation_repo 301 - .get_accounts_controlled_by(&auth.0.did) 310 + .get_accounts_controlled_by(&auth.did) 302 311 .await 303 312 { 304 313 Ok(a) => a, 305 314 Err(e) => { 306 315 tracing::error!("Failed to list controlled accounts: {:?}", e); 307 - return ApiError::InternalError(Some("Failed to list controlled accounts".into())) 308 - .into_response(); 316 + return Ok( 317 + ApiError::InternalError(Some("Failed to list controlled accounts".into())) 318 + .into_response(), 319 + ); 309 320 } 310 321 }; 311 322 312 - Json(ListControlledAccountsResponse { 323 + Ok(Json(ListControlledAccountsResponse { 313 324 accounts: accounts 314 325 .into_iter() 315 326 .map(|a| DelegatedAccountInfo { ··· 320 331 }) 321 332 .collect(), 322 333 }) 323 - .into_response() 334 + .into_response()) 324 335 } 325 336 326 337 #[derive(Debug, Deserialize)] ··· 355 366 356 367 pub async fn get_audit_log( 357 368 State(state): State<AppState>, 358 - auth: BearerAuth, 369 + auth: Auth<Active>, 359 370 Query(params): Query<AuditLogParams>, 360 - ) -> Response { 371 + ) -> Result<Response, ApiError> { 361 372 let limit = params.limit.clamp(1, 100); 362 373 let offset = params.offset.max(0); 363 374 364 375 let entries = match state 365 376 .delegation_repo 366 - .get_audit_log_for_account(&auth.0.did, limit, offset) 377 + .get_audit_log_for_account(&auth.did, limit, offset) 367 378 .await 368 379 { 369 380 Ok(e) => e, 370 381 Err(e) => { 371 382 tracing::error!("Failed to get audit log: {:?}", e); 372 - return ApiError::InternalError(Some("Failed to get audit log".into())).into_response(); 383 + return Ok( 384 + ApiError::InternalError(Some("Failed to get audit log".into())).into_response(), 385 + ); 373 386 } 374 387 }; 375 388 376 389 let total = state 377 390 .delegation_repo 378 - .count_audit_log_entries(&auth.0.did) 391 + .count_audit_log_entries(&auth.did) 379 392 .await 380 393 .unwrap_or_default(); 381 394 382 - Json(GetAuditLogResponse { 395 + Ok(Json(GetAuditLogResponse { 383 396 entries: entries 384 397 .into_iter() 385 398 .map(|e| AuditLogEntry { ··· 394 407 .collect(), 395 408 total, 396 409 }) 397 - .into_response() 410 + .into_response()) 398 411 } 399 412 400 413 #[derive(Debug, Serialize)] ··· 444 457 pub async fn create_delegated_account( 445 458 State(state): State<AppState>, 446 459 headers: HeaderMap, 447 - auth: BearerAuth, 460 + auth: Auth<Active>, 448 461 Json(input): Json<CreateDelegatedAccountInput>, 449 - ) -> Response { 462 + ) -> Result<Response, ApiError> { 450 463 let client_ip = extract_client_ip(&headers); 451 464 if !state 452 465 .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 453 466 .await 454 467 { 455 468 warn!(ip = %client_ip, "Delegated account creation rate limit exceeded"); 456 - return ApiError::RateLimitExceeded(Some( 469 + return Ok(ApiError::RateLimitExceeded(Some( 457 470 "Too many account creation attempts. Please try again later.".into(), 458 471 )) 459 - .into_response(); 472 + .into_response()); 460 473 } 461 474 462 475 if let Err(e) = scopes::validate_delegation_scopes(&input.controller_scopes) { 463 - return ApiError::InvalidScopes(e).into_response(); 476 + return Ok(ApiError::InvalidScopes(e).into_response()); 464 477 } 465 478 466 - match state.delegation_repo.has_any_controllers(&auth.0.did).await { 479 + match state.delegation_repo.has_any_controllers(&auth.did).await { 467 480 Ok(true) => { 468 - return ApiError::InvalidDelegation( 481 + return Ok(ApiError::InvalidDelegation( 469 482 "Cannot create delegated accounts from a controlled account".into(), 470 483 ) 471 - .into_response(); 484 + .into_response()); 472 485 } 473 486 Err(e) => { 474 487 tracing::error!("Failed to check controller status: {:?}", e); 475 - return ApiError::InternalError(Some("Failed to verify controller status".into())) 476 - .into_response(); 488 + return Ok( 489 + ApiError::InternalError(Some("Failed to verify controller status".into())) 490 + .into_response(), 491 + ); 477 492 } 478 493 Ok(false) => {} 479 494 } ··· 494 509 match crate::api::validation::validate_short_handle(handle_to_validate) { 495 510 Ok(h) => format!("{}.{}", h, hostname_for_handles), 496 511 Err(e) => { 497 - return ApiError::InvalidRequest(e.to_string()).into_response(); 512 + return Ok(ApiError::InvalidRequest(e.to_string()).into_response()); 498 513 } 499 514 } 500 515 } else { ··· 509 524 if let Some(ref email) = email 510 525 && !crate::api::validation::is_valid_email(email) 511 526 { 512 - return ApiError::InvalidEmail.into_response(); 527 + return Ok(ApiError::InvalidEmail.into_response()); 513 528 } 514 529 515 530 if let Some(ref code) = input.invite_code { ··· 520 535 .unwrap_or(false); 521 536 522 537 if !valid { 523 - return ApiError::InvalidInviteCode.into_response(); 538 + return Ok(ApiError::InvalidInviteCode.into_response()); 524 539 } 525 540 } else { 526 541 let invite_required = std::env::var("INVITE_CODE_REQUIRED") 527 542 .map(|v| v == "true" || v == "1") 528 543 .unwrap_or(false); 529 544 if invite_required { 530 - return ApiError::InviteCodeRequired.into_response(); 545 + return Ok(ApiError::InviteCodeRequired.into_response()); 531 546 } 532 547 } 533 548 ··· 542 557 Ok(k) => k, 543 558 Err(e) => { 544 559 error!("Error creating signing key: {:?}", e); 545 - return ApiError::InternalError(None).into_response(); 560 + return Ok(ApiError::InternalError(None).into_response()); 546 561 } 547 562 }; 548 563 ··· 558 573 Ok(r) => r, 559 574 Err(e) => { 560 575 error!("Error creating PLC genesis operation: {:?}", e); 561 - return ApiError::InternalError(Some("Failed to create PLC operation".into())) 562 - .into_response(); 576 + return Ok( 577 + ApiError::InternalError(Some("Failed to create PLC operation".into())) 578 + .into_response(), 579 + ); 563 580 } 564 581 }; 565 582 ··· 569 586 .await 570 587 { 571 588 error!("Failed to submit PLC genesis operation: {:?}", e); 572 - return ApiError::UpstreamErrorMsg(format!( 589 + return Ok(ApiError::UpstreamErrorMsg(format!( 573 590 "Failed to register DID with PLC directory: {}", 574 591 e 575 592 )) 576 - .into_response(); 593 + .into_response()); 577 594 } 578 595 579 596 let did = Did::new_unchecked(&genesis_result.did); 580 597 let handle = Handle::new_unchecked(&handle); 581 - info!(did = %did, handle = %handle, controller = %&auth.0.did, "Created DID for delegated account"); 598 + info!(did = %did, handle = %handle, controller = %&auth.did, "Created DID for delegated account"); 582 599 583 600 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 584 601 Ok(bytes) => bytes, 585 602 Err(e) => { 586 603 error!("Error encrypting signing key: {:?}", e); 587 - return ApiError::InternalError(None).into_response(); 604 + return Ok(ApiError::InternalError(None).into_response()); 588 605 } 589 606 }; 590 607 ··· 593 610 Ok(c) => c, 594 611 Err(e) => { 595 612 error!("Error persisting MST: {:?}", e); 596 - return ApiError::InternalError(None).into_response(); 613 + return Ok(ApiError::InternalError(None).into_response()); 597 614 } 598 615 }; 599 616 let rev = Tid::now(LimitedU32::MIN); ··· 602 619 Ok(result) => result, 603 620 Err(e) => { 604 621 error!("Error creating genesis commit: {:?}", e); 605 - return ApiError::InternalError(None).into_response(); 622 + return Ok(ApiError::InternalError(None).into_response()); 606 623 } 607 624 }; 608 625 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 609 626 Ok(c) => c, 610 627 Err(e) => { 611 628 error!("Error saving genesis commit: {:?}", e); 612 - return ApiError::InternalError(None).into_response(); 629 + return Ok(ApiError::InternalError(None).into_response()); 613 630 } 614 631 }; 615 632 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; ··· 618 635 handle: handle.clone(), 619 636 email: email.clone(), 620 637 did: did.clone(), 621 - controller_did: auth.0.did.clone(), 638 + controller_did: auth.did.clone(), 622 639 controller_scopes: input.controller_scopes.clone(), 623 640 encrypted_key_bytes, 624 641 encryption_version: crate::config::ENCRYPTION_VERSION, ··· 635 652 { 636 653 Ok(id) => id, 637 654 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { 638 - return ApiError::HandleNotAvailable(None).into_response(); 655 + return Ok(ApiError::HandleNotAvailable(None).into_response()); 639 656 } 640 657 Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { 641 - return ApiError::EmailTaken.into_response(); 658 + return Ok(ApiError::EmailTaken.into_response()); 642 659 } 643 660 Err(e) => { 644 661 error!("Error creating delegated account: {:?}", e); 645 - return ApiError::InternalError(None).into_response(); 662 + return Ok(ApiError::InternalError(None).into_response()); 646 663 } 647 664 }; 648 665 ··· 678 695 .delegation_repo 679 696 .log_delegation_action( 680 697 &did, 681 - &auth.0.did, 682 - Some(&auth.0.did), 698 + &auth.did, 699 + Some(&auth.did), 683 700 DelegationActionType::GrantCreated, 684 701 Some(json!({ 685 702 "account_created": true, ··· 690 707 ) 691 708 .await; 692 709 693 - info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created"); 710 + info!(did = %did, handle = %handle, controller = %&auth.did, "Delegated account created"); 694 711 695 - Json(CreateDelegatedAccountResponse { did, handle }).into_response() 712 + Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response()) 696 713 }
+7
crates/tranquil-pds/src/api/error.rs
··· 543 543 crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated, 544 544 crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown, 545 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 + } 546 553 crate::auth::extractor::AuthError::OAuthExpiredToken(msg) => { 547 554 Self::OAuthExpiredToken(Some(msg)) 548 555 }
+88 -102
crates/tranquil-pds/src/api/identity/did.rs
··· 1 1 use crate::api::{ApiError, DidResponse, EmptyResponse}; 2 - use crate::auth::BearerAuthAllowDeactivated; 2 + use crate::auth::{Auth, NotTakendown}; 3 3 use crate::plc::signing_key_to_did_key; 4 4 use crate::state::AppState; 5 5 use crate::types::Handle; ··· 518 518 519 519 pub async fn get_recommended_did_credentials( 520 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 - }; 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 + 538 534 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 539 535 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 - }; 536 + let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes) 537 + .map_err(|_| ApiError::InternalError(None))?; 544 538 let did_key = signing_key_to_did_key(&signing_key); 545 - let rotation_keys = if auth_user.did.starts_with("did:web:") { 539 + let rotation_keys = if auth.did.starts_with("did:web:") { 546 540 vec![] 547 541 } else { 548 542 let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") { ··· 556 550 }; 557 551 vec![server_rotation_key] 558 552 }; 559 - ( 553 + Ok(( 560 554 StatusCode::OK, 561 555 Json(GetRecommendedDidCredentialsOutput { 562 556 rotation_keys, ··· 570 564 }, 571 565 }), 572 566 ) 573 - .into_response() 567 + .into_response()) 574 568 } 575 569 576 570 #[derive(Deserialize)] ··· 580 574 581 575 pub async fn update_handle( 582 576 State(state): State<AppState>, 583 - auth: BearerAuthAllowDeactivated, 577 + auth: Auth<NotTakendown>, 584 578 Json(input): Json<UpdateHandleInput>, 585 - ) -> Response { 586 - let auth_user = auth.0; 579 + ) -> Result<Response, ApiError> { 587 580 if let Err(e) = crate::auth::scope_check::check_identity_scope( 588 - auth_user.is_oauth, 589 - auth_user.scope.as_deref(), 581 + auth.is_oauth(), 582 + auth.scope.as_deref(), 590 583 crate::oauth::scopes::IdentityAttr::Handle, 591 584 ) { 592 - return e; 585 + return Ok(e); 593 586 } 594 - let did = auth_user.did; 587 + let did = auth.did.clone(); 595 588 if !state 596 589 .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 597 590 .await 598 591 { 599 - return ApiError::RateLimitExceeded(Some( 592 + return Err(ApiError::RateLimitExceeded(Some( 600 593 "Too many handle updates. Try again later.".into(), 601 - )) 602 - .into_response(); 594 + ))); 603 595 } 604 596 if !state 605 597 .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 606 598 .await 607 599 { 608 - return ApiError::RateLimitExceeded(Some("Daily handle update limit exceeded.".into())) 609 - .into_response(); 600 + return Err(ApiError::RateLimitExceeded(Some( 601 + "Daily handle update limit exceeded.".into(), 602 + ))); 610 603 } 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 - }; 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))?; 616 610 let user_id = user_row.id; 617 611 let current_handle = user_row.handle; 618 612 let new_handle = input.handle.trim().to_ascii_lowercase(); 619 613 if new_handle.is_empty() { 620 - return ApiError::InvalidRequest("handle is required".into()).into_response(); 614 + return Err(ApiError::InvalidRequest("handle is required".into())); 621 615 } 622 616 if !new_handle 623 617 .chars() 624 618 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 625 619 { 626 - return ApiError::InvalidHandle(Some("Handle contains invalid characters".into())) 627 - .into_response(); 620 + return Err(ApiError::InvalidHandle(Some( 621 + "Handle contains invalid characters".into(), 622 + ))); 628 623 } 629 624 if new_handle.split('.').any(|segment| segment.is_empty()) { 630 - return ApiError::InvalidHandle(Some("Handle contains empty segment".into())) 631 - .into_response(); 625 + return Err(ApiError::InvalidHandle(Some( 626 + "Handle contains empty segment".into(), 627 + ))); 632 628 } 633 629 if new_handle 634 630 .split('.') 635 631 .any(|segment| segment.starts_with('-') || segment.ends_with('-')) 636 632 { 637 - return ApiError::InvalidHandle(Some( 633 + return Err(ApiError::InvalidHandle(Some( 638 634 "Handle segment cannot start or end with hyphen".into(), 639 - )) 640 - .into_response(); 635 + ))); 641 636 } 642 637 if crate::moderation::has_explicit_slur(&new_handle) { 643 - return ApiError::InvalidHandle(Some("Inappropriate language in handle".into())) 644 - .into_response(); 638 + return Err(ApiError::InvalidHandle(Some( 639 + "Inappropriate language in handle".into(), 640 + ))); 645 641 } 646 642 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 647 643 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 667 663 { 668 664 warn!("Failed to sequence identity event for handle update: {}", e); 669 665 } 670 - return EmptyResponse::ok().into_response(); 666 + return Ok(EmptyResponse::ok().into_response()); 671 667 } 672 668 if short_part.contains('.') { 673 - return ApiError::InvalidHandle(Some( 669 + return Err(ApiError::InvalidHandle(Some( 674 670 "Nested subdomains are not allowed. Use a simple handle without dots.".into(), 675 - )) 676 - .into_response(); 671 + ))); 677 672 } 678 673 if short_part.len() < 3 { 679 - return ApiError::InvalidHandle(Some("Handle too short".into())).into_response(); 674 + return Err(ApiError::InvalidHandle(Some("Handle too short".into()))); 680 675 } 681 676 if short_part.len() > 18 { 682 - return ApiError::InvalidHandle(Some("Handle too long".into())).into_response(); 677 + return Err(ApiError::InvalidHandle(Some("Handle too long".into()))); 683 678 } 684 679 full_handle 685 680 } else { ··· 691 686 { 692 687 warn!("Failed to sequence identity event for handle update: {}", e); 693 688 } 694 - return EmptyResponse::ok().into_response(); 689 + return Ok(EmptyResponse::ok().into_response()); 695 690 } 696 691 match crate::handle::verify_handle_ownership(&new_handle, &did).await { 697 692 Ok(()) => {} 698 693 Err(crate::handle::HandleResolutionError::NotFound) => { 699 - return ApiError::HandleNotAvailable(None).into_response(); 694 + return Err(ApiError::HandleNotAvailable(None)); 700 695 } 701 696 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { 702 - return ApiError::HandleNotAvailable(Some(format!( 697 + return Err(ApiError::HandleNotAvailable(Some(format!( 703 698 "Handle points to different DID. Expected {}, got {}", 704 699 expected, actual 705 - ))) 706 - .into_response(); 700 + )))); 707 701 } 708 702 Err(e) => { 709 703 warn!("Handle verification failed: {}", e); 710 - return ApiError::HandleNotAvailable(Some(format!( 704 + return Err(ApiError::HandleNotAvailable(Some(format!( 711 705 "Handle verification failed: {}", 712 706 e 713 - ))) 714 - .into_response(); 707 + )))); 715 708 } 716 709 } 717 710 new_handle.clone() 718 711 }; 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 712 + let handle_typed: Handle = handle 713 + .parse() 714 + .map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?; 715 + let handle_exists = state 726 716 .user_repo 727 717 .check_handle_exists(&handle_typed, user_id) 728 718 .await 729 - { 730 - Ok(exists) => exists, 731 - Err(_) => return ApiError::InternalError(None).into_response(), 732 - }; 719 + .map_err(|_| ApiError::InternalError(None))?; 733 720 if handle_exists { 734 - return ApiError::HandleTaken.into_response(); 721 + return Err(ApiError::HandleTaken); 735 722 } 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) => { 723 + state 724 + .user_repo 725 + .update_handle(user_id, &handle_typed) 726 + .await 727 + .map_err(|e| { 758 728 error!("DB error updating handle: {:?}", e); 759 - ApiError::InternalError(None).into_response() 760 - } 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); 761 746 } 747 + Ok(EmptyResponse::ok().into_response()) 762 748 } 763 749 764 750 pub async fn update_plc_handle(
+23 -24
crates/tranquil-pds/src/api/identity/plc/request.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAllowDeactivated; 3 + use crate::auth::{Auth, NotTakendown}; 4 4 use crate::state::AppState; 5 5 use axum::{ 6 6 extract::State, ··· 15 15 16 16 pub async fn request_plc_operation_signature( 17 17 State(state): State<AppState>, 18 - auth: BearerAuthAllowDeactivated, 19 - ) -> Response { 20 - let auth_user = auth.0; 18 + auth: Auth<NotTakendown>, 19 + ) -> Result<Response, ApiError> { 21 20 if let Err(e) = crate::auth::scope_check::check_identity_scope( 22 - auth_user.is_oauth, 23 - auth_user.scope.as_deref(), 21 + auth.is_oauth(), 22 + auth.scope.as_deref(), 24 23 crate::oauth::scopes::IdentityAttr::Wildcard, 25 24 ) { 26 - return e; 25 + return Ok(e); 27 26 } 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) => { 27 + let user_id = state 28 + .user_repo 29 + .get_id_by_did(&auth.did) 30 + .await 31 + .map_err(|e| { 32 32 error!("DB error: {:?}", e); 33 - return ApiError::InternalError(None).into_response(); 34 - } 35 - }; 33 + ApiError::InternalError(None) 34 + })? 35 + .ok_or(ApiError::AccountNotFound)?; 36 + 36 37 let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await; 37 38 let plc_token = generate_plc_token(); 38 39 let expires_at = Utc::now() + Duration::minutes(10); 39 - if let Err(e) = state 40 + state 40 41 .infra_repo 41 42 .insert_plc_token(user_id, &plc_token, expires_at) 42 43 .await 43 - { 44 - error!("Failed to create PLC token: {:?}", e); 45 - return ApiError::InternalError(None).into_response(); 46 - } 44 + .map_err(|e| { 45 + error!("Failed to create PLC token: {:?}", e); 46 + ApiError::InternalError(None) 47 + })?; 48 + 47 49 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 48 50 if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation( 49 51 state.user_repo.as_ref(), ··· 56 58 { 57 59 warn!("Failed to enqueue PLC operation notification: {:?}", e); 58 60 } 59 - info!( 60 - "PLC operation signature requested for user {}", 61 - auth_user.did 62 - ); 63 - EmptyResponse::ok().into_response() 61 + info!("PLC operation signature requested for user {}", auth.did); 62 + Ok(EmptyResponse::ok().into_response()) 64 63 }
+70 -85
crates/tranquil-pds/src/api/identity/plc/sign.rs
··· 1 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuthAllowDeactivated; 2 + use crate::auth::{Auth, NotTakendown}; 3 3 use crate::circuit_breaker::with_circuit_breaker; 4 4 use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation}; 5 5 use crate::state::AppState; ··· 40 40 41 41 pub async fn sign_plc_operation( 42 42 State(state): State<AppState>, 43 - auth: BearerAuthAllowDeactivated, 43 + auth: Auth<NotTakendown>, 44 44 Json(input): Json<SignPlcOperationInput>, 45 - ) -> Response { 46 - let auth_user = auth.0; 45 + ) -> Result<Response, ApiError> { 47 46 if let Err(e) = crate::auth::scope_check::check_identity_scope( 48 - auth_user.is_oauth, 49 - auth_user.scope.as_deref(), 47 + auth.is_oauth(), 48 + auth.scope.as_deref(), 50 49 crate::oauth::scopes::IdentityAttr::Wildcard, 51 50 ) { 52 - return e; 51 + return Ok(e); 53 52 } 54 - let did = &auth_user.did; 53 + let did = &auth.did; 55 54 if did.starts_with("did:web:") { 56 - return ApiError::InvalidRequest( 55 + return Err(ApiError::InvalidRequest( 57 56 "PLC operations are only valid for did:plc identities".into(), 58 - ) 59 - .into_response(); 57 + )); 60 58 } 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) => { 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| { 74 68 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) => { 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| { 84 78 error!("DB error: {:?}", e); 85 - return ApiError::InternalError(None).into_response(); 86 - } 87 - }; 79 + ApiError::InternalError(None) 80 + })? 81 + .ok_or_else(|| ApiError::InvalidToken(Some("Invalid or expired token".into())))?; 82 + 88 83 if Utc::now() > token_expiry { 89 84 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 90 - return ApiError::ExpiredToken(Some("Token has expired".into())).into_response(); 85 + return Err(ApiError::ExpiredToken(Some("Token has expired".into()))); 91 86 } 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) => { 87 + let key_row = state 88 + .user_repo 89 + .get_user_key_by_id(user_id) 90 + .await 91 + .map_err(|e| { 99 92 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) => { 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| { 107 99 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 - }; 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 + 118 108 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 119 109 let did_clone = did.clone(); 120 - let last_op = match with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 110 + let last_op = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 121 111 plc_client.get_last_op(&did_clone).await 122 112 }) 123 113 .await 124 - { 125 - Ok(op) => op, 126 - Err(e) => return ApiError::from(e).into_response(), 127 - }; 114 + .map_err(ApiError::from)?; 115 + 128 116 if last_op.is_tombstone() { 129 - return ApiError::from(PlcError::Tombstoned).into_response(); 117 + return Err(ApiError::from(PlcError::Tombstoned)); 130 118 } 131 119 let services = input.services.map(|s| { 132 120 s.into_iter() ··· 141 129 }) 142 130 .collect() 143 131 }); 144 - let unsigned_op = match create_update_op( 132 + let unsigned_op = create_update_op( 145 133 &last_op, 146 134 input.rotation_keys, 147 135 input.verification_methods, 148 136 input.also_known_as, 149 137 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) => { 138 + ) 139 + .map_err(|e| match e { 140 + PlcError::Tombstoned => ApiError::InvalidRequest("Cannot update tombstoned DID".into()), 141 + _ => { 156 142 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(); 143 + ApiError::InternalError(None) 165 144 } 166 - }; 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 + 167 152 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 168 153 info!("Signed PLC operation for user {}", did); 169 - ( 154 + Ok(( 170 155 StatusCode::OK, 171 156 Json(SignPlcOperationOutput { 172 157 operation: signed_op, 173 158 }), 174 159 ) 175 - .into_response() 160 + .into_response()) 176 161 }
+58 -61
crates/tranquil-pds/src/api/identity/plc/submit.rs
··· 1 1 use crate::api::{ApiError, EmptyResponse}; 2 - use crate::auth::BearerAuthAllowDeactivated; 2 + use crate::auth::{Auth, NotTakendown}; 3 3 use crate::circuit_breaker::with_circuit_breaker; 4 4 use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; 5 5 use crate::state::AppState; ··· 20 20 21 21 pub async fn submit_plc_operation( 22 22 State(state): State<AppState>, 23 - auth: BearerAuthAllowDeactivated, 23 + auth: Auth<NotTakendown>, 24 24 Json(input): Json<SubmitPlcOperationInput>, 25 - ) -> Response { 26 - let auth_user = auth.0; 25 + ) -> Result<Response, ApiError> { 27 26 if let Err(e) = crate::auth::scope_check::check_identity_scope( 28 - auth_user.is_oauth, 29 - auth_user.scope.as_deref(), 27 + auth.is_oauth(), 28 + auth.scope.as_deref(), 30 29 crate::oauth::scopes::IdentityAttr::Wildcard, 31 30 ) { 32 - return e; 31 + return Ok(e); 33 32 } 34 - let did = &auth_user.did; 33 + let did = &auth.did; 35 34 if did.starts_with("did:web:") { 36 - return ApiError::InvalidRequest( 35 + return Err(ApiError::InvalidRequest( 37 36 "PLC operations are only valid for did:plc identities".into(), 38 - ) 39 - .into_response(); 37 + )); 40 38 } 41 - if let Err(e) = validate_plc_operation(&input.operation) { 42 - return ApiError::InvalidRequest(format!("Invalid operation: {}", e)).into_response(); 43 - } 39 + validate_plc_operation(&input.operation) 40 + .map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?; 41 + 44 42 let op = &input.operation; 45 43 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 46 44 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) => { 45 + let user = state 46 + .user_repo 47 + .get_id_and_handle_by_did(did) 48 + .await 49 + .map_err(|e| { 51 50 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) => { 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| { 62 60 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) => { 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| { 70 67 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 - }; 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 + 81 76 let user_did_key = signing_key_to_did_key(&signing_key); 82 77 let server_rotation_key = 83 78 std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); ··· 86 81 .iter() 87 82 .any(|k| k.as_str() == Some(&server_rotation_key)); 88 83 if !has_server_key { 89 - return ApiError::InvalidRequest( 84 + return Err(ApiError::InvalidRequest( 90 85 "Rotation keys do not include server's rotation key".into(), 91 - ) 92 - .into_response(); 86 + )); 93 87 } 94 88 } 95 89 if let Some(services) = op.get("services").and_then(|v| v.as_object()) ··· 98 92 let service_type = pds.get("type").and_then(|v| v.as_str()); 99 93 let endpoint = pds.get("endpoint").and_then(|v| v.as_str()); 100 94 if service_type != Some("AtprotoPersonalDataServer") { 101 - return ApiError::InvalidRequest("Incorrect type on atproto_pds service".into()) 102 - .into_response(); 95 + return Err(ApiError::InvalidRequest( 96 + "Incorrect type on atproto_pds service".into(), 97 + )); 103 98 } 104 99 if endpoint != Some(&public_url) { 105 - return ApiError::InvalidRequest("Incorrect endpoint on atproto_pds service".into()) 106 - .into_response(); 100 + return Err(ApiError::InvalidRequest( 101 + "Incorrect endpoint on atproto_pds service".into(), 102 + )); 107 103 } 108 104 } 109 105 if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object()) 110 106 && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 111 107 && atproto_key != user_did_key 112 108 { 113 - return ApiError::InvalidRequest("Incorrect signing key in verificationMethods".into()) 114 - .into_response(); 109 + return Err(ApiError::InvalidRequest( 110 + "Incorrect signing key in verificationMethods".into(), 111 + )); 115 112 } 116 113 if let Some(also_known_as) = (!user.handle.is_empty()) 117 114 .then(|| op.get("alsoKnownAs").and_then(|v| v.as_array())) ··· 120 117 let expected_handle = format!("at://{}", user.handle); 121 118 let first_aka = also_known_as.first().and_then(|v| v.as_str()); 122 119 if first_aka != Some(&expected_handle) { 123 - return ApiError::InvalidRequest("Incorrect handle in alsoKnownAs".into()) 124 - .into_response(); 120 + return Err(ApiError::InvalidRequest( 121 + "Incorrect handle in alsoKnownAs".into(), 122 + )); 125 123 } 126 124 } 127 125 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 128 126 let operation_clone = input.operation.clone(); 129 127 let did_clone = did.clone(); 130 - if let Err(e) = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 128 + with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 131 129 plc_client 132 130 .send_operation(&did_clone, &operation_clone) 133 131 .await 134 132 }) 135 133 .await 136 - { 137 - return ApiError::from(e).into_response(); 138 - } 134 + .map_err(ApiError::from)?; 135 + 139 136 match state 140 137 .repo_repo 141 138 .insert_identity_event(did, Some(&user.handle)) ··· 157 154 warn!(did = %did, "Failed to refresh DID cache after PLC update"); 158 155 } 159 156 info!(did = %did, "PLC operation submitted successfully"); 160 - EmptyResponse::ok().into_response() 157 + Ok(EmptyResponse::ok().into_response()) 161 158 }
+5 -7
crates/tranquil-pds/src/api/moderation/mod.rs
··· 1 1 use crate::api::ApiError; 2 2 use crate::api::proxy_client::{is_ssrf_safe, proxy_client}; 3 - use crate::auth::extractor::BearerAuthAllowTakendown; 3 + use crate::auth::{AnyUser, Auth}; 4 4 use crate::state::AppState; 5 5 use axum::{ 6 6 Json, ··· 42 42 43 43 pub async fn create_report( 44 44 State(state): State<AppState>, 45 - auth: BearerAuthAllowTakendown, 45 + auth: Auth<AnyUser>, 46 46 Json(input): Json<CreateReportInput>, 47 47 ) -> Response { 48 - let auth_user = auth.0; 49 - let did = &auth_user.did; 48 + let did = &auth.did; 50 49 51 50 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; 51 + return proxy_to_report_service(&state, &auth, &service_url, &service_did, &input).await; 54 52 } 55 53 56 - create_report_locally(&state, did, auth_user.is_takendown(), input).await 54 + create_report_locally(&state, did, auth.status.is_takendown(), input).await 57 55 } 58 56 59 57 async fn proxy_to_report_service(
+82 -98
crates/tranquil-pds/src/api/notification_prefs.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuth; 2 + use crate::auth::{Active, Auth}; 3 3 use crate::state::AppState; 4 4 use axum::{ 5 5 Json, ··· 23 23 pub signal_verified: bool, 24 24 } 25 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 { 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 { 36 37 preferred_channel: prefs.preferred_channel, 37 38 email: prefs.email, 38 39 discord_id: prefs.discord_id, ··· 42 43 signal_number: prefs.signal_number, 43 44 signal_verified: prefs.signal_verified, 44 45 }) 45 - .into_response() 46 + .into_response()) 46 47 } 47 48 48 49 #[derive(Serialize)] ··· 62 63 pub notifications: Vec<NotificationHistoryEntry>, 63 64 } 64 65 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 - }; 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)?; 75 76 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 - }; 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 82 83 83 let sensitive_types = [ 84 84 "email_verification", ··· 111 111 }) 112 112 .collect(); 113 113 114 - Json(GetNotificationHistoryResponse { notifications }).into_response() 114 + Ok(Json(GetNotificationHistoryResponse { notifications }).into_response()) 115 115 } 116 116 117 117 #[derive(Deserialize)] ··· 184 184 185 185 pub async fn update_notification_prefs( 186 186 State(state): State<AppState>, 187 - auth: BearerAuth, 187 + auth: Auth<Active>, 188 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 - }; 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)?; 199 196 200 197 let user_id = user_row.id; 201 198 let handle = user_row.handle; ··· 206 203 if let Some(ref channel) = input.preferred_channel { 207 204 let valid_channels = ["email", "discord", "telegram", "signal"]; 208 205 if !valid_channels.contains(&channel.as_str()) { 209 - return ApiError::InvalidRequest( 206 + return Err(ApiError::InvalidRequest( 210 207 "Invalid channel. Must be one of: email, discord, telegram, signal".into(), 211 - ) 212 - .into_response(); 208 + )); 213 209 } 214 - if let Err(e) = state 210 + state 215 211 .user_repo 216 - .update_preferred_comms_channel(&user.did, channel) 212 + .update_preferred_comms_channel(&auth.did, channel) 217 213 .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"); 214 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 215 + info!(did = %auth.did, channel = %channel, "Updated preferred notification channel"); 222 216 } 223 217 224 218 if let Some(ref new_email) = input.email { 225 219 let email_clean = new_email.trim().to_lowercase(); 226 220 if email_clean.is_empty() { 227 - return ApiError::InvalidRequest("Email cannot be empty".into()).into_response(); 221 + return Err(ApiError::InvalidRequest("Email cannot be empty".into())); 228 222 } 229 223 230 224 if !crate::api::validation::is_valid_email(&email_clean) { 231 - return ApiError::InvalidEmail.into_response(); 225 + return Err(ApiError::InvalidEmail); 232 226 } 233 227 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( 228 + if current_email.as_ref().map(|e| e.to_lowercase()) != Some(email_clean.clone()) { 229 + request_channel_verification( 238 230 &state, 239 231 user_id, 240 - &user.did, 232 + &auth.did, 241 233 "email", 242 234 &email_clean, 243 235 Some(&handle), 244 236 ) 245 237 .await 246 - { 247 - return ApiError::InternalError(Some(e)).into_response(); 248 - } 238 + .map_err(|e| ApiError::InternalError(Some(e)))?; 249 239 verification_required.push("email".to_string()); 250 - info!(did = %user.did, "Requested email verification"); 240 + info!(did = %auth.did, "Requested email verification"); 251 241 } 252 242 } 253 243 254 244 if let Some(ref discord_id) = input.discord_id { 255 245 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"); 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"); 261 252 } 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 - } 253 + request_channel_verification(&state, user_id, &auth.did, "discord", discord_id, None) 254 + .await 255 + .map_err(|e| ApiError::InternalError(Some(e)))?; 269 256 verification_required.push("discord".to_string()); 270 - info!(did = %user.did, "Requested Discord verification"); 257 + info!(did = %auth.did, "Requested Discord verification"); 271 258 } 272 259 } 273 260 274 261 if let Some(ref telegram) = input.telegram_username { 275 262 let telegram_clean = telegram.trim_start_matches('@'); 276 263 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"); 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"); 282 270 } else { 283 - if let Err(e) = request_channel_verification( 271 + request_channel_verification( 284 272 &state, 285 273 user_id, 286 - &user.did, 274 + &auth.did, 287 275 "telegram", 288 276 telegram_clean, 289 277 None, 290 278 ) 291 279 .await 292 - { 293 - return ApiError::InternalError(Some(e)).into_response(); 294 - } 280 + .map_err(|e| ApiError::InternalError(Some(e)))?; 295 281 verification_required.push("telegram".to_string()); 296 - info!(did = %user.did, "Requested Telegram verification"); 282 + info!(did = %auth.did, "Requested Telegram verification"); 297 283 } 298 284 } 299 285 300 286 if let Some(ref signal) = input.signal_number { 301 287 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"); 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"); 307 294 } 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 - } 295 + request_channel_verification(&state, user_id, &auth.did, "signal", signal, None) 296 + .await 297 + .map_err(|e| ApiError::InternalError(Some(e)))?; 314 298 verification_required.push("signal".to_string()); 315 - info!(did = %user.did, "Requested Signal verification"); 299 + info!(did = %auth.did, "Requested Signal verification"); 316 300 } 317 301 } 318 302 319 - Json(UpdateNotificationPrefsResponse { 303 + Ok(Json(UpdateNotificationPrefsResponse { 320 304 success: true, 321 305 verification_required, 322 306 }) 323 - .into_response() 307 + .into_response()) 324 308 }
+1 -1
crates/tranquil-pds/src/api/proxy.rs
··· 238 238 { 239 239 Ok(auth_user) => { 240 240 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 241 - auth_user.is_oauth, 241 + auth_user.is_oauth(), 242 242 auth_user.scope.as_deref(), 243 243 &resolved.did, 244 244 method,
+57 -52
crates/tranquil-pds/src/api/repo/blob.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::{BearerAuthAllowDeactivated, BlobAuth, BlobAuthResult}; 2 + use crate::auth::{Auth, AuthAny, NotTakendown, Permissive}; 3 3 use crate::delegation::DelegationActionType; 4 4 use crate::state::AppState; 5 5 use crate::types::{CidLink, Did}; ··· 44 44 pub async fn upload_blob( 45 45 State(state): State<AppState>, 46 46 headers: axum::http::HeaderMap, 47 - auth: BlobAuth, 47 + auth: AuthAny<Permissive>, 48 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) => { 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 + } 53 59 let mime_type_for_check = headers 54 60 .get("content-type") 55 61 .and_then(|h| h.to_str().ok()) 56 62 .unwrap_or("application/octet-stream"); 57 63 if let Err(e) = crate::auth::scope_check::check_blob_scope( 58 - auth_user.is_oauth, 59 - auth_user.scope.as_deref(), 64 + user.is_oauth(), 65 + user.scope.as_deref(), 60 66 mime_type_for_check, 61 67 ) { 62 - return e; 68 + return Ok(e); 63 69 } 64 - let ctrl_did = auth_user.controller_did.clone(); 65 - (auth_user.did, ctrl_did) 70 + (user.did.clone(), user.controller_did.clone()) 66 71 } 67 72 }; 68 73 ··· 72 77 .await 73 78 .unwrap_or(false) 74 79 { 75 - return ApiError::Forbidden.into_response(); 80 + return Err(ApiError::Forbidden); 76 81 } 77 82 78 83 let client_mime_hint = headers ··· 80 85 .and_then(|h| h.to_str().ok()) 81 86 .unwrap_or("application/octet-stream"); 82 87 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 - }; 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))?; 89 95 90 96 let temp_key = format!("temp/{}", uuid::Uuid::new_v4()); 91 97 let max_size = get_max_blob_size() as u64; ··· 98 104 99 105 info!("Starting streaming blob upload to temp key: {}", temp_key); 100 106 101 - let upload_result = match state.blob_store.put_stream(&temp_key, pinned_stream).await { 102 - Ok(result) => result, 103 - Err(e) => { 107 + let upload_result = state 108 + .blob_store 109 + .put_stream(&temp_key, pinned_stream) 110 + .await 111 + .map_err(|e| { 104 112 error!("Failed to stream blob to storage: {:?}", e); 105 - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 106 - } 107 - }; 113 + ApiError::InternalError(Some("Failed to store blob".into())) 114 + })?; 108 115 109 116 let size = upload_result.size; 110 117 if size > max_size { 111 118 let _ = state.blob_store.delete(&temp_key).await; 112 - return ApiError::InvalidRequest(format!( 119 + return Err(ApiError::InvalidRequest(format!( 113 120 "Blob size {} exceeds maximum of {} bytes", 114 121 size, max_size 115 - )) 116 - .into_response(); 122 + ))); 117 123 } 118 124 119 125 let mime_type = match state.blob_store.get_head(&temp_key, 8192).await { ··· 129 135 Err(e) => { 130 136 let _ = state.blob_store.delete(&temp_key).await; 131 137 error!("Failed to create multihash for blob: {:?}", e); 132 - return ApiError::InternalError(Some("Failed to hash blob".into())).into_response(); 138 + return Err(ApiError::InternalError(Some("Failed to hash blob".into()))); 133 139 } 134 140 }; 135 141 let cid = Cid::new_v1(0x55, multihash); ··· 152 158 Err(e) => { 153 159 let _ = state.blob_store.delete(&temp_key).await; 154 160 error!("Failed to insert blob record: {:?}", e); 155 - return ApiError::InternalError(None).into_response(); 161 + return Err(ApiError::InternalError(None)); 156 162 } 157 163 }; 158 164 159 165 if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await { 160 166 let _ = state.blob_store.delete(&temp_key).await; 161 167 error!("Failed to copy blob to final location: {:?}", e); 162 - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 168 + return Err(ApiError::InternalError(Some("Failed to store blob".into()))); 163 169 } 164 170 165 171 let _ = state.blob_store.delete(&temp_key).await; ··· 183 189 .await; 184 190 } 185 191 186 - Json(json!({ 192 + Ok(Json(json!({ 187 193 "blob": { 188 194 "$type": "blob", 189 195 "ref": { ··· 193 199 "size": size 194 200 } 195 201 })) 196 - .into_response() 202 + .into_response()) 197 203 } 198 204 199 205 #[derive(Deserialize)] ··· 218 224 219 225 pub async fn list_missing_blobs( 220 226 State(state): State<AppState>, 221 - auth: BearerAuthAllowDeactivated, 227 + auth: Auth<NotTakendown>, 222 228 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) => { 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| { 230 236 error!("DB error fetching user: {:?}", e); 231 - return ApiError::InternalError(None).into_response(); 232 - } 233 - }; 237 + ApiError::InternalError(None) 238 + })? 239 + .ok_or(ApiError::InternalError(None))?; 240 + 234 241 let limit = params.limit.unwrap_or(500).clamp(1, 1000); 235 242 let cursor = params.cursor.as_deref(); 236 - let missing = match state 243 + let missing = state 237 244 .blob_repo 238 245 .list_missing_blobs(user.id, cursor, limit + 1) 239 246 .await 240 - { 241 - Ok(m) => m, 242 - Err(e) => { 247 + .map_err(|e| { 243 248 error!("DB error fetching missing blobs: {:?}", e); 244 - return ApiError::InternalError(None).into_response(); 245 - } 246 - }; 249 + ApiError::InternalError(None) 250 + })?; 251 + 247 252 let has_more = missing.len() > limit as usize; 248 253 let blobs: Vec<RecordBlob> = missing 249 254 .into_iter() ··· 258 263 } else { 259 264 None 260 265 }; 261 - ( 266 + Ok(( 262 267 StatusCode::OK, 263 268 Json(ListMissingBlobsOutput { 264 269 cursor: next_cursor, 265 270 blobs, 266 271 }), 267 272 ) 268 - .into_response() 273 + .into_response()) 269 274 }
+129 -131
crates/tranquil-pds/src/api/repo/import.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 3 use crate::api::repo::record::create_signed_commit; 4 - use crate::auth::BearerAuthAllowDeactivated; 4 + use crate::auth::{Auth, NotTakendown}; 5 5 use crate::state::AppState; 6 6 use crate::sync::import::{ImportError, apply_import, parse_car}; 7 7 use crate::sync::verify::CarVerifier; ··· 23 23 24 24 pub async fn import_repo( 25 25 State(state): State<AppState>, 26 - auth: BearerAuthAllowDeactivated, 26 + auth: Auth<NotTakendown>, 27 27 body: Bytes, 28 - ) -> Response { 28 + ) -> Result<Response, ApiError> { 29 29 let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS") 30 30 .map(|v| v != "false" && v != "0") 31 31 .unwrap_or(true); 32 32 if !accepting_imports { 33 - return ApiError::InvalidRequest("Service is not accepting repo imports".into()) 34 - .into_response(); 33 + return Err(ApiError::InvalidRequest( 34 + "Service is not accepting repo imports".into(), 35 + )); 35 36 } 36 37 let max_size: usize = std::env::var("MAX_IMPORT_SIZE") 37 38 .ok() 38 39 .and_then(|s| s.parse().ok()) 39 40 .unwrap_or(DEFAULT_MAX_IMPORT_SIZE); 40 41 if body.len() > max_size { 41 - return ApiError::PayloadTooLarge(format!( 42 + return Err(ApiError::PayloadTooLarge(format!( 42 43 "Import size exceeds limit of {} bytes", 43 44 max_size 44 - )) 45 - .into_response(); 45 + ))); 46 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) => { 47 + let did = &auth.did; 48 + let user = state 49 + .user_repo 50 + .get_by_did(did) 51 + .await 52 + .map_err(|e| { 55 53 error!("DB error fetching user: {:?}", e); 56 - return ApiError::InternalError(None).into_response(); 57 - } 58 - }; 54 + ApiError::InternalError(None) 55 + })? 56 + .ok_or(ApiError::AccountNotFound)?; 59 57 if user.takedown_ref.is_some() { 60 - return ApiError::AccountTakedown.into_response(); 58 + return Err(ApiError::AccountTakedown); 61 59 } 62 60 let user_id = user.id; 63 61 let (root, blocks) = match parse_car(&body).await { 64 62 Ok((r, b)) => (r, b), 65 63 Err(ImportError::InvalidRootCount) => { 66 - return ApiError::InvalidRequest("Expected exactly one root in CAR file".into()) 67 - .into_response(); 64 + return Err(ApiError::InvalidRequest( 65 + "Expected exactly one root in CAR file".into(), 66 + )); 68 67 } 69 68 Err(ImportError::CarParse(msg)) => { 70 - return ApiError::InvalidRequest(format!("Failed to parse CAR file: {}", msg)) 71 - .into_response(); 69 + return Err(ApiError::InvalidRequest(format!( 70 + "Failed to parse CAR file: {}", 71 + msg 72 + ))); 72 73 } 73 74 Err(e) => { 74 75 error!("CAR parsing error: {:?}", e); 75 - return ApiError::InvalidRequest(format!("Invalid CAR file: {}", e)).into_response(); 76 + return Err(ApiError::InvalidRequest(format!("Invalid CAR file: {}", e))); 76 77 } 77 78 }; 78 79 info!( ··· 82 83 root 83 84 ); 84 85 let Some(root_block) = blocks.get(&root) else { 85 - return ApiError::InvalidRequest("Root block not found in CAR file".into()).into_response(); 86 + return Err(ApiError::InvalidRequest( 87 + "Root block not found in CAR file".into(), 88 + )); 86 89 }; 87 90 let commit_did = match jacquard_repo::commit::Commit::from_cbor(root_block) { 88 91 Ok(commit) => commit.did().to_string(), 89 92 Err(e) => { 90 - return ApiError::InvalidRequest(format!("Invalid commit: {}", e)).into_response(); 93 + return Err(ApiError::InvalidRequest(format!("Invalid commit: {}", e))); 91 94 } 92 95 }; 93 96 if commit_did != *did { 94 - return ApiError::InvalidRepo(format!( 97 + return Err(ApiError::InvalidRepo(format!( 95 98 "CAR file is for DID {} but you are authenticated as {}", 96 99 commit_did, did 97 - )) 98 - .into_response(); 100 + ))); 99 101 } 100 102 let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION") 101 103 .map(|v| v == "true" || v == "1") ··· 117 119 commit_did, 118 120 expected_did, 119 121 }) => { 120 - return ApiError::InvalidRepo(format!( 122 + return Err(ApiError::InvalidRepo(format!( 121 123 "CAR file is for DID {} but you are authenticated as {}", 122 124 commit_did, expected_did 123 - )) 124 - .into_response(); 125 + ))); 125 126 } 126 127 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 127 - return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 128 - .into_response(); 128 + return Err(ApiError::InvalidRequest(format!( 129 + "MST validation failed: {}", 130 + msg 131 + ))); 129 132 } 130 133 Err(e) => { 131 134 error!("CAR structure verification error: {:?}", e); 132 - return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) 133 - .into_response(); 135 + return Err(ApiError::InvalidRequest(format!( 136 + "CAR verification failed: {}", 137 + e 138 + ))); 134 139 } 135 140 } 136 141 } else { ··· 147 152 commit_did, 148 153 expected_did, 149 154 }) => { 150 - return ApiError::InvalidRepo(format!( 155 + return Err(ApiError::InvalidRepo(format!( 151 156 "CAR file is for DID {} but you are authenticated as {}", 152 157 commit_did, expected_did 153 - )) 154 - .into_response(); 158 + ))); 155 159 } 156 160 Err(crate::sync::verify::VerifyError::InvalidSignature) => { 157 - return ApiError::InvalidRequest( 161 + return Err(ApiError::InvalidRequest( 158 162 "CAR file commit signature verification failed".into(), 159 - ) 160 - .into_response(); 163 + )); 161 164 } 162 165 Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => { 163 166 warn!("DID resolution failed during import verification: {}", msg); 164 - return ApiError::InvalidRequest(format!("Failed to verify DID: {}", msg)) 165 - .into_response(); 167 + return Err(ApiError::InvalidRequest(format!( 168 + "Failed to verify DID: {}", 169 + msg 170 + ))); 166 171 } 167 172 Err(crate::sync::verify::VerifyError::NoSigningKey) => { 168 - return ApiError::InvalidRequest( 173 + return Err(ApiError::InvalidRequest( 169 174 "DID document does not contain a signing key".into(), 170 - ) 171 - .into_response(); 175 + )); 172 176 } 173 177 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 174 - return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 175 - .into_response(); 178 + return Err(ApiError::InvalidRequest(format!( 179 + "MST validation failed: {}", 180 + msg 181 + ))); 176 182 } 177 183 Err(e) => { 178 184 error!("CAR verification error: {:?}", e); 179 - return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) 180 - .into_response(); 185 + return Err(ApiError::InvalidRequest(format!( 186 + "CAR verification failed: {}", 187 + e 188 + ))); 181 189 } 182 190 } 183 191 } ··· 227 235 } 228 236 } 229 237 } 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 + let key_row = state 239 + .user_repo 240 + .get_user_with_key_by_did(did) 241 + .await 242 + .map_err(|e| { 238 243 error!("DB error fetching signing key: {:?}", e); 239 - return ApiError::InternalError(None).into_response(); 240 - } 241 - }; 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 + })?; 242 250 let key_bytes = 243 - match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) { 244 - Ok(k) => k, 245 - Err(e) => { 251 + crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 252 + .map_err(|e| { 246 253 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 - }; 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 + })?; 257 260 let new_rev = Tid::now(LimitedU32::MIN); 258 261 let new_rev_str = new_rev.to_string(); 259 - let (commit_bytes, _sig) = match create_signed_commit( 262 + let (commit_bytes, _sig) = create_signed_commit( 260 263 did, 261 264 import_result.data_cid, 262 265 &new_rev_str, 263 266 None, 264 267 &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) => { 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 275 error!("Failed to store new commit block: {:?}", e); 276 - return ApiError::InternalError(None).into_response(); 277 - } 278 - }; 276 + ApiError::InternalError(None) 277 + })?; 279 278 let new_root_cid_link = CidLink::new_unchecked(new_root_cid.to_string()); 280 - if let Err(e) = state 279 + state 281 280 .repo_repo 282 281 .update_repo_root(user_id, &new_root_cid_link, &new_rev_str) 283 282 .await 284 - { 285 - error!("Failed to update repo root: {:?}", e); 286 - return ApiError::InternalError(None).into_response(); 287 - } 283 + .map_err(|e| { 284 + error!("Failed to update repo root: {:?}", e); 285 + ApiError::InternalError(None) 286 + })?; 288 287 let mut all_block_cids: Vec<Vec<u8>> = blocks.keys().map(|c| c.to_bytes()).collect(); 289 288 all_block_cids.push(new_root_cid.to_bytes()); 290 - if let Err(e) = state 289 + state 291 290 .repo_repo 292 291 .insert_user_blocks(user_id, &all_block_cids, &new_rev_str) 293 292 .await 294 - { 295 - error!("Failed to insert user_blocks: {:?}", e); 296 - return ApiError::InternalError(None).into_response(); 297 - } 293 + .map_err(|e| { 294 + error!("Failed to insert user_blocks: {:?}", e); 295 + ApiError::InternalError(None) 296 + })?; 298 297 let new_root_str = new_root_cid.to_string(); 299 298 info!( 300 299 "Created new commit for imported repo: cid={}, rev={}", ··· 324 323 ); 325 324 } 326 325 } 327 - EmptyResponse::ok().into_response() 326 + Ok(EmptyResponse::ok().into_response()) 328 327 } 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( 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 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(), 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 + )), 359 357 Err(e) => { 360 358 error!("Import error: {:?}", e); 361 - ApiError::InternalError(None).into_response() 359 + Err(ApiError::InternalError(None)) 362 360 } 363 361 } 364 362 }
+63 -65
crates/tranquil-pds/src/api/repo/record/batch.rs
··· 1 1 use super::validation::validate_record_with_status; 2 2 use crate::api::error::ApiError; 3 3 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; 4 - use crate::auth::BearerAuth; 4 + use crate::auth::{Active, Auth}; 5 5 use crate::delegation::DelegationActionType; 6 6 use crate::repo::tracking::TrackingBlockStore; 7 7 use crate::state::AppState; ··· 262 262 263 263 pub async fn apply_writes( 264 264 State(state): State<AppState>, 265 - auth: BearerAuth, 265 + auth: Auth<Active>, 266 266 Json(input): Json<ApplyWritesInput>, 267 - ) -> Response { 267 + ) -> Result<Response, ApiError> { 268 268 info!( 269 269 "apply_writes called: repo={}, writes={}", 270 270 input.repo, 271 271 input.writes.len() 272 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(); 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(); 278 277 if input.repo.as_str() != did { 279 - return ApiError::InvalidRepo("Repo does not match authenticated user".into()) 280 - .into_response(); 278 + return Err(ApiError::InvalidRepo( 279 + "Repo does not match authenticated user".into(), 280 + )); 281 281 } 282 282 if state 283 283 .user_repo ··· 285 285 .await 286 286 .unwrap_or(false) 287 287 { 288 - return ApiError::AccountMigrated.into_response(); 288 + return Err(ApiError::AccountMigrated); 289 289 } 290 290 let is_verified = state 291 291 .user_repo ··· 298 298 .await 299 299 .unwrap_or(false); 300 300 if !is_verified && !is_delegated { 301 - return ApiError::AccountNotVerified.into_response(); 301 + return Err(ApiError::AccountNotVerified); 302 302 } 303 303 if input.writes.is_empty() { 304 - return ApiError::InvalidRequest("writes array is empty".into()).into_response(); 304 + return Err(ApiError::InvalidRequest("writes array is empty".into())); 305 305 } 306 306 if input.writes.len() > MAX_BATCH_WRITES { 307 - return ApiError::InvalidRequest(format!("Too many writes (max {})", MAX_BATCH_WRITES)) 308 - .into_response(); 307 + return Err(ApiError::InvalidRequest(format!( 308 + "Too many writes (max {})", 309 + MAX_BATCH_WRITES 310 + ))); 309 311 } 310 312 311 313 let has_custom_scope = scope ··· 374 376 }) 375 377 .next() 376 378 { 377 - return err; 379 + return Ok(err); 378 380 } 379 381 } 380 382 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 - }; 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())))?; 395 399 if let Some(swap_commit) = &input.swap_commit 396 400 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 397 401 { 398 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 402 + return Err(ApiError::InvalidSwap(Some("Repo has been modified".into()))); 399 403 } 400 404 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 - }; 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())))?; 409 413 let original_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 410 414 let initial_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 411 415 let WriteAccumulator { ··· 424 428 .await 425 429 { 426 430 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 - } 431 + Err(response) => return Ok(response), 434 432 }; 433 + let new_mst_root = mst 434 + .persist() 435 + .await 436 + .map_err(|_| ApiError::InternalError(Some("Failed to persist MST".into())))?; 435 437 let (new_mst_blocks, old_mst_blocks) = { 436 438 let mut new_blocks = std::collections::BTreeMap::new(); 437 439 let mut old_blocks = std::collections::BTreeMap::new(); 438 440 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 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 446 447 .blocks_for_path(key, &mut old_blocks) 447 448 .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 - } 449 + .map_err(|_| { 450 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 451 + })?; 455 452 } 456 453 (new_blocks, old_blocks) 457 454 }; ··· 503 500 { 504 501 Ok(res) => res, 505 502 Err(e) if e.contains("ConcurrentModification") => { 506 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 503 + return Err(ApiError::InvalidSwap(Some("Repo has been modified".into()))); 507 504 } 508 505 Err(e) => { 509 506 error!("Commit failed: {}", e); 510 - return ApiError::InternalError(Some("Failed to commit changes".into())) 511 - .into_response(); 507 + return Err(ApiError::InternalError(Some( 508 + "Failed to commit changes".into(), 509 + ))); 512 510 } 513 511 }; 514 512 ··· 557 555 .await; 558 556 } 559 557 560 - ( 558 + Ok(( 561 559 StatusCode::OK, 562 560 Json(ApplyWritesOutput { 563 561 commit: CommitInfo { ··· 567 565 results, 568 566 }), 569 567 ) 570 - .into_response() 568 + .into_response()) 571 569 }
+46 -29
crates/tranquil-pds/src/api/repo/record/delete.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log}; 3 3 use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; 4 - use crate::auth::BearerAuth; 4 + use crate::auth::{Active, Auth}; 5 5 use crate::delegation::DelegationActionType; 6 6 use crate::repo::tracking::TrackingBlockStore; 7 7 use crate::state::AppState; ··· 40 40 41 41 pub async fn delete_record( 42 42 State(state): State<AppState>, 43 - auth: BearerAuth, 43 + auth: Auth<Active>, 44 44 Json(input): Json<DeleteRecordInput>, 45 - ) -> Response { 46 - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { 45 + ) -> Result<Response, crate::api::error::ApiError> { 46 + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 47 47 Ok(res) => res, 48 - Err(err_res) => return err_res, 48 + Err(err_res) => return Ok(err_res), 49 49 }; 50 50 51 51 if let Err(e) = crate::auth::scope_check::check_repo_scope( 52 - auth.is_oauth, 53 - auth.scope.as_deref(), 52 + repo_auth.is_oauth, 53 + repo_auth.scope.as_deref(), 54 54 crate::oauth::RepoAction::Delete, 55 55 &input.collection, 56 56 ) { 57 - return e; 57 + return Ok(e); 58 58 } 59 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; 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 64 65 65 if let Some(swap_commit) = &input.swap_commit 66 66 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 67 67 { 68 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 68 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 69 69 } 70 70 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 71 71 let commit_bytes = match tracking_store.get(&current_root_cid).await { 72 72 Ok(Some(b)) => b, 73 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 73 + _ => { 74 + return Ok( 75 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 76 + ); 77 + } 74 78 }; 75 79 let commit = match Commit::from_cbor(&commit_bytes) { 76 80 Ok(c) => c, 77 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 81 + _ => { 82 + return Ok( 83 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 84 + ); 85 + } 78 86 }; 79 87 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 80 88 let key = format!("{}/{}", input.collection, input.rkey); ··· 82 90 let expected_cid = Cid::from_str(swap_record_str).ok(); 83 91 let actual_cid = mst.get(&key).await.ok().flatten(); 84 92 if expected_cid != actual_cid { 85 - return ApiError::InvalidSwap(Some( 93 + return Ok(ApiError::InvalidSwap(Some( 86 94 "Record has been modified or does not exist".into(), 87 95 )) 88 - .into_response(); 96 + .into_response()); 89 97 } 90 98 } 91 99 let prev_record_cid = mst.get(&key).await.ok().flatten(); 92 100 if prev_record_cid.is_none() { 93 - return (StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response(); 101 + return Ok((StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response()); 94 102 } 95 103 let new_mst = match mst.delete(&key).await { 96 104 Ok(m) => m, 97 105 Err(e) => { 98 106 error!("Failed to delete from MST: {:?}", e); 99 - return ApiError::InternalError(Some(format!("Failed to delete from MST: {:?}", e))) 100 - .into_response(); 107 + return Ok(ApiError::InternalError(Some(format!( 108 + "Failed to delete from MST: {:?}", 109 + e 110 + ))) 111 + .into_response()); 101 112 } 102 113 }; 103 114 let new_mst_root = match new_mst.persist().await { 104 115 Ok(c) => c, 105 116 Err(e) => { 106 117 error!("Failed to persist MST: {:?}", e); 107 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 118 + return Ok( 119 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 120 + ); 108 121 } 109 122 }; 110 123 let collection_for_audit = input.collection.to_string(); ··· 121 134 .await 122 135 .is_err() 123 136 { 124 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 125 - .into_response(); 137 + return Ok( 138 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 139 + .into_response(), 140 + ); 126 141 } 127 142 if mst 128 143 .blocks_for_path(&key, &mut old_mst_blocks) 129 144 .await 130 145 .is_err() 131 146 { 132 - return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 133 - .into_response(); 147 + return Ok( 148 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 149 + .into_response(), 150 + ); 134 151 } 135 152 let mut relevant_blocks = new_mst_blocks.clone(); 136 153 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 169 186 { 170 187 Ok(res) => res, 171 188 Err(e) if e.contains("ConcurrentModification") => { 172 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 189 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 173 190 } 174 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 191 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 175 192 }; 176 193 177 194 if let Some(ref controller) = controller_did { ··· 202 219 error!("Failed to remove backlinks for {}: {}", deleted_uri, e); 203 220 } 204 221 205 - ( 222 + Ok(( 206 223 StatusCode::OK, 207 224 Json(DeleteRecordOutput { 208 225 commit: Some(CommitInfo { ··· 211 228 }), 212 229 }), 213 230 ) 214 - .into_response() 231 + .into_response()) 215 232 } 216 233 217 234 use crate::types::Did;
+113 -80
crates/tranquil-pds/src/api/repo/record/write.rs
··· 3 3 use crate::api::repo::record::utils::{ 4 4 CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, 5 5 }; 6 - use crate::auth::{AuthenticatedUser, BearerAuth}; 6 + use crate::auth::{Active, Auth}; 7 7 use crate::delegation::DelegationActionType; 8 8 use crate::repo::tracking::TrackingBlockStore; 9 9 use crate::state::AppState; ··· 34 34 35 35 pub async fn prepare_repo_write( 36 36 state: &AppState, 37 - auth_user: AuthenticatedUser, 37 + auth_user: &crate::auth::AuthenticatedUser, 38 38 repo: &AtIdentifier, 39 39 ) -> Result<RepoWriteAuth, Response> { 40 40 if repo.as_str() != auth_user.did.as_str() { ··· 90 90 did: auth_user.did.clone(), 91 91 user_id, 92 92 current_root_cid, 93 - is_oauth: auth_user.is_oauth, 94 - scope: auth_user.scope, 93 + is_oauth: auth_user.is_oauth(), 94 + scope: auth_user.scope.clone(), 95 95 controller_did: auth_user.controller_did.clone(), 96 96 }) 97 97 } ··· 124 124 } 125 125 pub async fn create_record( 126 126 State(state): State<AppState>, 127 - auth: BearerAuth, 127 + auth: Auth<Active>, 128 128 Json(input): Json<CreateRecordInput>, 129 - ) -> Response { 130 - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { 129 + ) -> Result<Response, crate::api::error::ApiError> { 130 + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 131 131 Ok(res) => res, 132 - Err(err_res) => return err_res, 132 + Err(err_res) => return Ok(err_res), 133 133 }; 134 134 135 135 if let Err(e) = crate::auth::scope_check::check_repo_scope( 136 - auth.is_oauth, 137 - auth.scope.as_deref(), 136 + repo_auth.is_oauth, 137 + repo_auth.scope.as_deref(), 138 138 crate::oauth::RepoAction::Create, 139 139 &input.collection, 140 140 ) { 141 - return e; 141 + return Ok(e); 142 142 } 143 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; 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 148 149 149 if let Some(swap_commit) = &input.swap_commit 150 150 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 151 151 { 152 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 152 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 153 153 } 154 154 155 155 let validation_status = if input.validate == Some(false) { ··· 163 163 require_lexicon, 164 164 ) { 165 165 Ok(status) => Some(status), 166 - Err(err_response) => return *err_response, 166 + Err(err_response) => return Ok(*err_response), 167 167 } 168 168 }; 169 169 let rkey = input.rkey.unwrap_or_else(Rkey::generate); ··· 171 171 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 172 172 let commit_bytes = match tracking_store.get(&current_root_cid).await { 173 173 Ok(Some(b)) => b, 174 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 174 + _ => { 175 + return Ok( 176 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 177 + ); 178 + } 175 179 }; 176 180 let commit = match Commit::from_cbor(&commit_bytes) { 177 181 Ok(c) => c, 178 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 182 + _ => { 183 + return Ok( 184 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 185 + ); 186 + } 179 187 }; 180 188 let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 181 189 let initial_mst_root = commit.data; ··· 197 205 Ok(c) => c, 198 206 Err(e) => { 199 207 error!("Failed to check backlink conflicts: {}", e); 200 - return ApiError::InternalError(None).into_response(); 208 + return Ok(ApiError::InternalError(None).into_response()); 201 209 } 202 210 }; 203 211 ··· 250 258 let record_ipld = crate::util::json_to_ipld(&input.record); 251 259 let mut record_bytes = Vec::new(); 252 260 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 253 - return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); 261 + return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); 254 262 } 255 263 let record_cid = match tracking_store.put(&record_bytes).await { 256 264 Ok(c) => c, 257 265 _ => { 258 - return ApiError::InternalError(Some("Failed to save record block".into())) 259 - .into_response(); 266 + return Ok( 267 + ApiError::InternalError(Some("Failed to save record block".into())).into_response(), 268 + ); 260 269 } 261 270 }; 262 271 let key = format!("{}/{}", input.collection, rkey); ··· 271 280 272 281 let new_mst = match mst.add(&key, record_cid).await { 273 282 Ok(m) => m, 274 - _ => return ApiError::InternalError(Some("Failed to add to MST".into())).into_response(), 283 + _ => { 284 + return Ok(ApiError::InternalError(Some("Failed to add to MST".into())).into_response()); 285 + } 275 286 }; 276 287 let new_mst_root = match new_mst.persist().await { 277 288 Ok(c) => c, 278 - _ => return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 289 + _ => { 290 + return Ok( 291 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 292 + ); 293 + } 279 294 }; 280 295 281 296 ops.push(RecordOp::Create { ··· 290 305 .await 291 306 .is_err() 292 307 { 293 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 294 - .into_response(); 308 + return Ok( 309 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 310 + .into_response(), 311 + ); 295 312 } 296 313 297 314 let mut relevant_blocks = new_mst_blocks.clone(); ··· 333 350 { 334 351 Ok(res) => res, 335 352 Err(e) if e.contains("ConcurrentModification") => { 336 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 353 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 337 354 } 338 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 355 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 339 356 }; 340 357 341 358 for conflict_uri in conflict_uris_to_cleanup { ··· 375 392 error!("Failed to add backlinks for {}: {}", created_uri, e); 376 393 } 377 394 378 - ( 395 + Ok(( 379 396 StatusCode::OK, 380 397 Json(CreateRecordOutput { 381 398 uri: created_uri, ··· 387 404 validation_status: validation_status.map(|s| s.to_string()), 388 405 }), 389 406 ) 390 - .into_response() 407 + .into_response()) 391 408 } 392 409 #[derive(Deserialize)] 393 410 #[allow(dead_code)] ··· 414 431 } 415 432 pub async fn put_record( 416 433 State(state): State<AppState>, 417 - auth: BearerAuth, 434 + auth: Auth<Active>, 418 435 Json(input): Json<PutRecordInput>, 419 - ) -> Response { 420 - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { 436 + ) -> Result<Response, crate::api::error::ApiError> { 437 + let repo_auth = match prepare_repo_write(&state, &auth, &input.repo).await { 421 438 Ok(res) => res, 422 - Err(err_res) => return err_res, 439 + Err(err_res) => return Ok(err_res), 423 440 }; 424 441 425 442 if let Err(e) = crate::auth::scope_check::check_repo_scope( 426 - auth.is_oauth, 427 - auth.scope.as_deref(), 443 + repo_auth.is_oauth, 444 + repo_auth.scope.as_deref(), 428 445 crate::oauth::RepoAction::Create, 429 446 &input.collection, 430 447 ) { 431 - return e; 448 + return Ok(e); 432 449 } 433 450 if let Err(e) = crate::auth::scope_check::check_repo_scope( 434 - auth.is_oauth, 435 - auth.scope.as_deref(), 451 + repo_auth.is_oauth, 452 + repo_auth.scope.as_deref(), 436 453 crate::oauth::RepoAction::Update, 437 454 &input.collection, 438 455 ) { 439 - return e; 456 + return Ok(e); 440 457 } 441 458 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; 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; 446 463 447 464 if let Some(swap_commit) = &input.swap_commit 448 465 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 449 466 { 450 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 467 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 451 468 } 452 469 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 453 470 let commit_bytes = match tracking_store.get(&current_root_cid).await { 454 471 Ok(Some(b)) => b, 455 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 472 + _ => { 473 + return Ok( 474 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 475 + ); 476 + } 456 477 }; 457 478 let commit = match Commit::from_cbor(&commit_bytes) { 458 479 Ok(c) => c, 459 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 480 + _ => { 481 + return Ok( 482 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 483 + ); 484 + } 460 485 }; 461 486 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 462 487 let key = format!("{}/{}", input.collection, input.rkey); ··· 471 496 require_lexicon, 472 497 ) { 473 498 Ok(status) => Some(status), 474 - Err(err_response) => return *err_response, 499 + Err(err_response) => return Ok(*err_response), 475 500 } 476 501 }; 477 502 if let Some(swap_record_str) = &input.swap_record { 478 503 let expected_cid = Cid::from_str(swap_record_str).ok(); 479 504 let actual_cid = mst.get(&key).await.ok().flatten(); 480 505 if expected_cid != actual_cid { 481 - return ApiError::InvalidSwap(Some( 506 + return Ok(ApiError::InvalidSwap(Some( 482 507 "Record has been modified or does not exist".into(), 483 508 )) 484 - .into_response(); 509 + .into_response()); 485 510 } 486 511 } 487 512 let existing_cid = mst.get(&key).await.ok().flatten(); 488 513 let record_ipld = crate::util::json_to_ipld(&input.record); 489 514 let mut record_bytes = Vec::new(); 490 515 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 491 - return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); 516 + return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); 492 517 } 493 518 let record_cid = match tracking_store.put(&record_bytes).await { 494 519 Ok(c) => c, 495 520 _ => { 496 - return ApiError::InternalError(Some("Failed to save record block".into())) 497 - .into_response(); 521 + return Ok( 522 + ApiError::InternalError(Some("Failed to save record block".into())).into_response(), 523 + ); 498 524 } 499 525 }; 500 526 if existing_cid == Some(record_cid) { 501 - return ( 527 + return Ok(( 502 528 StatusCode::OK, 503 529 Json(PutRecordOutput { 504 530 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 507 533 validation_status: validation_status.map(|s| s.to_string()), 508 534 }), 509 535 ) 510 - .into_response(); 536 + .into_response()); 511 537 } 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(); 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 + } 518 546 } 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(); 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 + } 526 554 } 527 - } 528 - }; 555 + }; 529 556 let new_mst_root = match new_mst.persist().await { 530 557 Ok(c) => c, 531 558 Err(_) => { 532 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 559 + return Ok( 560 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 561 + ); 533 562 } 534 563 }; 535 564 let op = if existing_cid.is_some() { ··· 553 582 .await 554 583 .is_err() 555 584 { 556 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 557 - .into_response(); 585 + return Ok( 586 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 587 + .into_response(), 588 + ); 558 589 } 559 590 if mst 560 591 .blocks_for_path(&key, &mut old_mst_blocks) 561 592 .await 562 593 .is_err() 563 594 { 564 - return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 565 - .into_response(); 595 + return Ok( 596 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 597 + .into_response(), 598 + ); 566 599 } 567 600 let mut relevant_blocks = new_mst_blocks.clone(); 568 601 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 604 637 { 605 638 Ok(res) => res, 606 639 Err(e) if e.contains("ConcurrentModification") => { 607 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 640 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 608 641 } 609 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 642 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 610 643 }; 611 644 612 645 if let Some(ref controller) = controller_did { ··· 628 661 .await; 629 662 } 630 663 631 - ( 664 + Ok(( 632 665 StatusCode::OK, 633 666 Json(PutRecordOutput { 634 667 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 640 673 validation_status: validation_status.map(|s| s.to_string()), 641 674 }), 642 675 ) 643 - .into_response() 676 + .into_response()) 644 677 }
+51 -52
crates/tranquil-pds/src/api/server/account_status.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 + use crate::auth::{Active, Auth, NotTakendown}; 3 4 use crate::cache::Cache; 4 5 use crate::plc::PlcClient; 5 6 use crate::state::AppState; ··· 40 41 41 42 pub async fn check_account_status( 42 43 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 - }; 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))?; 52 53 let is_active = state 53 54 .user_repo 54 - .is_account_active_by_did(&did) 55 + .is_account_active_by_did(did) 55 56 .await 56 57 .ok() 57 58 .flatten() ··· 95 96 .await 96 97 .unwrap_or(0); 97 98 let valid_did = 98 - is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), &did).await; 99 - ( 99 + is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), did).await; 100 + Ok(( 100 101 StatusCode::OK, 101 102 Json(CheckAccountStatusOutput { 102 103 activated: is_active, ··· 110 111 imported_blobs, 111 112 }), 112 113 ) 113 - .into_response() 114 + .into_response()) 114 115 } 115 116 116 117 async fn is_valid_did_for_service( ··· 305 306 306 307 pub async fn activate_account( 307 308 State(state): State<AppState>, 308 - auth: crate::auth::BearerAuthAllowDeactivated, 309 - ) -> Response { 309 + auth: Auth<NotTakendown>, 310 + ) -> Result<Response, ApiError> { 310 311 info!("[MIGRATION] activateAccount called"); 311 - let auth_user = auth.0; 312 312 info!( 313 313 "[MIGRATION] activateAccount: Authenticated user did={}", 314 - auth_user.did 314 + auth.did 315 315 ); 316 316 317 317 if let Err(e) = crate::auth::scope_check::check_account_scope( 318 - auth_user.is_oauth, 319 - auth_user.scope.as_deref(), 318 + auth.is_oauth(), 319 + auth.scope.as_deref(), 320 320 crate::oauth::scopes::AccountAttr::Repo, 321 321 crate::oauth::scopes::AccountAction::Manage, 322 322 ) { 323 323 info!("[MIGRATION] activateAccount: Scope check failed"); 324 - return e; 324 + return Ok(e); 325 325 } 326 326 327 - let did = auth_user.did; 327 + let did = auth.did.clone(); 328 328 329 329 info!( 330 330 "[MIGRATION] activateAccount: Validating DID document for did={}", ··· 344 344 did, 345 345 did_validation_start.elapsed() 346 346 ); 347 - return e.into_response(); 347 + return Err(e); 348 348 } 349 349 info!( 350 350 "[MIGRATION] activateAccount: DID document validation SUCCESS for {} (took {:?})", ··· 450 450 ); 451 451 } 452 452 info!("[MIGRATION] activateAccount: SUCCESS for did={}", did); 453 - EmptyResponse::ok().into_response() 453 + Ok(EmptyResponse::ok().into_response()) 454 454 } 455 455 Err(e) => { 456 456 error!( 457 457 "[MIGRATION] activateAccount: DB error activating account: {:?}", 458 458 e 459 459 ); 460 - ApiError::InternalError(None).into_response() 460 + Err(ApiError::InternalError(None)) 461 461 } 462 462 } 463 463 } ··· 470 470 471 471 pub async fn deactivate_account( 472 472 State(state): State<AppState>, 473 - auth: crate::auth::BearerAuth, 473 + auth: Auth<Active>, 474 474 Json(input): Json<DeactivateAccountInput>, 475 - ) -> Response { 476 - let auth_user = auth.0; 477 - 475 + ) -> Result<Response, ApiError> { 478 476 if let Err(e) = crate::auth::scope_check::check_account_scope( 479 - auth_user.is_oauth, 480 - auth_user.scope.as_deref(), 477 + auth.is_oauth(), 478 + auth.scope.as_deref(), 481 479 crate::oauth::scopes::AccountAttr::Repo, 482 480 crate::oauth::scopes::AccountAction::Manage, 483 481 ) { 484 - return e; 482 + return Ok(e); 485 483 } 486 484 487 485 let delete_after: Option<chrono::DateTime<chrono::Utc>> = input ··· 490 488 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) 491 489 .map(|dt| dt.with_timezone(&chrono::Utc)); 492 490 493 - let did = auth_user.did; 491 + let did = auth.did.clone(); 494 492 495 493 let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten(); 496 494 ··· 511 509 { 512 510 warn!("Failed to sequence account deactivated event: {}", e); 513 511 } 514 - EmptyResponse::ok().into_response() 512 + Ok(EmptyResponse::ok().into_response()) 515 513 } 516 - Ok(false) => EmptyResponse::ok().into_response(), 514 + Ok(false) => Ok(EmptyResponse::ok().into_response()), 517 515 Err(e) => { 518 516 error!("DB error deactivating account: {:?}", e); 519 - ApiError::InternalError(None).into_response() 517 + Err(ApiError::InternalError(None)) 520 518 } 521 519 } 522 520 } 523 521 524 522 pub async fn request_account_delete( 525 523 State(state): State<AppState>, 526 - auth: crate::auth::BearerAuthAllowDeactivated, 527 - ) -> Response { 528 - let did = &auth.0.did; 524 + auth: Auth<NotTakendown>, 525 + ) -> Result<Response, ApiError> { 526 + let did = &auth.did; 529 527 530 528 if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await { 531 - return crate::api::server::reauth::legacy_mfa_required_response( 529 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 532 530 &*state.user_repo, 533 531 &*state.session_repo, 534 532 did, 535 533 ) 536 - .await; 534 + .await); 537 535 } 538 536 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 - }; 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))?; 545 544 let confirmation_token = Uuid::new_v4().to_string(); 546 545 let expires_at = Utc::now() + Duration::minutes(15); 547 - if let Err(e) = state 546 + state 548 547 .infra_repo 549 548 .create_deletion_request(&confirmation_token, did, expires_at) 550 549 .await 551 - { 552 - error!("DB error creating deletion token: {:?}", e); 553 - return ApiError::InternalError(None).into_response(); 554 - } 550 + .map_err(|e| { 551 + error!("DB error creating deletion token: {:?}", e); 552 + ApiError::InternalError(None) 553 + })?; 555 554 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 556 555 if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion( 557 556 state.user_repo.as_ref(), ··· 565 564 warn!("Failed to enqueue account deletion notification: {:?}", e); 566 565 } 567 566 info!("Account deletion requested for user {}", did); 568 - EmptyResponse::ok().into_response() 567 + Ok(EmptyResponse::ok().into_response()) 569 568 } 570 569 571 570 #[derive(Deserialize)]
+126 -122
crates/tranquil-pds/src/api/server/app_password.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::{BearerAuth, generate_app_password}; 3 + use crate::auth::{Active, Auth, generate_app_password}; 4 4 use crate::delegation::{DelegationActionType, intersect_scopes}; 5 5 use crate::state::{AppState, RateLimitKind}; 6 6 use axum::{ ··· 33 33 34 34 pub async fn list_app_passwords( 35 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) => { 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| { 42 43 error!("DB error getting user: {:?}", e); 43 - return ApiError::InternalError(None).into_response(); 44 - } 45 - }; 44 + ApiError::InternalError(None) 45 + })? 46 + .ok_or(ApiError::AccountNotFound)?; 46 47 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) => { 48 + let rows = state 49 + .session_repo 50 + .list_app_passwords(user.id) 51 + .await 52 + .map_err(|e| { 65 53 error!("DB error listing app passwords: {:?}", e); 66 - ApiError::InternalError(None).into_response() 67 - } 68 - } 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()) 69 70 } 70 71 71 72 #[derive(Deserialize)] ··· 89 90 pub async fn create_app_password( 90 91 State(state): State<AppState>, 91 92 headers: HeaderMap, 92 - BearerAuth(auth_user): BearerAuth, 93 + auth: Auth<Active>, 93 94 Json(input): Json<CreateAppPasswordInput>, 94 - ) -> Response { 95 + ) -> Result<Response, ApiError> { 95 96 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 96 97 if !state 97 98 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 98 99 .await 99 100 { 100 101 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 101 - return ApiError::RateLimitExceeded(None).into_response(); 102 + return Err(ApiError::RateLimitExceeded(None)); 102 103 } 103 104 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) => { 105 + let user = state 106 + .user_repo 107 + .get_by_did(&auth.did) 108 + .await 109 + .map_err(|e| { 108 110 error!("DB error getting user: {:?}", e); 109 - return ApiError::InternalError(None).into_response(); 110 - } 111 - }; 111 + ApiError::InternalError(None) 112 + })? 113 + .ok_or(ApiError::AccountNotFound)?; 112 114 113 115 let name = input.name.trim(); 114 116 if name.is_empty() { 115 - return ApiError::InvalidRequest("name is required".into()).into_response(); 117 + return Err(ApiError::InvalidRequest("name is required".into())); 116 118 } 117 119 118 - match state 120 + if state 119 121 .session_repo 120 122 .get_app_password_by_name(user.id, name) 121 123 .await 122 - { 123 - Ok(Some(_)) => return ApiError::DuplicateAppPassword.into_response(), 124 - Err(e) => { 124 + .map_err(|e| { 125 125 error!("DB error checking app password: {:?}", e); 126 - return ApiError::InternalError(None).into_response(); 127 - } 128 - Ok(None) => {} 126 + ApiError::InternalError(None) 127 + })? 128 + .is_some() 129 + { 130 + return Err(ApiError::DuplicateAppPassword); 129 131 } 130 132 131 - let (final_scopes, controller_did) = if let Some(ref controller) = auth_user.controller_did { 133 + let (final_scopes, controller_did) = if let Some(ref controller) = auth.controller_did { 132 134 let grant = state 133 135 .delegation_repo 134 - .get_delegation(&auth_user.did, controller) 136 + .get_delegation(&auth.did, controller) 135 137 .await 136 138 .ok() 137 139 .flatten(); ··· 141 143 let intersected = intersect_scopes(requested, &granted_scopes); 142 144 143 145 if intersected.is_empty() && !granted_scopes.is_empty() { 144 - return ApiError::InsufficientScope(None).into_response(); 146 + return Err(ApiError::InsufficientScope(None)); 145 147 } 146 148 147 149 let scope_result = if intersected.is_empty() { ··· 157 159 let password = generate_app_password(); 158 160 159 161 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 - }; 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 + })?; 175 173 176 174 let privileged = input.privileged.unwrap_or(false); 177 175 let created_at = chrono::Utc::now(); ··· 185 183 created_by_controller_did: controller_did.clone(), 186 184 }; 187 185 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) => { 186 + state 187 + .session_repo 188 + .create_app_password(&create_data) 189 + .await 190 + .map_err(|e| { 218 191 error!("DB error creating app password: {:?}", e); 219 - ApiError::InternalError(None).into_response() 220 - } 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; 221 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()) 222 221 } 223 222 224 223 #[derive(Deserialize)] ··· 228 227 229 228 pub async fn revoke_app_password( 230 229 State(state): State<AppState>, 231 - BearerAuth(auth_user): BearerAuth, 230 + auth: Auth<Active>, 232 231 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) => { 232 + ) -> Result<Response, ApiError> { 233 + let user = state 234 + .user_repo 235 + .get_by_did(&auth.did) 236 + .await 237 + .map_err(|e| { 238 238 error!("DB error getting user: {:?}", e); 239 - return ApiError::InternalError(None).into_response(); 240 - } 241 - }; 239 + ApiError::InternalError(None) 240 + })? 241 + .ok_or(ApiError::AccountNotFound)?; 242 242 243 243 let name = input.name.trim(); 244 244 if name.is_empty() { 245 - return ApiError::InvalidRequest("name is required".into()).into_response(); 245 + return Err(ApiError::InvalidRequest("name is required".into())); 246 246 } 247 247 248 248 let sessions_to_invalidate = state 249 249 .session_repo 250 - .get_session_jtis_by_app_password(&auth_user.did, name) 250 + .get_session_jtis_by_app_password(&auth.did, name) 251 251 .await 252 252 .unwrap_or_default(); 253 253 254 - if let Err(e) = state 254 + state 255 255 .session_repo 256 - .delete_sessions_by_app_password(&auth_user.did, name) 256 + .delete_sessions_by_app_password(&auth.did, name) 257 257 .await 258 - { 259 - error!("DB error revoking sessions for app password: {:?}", e); 260 - return ApiError::InternalError(None).into_response(); 261 - } 258 + .map_err(|e| { 259 + error!("DB error revoking sessions for app password: {:?}", e); 260 + ApiError::InternalError(None) 261 + })?; 262 262 263 263 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 264 - let cache_key = format!("auth:session:{}:{}", &auth_user.did, jti); 264 + let cache_key = format!("auth:session:{}:{}", &auth.did, jti); 265 265 let cache = state.cache.clone(); 266 266 async move { 267 267 let _ = cache.delete(&cache_key).await; ··· 269 269 })) 270 270 .await; 271 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 - } 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 + })?; 276 280 277 - EmptyResponse::ok().into_response() 281 + Ok(EmptyResponse::ok().into_response()) 278 282 }
+92 -89
crates/tranquil-pds/src/api/server/email.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 3 - use crate::auth::BearerAuth; 3 + use crate::auth::{Active, Auth}; 4 4 use crate::state::{AppState, RateLimitKind}; 5 5 use axum::{ 6 6 Json, ··· 45 45 pub async fn request_email_update( 46 46 State(state): State<AppState>, 47 47 headers: axum::http::HeaderMap, 48 - auth: BearerAuth, 48 + auth: Auth<Active>, 49 49 input: Option<Json<RequestEmailUpdateInput>>, 50 - ) -> Response { 50 + ) -> Result<Response, ApiError> { 51 51 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 52 52 if !state 53 53 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 54 54 .await 55 55 { 56 56 warn!(ip = %client_ip, "Email update rate limit exceeded"); 57 - return ApiError::RateLimitExceeded(None).into_response(); 57 + return Err(ApiError::RateLimitExceeded(None)); 58 58 } 59 59 60 60 if let Err(e) = crate::auth::scope_check::check_account_scope( 61 - auth.0.is_oauth, 62 - auth.0.scope.as_deref(), 61 + auth.is_oauth(), 62 + auth.scope.as_deref(), 63 63 crate::oauth::scopes::AccountAttr::Email, 64 64 crate::oauth::scopes::AccountAction::Manage, 65 65 ) { 66 - return e; 66 + return Ok(e); 67 67 } 68 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) => { 69 + let user = state 70 + .user_repo 71 + .get_email_info_by_did(&auth.did) 72 + .await 73 + .map_err(|e| { 75 74 error!("DB error: {:?}", e); 76 - return ApiError::InternalError(None).into_response(); 77 - } 78 - }; 75 + ApiError::InternalError(None) 76 + })? 77 + .ok_or(ApiError::AccountNotFound)?; 79 78 80 79 let Some(current_email) = user.email else { 81 - return ApiError::InvalidRequest("account does not have an email address".into()) 82 - .into_response(); 80 + return Err(ApiError::InvalidRequest( 81 + "account does not have an email address".into(), 82 + )); 83 83 }; 84 84 85 85 let token_required = user.email_verified; 86 86 87 87 if token_required { 88 88 let code = crate::auth::verification_token::generate_channel_update_token( 89 - &auth.0.did, 89 + &auth.did, 90 90 "email_update", 91 91 &current_email.to_lowercase(), 92 92 ); ··· 103 103 authorized: false, 104 104 }; 105 105 if let Ok(json) = serde_json::to_string(&pending) { 106 - let cache_key = email_update_cache_key(&auth.0.did); 106 + let cache_key = email_update_cache_key(&auth.did); 107 107 if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await { 108 108 warn!("Failed to cache pending email update: {:?}", e); 109 109 } ··· 127 127 } 128 128 129 129 info!("Email update requested for user {}", user.id); 130 - TokenRequiredResponse::response(token_required).into_response() 130 + Ok(TokenRequiredResponse::response(token_required).into_response()) 131 131 } 132 132 133 133 #[derive(Deserialize)] ··· 140 140 pub async fn confirm_email( 141 141 State(state): State<AppState>, 142 142 headers: axum::http::HeaderMap, 143 - auth: BearerAuth, 143 + auth: Auth<Active>, 144 144 Json(input): Json<ConfirmEmailInput>, 145 - ) -> Response { 145 + ) -> Result<Response, ApiError> { 146 146 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 147 147 if !state 148 148 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 149 149 .await 150 150 { 151 151 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 152 - return ApiError::RateLimitExceeded(None).into_response(); 152 + return Err(ApiError::RateLimitExceeded(None)); 153 153 } 154 154 155 155 if let Err(e) = crate::auth::scope_check::check_account_scope( 156 - auth.0.is_oauth, 157 - auth.0.scope.as_deref(), 156 + auth.is_oauth(), 157 + auth.scope.as_deref(), 158 158 crate::oauth::scopes::AccountAttr::Email, 159 159 crate::oauth::scopes::AccountAction::Manage, 160 160 ) { 161 - return e; 161 + return Ok(e); 162 162 } 163 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) => { 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| { 171 170 error!("DB error: {:?}", e); 172 - return ApiError::InternalError(None).into_response(); 173 - } 174 - }; 171 + ApiError::InternalError(None) 172 + })? 173 + .ok_or(ApiError::AccountNotFound)?; 175 174 176 175 let Some(ref email) = user.email else { 177 - return ApiError::InvalidEmail.into_response(); 176 + return Err(ApiError::InvalidEmail); 178 177 }; 179 178 let current_email = email.to_lowercase(); 180 179 181 180 let provided_email = input.email.trim().to_lowercase(); 182 181 if provided_email != current_email { 183 - return ApiError::InvalidEmail.into_response(); 182 + return Err(ApiError::InvalidEmail); 184 183 } 185 184 186 185 if user.email_verified { 187 - return EmptyResponse::ok().into_response(); 186 + return Ok(EmptyResponse::ok().into_response()); 188 187 } 189 188 190 189 let confirmation_code = ··· 199 198 match verified { 200 199 Ok(token_data) => { 201 200 if token_data.did != did.as_str() { 202 - return ApiError::InvalidToken(None).into_response(); 201 + return Err(ApiError::InvalidToken(None)); 203 202 } 204 203 } 205 204 Err(crate::auth::verification_token::VerifyError::Expired) => { 206 - return ApiError::ExpiredToken(None).into_response(); 205 + return Err(ApiError::ExpiredToken(None)); 207 206 } 208 207 Err(_) => { 209 - return ApiError::InvalidToken(None).into_response(); 208 + return Err(ApiError::InvalidToken(None)); 210 209 } 211 210 } 212 211 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 - } 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 + })?; 217 220 218 221 info!("Email confirmed for user {}", user.id); 219 - EmptyResponse::ok().into_response() 222 + Ok(EmptyResponse::ok().into_response()) 220 223 } 221 224 222 225 #[derive(Deserialize)] ··· 230 233 231 234 pub async fn update_email( 232 235 State(state): State<AppState>, 233 - auth: BearerAuth, 236 + auth: Auth<Active>, 234 237 Json(input): Json<UpdateEmailInput>, 235 - ) -> Response { 236 - let auth_user = auth.0; 237 - 238 + ) -> Result<Response, ApiError> { 238 239 if let Err(e) = crate::auth::scope_check::check_account_scope( 239 - auth_user.is_oauth, 240 - auth_user.scope.as_deref(), 240 + auth.is_oauth(), 241 + auth.scope.as_deref(), 241 242 crate::oauth::scopes::AccountAttr::Email, 242 243 crate::oauth::scopes::AccountAction::Manage, 243 244 ) { 244 - return e; 245 + return Ok(e); 245 246 } 246 247 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) => { 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 254 error!("DB error: {:?}", e); 255 - return ApiError::InternalError(None).into_response(); 256 - } 257 - }; 255 + ApiError::InternalError(None) 256 + })? 257 + .ok_or(ApiError::AccountNotFound)?; 258 258 259 259 let user_id = user.id; 260 260 let current_email = user.email.clone(); ··· 262 262 let new_email = input.email.trim().to_lowercase(); 263 263 264 264 if !crate::api::validation::is_valid_email(&new_email) { 265 - return ApiError::InvalidRequest( 265 + return Err(ApiError::InvalidRequest( 266 266 "This email address is not supported, please use a different email.".into(), 267 - ) 268 - .into_response(); 267 + )); 269 268 } 270 269 271 270 if let Some(ref current) = current_email 272 271 && new_email == current.to_lowercase() 273 272 { 274 - return EmptyResponse::ok().into_response(); 273 + return Ok(EmptyResponse::ok().into_response()); 275 274 } 276 275 277 276 if email_verified { ··· 290 289 291 290 if !authorized_via_link { 292 291 let Some(ref t) = input.token else { 293 - return ApiError::TokenRequired.into_response(); 292 + return Err(ApiError::TokenRequired); 294 293 }; 295 294 let confirmation_token = 296 295 crate::auth::verification_token::normalize_token_input(t.trim()); ··· 309 308 match verified { 310 309 Ok(token_data) => { 311 310 if token_data.did != did.as_str() { 312 - return ApiError::InvalidToken(None).into_response(); 311 + return Err(ApiError::InvalidToken(None)); 313 312 } 314 313 } 315 314 Err(crate::auth::verification_token::VerifyError::Expired) => { 316 - return ApiError::ExpiredToken(None).into_response(); 315 + return Err(ApiError::ExpiredToken(None)); 317 316 } 318 317 Err(_) => { 319 - return ApiError::InvalidToken(None).into_response(); 318 + return Err(ApiError::InvalidToken(None)); 320 319 } 321 320 } 322 321 } 323 322 } 324 323 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 - } 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 + })?; 329 332 330 333 let verification_token = 331 334 crate::auth::verification_token::generate_signup_token(did, "email", &new_email); ··· 358 361 } 359 362 360 363 info!("Email updated for user {}", user_id); 361 - EmptyResponse::ok().into_response() 364 + Ok(EmptyResponse::ok().into_response()) 362 365 } 363 366 364 367 #[derive(Deserialize)] ··· 497 500 pub async fn check_email_update_status( 498 501 State(state): State<AppState>, 499 502 headers: axum::http::HeaderMap, 500 - auth: BearerAuth, 501 - ) -> Response { 503 + auth: Auth<Active>, 504 + ) -> Result<Response, ApiError> { 502 505 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 503 506 if !state 504 507 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 505 508 .await 506 509 { 507 - return ApiError::RateLimitExceeded(None).into_response(); 510 + return Err(ApiError::RateLimitExceeded(None)); 508 511 } 509 512 510 513 if let Err(e) = crate::auth::scope_check::check_account_scope( 511 - auth.0.is_oauth, 512 - auth.0.scope.as_deref(), 514 + auth.is_oauth(), 515 + auth.scope.as_deref(), 513 516 crate::oauth::scopes::AccountAttr::Email, 514 517 crate::oauth::scopes::AccountAction::Read, 515 518 ) { 516 - return e; 519 + return Ok(e); 517 520 } 518 521 519 - let cache_key = email_update_cache_key(&auth.0.did); 522 + let cache_key = email_update_cache_key(&auth.did); 520 523 let pending_json = match state.cache.get(&cache_key).await { 521 524 Some(json) => json, 522 525 None => { 523 - return Json(json!({ "pending": false, "authorized": false })).into_response(); 526 + return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); 524 527 } 525 528 }; 526 529 527 530 let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) { 528 531 Ok(p) => p, 529 532 Err(_) => { 530 - return Json(json!({ "pending": false, "authorized": false })).into_response(); 533 + return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); 531 534 } 532 535 }; 533 536 534 - Json(json!({ 537 + Ok(Json(json!({ 535 538 "pending": true, 536 539 "authorized": pending.authorized, 537 540 "newEmail": pending.new_email, 538 541 })) 539 - .into_response() 542 + .into_response()) 540 543 } 541 544 542 545 #[derive(Deserialize)]
+46 -48
crates/tranquil-pds/src/api/server/invite.rs
··· 1 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuth; 3 - use crate::auth::extractor::BearerAuthAdmin; 2 + use crate::auth::{Active, Admin, Auth}; 4 3 use crate::state::AppState; 5 4 use crate::types::Did; 6 5 use axum::{ ··· 44 43 45 44 pub async fn create_invite_code( 46 45 State(state): State<AppState>, 47 - BearerAuthAdmin(auth_user): BearerAuthAdmin, 46 + auth: Auth<Admin>, 48 47 Json(input): Json<CreateInviteCodeInput>, 49 - ) -> Response { 48 + ) -> Result<Response, ApiError> { 50 49 if input.use_count < 1 { 51 - return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 50 + return Err(ApiError::InvalidRequest( 51 + "useCount must be at least 1".into(), 52 + )); 52 53 } 53 54 54 55 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(), 56 + Some(acct) => acct 57 + .parse() 58 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?, 59 + None => auth.did.clone(), 60 60 }; 61 61 let code = gen_invite_code(); 62 62 ··· 65 65 .create_invite_code(&code, input.use_count, Some(&for_account)) 66 66 .await 67 67 { 68 - Ok(true) => Json(CreateInviteCodeOutput { code }).into_response(), 68 + Ok(true) => Ok(Json(CreateInviteCodeOutput { code }).into_response()), 69 69 Ok(false) => { 70 70 error!("No admin user found to create invite code"); 71 - ApiError::InternalError(None).into_response() 71 + Err(ApiError::InternalError(None)) 72 72 } 73 73 Err(e) => { 74 74 error!("DB error creating invite code: {:?}", e); 75 - ApiError::InternalError(None).into_response() 75 + Err(ApiError::InternalError(None)) 76 76 } 77 77 } 78 78 } ··· 98 98 99 99 pub async fn create_invite_codes( 100 100 State(state): State<AppState>, 101 - BearerAuthAdmin(auth_user): BearerAuthAdmin, 101 + auth: Auth<Admin>, 102 102 Json(input): Json<CreateInviteCodesInput>, 103 - ) -> Response { 103 + ) -> Result<Response, ApiError> { 104 104 if input.use_count < 1 { 105 - return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 105 + return Err(ApiError::InvalidRequest( 106 + "useCount must be at least 1".into(), 107 + )); 106 108 } 107 109 108 110 let code_count = input.code_count.unwrap_or(1).max(1); 109 111 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()], 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 118 }; 119 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) => { 120 + let admin_user_id = state 121 + .user_repo 122 + .get_any_admin_user_id() 123 + .await 124 + .map_err(|e| { 127 125 error!("DB error looking up admin user: {:?}", e); 128 - return ApiError::InternalError(None).into_response(); 129 - } 130 - }; 126 + ApiError::InternalError(None) 127 + })? 128 + .ok_or_else(|| { 129 + error!("No admin user found to create invite codes"); 130 + ApiError::InternalError(None) 131 + })?; 131 132 132 133 let result = futures::future::try_join_all(for_accounts.into_iter().map(|account| { 133 134 let infra_repo = state.infra_repo.clone(); ··· 146 147 .await; 147 148 148 149 match result { 149 - Ok(result_codes) => Json(CreateInviteCodesOutput { 150 + Ok(result_codes) => Ok(Json(CreateInviteCodesOutput { 150 151 codes: result_codes, 151 152 }) 152 - .into_response(), 153 + .into_response()), 153 154 Err(e) => { 154 155 error!("DB error creating invite codes: {:?}", e); 155 - ApiError::InternalError(None).into_response() 156 + Err(ApiError::InternalError(None)) 156 157 } 157 158 } 158 159 } ··· 192 193 193 194 pub async fn get_account_invite_codes( 194 195 State(state): State<AppState>, 195 - BearerAuth(auth_user): BearerAuth, 196 + auth: Auth<Active>, 196 197 axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>, 197 - ) -> Response { 198 + ) -> Result<Response, ApiError> { 198 199 let include_used = params.include_used.unwrap_or(true); 199 200 200 - let codes_info = match state 201 + let codes_info = state 201 202 .infra_repo 202 - .get_invite_codes_for_account(&auth_user.did) 203 + .get_invite_codes_for_account(&auth.did) 203 204 .await 204 - { 205 - Ok(info) => info, 206 - Err(e) => { 205 + .map_err(|e| { 207 206 error!("DB error fetching invite codes: {:?}", e); 208 - return ApiError::InternalError(None).into_response(); 209 - } 210 - }; 207 + ApiError::InternalError(None) 208 + })?; 211 209 212 210 let filtered_codes: Vec<_> = codes_info 213 211 .into_iter() ··· 254 252 .await; 255 253 256 254 let codes: Vec<InviteCode> = codes.into_iter().flatten().collect(); 257 - Json(GetAccountInviteCodesOutput { codes }).into_response() 255 + Ok(Json(GetAccountInviteCodesOutput { codes }).into_response()) 258 256 }
+48 -51
crates/tranquil-pds/src/api/server/migration.rs
··· 1 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuth; 2 + use crate::auth::{Active, Auth}; 3 3 use crate::state::AppState; 4 4 use axum::{ 5 5 Json, ··· 36 36 37 37 pub async fn update_did_document( 38 38 State(state): State<AppState>, 39 - auth: BearerAuth, 39 + auth: Auth<Active>, 40 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( 41 + ) -> Result<Response, ApiError> { 42 + if !auth.did.starts_with("did:web:") { 43 + return Err(ApiError::InvalidRequest( 46 44 "DID document updates are only available for did:web accounts".into(), 47 - ) 48 - .into_response(); 45 + )); 49 46 } 50 47 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) => { 48 + let user = state 49 + .user_repo 50 + .get_user_for_did_doc(&auth.did) 51 + .await 52 + .map_err(|e| { 55 53 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 - } 54 + ApiError::InternalError(None) 55 + })? 56 + .ok_or(ApiError::AccountNotFound)?; 63 57 64 58 if let Some(ref methods) = input.verification_methods { 65 59 if methods.is_empty() { 66 - return ApiError::InvalidRequest("verification_methods cannot be empty".into()) 67 - .into_response(); 60 + return Err(ApiError::InvalidRequest( 61 + "verification_methods cannot be empty".into(), 62 + )); 68 63 } 69 64 let validation_error = methods.iter().find_map(|method| { 70 65 if method.id.is_empty() { ··· 80 75 } 81 76 }); 82 77 if let Some(err) = validation_error { 83 - return ApiError::InvalidRequest(err.into()).into_response(); 78 + return Err(ApiError::InvalidRequest(err.into())); 84 79 } 85 80 } 86 81 87 82 if let Some(ref handles) = input.also_known_as 88 83 && handles.iter().any(|h| !h.starts_with("at://")) 89 84 { 90 - return ApiError::InvalidRequest("alsoKnownAs entries must be at:// URIs".into()) 91 - .into_response(); 85 + return Err(ApiError::InvalidRequest( 86 + "alsoKnownAs entries must be at:// URIs".into(), 87 + )); 92 88 } 93 89 94 90 if let Some(ref endpoint) = input.service_endpoint { 95 91 let endpoint = endpoint.trim(); 96 92 if !endpoint.starts_with("https://") { 97 - return ApiError::InvalidRequest("serviceEndpoint must start with https://".into()) 98 - .into_response(); 93 + return Err(ApiError::InvalidRequest( 94 + "serviceEndpoint must start with https://".into(), 95 + )); 99 96 } 100 97 } 101 98 ··· 106 103 107 104 let also_known_as: Option<Vec<String>> = input.also_known_as.clone(); 108 105 109 - if let Err(e) = state 106 + state 110 107 .user_repo 111 108 .upsert_did_web_overrides(user.id, verification_methods_json, also_known_as) 112 109 .await 113 - { 114 - tracing::error!("DB error upserting did_web_overrides: {:?}", e); 115 - return ApiError::InternalError(None).into_response(); 116 - } 110 + .map_err(|e| { 111 + tracing::error!("DB error upserting did_web_overrides: {:?}", e); 112 + ApiError::InternalError(None) 113 + })?; 117 114 118 115 if let Some(ref endpoint) = input.service_endpoint { 119 116 let endpoint_clean = endpoint.trim().trim_end_matches('/'); 120 - if let Err(e) = state 117 + state 121 118 .user_repo 122 - .update_migrated_to_pds(&auth_user.did, endpoint_clean) 119 + .update_migrated_to_pds(&auth.did, endpoint_clean) 123 120 .await 124 - { 125 - tracing::error!("DB error updating service endpoint: {:?}", e); 126 - return ApiError::InternalError(None).into_response(); 127 - } 121 + .map_err(|e| { 122 + tracing::error!("DB error updating service endpoint: {:?}", e); 123 + ApiError::InternalError(None) 124 + })?; 128 125 } 129 126 130 - let did_doc = build_did_document(&state, &auth_user.did).await; 127 + let did_doc = build_did_document(&state, &auth.did).await; 131 128 132 - tracing::info!("Updated DID document for {}", &auth_user.did); 129 + tracing::info!("Updated DID document for {}", &auth.did); 133 130 134 - ( 131 + Ok(( 135 132 StatusCode::OK, 136 133 Json(UpdateDidDocumentOutput { 137 134 success: true, 138 135 did_document: did_doc, 139 136 }), 140 137 ) 141 - .into_response() 138 + .into_response()) 142 139 } 143 140 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( 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( 149 147 "This endpoint is only available for did:web accounts".into(), 150 - ) 151 - .into_response(); 148 + )); 152 149 } 153 150 154 - let did_doc = build_did_document(&state, &auth_user.did).await; 151 + let did_doc = build_did_document(&state, &auth.did).await; 155 152 156 - (StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response() 153 + Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response()) 157 154 } 158 155 159 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 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuth; 4 3 use crate::auth::webauthn::WebAuthnConfig; 4 + use crate::auth::{Active, Auth}; 5 5 use crate::state::AppState; 6 6 use axum::{ 7 7 Json, ··· 34 34 35 35 pub async fn start_passkey_registration( 36 36 State(state): State<AppState>, 37 - auth: BearerAuth, 37 + auth: Auth<Active>, 38 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 - }; 39 + ) -> Result<Response, ApiError> { 40 + let webauthn = get_webauthn()?; 44 41 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) => { 42 + let handle = state 43 + .user_repo 44 + .get_handle_by_did(&auth.did) 45 + .await 46 + .map_err(|e| { 51 47 error!("DB error fetching user: {:?}", e); 52 - return ApiError::InternalError(None).into_response(); 53 - } 54 - }; 48 + ApiError::InternalError(None) 49 + })? 50 + .ok_or(ApiError::AccountNotFound)?; 55 51 56 - let existing_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { 57 - Ok(passkeys) => passkeys, 58 - Err(e) => { 52 + let existing_passkeys = state 53 + .user_repo 54 + .get_passkeys_for_user(&auth.did) 55 + .await 56 + .map_err(|e| { 59 57 error!("DB error fetching existing passkeys: {:?}", e); 60 - return ApiError::InternalError(None).into_response(); 61 - } 62 - }; 58 + ApiError::InternalError(None) 59 + })?; 63 60 64 61 let exclude_credentials: Vec<CredentialID> = existing_passkeys 65 62 .iter() ··· 68 65 69 66 let display_name = input.friendly_name.as_deref().unwrap_or(&handle); 70 67 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) => { 68 + let (ccr, reg_state) = webauthn 69 + .start_registration(&auth.did, &handle, display_name, exclude_credentials) 70 + .map_err(|e| { 79 71 error!("Failed to start passkey registration: {}", e); 80 - return ApiError::InternalError(Some("Failed to start registration".into())) 81 - .into_response(); 82 - } 83 - }; 72 + ApiError::InternalError(Some("Failed to start registration".into())) 73 + })?; 84 74 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 - }; 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 + })?; 92 79 93 - if let Err(e) = state 80 + state 94 81 .user_repo 95 - .save_webauthn_challenge(&auth.0.did, "registration", &state_json) 82 + .save_webauthn_challenge(&auth.did, "registration", &state_json) 96 83 .await 97 - { 98 - error!("Failed to save registration state: {:?}", e); 99 - return ApiError::InternalError(None).into_response(); 100 - } 84 + .map_err(|e| { 85 + error!("Failed to save registration state: {:?}", e); 86 + ApiError::InternalError(None) 87 + })?; 101 88 102 89 let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 103 90 104 - info!(did = %auth.0.did, "Passkey registration started"); 91 + info!(did = %auth.did, "Passkey registration started"); 105 92 106 - Json(StartRegistrationResponse { options }).into_response() 93 + Ok(Json(StartRegistrationResponse { options }).into_response()) 107 94 } 108 95 109 96 #[derive(Deserialize)] ··· 122 109 123 110 pub async fn finish_passkey_registration( 124 111 State(state): State<AppState>, 125 - auth: BearerAuth, 112 + auth: Auth<Active>, 126 113 Json(input): Json<FinishRegistrationInput>, 127 - ) -> Response { 128 - let webauthn = match get_webauthn() { 129 - Ok(w) => w, 130 - Err(e) => return e.into_response(), 131 - }; 114 + ) -> Result<Response, ApiError> { 115 + let webauthn = get_webauthn()?; 132 116 133 - let reg_state_json = match state 117 + let reg_state_json = state 134 118 .user_repo 135 - .load_webauthn_challenge(&auth.0.did, "registration") 119 + .load_webauthn_challenge(&auth.did, "registration") 136 120 .await 137 - { 138 - Ok(Some(json)) => json, 139 - Ok(None) => { 140 - return ApiError::NoRegistrationInProgress.into_response(); 141 - } 142 - Err(e) => { 121 + .map_err(|e| { 143 122 error!("DB error loading registration state: {:?}", e); 144 - return ApiError::InternalError(None).into_response(); 145 - } 146 - }; 123 + ApiError::InternalError(None) 124 + })? 125 + .ok_or(ApiError::NoRegistrationInProgress)?; 147 126 148 - let reg_state: SecurityKeyRegistration = match serde_json::from_str(&reg_state_json) { 149 - Ok(s) => s, 150 - Err(e) => { 127 + let reg_state: SecurityKeyRegistration = 128 + serde_json::from_str(&reg_state_json).map_err(|e| { 151 129 error!("Failed to deserialize registration state: {:?}", e); 152 - return ApiError::InternalError(None).into_response(); 153 - } 154 - }; 130 + ApiError::InternalError(None) 131 + })?; 155 132 156 - let credential: RegisterPublicKeyCredential = match serde_json::from_value(input.credential) { 157 - Ok(c) => c, 158 - Err(e) => { 133 + let credential: RegisterPublicKeyCredential = serde_json::from_value(input.credential) 134 + .map_err(|e| { 159 135 warn!("Failed to parse credential: {:?}", e); 160 - return ApiError::InvalidCredential.into_response(); 161 - } 162 - }; 136 + ApiError::InvalidCredential 137 + })?; 163 138 164 - let passkey = match webauthn.finish_registration(&credential, &reg_state) { 165 - Ok(pk) => pk, 166 - Err(e) => { 139 + let passkey = webauthn 140 + .finish_registration(&credential, &reg_state) 141 + .map_err(|e| { 167 142 warn!("Failed to finish passkey registration: {}", e); 168 - return ApiError::RegistrationFailed.into_response(); 169 - } 170 - }; 143 + ApiError::RegistrationFailed 144 + })?; 171 145 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 - }; 146 + let public_key = serde_json::to_vec(&passkey).map_err(|e| { 147 + error!("Failed to serialize passkey: {:?}", e); 148 + ApiError::InternalError(None) 149 + })?; 179 150 180 - let passkey_id = match state 151 + let passkey_id = state 181 152 .user_repo 182 153 .save_passkey( 183 - &auth.0.did, 154 + &auth.did, 184 155 passkey.cred_id(), 185 156 &public_key, 186 157 input.friendly_name.as_deref(), 187 158 ) 188 159 .await 189 - { 190 - Ok(id) => id, 191 - Err(e) => { 160 + .map_err(|e| { 192 161 error!("Failed to save passkey: {:?}", e); 193 - return ApiError::InternalError(None).into_response(); 194 - } 195 - }; 162 + ApiError::InternalError(None) 163 + })?; 196 164 197 165 if let Err(e) = state 198 166 .user_repo 199 - .delete_webauthn_challenge(&auth.0.did, "registration") 167 + .delete_webauthn_challenge(&auth.did, "registration") 200 168 .await 201 169 { 202 170 warn!("Failed to delete registration state: {:?}", e); ··· 207 175 passkey.cred_id(), 208 176 ); 209 177 210 - info!(did = %auth.0.did, passkey_id = %passkey_id, "Passkey registered"); 178 + info!(did = %auth.did, passkey_id = %passkey_id, "Passkey registered"); 211 179 212 - Json(FinishRegistrationResponse { 180 + Ok(Json(FinishRegistrationResponse { 213 181 id: passkey_id.to_string(), 214 182 credential_id: credential_id_base64, 215 183 }) 216 - .into_response() 184 + .into_response()) 217 185 } 218 186 219 187 #[derive(Serialize)] ··· 232 200 pub passkeys: Vec<PasskeyInfo>, 233 201 } 234 202 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) => { 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| { 239 212 error!("DB error fetching passkeys: {:?}", e); 240 - return ApiError::InternalError(None).into_response(); 241 - } 242 - }; 213 + ApiError::InternalError(None) 214 + })?; 243 215 244 216 let passkey_infos: Vec<PasskeyInfo> = passkeys 245 217 .into_iter() ··· 252 224 }) 253 225 .collect(); 254 226 255 - Json(ListPasskeysResponse { 227 + Ok(Json(ListPasskeysResponse { 256 228 passkeys: passkey_infos, 257 229 }) 258 - .into_response() 230 + .into_response()) 259 231 } 260 232 261 233 #[derive(Deserialize)] ··· 266 238 267 239 pub async fn delete_passkey( 268 240 State(state): State<AppState>, 269 - auth: BearerAuth, 241 + auth: Auth<Active>, 270 242 Json(input): Json<DeletePasskeyInput>, 271 - ) -> Response { 272 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 273 - .await 243 + ) -> Result<Response, ApiError> { 244 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 274 245 { 275 - return crate::api::server::reauth::legacy_mfa_required_response( 246 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 276 247 &*state.user_repo, 277 248 &*state.session_repo, 278 - &auth.0.did, 249 + &auth.did, 279 250 ) 280 - .await; 251 + .await); 281 252 } 282 253 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( 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( 285 256 &*state.user_repo, 286 257 &*state.session_repo, 287 - &auth.0.did, 258 + &auth.did, 288 259 ) 289 - .await; 260 + .await); 290 261 } 291 262 292 - let id: uuid::Uuid = match input.id.parse() { 293 - Ok(id) => id, 294 - Err(_) => { 295 - return ApiError::InvalidId.into_response(); 296 - } 297 - }; 263 + let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 298 264 299 - match state.user_repo.delete_passkey(id, &auth.0.did).await { 265 + match state.user_repo.delete_passkey(id, &auth.did).await { 300 266 Ok(true) => { 301 - info!(did = %auth.0.did, passkey_id = %id, "Passkey deleted"); 302 - EmptyResponse::ok().into_response() 267 + info!(did = %auth.did, passkey_id = %id, "Passkey deleted"); 268 + Ok(EmptyResponse::ok().into_response()) 303 269 } 304 - Ok(false) => ApiError::PasskeyNotFound.into_response(), 270 + Ok(false) => Err(ApiError::PasskeyNotFound), 305 271 Err(e) => { 306 272 error!("DB error deleting passkey: {:?}", e); 307 - ApiError::InternalError(None).into_response() 273 + Err(ApiError::InternalError(None)) 308 274 } 309 275 } 310 276 } ··· 318 284 319 285 pub async fn update_passkey( 320 286 State(state): State<AppState>, 321 - auth: BearerAuth, 287 + auth: Auth<Active>, 322 288 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 - }; 289 + ) -> Result<Response, ApiError> { 290 + let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 330 291 331 292 match state 332 293 .user_repo 333 - .update_passkey_name(id, &auth.0.did, &input.friendly_name) 294 + .update_passkey_name(id, &auth.did, &input.friendly_name) 334 295 .await 335 296 { 336 297 Ok(true) => { 337 - info!(did = %auth.0.did, passkey_id = %id, "Passkey renamed"); 338 - EmptyResponse::ok().into_response() 298 + info!(did = %auth.did, passkey_id = %id, "Passkey renamed"); 299 + Ok(EmptyResponse::ok().into_response()) 339 300 } 340 - Ok(false) => ApiError::PasskeyNotFound.into_response(), 301 + Ok(false) => Err(ApiError::PasskeyNotFound), 341 302 Err(e) => { 342 303 error!("DB error updating passkey: {:?}", e); 343 - ApiError::InternalError(None).into_response() 304 + Err(ApiError::InternalError(None)) 344 305 } 345 306 } 346 307 }
+127 -126
crates/tranquil-pds/src/api/server/password.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; 3 - use crate::auth::BearerAuth; 3 + use crate::auth::{Active, Auth}; 4 4 use crate::state::{AppState, RateLimitKind}; 5 5 use crate::types::PlainPassword; 6 6 use crate::validation::validate_password; ··· 227 227 228 228 pub async fn change_password( 229 229 State(state): State<AppState>, 230 - auth: BearerAuth, 230 + auth: Auth<Active>, 231 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 232 + ) -> Result<Response, ApiError> { 233 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 235 234 { 236 - return crate::api::server::reauth::legacy_mfa_required_response( 235 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 237 236 &*state.user_repo, 238 237 &*state.session_repo, 239 - &auth.0.did, 238 + &auth.did, 240 239 ) 241 - .await; 240 + .await); 242 241 } 243 242 244 243 let current_password = &input.current_password; 245 244 let new_password = &input.new_password; 246 245 if current_password.is_empty() { 247 - return ApiError::InvalidRequest("currentPassword is required".into()).into_response(); 246 + return Err(ApiError::InvalidRequest( 247 + "currentPassword is required".into(), 248 + )); 248 249 } 249 250 if new_password.is_empty() { 250 - return ApiError::InvalidRequest("newPassword is required".into()).into_response(); 251 + return Err(ApiError::InvalidRequest("newPassword is required".into())); 251 252 } 252 253 if let Err(e) = validate_password(new_password) { 253 - return ApiError::InvalidRequest(e.to_string()).into_response(); 254 + return Err(ApiError::InvalidRequest(e.to_string())); 254 255 } 255 - let user = match state 256 + let user = state 256 257 .user_repo 257 - .get_id_and_password_hash_by_did(&auth.0.did) 258 + .get_id_and_password_hash_by_did(&auth.did) 258 259 .await 259 - { 260 - Ok(Some(u)) => u, 261 - Ok(None) => { 262 - return ApiError::AccountNotFound.into_response(); 263 - } 264 - Err(e) => { 260 + .map_err(|e| { 265 261 error!("DB error in change_password: {:?}", e); 266 - return ApiError::InternalError(None).into_response(); 267 - } 268 - }; 262 + ApiError::InternalError(None) 263 + })? 264 + .ok_or(ApiError::AccountNotFound)?; 265 + 269 266 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 - }; 267 + let valid = verify(current_password, &password_hash).map_err(|e| { 268 + error!("Password verification error: {:?}", e); 269 + ApiError::InternalError(None) 270 + })?; 277 271 if !valid { 278 - return ApiError::InvalidPassword("Current password is incorrect".into()).into_response(); 272 + return Err(ApiError::InvalidPassword( 273 + "Current password is incorrect".into(), 274 + )); 279 275 } 280 276 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 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 294 289 .user_repo 295 290 .update_password_hash(user_id, &new_hash) 296 291 .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() 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()) 303 299 } 304 300 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(), 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), 309 308 Err(e) => { 310 309 error!("DB error: {:?}", e); 311 - ApiError::InternalError(None).into_response() 310 + Err(ApiError::InternalError(None)) 312 311 } 313 312 } 314 313 } 315 314 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 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 319 320 { 320 - return crate::api::server::reauth::legacy_mfa_required_response( 321 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 321 322 &*state.user_repo, 322 323 &*state.session_repo, 323 - &auth.0.did, 324 + &auth.did, 324 325 ) 325 - .await; 326 + .await); 326 327 } 327 328 328 329 if crate::api::server::reauth::check_reauth_required_cached( 329 330 &*state.session_repo, 330 331 &state.cache, 331 - &auth.0.did, 332 + &auth.did, 332 333 ) 333 334 .await 334 335 { 335 - return crate::api::server::reauth::reauth_required_response( 336 + return Ok(crate::api::server::reauth::reauth_required_response( 336 337 &*state.user_repo, 337 338 &*state.session_repo, 338 - &auth.0.did, 339 + &auth.did, 339 340 ) 340 - .await; 341 + .await); 341 342 } 342 343 343 344 let has_passkeys = state 344 345 .user_repo 345 - .has_passkeys(&auth.0.did) 346 + .has_passkeys(&auth.did) 346 347 .await 347 348 .unwrap_or(false); 348 349 if !has_passkeys { 349 - return ApiError::InvalidRequest( 350 + return Err(ApiError::InvalidRequest( 350 351 "You must have at least one passkey registered before removing your password".into(), 351 - ) 352 - .into_response(); 352 + )); 353 353 } 354 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) => { 355 + let user = state 356 + .user_repo 357 + .get_password_info_by_did(&auth.did) 358 + .await 359 + .map_err(|e| { 361 360 error!("DB error: {:?}", e); 362 - return ApiError::InternalError(None).into_response(); 363 - } 364 - }; 361 + ApiError::InternalError(None) 362 + })? 363 + .ok_or(ApiError::AccountNotFound)?; 365 364 366 365 if user.password_hash.is_none() { 367 - return ApiError::InvalidRequest("Account already has no password".into()).into_response(); 366 + return Err(ApiError::InvalidRequest( 367 + "Account already has no password".into(), 368 + )); 368 369 } 369 370 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 - } 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 + })?; 374 379 375 - info!(did = %&auth.0.did, "Password removed - account is now passkey-only"); 376 - SuccessResponse::ok().into_response() 380 + info!(did = %&auth.did, "Password removed - account is now passkey-only"); 381 + Ok(SuccessResponse::ok().into_response()) 377 382 } 378 383 379 384 #[derive(Deserialize)] ··· 384 389 385 390 pub async fn set_password( 386 391 State(state): State<AppState>, 387 - auth: BearerAuth, 392 + auth: Auth<Active>, 388 393 Json(input): Json<SetPasswordInput>, 389 - ) -> Response { 394 + ) -> Result<Response, ApiError> { 390 395 let has_password = state 391 396 .user_repo 392 - .has_password_by_did(&auth.0.did) 397 + .has_password_by_did(&auth.did) 393 398 .await 394 399 .ok() 395 400 .flatten() 396 401 .unwrap_or(false); 397 402 let has_passkeys = state 398 403 .user_repo 399 - .has_passkeys(&auth.0.did) 404 + .has_passkeys(&auth.did) 400 405 .await 401 406 .unwrap_or(false); 402 407 let has_totp = state 403 408 .user_repo 404 - .has_totp_enabled(&auth.0.did) 409 + .has_totp_enabled(&auth.did) 405 410 .await 406 411 .unwrap_or(false); 407 412 ··· 411 416 && crate::api::server::reauth::check_reauth_required_cached( 412 417 &*state.session_repo, 413 418 &state.cache, 414 - &auth.0.did, 419 + &auth.did, 415 420 ) 416 421 .await 417 422 { 418 - return crate::api::server::reauth::reauth_required_response( 423 + return Ok(crate::api::server::reauth::reauth_required_response( 419 424 &*state.user_repo, 420 425 &*state.session_repo, 421 - &auth.0.did, 426 + &auth.did, 422 427 ) 423 - .await; 428 + .await); 424 429 } 425 430 426 431 let new_password = &input.new_password; 427 432 if new_password.is_empty() { 428 - return ApiError::InvalidRequest("newPassword is required".into()).into_response(); 433 + return Err(ApiError::InvalidRequest("newPassword is required".into())); 429 434 } 430 435 if let Err(e) = validate_password(new_password) { 431 - return ApiError::InvalidRequest(e.to_string()).into_response(); 436 + return Err(ApiError::InvalidRequest(e.to_string())); 432 437 } 433 438 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) => { 439 + let user = state 440 + .user_repo 441 + .get_password_info_by_did(&auth.did) 442 + .await 443 + .map_err(|e| { 440 444 error!("DB error: {:?}", e); 441 - return ApiError::InternalError(None).into_response(); 442 - } 443 - }; 445 + ApiError::InternalError(None) 446 + })? 447 + .ok_or(ApiError::AccountNotFound)?; 444 448 445 449 if user.password_hash.is_some() { 446 - return ApiError::InvalidRequest( 450 + return Err(ApiError::InvalidRequest( 447 451 "Account already has a password. Use changePassword instead.".into(), 448 - ) 449 - .into_response(); 452 + )); 450 453 } 451 454 452 455 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 - }; 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 + })?; 465 466 466 - if let Err(e) = state 467 + state 467 468 .user_repo 468 469 .set_new_user_password(user.id, &new_hash) 469 470 .await 470 - { 471 - error!("DB error setting password: {:?}", e); 472 - return ApiError::InternalError(None).into_response(); 473 - } 471 + .map_err(|e| { 472 + error!("DB error setting password: {:?}", e); 473 + ApiError::InternalError(None) 474 + })?; 474 475 475 - info!(did = %&auth.0.did, "Password set for passkey-only account"); 476 - SuccessResponse::ok().into_response() 476 + info!(did = %&auth.did, "Password set for passkey-only account"); 477 + Ok(SuccessResponse::ok().into_response()) 477 478 }
+127 -145
crates/tranquil-pds/src/api/server/reauth.rs
··· 10 10 use tracing::{error, info, warn}; 11 11 use tranquil_db_traits::{SessionRepository, UserRepository}; 12 12 13 - use crate::auth::BearerAuth; 13 + use crate::auth::{Active, Auth}; 14 14 use crate::state::{AppState, RateLimitKind}; 15 15 use crate::types::PlainPassword; 16 16 ··· 24 24 pub available_methods: Vec<String>, 25 25 } 26 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) => { 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| { 31 36 error!("DB error: {:?}", e); 32 - return ApiError::InternalError(None).into_response(); 33 - } 34 - }; 37 + ApiError::InternalError(None) 38 + })?; 35 39 36 40 let reauth_required = is_reauth_required(last_reauth_at); 37 41 let available_methods = 38 - get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.0.did).await; 42 + get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.did).await; 39 43 40 - Json(ReauthStatusResponse { 44 + Ok(Json(ReauthStatusResponse { 41 45 last_reauth_at, 42 46 reauth_required, 43 47 available_methods, 44 48 }) 45 - .into_response() 49 + .into_response()) 46 50 } 47 51 48 52 #[derive(Deserialize)] ··· 59 63 60 64 pub async fn reauth_password( 61 65 State(state): State<AppState>, 62 - auth: BearerAuth, 66 + auth: Auth<Active>, 63 67 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) => { 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| { 71 74 error!("DB error: {:?}", e); 72 - return ApiError::InternalError(None).into_response(); 73 - } 74 - }; 75 + ApiError::InternalError(None) 76 + })? 77 + .ok_or(ApiError::AccountNotFound)?; 75 78 76 79 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 77 80 78 81 if !password_valid { 79 82 let app_password_hashes = state 80 83 .session_repo 81 - .get_app_password_hashes_by_did(&auth.0.did) 84 + .get_app_password_hashes_by_did(&auth.did) 82 85 .await 83 86 .unwrap_or_default(); 84 87 ··· 87 90 }); 88 91 89 92 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(); 93 + warn!(did = %&auth.did, "Re-auth failed: invalid password"); 94 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 92 95 } 93 96 } 94 97 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) => { 98 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 99 + .await 100 + .map_err(|e| { 101 101 error!("DB error updating reauth: {:?}", e); 102 - ApiError::InternalError(None).into_response() 103 - } 104 - } 102 + ApiError::InternalError(None) 103 + })?; 104 + 105 + info!(did = %&auth.did, "Re-auth successful via password"); 106 + Ok(Json(ReauthResponse { reauthed_at }).into_response()) 105 107 } 106 108 107 109 #[derive(Deserialize)] ··· 112 114 113 115 pub async fn reauth_totp( 114 116 State(state): State<AppState>, 115 - auth: BearerAuth, 117 + auth: Auth<Active>, 116 118 Json(input): Json<TotpReauthInput>, 117 - ) -> Response { 119 + ) -> Result<Response, ApiError> { 118 120 if !state 119 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 121 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 120 122 .await 121 123 { 122 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 123 - return ApiError::RateLimitExceeded(Some( 124 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 125 + return Err(ApiError::RateLimitExceeded(Some( 124 126 "Too many verification attempts. Please try again in a few minutes.".into(), 125 - )) 126 - .into_response(); 127 + ))); 127 128 } 128 129 129 130 let valid = 130 - crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.0.did, &input.code) 131 + crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.did, &input.code) 131 132 .await; 132 133 133 134 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(); 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 + ))); 136 139 } 137 140 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) => { 141 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 142 + .await 143 + .map_err(|e| { 144 144 error!("DB error updating reauth: {:?}", e); 145 - ApiError::InternalError(None).into_response() 146 - } 147 - } 145 + ApiError::InternalError(None) 146 + })?; 147 + 148 + info!(did = %&auth.did, "Re-auth successful via TOTP"); 149 + Ok(Json(ReauthResponse { reauthed_at }).into_response()) 148 150 } 149 151 150 152 #[derive(Serialize)] ··· 153 155 pub options: serde_json::Value, 154 156 } 155 157 156 - pub async fn reauth_passkey_start(State(state): State<AppState>, auth: BearerAuth) -> Response { 158 + pub async fn reauth_passkey_start( 159 + State(state): State<AppState>, 160 + auth: Auth<Active>, 161 + ) -> Result<Response, ApiError> { 157 162 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 158 163 159 - let stored_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { 160 - Ok(pks) => pks, 161 - Err(e) => { 164 + let stored_passkeys = state 165 + .user_repo 166 + .get_passkeys_for_user(&auth.did) 167 + .await 168 + .map_err(|e| { 162 169 error!("Failed to get passkeys: {:?}", e); 163 - return ApiError::InternalError(None).into_response(); 164 - } 165 - }; 170 + ApiError::InternalError(None) 171 + })?; 166 172 167 173 if stored_passkeys.is_empty() { 168 - return ApiError::NoPasskeys.into_response(); 174 + return Err(ApiError::NoPasskeys); 169 175 } 170 176 171 177 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys ··· 174 180 .collect(); 175 181 176 182 if passkeys.is_empty() { 177 - return ApiError::InternalError(Some("Failed to load passkeys".into())).into_response(); 183 + return Err(ApiError::InternalError(Some( 184 + "Failed to load passkeys".into(), 185 + ))); 178 186 } 179 187 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 - }; 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 + })?; 187 192 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 - }; 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 + })?; 195 197 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 - }; 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 + })?; 203 202 204 - if let Err(e) = state 203 + state 205 204 .user_repo 206 - .save_webauthn_challenge(&auth.0.did, "authentication", &state_json) 205 + .save_webauthn_challenge(&auth.did, "authentication", &state_json) 207 206 .await 208 - { 209 - error!("Failed to save authentication state: {:?}", e); 210 - return ApiError::InternalError(None).into_response(); 211 - } 207 + .map_err(|e| { 208 + error!("Failed to save authentication state: {:?}", e); 209 + ApiError::InternalError(None) 210 + })?; 212 211 213 212 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 214 - Json(PasskeyReauthStartResponse { options }).into_response() 213 + Ok(Json(PasskeyReauthStartResponse { options }).into_response()) 215 214 } 216 215 217 216 #[derive(Deserialize)] ··· 222 221 223 222 pub async fn reauth_passkey_finish( 224 223 State(state): State<AppState>, 225 - auth: BearerAuth, 224 + auth: Auth<Active>, 226 225 Json(input): Json<PasskeyReauthFinishInput>, 227 - ) -> Response { 226 + ) -> Result<Response, ApiError> { 228 227 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 229 228 230 - let auth_state_json = match state 229 + let auth_state_json = state 231 230 .user_repo 232 - .load_webauthn_challenge(&auth.0.did, "authentication") 231 + .load_webauthn_challenge(&auth.did, "authentication") 233 232 .await 234 - { 235 - Ok(Some(json)) => json, 236 - Ok(None) => { 237 - return ApiError::NoChallengeInProgress.into_response(); 238 - } 239 - Err(e) => { 233 + .map_err(|e| { 240 234 error!("Failed to load authentication state: {:?}", e); 241 - return ApiError::InternalError(None).into_response(); 242 - } 243 - }; 235 + ApiError::InternalError(None) 236 + })? 237 + .ok_or(ApiError::NoChallengeInProgress)?; 244 238 245 239 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 - }; 240 + serde_json::from_str(&auth_state_json).map_err(|e| { 241 + error!("Failed to deserialize authentication state: {:?}", e); 242 + ApiError::InternalError(None) 243 + })?; 253 244 254 245 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 - }; 246 + serde_json::from_value(input.credential).map_err(|e| { 247 + warn!("Failed to parse credential: {:?}", e); 248 + ApiError::InvalidCredential 249 + })?; 262 250 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 - }; 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 + })?; 270 255 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 - }; 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 + })?; 279 262 280 263 let cred_id_bytes = auth_result.cred_id().as_ref(); 281 264 match state ··· 284 267 .await 285 268 { 286 269 Ok(false) => { 287 - warn!(did = %&auth.0.did, "Passkey counter anomaly detected - possible cloned key"); 270 + warn!(did = %&auth.did, "Passkey counter anomaly detected - possible cloned key"); 288 271 let _ = state 289 272 .user_repo 290 - .delete_webauthn_challenge(&auth.0.did, "authentication") 273 + .delete_webauthn_challenge(&auth.did, "authentication") 291 274 .await; 292 - return ApiError::PasskeyCounterAnomaly.into_response(); 275 + return Err(ApiError::PasskeyCounterAnomaly); 293 276 } 294 277 Err(e) => { 295 278 error!("Failed to update passkey counter: {:?}", e); ··· 299 282 300 283 let _ = state 301 284 .user_repo 302 - .delete_webauthn_challenge(&auth.0.did, "authentication") 285 + .delete_webauthn_challenge(&auth.did, "authentication") 303 286 .await; 304 287 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) => { 288 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.did) 289 + .await 290 + .map_err(|e| { 311 291 error!("DB error updating reauth: {:?}", e); 312 - ApiError::InternalError(None).into_response() 313 - } 314 - } 292 + ApiError::InternalError(None) 293 + })?; 294 + 295 + info!(did = %&auth.did, "Re-auth successful via passkey"); 296 + Ok(Json(ReauthResponse { reauthed_at }).into_response()) 315 297 } 316 298 317 299 pub async fn update_last_reauth_cached(
+4 -4
crates/tranquil-pds/src/api/server/service_auth.rs
··· 95 95 { 96 96 Ok(result) => crate::auth::AuthenticatedUser { 97 97 did: Did::new_unchecked(result.did), 98 - is_oauth: true, 99 98 is_admin: false, 100 99 status: AccountStatus::Active, 101 100 scope: result.scope, 102 101 key_bytes: None, 103 102 controller_did: None, 103 + auth_source: crate::auth::AuthSource::OAuth, 104 104 }, 105 105 Err(crate::oauth::OAuthError::UseDpopNonce(nonce)) => { 106 106 return ( ··· 131 131 }; 132 132 info!( 133 133 did = %&auth_user.did, 134 - is_oauth = auth_user.is_oauth, 134 + is_oauth = auth_user.is_oauth(), 135 135 has_key = auth_user.key_bytes.is_some(), 136 136 "getServiceAuth auth validated" 137 137 ); ··· 180 180 181 181 if let Some(method) = lxm { 182 182 if let Err(e) = crate::auth::scope_check::check_rpc_scope( 183 - auth_user.is_oauth, 183 + auth_user.is_oauth(), 184 184 auth_user.scope.as_deref(), 185 185 &params.aud, 186 186 method, 187 187 ) { 188 188 return e; 189 189 } 190 - } else if auth_user.is_oauth { 190 + } else if auth_user.is_oauth() { 191 191 let permissions = auth_user.permissions(); 192 192 if !permissions.has_full_access() { 193 193 return ApiError::InvalidRequest(
+169 -174
crates/tranquil-pds/src/api/server/session.rs
··· 1 1 use crate::api::error::ApiError; 2 2 use crate::api::{EmptyResponse, SuccessResponse}; 3 - use crate::auth::{BearerAuth, BearerAuthAllowDeactivated}; 3 + use crate::auth::{Active, Auth, NotTakendown}; 4 4 use crate::state::{AppState, RateLimitKind}; 5 5 use crate::types::{AccountState, Did, Handle, PlainPassword}; 6 6 use axum::{ ··· 279 279 280 280 pub async fn get_session( 281 281 State(state): State<AppState>, 282 - BearerAuthAllowDeactivated(auth_user): BearerAuthAllowDeactivated, 283 - ) -> Response { 284 - let permissions = auth_user.permissions(); 282 + auth: Auth<NotTakendown>, 283 + ) -> Result<Response, ApiError> { 284 + let permissions = auth.permissions(); 285 285 let can_read_email = permissions.allows_email_read(); 286 286 287 - let did_for_doc = auth_user.did.clone(); 287 + let did_for_doc = auth.did.clone(); 288 288 let did_resolver = state.did_resolver.clone(); 289 289 let (db_result, did_doc) = tokio::join!( 290 - state.user_repo.get_session_info_by_did(&auth_user.did), 290 + state.user_repo.get_session_info_by_did(&auth.did), 291 291 did_resolver.resolve_did_document(&did_for_doc) 292 292 ); 293 293 match db_result { ··· 316 316 let email_confirmed_value = can_read_email && row.email_verified; 317 317 let mut response = json!({ 318 318 "handle": handle, 319 - "did": &auth_user.did, 319 + "did": &auth.did, 320 320 "active": account_state.is_active(), 321 321 "preferredChannel": preferred_channel, 322 322 "preferredChannelVerified": preferred_channel_verified, ··· 337 337 if let Some(doc) = did_doc { 338 338 response["didDoc"] = doc; 339 339 } 340 - Json(response).into_response() 340 + Ok(Json(response).into_response()) 341 341 } 342 - Ok(None) => ApiError::AuthenticationFailed(None).into_response(), 342 + Ok(None) => Err(ApiError::AuthenticationFailed(None)), 343 343 Err(e) => { 344 344 error!("Database error in get_session: {:?}", e); 345 - ApiError::InternalError(None).into_response() 345 + Err(ApiError::InternalError(None)) 346 346 } 347 347 } 348 348 } ··· 350 350 pub async fn delete_session( 351 351 State(state): State<AppState>, 352 352 headers: axum::http::HeaderMap, 353 - _auth: BearerAuth, 354 - ) -> Response { 355 - let extracted = match crate::auth::extract_auth_token_from_header( 353 + _auth: Auth<Active>, 354 + ) -> Result<Response, ApiError> { 355 + let extracted = crate::auth::extract_auth_token_from_header( 356 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 - }; 357 + ) 358 + .ok_or(ApiError::AuthenticationRequired)?; 359 + let jti = crate::auth::get_jti_from_token(&extracted.token) 360 + .map_err(|_| ApiError::AuthenticationFailed(None))?; 365 361 let did = crate::auth::get_did_from_token(&extracted.token).ok(); 366 362 match state.session_repo.delete_session_by_access_jti(&jti).await { 367 363 Ok(rows) if rows > 0 => { ··· 369 365 let session_cache_key = format!("auth:session:{}:{}", did, jti); 370 366 let _ = state.cache.delete(&session_cache_key).await; 371 367 } 372 - EmptyResponse::ok().into_response() 368 + Ok(EmptyResponse::ok().into_response()) 373 369 } 374 - Ok(_) => ApiError::AuthenticationFailed(None).into_response(), 375 - Err(_) => ApiError::AuthenticationFailed(None).into_response(), 370 + Ok(_) => Err(ApiError::AuthenticationFailed(None)), 371 + Err(_) => Err(ApiError::AuthenticationFailed(None)), 376 372 } 377 373 } 378 374 ··· 796 792 pub async fn list_sessions( 797 793 State(state): State<AppState>, 798 794 headers: HeaderMap, 799 - auth: BearerAuth, 800 - ) -> Response { 795 + auth: Auth<Active>, 796 + ) -> Result<Response, ApiError> { 801 797 let current_jti = headers 802 798 .get("authorization") 803 799 .and_then(|v| v.to_str().ok()) 804 800 .and_then(|v| v.strip_prefix("Bearer ")) 805 801 .and_then(|token| crate::auth::get_jti_from_token(token).ok()); 806 802 807 - let jwt_rows = match state.session_repo.list_sessions_by_did(&auth.0.did).await { 808 - Ok(rows) => rows, 809 - Err(e) => { 803 + let jwt_rows = state 804 + .session_repo 805 + .list_sessions_by_did(&auth.did) 806 + .await 807 + .map_err(|e| { 810 808 error!("DB error fetching JWT sessions: {:?}", e); 811 - return ApiError::InternalError(None).into_response(); 812 - } 813 - }; 809 + ApiError::InternalError(None) 810 + })?; 814 811 815 - let oauth_rows = match state.oauth_repo.list_sessions_by_did(&auth.0.did).await { 816 - Ok(rows) => rows, 817 - Err(e) => { 812 + let oauth_rows = state 813 + .oauth_repo 814 + .list_sessions_by_did(&auth.did) 815 + .await 816 + .map_err(|e| { 818 817 error!("DB error fetching OAuth sessions: {:?}", e); 819 - return ApiError::InternalError(None).into_response(); 820 - } 821 - }; 818 + ApiError::InternalError(None) 819 + })?; 822 820 823 821 let jwt_sessions = jwt_rows.into_iter().map(|row| SessionInfo { 824 822 id: format!("jwt:{}", row.id), ··· 829 827 is_current: current_jti.as_ref() == Some(&row.access_jti), 830 828 }); 831 829 832 - let is_oauth = auth.0.is_oauth; 830 + let is_oauth = auth.is_oauth(); 833 831 let oauth_sessions = oauth_rows.into_iter().map(|row| { 834 832 let client_name = extract_client_name(&row.client_id); 835 833 let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str()); ··· 846 844 let mut sessions: Vec<SessionInfo> = jwt_sessions.chain(oauth_sessions).collect(); 847 845 sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); 848 846 849 - (StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response() 847 + Ok((StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response()) 850 848 } 851 849 852 850 fn extract_client_name(client_id: &str) -> String { ··· 867 865 868 866 pub async fn revoke_session( 869 867 State(state): State<AppState>, 870 - auth: BearerAuth, 868 + auth: Auth<Active>, 871 869 Json(input): Json<RevokeSessionInput>, 872 - ) -> Response { 870 + ) -> Result<Response, ApiError> { 873 871 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 872 + let session_id: i32 = jwt_id 873 + .parse() 874 + .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; 875 + let access_jti = state 878 876 .session_repo 879 - .get_session_access_jti_by_id(session_id, &auth.0.did) 877 + .get_session_access_jti_by_id(session_id, &auth.did) 880 878 .await 881 - { 882 - Ok(Some(jti)) => jti, 883 - Ok(None) => { 884 - return ApiError::SessionNotFound.into_response(); 885 - } 886 - Err(e) => { 879 + .map_err(|e| { 887 880 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); 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); 896 893 if let Err(e) = state.cache.delete(&cache_key).await { 897 894 warn!("Failed to invalidate session cache: {:?}", e); 898 895 } 899 - info!(did = %&auth.0.did, session_id = %session_id, "JWT session revoked"); 896 + info!(did = %&auth.did, session_id = %session_id, "JWT session revoked"); 900 897 } 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 898 + let session_id: i32 = oauth_id 899 + .parse() 900 + .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; 901 + let deleted = state 905 902 .oauth_repo 906 - .delete_session_by_id(session_id, &auth.0.did) 903 + .delete_session_by_id(session_id, &auth.did) 907 904 .await 908 - { 909 - Ok(0) => { 910 - return ApiError::SessionNotFound.into_response(); 911 - } 912 - Err(e) => { 905 + .map_err(|e| { 913 906 error!("DB error deleting OAuth session: {:?}", e); 914 - return ApiError::InternalError(None).into_response(); 915 - } 916 - _ => {} 907 + ApiError::InternalError(None) 908 + })?; 909 + if deleted == 0 { 910 + return Err(ApiError::SessionNotFound); 917 911 } 918 - info!(did = %&auth.0.did, session_id = %session_id, "OAuth session revoked"); 912 + info!(did = %&auth.did, session_id = %session_id, "OAuth session revoked"); 919 913 } else { 920 - return ApiError::InvalidRequest("Invalid session ID format".into()).into_response(); 914 + return Err(ApiError::InvalidRequest("Invalid session ID format".into())); 921 915 } 922 - EmptyResponse::ok().into_response() 916 + Ok(EmptyResponse::ok().into_response()) 923 917 } 924 918 925 919 pub async fn revoke_all_sessions( 926 920 State(state): State<AppState>, 927 921 headers: HeaderMap, 928 - auth: BearerAuth, 929 - ) -> Response { 930 - let current_jti = crate::auth::extract_auth_token_from_header( 922 + auth: Auth<Active>, 923 + ) -> Result<Response, ApiError> { 924 + let jti = crate::auth::extract_auth_token_from_header( 931 925 headers.get("authorization").and_then(|v| v.to_str().ok()), 932 926 ) 933 - .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()); 927 + .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()) 928 + .ok_or(ApiError::InvalidToken(None))?; 934 929 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 - } 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 + })?; 944 939 let jti_typed = TokenId::from(jti.clone()); 945 - if let Err(e) = state 940 + state 946 941 .oauth_repo 947 - .delete_sessions_by_did_except(&auth.0.did, &jti_typed) 942 + .delete_sessions_by_did_except(&auth.did, &jti_typed) 948 943 .await 949 - { 950 - error!("DB error revoking OAuth sessions: {:?}", e); 951 - return ApiError::InternalError(None).into_response(); 952 - } 944 + .map_err(|e| { 945 + error!("DB error revoking OAuth sessions: {:?}", e); 946 + ApiError::InternalError(None) 947 + })?; 953 948 } else { 954 - if let Err(e) = state 949 + state 955 950 .session_repo 956 - .delete_sessions_by_did_except_jti(&auth.0.did, jti) 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) 957 960 .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 - } 961 + .map_err(|e| { 962 + error!("DB error revoking OAuth sessions: {:?}", e); 963 + ApiError::InternalError(None) 964 + })?; 966 965 } 967 966 968 - info!(did = %&auth.0.did, "All other sessions revoked"); 969 - SuccessResponse::ok().into_response() 967 + info!(did = %&auth.did, "All other sessions revoked"); 968 + Ok(SuccessResponse::ok().into_response()) 970 969 } 971 970 972 971 #[derive(Serialize)] ··· 978 977 979 978 pub async fn get_legacy_login_preference( 980 979 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) => { 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| { 991 987 error!("DB error: {:?}", e); 992 - ApiError::InternalError(None).into_response() 993 - } 994 - } 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()) 995 996 } 996 997 997 998 #[derive(Deserialize)] ··· 1002 1003 1003 1004 pub async fn update_legacy_login_preference( 1004 1005 State(state): State<AppState>, 1005 - auth: BearerAuth, 1006 + auth: Auth<Active>, 1006 1007 Json(input): Json<UpdateLegacyLoginInput>, 1007 - ) -> Response { 1008 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 1009 - .await 1008 + ) -> Result<Response, ApiError> { 1009 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 1010 1010 { 1011 - return crate::api::server::reauth::legacy_mfa_required_response( 1011 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 1012 1012 &*state.user_repo, 1013 1013 &*state.session_repo, 1014 - &auth.0.did, 1014 + &auth.did, 1015 1015 ) 1016 - .await; 1016 + .await); 1017 1017 } 1018 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( 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 1021 &*state.user_repo, 1022 1022 &*state.session_repo, 1023 - &auth.0.did, 1023 + &auth.did, 1024 1024 ) 1025 - .await; 1025 + .await); 1026 1026 } 1027 1027 1028 - match state 1028 + let updated = state 1029 1029 .user_repo 1030 - .update_legacy_login(&auth.0.did, input.allow_legacy_login) 1030 + .update_legacy_login(&auth.did, input.allow_legacy_login) 1031 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) => { 1032 + .map_err(|e| { 1046 1033 error!("DB error: {:?}", e); 1047 - ApiError::InternalError(None).into_response() 1048 - } 1034 + ApiError::InternalError(None) 1035 + })?; 1036 + if !updated { 1037 + return Err(ApiError::AccountNotFound); 1049 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()) 1050 1048 } 1051 1049 1052 1050 use crate::comms::VALID_LOCALES; ··· 1059 1057 1060 1058 pub async fn update_locale( 1061 1059 State(state): State<AppState>, 1062 - auth: BearerAuth, 1060 + auth: Auth<Active>, 1063 1061 Json(input): Json<UpdateLocaleInput>, 1064 - ) -> Response { 1062 + ) -> Result<Response, ApiError> { 1065 1063 if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) { 1066 - return ApiError::InvalidRequest(format!( 1064 + return Err(ApiError::InvalidRequest(format!( 1067 1065 "Invalid locale. Valid options: {}", 1068 1066 VALID_LOCALES.join(", ") 1069 - )) 1070 - .into_response(); 1067 + ))); 1071 1068 } 1072 1069 1073 - match state 1070 + let updated = state 1074 1071 .user_repo 1075 - .update_locale(&auth.0.did, &input.preferred_locale) 1072 + .update_locale(&auth.did, &input.preferred_locale) 1076 1073 .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) => { 1074 + .map_err(|e| { 1091 1075 error!("DB error updating locale: {:?}", e); 1092 - ApiError::InternalError(None).into_response() 1093 - } 1076 + ApiError::InternalError(None) 1077 + })?; 1078 + if !updated { 1079 + return Err(ApiError::AccountNotFound); 1094 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()) 1095 1090 }
+161 -161
crates/tranquil-pds/src/api/server/totp.rs
··· 1 1 use crate::api::EmptyResponse; 2 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuth; 3 + use crate::auth::{Active, Auth}; 4 4 use crate::auth::{ 5 5 decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, 6 6 generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, ··· 26 26 pub qr_base64: String, 27 27 } 28 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(), 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), 32 35 Ok(_) => {} 33 36 Err(e) => { 34 37 error!("DB error checking TOTP: {:?}", e); 35 - return ApiError::InternalError(None).into_response(); 38 + return Err(ApiError::InternalError(None)); 36 39 } 37 40 } 38 41 39 42 let secret = generate_totp_secret(); 40 43 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) => { 44 + let handle = state 45 + .user_repo 46 + .get_handle_by_did(&auth.did) 47 + .await 48 + .map_err(|e| { 45 49 error!("DB error fetching handle: {:?}", e); 46 - return ApiError::InternalError(None).into_response(); 47 - } 48 - }; 50 + ApiError::InternalError(None) 51 + })? 52 + .ok_or(ApiError::AccountNotFound)?; 49 53 50 54 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 51 55 let uri = generate_totp_uri(&secret, &handle, &hostname); 52 56 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 - }; 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 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 - }; 62 + let encrypted_secret = encrypt_totp_secret(&secret).map_err(|e| { 63 + error!("Failed to encrypt TOTP secret: {:?}", e); 64 + ApiError::InternalError(None) 65 + })?; 69 66 70 - if let Err(e) = state 67 + state 71 68 .user_repo 72 - .upsert_totp_secret(&auth.0.did, &encrypted_secret, ENCRYPTION_VERSION) 69 + .upsert_totp_secret(&auth.did, &encrypted_secret, ENCRYPTION_VERSION) 73 70 .await 74 - { 75 - error!("Failed to store TOTP secret: {:?}", e); 76 - return ApiError::InternalError(None).into_response(); 77 - } 71 + .map_err(|e| { 72 + error!("Failed to store TOTP secret: {:?}", e); 73 + ApiError::InternalError(None) 74 + })?; 78 75 79 76 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 80 77 81 - info!(did = %&auth.0.did, "TOTP secret created (pending verification)"); 78 + info!(did = %&auth.did, "TOTP secret created (pending verification)"); 82 79 83 - Json(CreateTotpSecretResponse { 80 + Ok(Json(CreateTotpSecretResponse { 84 81 secret: secret_base32, 85 82 uri, 86 83 qr_base64: qr_code, 87 84 }) 88 - .into_response() 85 + .into_response()) 89 86 } 90 87 91 88 #[derive(Deserialize)] ··· 101 98 102 99 pub async fn enable_totp( 103 100 State(state): State<AppState>, 104 - auth: BearerAuth, 101 + auth: Auth<Active>, 105 102 Json(input): Json<EnableTotpInput>, 106 - ) -> Response { 103 + ) -> Result<Response, ApiError> { 107 104 if !state 108 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 105 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 109 106 .await 110 107 { 111 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 112 - return ApiError::RateLimitExceeded(None).into_response(); 108 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 109 + return Err(ApiError::RateLimitExceeded(None)); 113 110 } 114 111 115 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 112 + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 116 113 Ok(Some(row)) => row, 117 - Ok(None) => return ApiError::TotpNotEnabled.into_response(), 114 + Ok(None) => return Err(ApiError::TotpNotEnabled), 118 115 Err(e) => { 119 116 error!("DB error fetching TOTP: {:?}", e); 120 - return ApiError::InternalError(None).into_response(); 117 + return Err(ApiError::InternalError(None)); 121 118 } 122 119 }; 123 120 124 121 if totp_record.verified { 125 - return ApiError::TotpAlreadyEnabled.into_response(); 122 + return Err(ApiError::TotpAlreadyEnabled); 126 123 } 127 124 128 - let secret = match decrypt_totp_secret( 125 + let secret = decrypt_totp_secret( 129 126 &totp_record.secret_encrypted, 130 127 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 - }; 128 + ) 129 + .map_err(|e| { 130 + error!("Failed to decrypt TOTP secret: {:?}", e); 131 + ApiError::InternalError(None) 132 + })?; 138 133 139 134 let code = input.code.trim(); 140 135 if !verify_totp_code(&secret, code) { 141 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 136 + return Err(ApiError::InvalidCode(Some( 137 + "Invalid verification code".into(), 138 + ))); 142 139 } 143 140 144 141 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) => { 142 + let backup_hashes: Vec<_> = backup_codes 143 + .iter() 144 + .map(|c| hash_backup_code(c)) 145 + .collect::<Result<Vec<_>, _>>() 146 + .map_err(|e| { 150 147 error!("Failed to hash backup code: {:?}", e); 151 - return ApiError::InternalError(None).into_response(); 152 - } 153 - }; 148 + ApiError::InternalError(None) 149 + })?; 154 150 155 - if let Err(e) = state 151 + state 156 152 .user_repo 157 - .enable_totp_with_backup_codes(&auth.0.did, &backup_hashes) 153 + .enable_totp_with_backup_codes(&auth.did, &backup_hashes) 158 154 .await 159 - { 160 - error!("Failed to enable TOTP: {:?}", e); 161 - return ApiError::InternalError(None).into_response(); 162 - } 155 + .map_err(|e| { 156 + error!("Failed to enable TOTP: {:?}", e); 157 + ApiError::InternalError(None) 158 + })?; 163 159 164 - info!(did = %&auth.0.did, "TOTP enabled with {} backup codes", backup_codes.len()); 160 + info!(did = %&auth.did, "TOTP enabled with {} backup codes", backup_codes.len()); 165 161 166 - Json(EnableTotpResponse { backup_codes }).into_response() 162 + Ok(Json(EnableTotpResponse { backup_codes }).into_response()) 167 163 } 168 164 169 165 #[derive(Deserialize)] ··· 174 170 175 171 pub async fn disable_totp( 176 172 State(state): State<AppState>, 177 - auth: BearerAuth, 173 + auth: Auth<Active>, 178 174 Json(input): Json<DisableTotpInput>, 179 - ) -> Response { 180 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 181 - .await 175 + ) -> Result<Response, ApiError> { 176 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.did).await 182 177 { 183 - return crate::api::server::reauth::legacy_mfa_required_response( 178 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 184 179 &*state.user_repo, 185 180 &*state.session_repo, 186 - &auth.0.did, 181 + &auth.did, 187 182 ) 188 - .await; 183 + .await); 189 184 } 190 185 191 186 if !state 192 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 187 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 193 188 .await 194 189 { 195 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 196 - return ApiError::RateLimitExceeded(None).into_response(); 190 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 191 + return Err(ApiError::RateLimitExceeded(None)); 197 192 } 198 193 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) => { 194 + let password_hash = state 195 + .user_repo 196 + .get_password_hash_by_did(&auth.did) 197 + .await 198 + .map_err(|e| { 203 199 error!("DB error fetching user: {:?}", e); 204 - return ApiError::InternalError(None).into_response(); 205 - } 206 - }; 200 + ApiError::InternalError(None) 201 + })? 202 + .ok_or(ApiError::AccountNotFound)?; 207 203 208 204 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 209 205 if !password_valid { 210 - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 206 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 211 207 } 212 208 213 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 209 + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 214 210 Ok(Some(row)) if row.verified => row, 215 - Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 211 + Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 216 212 Err(e) => { 217 213 error!("DB error fetching TOTP: {:?}", e); 218 - return ApiError::InternalError(None).into_response(); 214 + return Err(ApiError::InternalError(None)); 219 215 } 220 216 }; 221 217 222 218 let code = input.code.trim(); 223 219 let code_valid = if is_backup_code_format(code) { 224 - verify_backup_code_for_user(&state, &auth.0.did, code).await 220 + verify_backup_code_for_user(&state, &auth.did, code).await 225 221 } else { 226 - let secret = match decrypt_totp_secret( 222 + let secret = decrypt_totp_secret( 227 223 &totp_record.secret_encrypted, 228 224 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 - }; 225 + ) 226 + .map_err(|e| { 227 + error!("Failed to decrypt TOTP secret: {:?}", e); 228 + ApiError::InternalError(None) 229 + })?; 236 230 verify_totp_code(&secret, code) 237 231 }; 238 232 239 233 if !code_valid { 240 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 234 + return Err(ApiError::InvalidCode(Some( 235 + "Invalid verification code".into(), 236 + ))); 241 237 } 242 238 243 - if let Err(e) = state 239 + state 244 240 .user_repo 245 - .delete_totp_and_backup_codes(&auth.0.did) 241 + .delete_totp_and_backup_codes(&auth.did) 246 242 .await 247 - { 248 - error!("Failed to delete TOTP: {:?}", e); 249 - return ApiError::InternalError(None).into_response(); 250 - } 243 + .map_err(|e| { 244 + error!("Failed to delete TOTP: {:?}", e); 245 + ApiError::InternalError(None) 246 + })?; 251 247 252 - info!(did = %&auth.0.did, "TOTP disabled"); 248 + info!(did = %&auth.did, "TOTP disabled"); 253 249 254 - EmptyResponse::ok().into_response() 250 + Ok(EmptyResponse::ok().into_response()) 255 251 } 256 252 257 253 #[derive(Serialize)] ··· 262 258 pub backup_codes_remaining: i64, 263 259 } 264 260 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 { 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 { 267 266 Ok(Some(row)) => row.verified, 268 267 Ok(None) => false, 269 268 Err(e) => { 270 269 error!("DB error fetching TOTP status: {:?}", e); 271 - return ApiError::InternalError(None).into_response(); 270 + return Err(ApiError::InternalError(None)); 272 271 } 273 272 }; 274 273 275 - let backup_count = match state.user_repo.count_unused_backup_codes(&auth.0.did).await { 276 - Ok(count) => count, 277 - Err(e) => { 274 + let backup_count = state 275 + .user_repo 276 + .count_unused_backup_codes(&auth.did) 277 + .await 278 + .map_err(|e| { 278 279 error!("DB error counting backup codes: {:?}", e); 279 - return ApiError::InternalError(None).into_response(); 280 - } 281 - }; 280 + ApiError::InternalError(None) 281 + })?; 282 282 283 - Json(GetTotpStatusResponse { 283 + Ok(Json(GetTotpStatusResponse { 284 284 enabled, 285 285 has_backup_codes: backup_count > 0, 286 286 backup_codes_remaining: backup_count, 287 287 }) 288 - .into_response() 288 + .into_response()) 289 289 } 290 290 291 291 #[derive(Deserialize)] ··· 302 302 303 303 pub async fn regenerate_backup_codes( 304 304 State(state): State<AppState>, 305 - auth: BearerAuth, 305 + auth: Auth<Active>, 306 306 Json(input): Json<RegenerateBackupCodesInput>, 307 - ) -> Response { 307 + ) -> Result<Response, ApiError> { 308 308 if !state 309 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 309 + .check_rate_limit(RateLimitKind::TotpVerify, &auth.did) 310 310 .await 311 311 { 312 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 313 - return ApiError::RateLimitExceeded(None).into_response(); 312 + warn!(did = %&auth.did, "TOTP verification rate limit exceeded"); 313 + return Err(ApiError::RateLimitExceeded(None)); 314 314 } 315 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) => { 316 + let password_hash = state 317 + .user_repo 318 + .get_password_hash_by_did(&auth.did) 319 + .await 320 + .map_err(|e| { 320 321 error!("DB error fetching user: {:?}", e); 321 - return ApiError::InternalError(None).into_response(); 322 - } 323 - }; 322 + ApiError::InternalError(None) 323 + })? 324 + .ok_or(ApiError::AccountNotFound)?; 324 325 325 326 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 326 327 if !password_valid { 327 - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 328 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 328 329 } 329 330 330 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 331 + let totp_record = match state.user_repo.get_totp_record(&auth.did).await { 331 332 Ok(Some(row)) if row.verified => row, 332 - Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 333 + Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 333 334 Err(e) => { 334 335 error!("DB error fetching TOTP: {:?}", e); 335 - return ApiError::InternalError(None).into_response(); 336 + return Err(ApiError::InternalError(None)); 336 337 } 337 338 }; 338 339 339 - let secret = match decrypt_totp_secret( 340 + let secret = decrypt_totp_secret( 340 341 &totp_record.secret_encrypted, 341 342 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 - }; 343 + ) 344 + .map_err(|e| { 345 + error!("Failed to decrypt TOTP secret: {:?}", e); 346 + ApiError::InternalError(None) 347 + })?; 349 348 350 349 let code = input.code.trim(); 351 350 if !verify_totp_code(&secret, code) { 352 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 351 + return Err(ApiError::InvalidCode(Some( 352 + "Invalid verification code".into(), 353 + ))); 353 354 } 354 355 355 356 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) => { 357 + let backup_hashes: Vec<_> = backup_codes 358 + .iter() 359 + .map(|c| hash_backup_code(c)) 360 + .collect::<Result<Vec<_>, _>>() 361 + .map_err(|e| { 361 362 error!("Failed to hash backup code: {:?}", e); 362 - return ApiError::InternalError(None).into_response(); 363 - } 364 - }; 363 + ApiError::InternalError(None) 364 + })?; 365 365 366 - if let Err(e) = state 366 + state 367 367 .user_repo 368 - .replace_backup_codes(&auth.0.did, &backup_hashes) 368 + .replace_backup_codes(&auth.did, &backup_hashes) 369 369 .await 370 - { 371 - error!("Failed to regenerate backup codes: {:?}", e); 372 - return ApiError::InternalError(None).into_response(); 373 - } 370 + .map_err(|e| { 371 + error!("Failed to regenerate backup codes: {:?}", e); 372 + ApiError::InternalError(None) 373 + })?; 374 374 375 - info!(did = %&auth.0.did, "Backup codes regenerated"); 375 + info!(did = %&auth.did, "Backup codes regenerated"); 376 376 377 - Json(RegenerateBackupCodesResponse { backup_codes }).into_response() 377 + Ok(Json(RegenerateBackupCodesResponse { backup_codes }).into_response()) 378 378 } 379 379 380 380 async fn verify_backup_code_for_user(
+57 -55
crates/tranquil-pds/src/api/server/trusted_devices.rs
··· 11 11 use tranquil_db_traits::OAuthRepository; 12 12 use tranquil_types::DeviceId; 13 13 14 - use crate::auth::BearerAuth; 14 + use crate::auth::{Active, Auth}; 15 15 use crate::state::AppState; 16 16 17 17 const TRUST_DURATION_DAYS: i64 = 30; ··· 71 71 pub devices: Vec<TrustedDevice>, 72 72 } 73 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) => { 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| { 96 83 error!("DB error: {:?}", e); 97 - ApiError::InternalError(None).into_response() 98 - } 99 - } 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()) 100 104 } 101 105 102 106 #[derive(Deserialize)] ··· 107 111 108 112 pub async fn revoke_trusted_device( 109 113 State(state): State<AppState>, 110 - auth: BearerAuth, 114 + auth: Auth<Active>, 111 115 Json(input): Json<RevokeTrustedDeviceInput>, 112 - ) -> Response { 116 + ) -> Result<Response, ApiError> { 113 117 let device_id = DeviceId::from(input.device_id.clone()); 114 118 match state 115 119 .oauth_repo 116 - .device_belongs_to_user(&device_id, &auth.0.did) 120 + .device_belongs_to_user(&device_id, &auth.did) 117 121 .await 118 122 { 119 123 Ok(true) => {} 120 124 Ok(false) => { 121 - return ApiError::DeviceNotFound.into_response(); 125 + return Err(ApiError::DeviceNotFound); 122 126 } 123 127 Err(e) => { 124 128 error!("DB error: {:?}", e); 125 - return ApiError::InternalError(None).into_response(); 129 + return Err(ApiError::InternalError(None)); 126 130 } 127 131 } 128 132 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) => { 133 + state 134 + .oauth_repo 135 + .revoke_device_trust(&device_id) 136 + .await 137 + .map_err(|e| { 135 138 error!("DB error: {:?}", e); 136 - ApiError::InternalError(None).into_response() 137 - } 138 - } 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()) 139 144 } 140 145 141 146 #[derive(Deserialize)] ··· 147 152 148 153 pub async fn update_trusted_device( 149 154 State(state): State<AppState>, 150 - auth: BearerAuth, 155 + auth: Auth<Active>, 151 156 Json(input): Json<UpdateTrustedDeviceInput>, 152 - ) -> Response { 157 + ) -> Result<Response, ApiError> { 153 158 let device_id = DeviceId::from(input.device_id.clone()); 154 159 match state 155 160 .oauth_repo 156 - .device_belongs_to_user(&device_id, &auth.0.did) 161 + .device_belongs_to_user(&device_id, &auth.did) 157 162 .await 158 163 { 159 164 Ok(true) => {} 160 165 Ok(false) => { 161 - return ApiError::DeviceNotFound.into_response(); 166 + return Err(ApiError::DeviceNotFound); 162 167 } 163 168 Err(e) => { 164 169 error!("DB error: {:?}", e); 165 - return ApiError::InternalError(None).into_response(); 170 + return Err(ApiError::InternalError(None)); 166 171 } 167 172 } 168 173 169 - match state 174 + state 170 175 .oauth_repo 171 176 .update_device_friendly_name(&device_id, input.friendly_name.as_deref()) 172 177 .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) => { 178 + .map_err(|e| { 179 179 error!("DB error: {:?}", e); 180 - ApiError::InternalError(None).into_response() 181 - } 182 - } 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()) 183 185 } 184 186 185 187 pub async fn get_device_trust_state(
+8 -10
crates/tranquil-pds/src/api/temp.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::{BearerAuth, OptionalBearerAuth}; 2 + use crate::auth::{Active, Auth, Permissive}; 3 3 use crate::state::AppState; 4 4 use axum::{ 5 5 Json, ··· 21 21 pub estimated_time_ms: Option<i64>, 22 22 } 23 23 24 - pub async fn check_signup_queue(auth: OptionalBearerAuth) -> Response { 25 - if let Some(user) = auth.0 26 - && user.is_oauth 24 + pub async fn check_signup_queue(auth: Option<Auth<Permissive>>) -> Response { 25 + if let Some(ref user) = auth 26 + && user.is_oauth() 27 27 { 28 28 return ApiError::Forbidden.into_response(); 29 29 } ··· 49 49 50 50 pub async fn dereference_scope( 51 51 State(state): State<AppState>, 52 - auth: BearerAuth, 52 + _auth: Auth<Active>, 53 53 Json(input): Json<DereferenceScopeInput>, 54 - ) -> Response { 55 - let _ = auth; 56 - 54 + ) -> Result<Response, ApiError> { 57 55 let scope_parts: Vec<&str> = input.scope.split_whitespace().collect(); 58 56 let mut resolved_scopes: Vec<String> = Vec::new(); 59 57 ··· 118 116 } 119 117 } 120 118 121 - Json(DereferenceScopeOutput { 119 + Ok(Json(DereferenceScopeOutput { 122 120 scope: resolved_scopes.join(" "), 123 121 }) 124 - .into_response() 122 + .into_response()) 125 123 }
+1 -1
crates/tranquil-pds/src/auth/auth_extractor.rs
··· 161 161 .await 162 162 .unwrap(); 163 163 164 - assert_eq!(res.status(), StatusCode::OK, "OAuth token should work with BearerAuth extractor"); 164 + assert_eq!(res.status(), StatusCode::OK, "OAuth token should work with RequiredAuth extractor"); 165 165 let body: Value = res.json().await.unwrap(); 166 166 assert_eq!(body["did"].as_str().unwrap(), did); 167 167 }
+371 -405
crates/tranquil-pds/src/auth/extractor.rs
··· 1 + use std::marker::PhantomData; 2 + 1 3 use axum::{ 2 - extract::FromRequestParts, 4 + extract::{FromRequestParts, OptionalFromRequestParts}, 3 5 http::{StatusCode, header::AUTHORIZATION, request::Parts}, 4 6 response::{IntoResponse, Response}, 5 7 }; 6 8 use tracing::{debug, error, info}; 7 9 8 10 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, 11 + AccountStatus, AuthSource, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, 12 + is_service_token, validate_bearer_token_for_service_auth, 12 13 }; 13 14 use crate::api::error::ApiError; 15 + use crate::oauth::scopes::{RepoAction, ScopePermissions}; 14 16 use crate::state::AppState; 15 17 use crate::types::Did; 16 18 use crate::util::build_full_url; 17 - 18 - pub struct BearerAuth(pub AuthenticatedUser); 19 19 20 20 #[derive(Debug)] 21 21 pub enum AuthError { ··· 26 26 AccountDeactivated, 27 27 AccountTakedown, 28 28 AdminRequired, 29 + ServiceAuthNotAllowed, 30 + SigningKeyRequired, 31 + InsufficientScope(String), 29 32 OAuthExpiredToken(String), 30 33 UseDpopNonce(String), 31 34 InvalidDpopProof(String), ··· 56 59 })), 57 60 ) 58 61 .into_response(), 62 + Self::InsufficientScope(msg) => ApiError::InsufficientScope(Some(msg)).into_response(), 59 63 other => ApiError::from(other).into_response(), 60 64 } 61 65 } 62 66 } 63 67 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) 68 + pub struct ExtractedToken { 69 + pub token: String, 70 + pub is_dpop: bool, 83 71 } 84 72 85 73 pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option<String> { ··· 100 88 } 101 89 102 90 Some(token.to_string()) 103 - } 104 - 105 - pub struct ExtractedToken { 106 - pub token: String, 107 - pub is_dpop: bool, 108 91 } 109 92 110 93 pub fn extract_auth_token_from_header(auth_header: Option<&str>) -> Option<ExtractedToken> { ··· 136 119 None 137 120 } 138 121 139 - #[derive(Default)] 140 - struct StatusCheckFlags { 141 - allow_deactivated: bool, 142 - allow_takendown: bool, 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 + } 143 208 } 144 209 145 210 async fn verify_oauth_token_and_build_user( ··· 148 213 dpop_proof: Option<&str>, 149 214 method: &str, 150 215 uri: &str, 151 - flags: StatusCheckFlags, 152 216 ) -> Result<AuthenticatedUser, AuthError> { 153 217 match crate::oauth::verify::verify_oauth_access_token( 154 218 state.oauth_repo.as_ref(), ··· 171 235 user_info.takedown_ref.as_deref(), 172 236 user_info.deactivated_at, 173 237 ); 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 238 Ok(AuthenticatedUser { 181 239 did: result.did, 182 240 key_bytes: user_info.key_bytes.and_then(|kb| { 183 241 crate::config::decrypt_key(&kb, user_info.encryption_version).ok() 184 242 }), 185 - is_oauth: true, 186 243 is_admin: user_info.is_admin, 187 244 status, 188 245 scope: result.scope, 189 246 controller_did: None, 247 + auth_source: AuthSource::OAuth, 190 248 }) 191 249 } 192 250 Err(crate::oauth::OAuthError::ExpiredToken(msg)) => Err(AuthError::OAuthExpiredToken(msg)), ··· 198 256 } 199 257 } 200 258 201 - impl FromRequestParts<AppState> for BearerAuth { 202 - type Rejection = AuthError; 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 + })?; 203 268 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)?; 269 + debug!("Service token verified for DID: {}", claims.iss); 270 + Ok(claims) 271 + } 214 272 215 - let extracted = 216 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 273 + enum ExtractedAuth { 274 + User(AuthenticatedUser), 275 + Service(ServiceTokenClaims), 276 + } 217 277 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()); 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)?; 221 288 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(_) => {} 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); 244 309 } 310 + Err(_) => {} 311 + } 245 312 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) 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), 256 325 } 257 326 } 258 327 259 - pub struct BearerAuthAllowDeactivated(pub AuthenticatedUser); 260 - 261 - impl FromRequestParts<AppState> for BearerAuthAllowDeactivated { 262 - type Rejection = AuthError; 328 + pub struct Auth<P: AuthPolicy = Active>(pub AuthenticatedUser, PhantomData<P>); 263 329 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)?; 330 + impl<P: AuthPolicy> Auth<P> { 331 + pub fn into_inner(self) -> AuthenticatedUser { 332 + self.0 333 + } 274 334 275 - let extracted = 276 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 335 + pub fn needs_scope_check(&self) -> bool { 336 + self.0.is_oauth() 337 + } 277 338 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()); 339 + pub fn permissions(&self) -> ScopePermissions { 340 + self.0.permissions() 341 + } 281 342 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(_) => {} 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(()); 300 347 } 348 + self.permissions() 349 + .assert_repo(action, collection) 350 + .map_err(|e| ApiError::InsufficientScope(Some(e.to_string())).into_response()) 351 + } 352 + } 301 353 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) 354 + impl<P: AuthPolicy> std::ops::Deref for Auth<P> { 355 + type Target = AuthenticatedUser; 356 + 357 + fn deref(&self) -> &Self::Target { 358 + &self.0 315 359 } 316 360 } 317 361 318 - pub struct BearerAuthAllowTakendown(pub AuthenticatedUser); 319 - 320 - impl FromRequestParts<AppState> for BearerAuthAllowTakendown { 362 + impl<P: AuthPolicy> FromRequestParts<AppState> for Auth<P> { 321 363 type Rejection = AuthError; 322 364 323 365 async fn from_request_parts( 324 366 parts: &mut Parts, 325 367 state: &AppState, 326 368 ) -> 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) 369 + let user = extract_user_auth_internal(parts, state).await?; 370 + P::validate(&user)?; 371 + Ok(Auth(user, PhantomData)) 374 372 } 375 373 } 376 374 377 - pub struct BearerAuthAdmin(pub AuthenticatedUser); 378 - 379 - impl FromRequestParts<AppState> for BearerAuthAdmin { 375 + impl<P: AuthPolicy> OptionalFromRequestParts<AppState> for Auth<P> { 380 376 type Rejection = AuthError; 381 377 382 378 async fn from_request_parts( 383 379 parts: &mut Parts, 384 380 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)); 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))) 412 386 } 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(_) => {} 387 + Err(AuthError::MissingToken) => Ok(None), 388 + Err(e) => Err(e), 424 389 } 390 + } 391 + } 425 392 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?; 393 + pub struct ServiceAuth { 394 + pub did: Did, 395 + pub claims: ServiceTokenClaims, 396 + } 435 397 436 - if !user.is_admin { 437 - return Err(AuthError::AdminRequired); 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 + )), 438 409 } 439 - Ok(BearerAuthAdmin(user)) 440 410 } 441 411 } 442 412 443 - pub struct OptionalBearerAuth(pub Option<AuthenticatedUser>); 444 - 445 - impl FromRequestParts<AppState> for OptionalBearerAuth { 413 + impl FromRequestParts<AppState> for ServiceAuth { 446 414 type Rejection = AuthError; 447 415 448 416 async fn from_request_parts( 449 417 parts: &mut Parts, 450 418 state: &AppState, 451 419 ) -> 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 - }; 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 + } 459 432 460 - let extracted = match extract_auth_token_from_header(Some(auth_header)) { 461 - Some(e) => e, 462 - None => return Ok(OptionalBearerAuth(None)), 463 - }; 433 + pub enum AuthAny<P: AuthPolicy = Active> { 434 + User(Auth<P>), 435 + Service(ServiceAuth), 436 + } 464 437 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()); 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 + } 468 452 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 - }; 453 + pub fn as_service(&self) -> Option<&ServiceAuth> { 454 + match self { 455 + Self::User(_) => None, 456 + Self::Service(auth) => Some(auth), 477 457 } 458 + } 478 459 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 - )) 460 + pub fn is_service(&self) -> bool { 461 + matches!(self, Self::Service(_)) 491 462 } 492 - } 493 463 494 - pub struct ServiceAuth { 495 - pub claims: ServiceTokenClaims, 496 - pub did: Did, 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 + } 497 470 } 498 471 499 - impl FromRequestParts<AppState> for ServiceAuth { 472 + impl<P: AuthPolicy> FromRequestParts<AppState> for AuthAny<P> { 500 473 type Rejection = AuthError; 501 474 502 475 async fn from_request_parts( 503 476 parts: &mut Parts, 504 - _state: &AppState, 477 + state: &AppState, 505 478 ) -> 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); 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 + } 518 491 } 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 492 } 538 493 } 539 494 540 - pub struct OptionalServiceAuth(pub Option<ServiceTokenClaims>); 541 - 542 - impl FromRequestParts<AppState> for OptionalServiceAuth { 543 - type Rejection = std::convert::Infallible; 495 + impl<P: AuthPolicy> OptionalFromRequestParts<AppState> for AuthAny<P> { 496 + type Rejection = AuthError; 544 497 545 498 async fn from_request_parts( 546 499 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))) 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)))) 571 506 } 572 - Err(e) => { 573 - debug!("Service token verification failed (optional): {:?}", e); 574 - Ok(OptionalServiceAuth(None)) 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 }))) 575 513 } 514 + Err(AuthError::MissingToken) => Ok(None), 515 + Err(e) => Err(e), 576 516 } 577 517 } 578 518 } 579 519 580 - pub enum BlobAuthResult { 581 - Service { did: Did }, 582 - User(AuthenticatedUser), 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>, 583 529 } 584 530 585 - pub struct BlobAuth(pub BlobAuthResult); 531 + impl<P: AuthPolicy> SigningAuth<P> { 532 + pub fn needs_scope_check(&self) -> bool { 533 + self.is_oauth 534 + } 586 535 587 - impl FromRequestParts<AppState> for BlobAuth { 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> { 588 560 type Rejection = AuthError; 589 561 590 562 async fn from_request_parts( 591 563 parts: &mut Parts, 592 564 state: &AppState, 593 565 ) -> 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)?; 566 + let user = extract_user_auth_internal(parts, state).await?; 567 + P::validate(&user)?; 600 568 601 - let extracted = 602 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 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 + }; 603 586 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 - })?; 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 + } 614 599 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 - } 600 + #[cfg(test)] 601 + fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> { 602 + let auth_header = auth_header.trim(); 623 603 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"); 604 + if auth_header.len() < 8 { 605 + return Err(AuthError::InvalidFormat); 606 + } 626 607 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 - } 608 + let prefix = &auth_header[..7]; 609 + if !prefix.eq_ignore_ascii_case("bearer ") { 610 + return Err(AuthError::InvalidFormat); 611 + } 638 612 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))) 613 + let token = auth_header[7..].trim(); 614 + if token.is_empty() { 615 + return Err(AuthError::InvalidFormat); 652 616 } 617 + 618 + Ok(token) 653 619 } 654 620 655 621 #[cfg(test)]
+75 -8
crates/tranquil-pds/src/auth/mod.rs
··· 3 3 use std::time::Duration; 4 4 5 5 use crate::AccountStatus; 6 + use crate::api::ApiError; 6 7 use crate::cache::Cache; 7 8 use crate::oauth::scopes::ScopePermissions; 8 9 use crate::types::Did; ··· 16 17 pub mod webauthn; 17 18 18 19 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, 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, 22 23 }; 23 24 pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; 24 25 ··· 94 95 } 95 96 } 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 + 97 121 pub struct AuthenticatedUser { 98 122 pub did: Did, 99 123 pub key_bytes: Option<Vec<u8>>, 100 - pub is_oauth: bool, 101 124 pub is_admin: bool, 102 125 pub status: AccountStatus, 103 126 pub scope: Option<String>, 104 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 + } 105 172 } 106 173 107 174 impl AuthenticatedUser { ··· 111 178 { 112 179 return ScopePermissions::from_scope_string(Some(scope)); 113 180 } 114 - if !self.is_oauth { 181 + if !self.is_oauth() { 115 182 return ScopePermissions::from_scope_string(Some("atproto")); 116 183 } 117 184 ScopePermissions::from_scope_string(self.scope.as_deref()) ··· 349 416 return Ok(AuthenticatedUser { 350 417 did: did.clone(), 351 418 key_bytes: Some(decrypted_key), 352 - is_oauth: false, 353 419 is_admin, 354 420 status, 355 421 scope: token_data.claims.scope.clone(), 356 422 controller_did, 423 + auth_source: AuthSource::Session, 357 424 }); 358 425 } 359 426 } ··· 397 464 return Ok(AuthenticatedUser { 398 465 did: Did::new_unchecked(oauth_token.did), 399 466 key_bytes, 400 - is_oauth: true, 401 467 is_admin: oauth_token.is_admin, 402 468 status, 403 469 scope: oauth_info.scope, 404 470 controller_did: oauth_info.controller_did.map(Did::new_unchecked), 471 + auth_source: AuthSource::OAuth, 405 472 }); 406 473 } else { 407 474 return Err(TokenValidationError::TokenExpired); ··· 481 548 Ok(AuthenticatedUser { 482 549 did: Did::new_unchecked(result.did), 483 550 key_bytes, 484 - is_oauth: true, 485 551 is_admin: user_info.is_admin, 486 552 status, 487 553 scope: result.scope, 488 554 controller_did: None, 555 + auth_source: AuthSource::OAuth, 489 556 }) 490 557 } 491 558 Err(crate::oauth::OAuthError::ExpiredToken(_)) => {
+13 -6
crates/tranquil-pds/src/oauth/endpoints/authorize.rs
··· 1 1 use crate::comms::{channel_display_name, comms_repo::enqueue_2fa_code}; 2 2 use crate::oauth::{ 3 3 AuthFlowState, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, SessionId, 4 - db::should_show_consent, 5 - scopes::expand_include_scopes, 4 + db::should_show_consent, scopes::expand_include_scopes, 6 5 }; 7 6 use crate::state::{AppState, RateLimitKind}; 8 7 use crate::types::{Did, Handle, PlainPassword}; ··· 3645 3644 pub async fn establish_session( 3646 3645 State(state): State<AppState>, 3647 3646 headers: HeaderMap, 3648 - auth: crate::auth::BearerAuth, 3647 + auth: crate::auth::Auth<crate::auth::Active>, 3649 3648 ) -> Response { 3650 - let did = &auth.0.did; 3649 + let did = &auth.did; 3651 3650 3652 3651 let existing_device = extract_device_cookie(&headers); 3653 3652 ··· 3670 3669 }; 3671 3670 let device_typed = DeviceIdType::from(new_id.0.clone()); 3672 3671 3673 - if let Err(e) = state.oauth_repo.create_device(&device_typed, &device_data).await { 3672 + if let Err(e) = state 3673 + .oauth_repo 3674 + .create_device(&device_typed, &device_data) 3675 + .await 3676 + { 3674 3677 tracing::error!(error = ?e, "Failed to create device"); 3675 3678 return ( 3676 3679 StatusCode::INTERNAL_SERVER_ERROR, ··· 3682 3685 .into_response(); 3683 3686 } 3684 3687 3685 - if let Err(e) = state.oauth_repo.upsert_account_device(did, &device_typed).await { 3688 + if let Err(e) = state 3689 + .oauth_repo 3690 + .upsert_account_device(did, &device_typed) 3691 + .await 3692 + { 3686 3693 tracing::error!(error = ?e, "Failed to link device to account"); 3687 3694 return ( 3688 3695 StatusCode::INTERNAL_SERVER_ERROR,
+7 -7
crates/tranquil-pds/src/oauth/endpoints/delegation.rs
··· 1 - use crate::auth::BearerAuth; 1 + use crate::auth::{Active, Auth}; 2 2 use crate::delegation::DelegationActionType; 3 3 use crate::state::{AppState, RateLimitKind}; 4 4 use crate::types::PlainPassword; ··· 463 463 pub async fn delegation_auth_token( 464 464 State(state): State<AppState>, 465 465 headers: HeaderMap, 466 - auth: BearerAuth, 466 + auth: Auth<Active>, 467 467 Json(form): Json<DelegationTokenAuthSubmit>, 468 468 ) -> Response { 469 - let controller_did = auth.0.did; 469 + let controller_did = &auth.did; 470 470 471 471 let delegated_did: Did = match form.delegated_did.parse() { 472 472 Ok(d) => d, ··· 510 510 511 511 let grant = match state 512 512 .delegation_repo 513 - .get_delegation(&delegated_did, &controller_did) 513 + .get_delegation(&delegated_did, controller_did) 514 514 .await 515 515 { 516 516 Ok(Some(g)) => g, ··· 551 551 552 552 if state 553 553 .oauth_repo 554 - .set_controller_did(&request_id, &controller_did) 554 + .set_controller_did(&request_id, controller_did) 555 555 .await 556 556 .is_err() 557 557 { ··· 574 574 .delegation_repo 575 575 .log_delegation_action( 576 576 &delegated_did, 577 - &controller_did, 578 - Some(&controller_did), 577 + controller_did, 578 + Some(controller_did), 579 579 DelegationActionType::TokenIssued, 580 580 Some(serde_json::json!({ 581 581 "client_id": request.client_id,
+1 -1
crates/tranquil-pds/src/oauth/verify.rs
··· 396 396 token: &str, 397 397 ) -> Result<LegacyAuthResult, ()> { 398 398 match crate::auth::validate_bearer_token(user_repo, token).await { 399 - Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did }), 399 + Ok(user) if !user.is_oauth() => Ok(LegacyAuthResult { did: user.did }), 400 400 _ => Err(()), 401 401 } 402 402 }
+2 -2
crates/tranquil-pds/src/sso/endpoints.rs
··· 644 644 645 645 pub async fn get_linked_accounts( 646 646 State(state): State<AppState>, 647 - crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth, 647 + auth: crate::auth::Auth<crate::auth::Active>, 648 648 ) -> Result<Json<LinkedAccountsResponse>, ApiError> { 649 649 let identities = state 650 650 .sso_repo ··· 679 679 680 680 pub async fn unlink_account( 681 681 State(state): State<AppState>, 682 - crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth, 682 + auth: crate::auth::Auth<crate::auth::Active>, 683 683 Json(input): Json<UnlinkAccountRequest>, 684 684 ) -> Result<Json<UnlinkAccountResponse>, ApiError> { 685 685 if !state
+55 -35
crates/tranquil-scopes/src/permission_set.rs
··· 149 149 return Err(format!("Invalid NSID format: {}", nsid)); 150 150 } 151 151 152 - let authority = parts[..2].iter().rev().cloned().collect::<Vec<_>>().join("."); 152 + let authority = parts[..2] 153 + .iter() 154 + .rev() 155 + .cloned() 156 + .collect::<Vec<_>>() 157 + .join("."); 153 158 debug!(nsid, authority = %authority, "Resolving lexicon DID authority via DNS"); 154 159 155 160 let did = resolve_lexicon_did_authority(&authority).await?; ··· 279 284 ) -> String { 280 285 let mut scopes: Vec<String> = Vec::new(); 281 286 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()); 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()); 290 297 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)); 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 + }); 297 305 }); 298 - }); 306 + } 299 307 } 300 - } 301 - "rpc" => { 302 - if let Some(lxms) = &perm.lxm { 303 - let perm_aud = perm.aud.as_deref().or(default_aud); 308 + "rpc" => { 309 + if let Some(lxms) = &perm.lxm { 310 + let perm_aud = perm.aud.as_deref().or(default_aud); 304 311 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 + 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 + } 312 320 } 313 - } 314 - _ => {} 315 - }); 321 + _ => {} 322 + }); 316 323 317 324 scopes.join(" ") 318 325 } ··· 334 341 335 342 #[test] 336 343 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"); 344 + let (nsid, aud) = 345 + parse_include_scope("io.atcr.authFullApp?foo=bar&aud=did:web:example.com&baz=qux"); 338 346 assert_eq!(nsid, "io.atcr.authFullApp"); 339 347 assert_eq!(aud, Some("did:web:example.com")); 340 348 } ··· 443 451 aud: None, 444 452 }]; 445 453 446 - let expanded = build_expanded_scopes(&permissions, Some("did:web:api.example.com"), "io.atcr"); 454 + let expanded = 455 + build_expanded_scopes(&permissions, Some("did:web:api.example.com"), "io.atcr"); 447 456 assert!(expanded.contains("rpc:io.atcr.getManifest?aud=did:web:api.example.com")); 448 457 } 449 458 ··· 583 592 cache_key.to_string(), 584 593 CachedLexicon { 585 594 expanded_scope: "old_value".to_string(), 586 - cached_at: std::time::Instant::now() - std::time::Duration::from_secs(CACHE_TTL_SECS + 1), 595 + cached_at: std::time::Instant::now() 596 + - std::time::Duration::from_secs(CACHE_TTL_SECS + 1), 587 597 }, 588 598 ); 589 599 } ··· 601 611 fn test_nsid_authority_extraction_for_dns() { 602 612 let nsid = "io.atcr.authFullApp"; 603 613 let parts: Vec<&str> = nsid.split('.').collect(); 604 - let authority = parts[..2].iter().rev().cloned().collect::<Vec<_>>().join("."); 614 + let authority = parts[..2] 615 + .iter() 616 + .rev() 617 + .cloned() 618 + .collect::<Vec<_>>() 619 + .join("."); 605 620 assert_eq!(authority, "atcr.io"); 606 621 607 622 let nsid2 = "app.bsky.feed.post"; 608 623 let parts2: Vec<&str> = nsid2.split('.').collect(); 609 - let authority2 = parts2[..2].iter().rev().cloned().collect::<Vec<_>>().join("."); 624 + let authority2 = parts2[..2] 625 + .iter() 626 + .rev() 627 + .cloned() 628 + .collect::<Vec<_>>() 629 + .join("."); 610 630 assert_eq!(authority2, "bsky.app"); 611 631 } 612 632 }
-1
crates/tranquil-scopes/src/permissions.rs
··· 556 556 "app.bsky.feed.getAuthorFeed" 557 557 )); 558 558 } 559 - 560 559 }