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

fix: oauth consolidation, include-scope improvements #4

merged opened by lewis.moe targeting main from fix/oauth-on-niche-apps
  • auth extraction should be happening in the auth crate, yes, who coulda thought
  • include: scope should actually be doing the right thing and going out and requesting stuff to expand out the perms
  • more tests!!!1!
  • more correct parsing of the #bsky-appview or whatever suffixes on did webs that come through auth
Labels

None yet.

assignee
Participants 2
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3md3bluniqt22
+2680 -2784
Interdiff #1 โ†’ #2
.sqlx/query-06eb7c6e1983b6121526ba63612236391290c2e63d37d2bb1cd89ea822950a82.json

This file has not been changed.

.sqlx/query-5031b96c65078d6c54954ce6e57ff9cbba4c48dd8a7546882ab5647114ffab4a.json

This file has not been changed.

.sqlx/query-6258398accee69e0c5f455a3c0ecc273b3da6ef5bb4d8660adafe63d8e3cd2d4.json

This file has not been changed.

.sqlx/query-a4dc8fb22bd094d414c55b9da20b610f7b122b485ab0fd0d0646d68ae8e64fe6.json

This file has not been changed.

.sqlx/query-dec3a21a8e60cc8d2c5dad727750bc88f5535dedae244f7b6e4afa95769b8f1a.json

This file has not been changed.

Cargo.lock

This file has not been changed.

crates/tranquil-pds/src/api/error.rs

This file has not been changed.

crates/tranquil-pds/src/api/identity/account.rs

This file has not been changed.

crates/tranquil-pds/src/api/proxy.rs

This file has not been changed.

+69 -48
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::{AuthenticatedEntity, RequiredAuth}; 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: RequiredAuth, 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.0 { 51 + AuthenticatedEntity::Service { did, claims } => { 52 + match &claims.lxm { 53 + Some(lxm) if lxm == "*" || lxm == "com.atproto.repo.uploadBlob" => {} 54 + Some(lxm) => { 55 + return Err(ApiError::AuthorizationError(format!( 56 + "Token lxm '{}' does not permit 'com.atproto.repo.uploadBlob'", 57 + lxm 58 + ))); 59 + } 60 + None => { 61 + return Err(ApiError::AuthorizationError( 62 + "Token missing lxm claim".to_string(), 63 + )); 64 + } 65 + } 66 + (did.clone(), None) 67 + } 68 + AuthenticatedEntity::User(auth_user) => { 69 + if auth_user.status.is_takendown() { 70 + return Err(ApiError::AccountTakedown); 71 + } 53 72 let mime_type_for_check = headers 54 73 .get("content-type") 55 74 .and_then(|h| h.to_str().ok()) ··· 59 78 auth_user.scope.as_deref(), 60 79 mime_type_for_check, 61 80 ) { 62 - return e; 81 + return Ok(e); 63 82 } 64 83 let ctrl_did = auth_user.controller_did.clone(); 65 - (auth_user.did, ctrl_did) 84 + (auth_user.did.clone(), ctrl_did) 66 85 } 67 86 }; 68 87 ··· 72 91 .await 73 92 .unwrap_or(false) 74 93 { 75 - return ApiError::Forbidden.into_response(); 94 + return Err(ApiError::Forbidden); 76 95 } 77 96 78 97 let client_mime_hint = headers ··· 80 99 .and_then(|h| h.to_str().ok()) 81 100 .unwrap_or("application/octet-stream"); 82 101 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 - }; 102 + let user_id = state 103 + .user_repo 104 + .get_id_by_did(&did) 105 + .await 106 + .ok() 107 + .flatten() 108 + .ok_or(ApiError::InternalError(None))?; 89 109 90 110 let temp_key = format!("temp/{}", uuid::Uuid::new_v4()); 91 111 let max_size = get_max_blob_size() as u64; ··· 98 118 99 119 info!("Starting streaming blob upload to temp key: {}", temp_key); 100 120 101 - let upload_result = match state.blob_store.put_stream(&temp_key, pinned_stream).await { 102 - Ok(result) => result, 103 - Err(e) => { 121 + let upload_result = state 122 + .blob_store 123 + .put_stream(&temp_key, pinned_stream) 124 + .await 125 + .map_err(|e| { 104 126 error!("Failed to stream blob to storage: {:?}", e); 105 - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 106 - } 107 - }; 127 + ApiError::InternalError(Some("Failed to store blob".into())) 128 + })?; 108 129 109 130 let size = upload_result.size; 110 131 if size > max_size { 111 132 let _ = state.blob_store.delete(&temp_key).await; 112 - return ApiError::InvalidRequest(format!( 133 + return Err(ApiError::InvalidRequest(format!( 113 134 "Blob size {} exceeds maximum of {} bytes", 114 135 size, max_size 115 - )) 116 - .into_response(); 136 + ))); 117 137 } 118 138 119 139 let mime_type = match state.blob_store.get_head(&temp_key, 8192).await { ··· 129 149 Err(e) => { 130 150 let _ = state.blob_store.delete(&temp_key).await; 131 151 error!("Failed to create multihash for blob: {:?}", e); 132 - return ApiError::InternalError(Some("Failed to hash blob".into())).into_response(); 152 + return Err(ApiError::InternalError(Some("Failed to hash blob".into()))); 133 153 } 134 154 }; 135 155 let cid = Cid::new_v1(0x55, multihash); ··· 152 172 Err(e) => { 153 173 let _ = state.blob_store.delete(&temp_key).await; 154 174 error!("Failed to insert blob record: {:?}", e); 155 - return ApiError::InternalError(None).into_response(); 175 + return Err(ApiError::InternalError(None)); 156 176 } 157 177 }; 158 178 159 179 if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await { 160 180 let _ = state.blob_store.delete(&temp_key).await; 161 181 error!("Failed to copy blob to final location: {:?}", e); 162 - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 182 + return Err(ApiError::InternalError(Some("Failed to store blob".into()))); 163 183 } 164 184 165 185 let _ = state.blob_store.delete(&temp_key).await; ··· 183 203 .await; 184 204 } 185 205 186 - Json(json!({ 206 + Ok(Json(json!({ 187 207 "blob": { 188 208 "$type": "blob", 189 209 "ref": { ··· 193 213 "size": size 194 214 } 195 215 })) 196 - .into_response() 216 + .into_response()) 197 217 } 198 218 199 219 #[derive(Deserialize)] ··· 218 238 219 239 pub async fn list_missing_blobs( 220 240 State(state): State<AppState>, 221 - auth: BearerAuthAllowDeactivated, 241 + auth: RequiredAuth, 222 242 Query(params): Query<ListMissingBlobsParams>, 223 - ) -> Response { 224 - let auth_user = auth.0; 243 + ) -> Result<Response, ApiError> { 244 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 245 + 225 246 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) => { 247 + let user = state 248 + .user_repo 249 + .get_by_did(did) 250 + .await 251 + .map_err(|e| { 230 252 error!("DB error fetching user: {:?}", e); 231 - return ApiError::InternalError(None).into_response(); 232 - } 233 - }; 253 + ApiError::InternalError(None) 254 + })? 255 + .ok_or(ApiError::InternalError(None))?; 256 + 234 257 let limit = params.limit.unwrap_or(500).clamp(1, 1000); 235 258 let cursor = params.cursor.as_deref(); 236 - let missing = match state 259 + let missing = state 237 260 .blob_repo 238 261 .list_missing_blobs(user.id, cursor, limit + 1) 239 262 .await 240 - { 241 - Ok(m) => m, 242 - Err(e) => { 263 + .map_err(|e| { 243 264 error!("DB error fetching missing blobs: {:?}", e); 244 - return ApiError::InternalError(None).into_response(); 245 - } 246 - }; 265 + ApiError::InternalError(None) 266 + })?; 267 + 247 268 let has_more = missing.len() > limit as usize; 248 269 let blobs: Vec<RecordBlob> = missing 249 270 .into_iter() ··· 258 279 } else { 259 280 None 260 281 }; 261 - ( 282 + Ok(( 262 283 StatusCode::OK, 263 284 Json(ListMissingBlobsOutput { 264 285 cursor: next_cursor, 265 286 blobs, 266 287 }), 267 288 ) 268 - .into_response() 289 + .into_response()) 269 290 }
+41 -23
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::RequiredAuth; 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: RequiredAuth, 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 user = auth.0.require_user()?.require_active()?; 47 + let auth = match prepare_repo_write(&state, user, &input.repo).await { 47 48 Ok(res) => res, 48 - Err(err_res) => return err_res, 49 + Err(err_res) => return Ok(err_res), 49 50 }; 50 51 51 52 if let Err(e) = crate::auth::scope_check::check_repo_scope( ··· 54 55 crate::oauth::RepoAction::Delete, 55 56 &input.collection, 56 57 ) { 57 - return e; 58 + return Ok(e); 58 59 } 59 60 60 61 let did = auth.did; ··· 65 66 if let Some(swap_commit) = &input.swap_commit 66 67 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 67 68 { 68 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 69 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 69 70 } 70 71 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 71 72 let commit_bytes = match tracking_store.get(&current_root_cid).await { 72 73 Ok(Some(b)) => b, 73 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 74 + _ => { 75 + return Ok( 76 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 77 + ); 78 + } 74 79 }; 75 80 let commit = match Commit::from_cbor(&commit_bytes) { 76 81 Ok(c) => c, 77 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 82 + _ => { 83 + return Ok( 84 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 85 + ); 86 + } 78 87 }; 79 88 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 80 89 let key = format!("{}/{}", input.collection, input.rkey); ··· 82 91 let expected_cid = Cid::from_str(swap_record_str).ok(); 83 92 let actual_cid = mst.get(&key).await.ok().flatten(); 84 93 if expected_cid != actual_cid { 85 - return ApiError::InvalidSwap(Some( 94 + return Ok(ApiError::InvalidSwap(Some( 86 95 "Record has been modified or does not exist".into(), 87 96 )) 88 - .into_response(); 97 + .into_response()); 89 98 } 90 99 } 91 100 let prev_record_cid = mst.get(&key).await.ok().flatten(); 92 101 if prev_record_cid.is_none() { 93 - return (StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response(); 102 + return Ok((StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response()); 94 103 } 95 104 let new_mst = match mst.delete(&key).await { 96 105 Ok(m) => m, 97 106 Err(e) => { 98 107 error!("Failed to delete from MST: {:?}", e); 99 - return ApiError::InternalError(Some(format!("Failed to delete from MST: {:?}", e))) 100 - .into_response(); 108 + return Ok(ApiError::InternalError(Some(format!( 109 + "Failed to delete from MST: {:?}", 110 + e 111 + ))) 112 + .into_response()); 101 113 } 102 114 }; 103 115 let new_mst_root = match new_mst.persist().await { 104 116 Ok(c) => c, 105 117 Err(e) => { 106 118 error!("Failed to persist MST: {:?}", e); 107 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 119 + return Ok( 120 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 121 + ); 108 122 } 109 123 }; 110 124 let collection_for_audit = input.collection.to_string(); ··· 121 135 .await 122 136 .is_err() 123 137 { 124 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 125 - .into_response(); 138 + return Ok( 139 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 140 + .into_response(), 141 + ); 126 142 } 127 143 if mst 128 144 .blocks_for_path(&key, &mut old_mst_blocks) 129 145 .await 130 146 .is_err() 131 147 { 132 - return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 133 - .into_response(); 148 + return Ok( 149 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 150 + .into_response(), 151 + ); 134 152 } 135 153 let mut relevant_blocks = new_mst_blocks.clone(); 136 154 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 169 187 { 170 188 Ok(res) => res, 171 189 Err(e) if e.contains("ConcurrentModification") => { 172 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 190 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 173 191 } 174 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 192 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 175 193 }; 176 194 177 195 if let Some(ref controller) = controller_did { ··· 202 220 error!("Failed to remove backlinks for {}: {}", deleted_uri, e); 203 221 } 204 222 205 - ( 223 + Ok(( 206 224 StatusCode::OK, 207 225 Json(DeleteRecordOutput { 208 226 commit: Some(CommitInfo { ··· 211 229 }), 212 230 }), 213 231 ) 214 - .into_response() 232 + .into_response()) 215 233 } 216 234 217 235 use crate::types::Did;
+100 -65
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::RequiredAuth; 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() { ··· 91 91 user_id, 92 92 current_root_cid, 93 93 is_oauth: auth_user.is_oauth, 94 - scope: auth_user.scope, 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: RequiredAuth, 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 user = auth.0.require_user()?.require_active()?; 131 + let auth = match prepare_repo_write(&state, user, &input.repo).await { 131 132 Ok(res) => res, 132 - Err(err_res) => return err_res, 133 + Err(err_res) => return Ok(err_res), 133 134 }; 134 135 135 136 if let Err(e) = crate::auth::scope_check::check_repo_scope( ··· 138 139 crate::oauth::RepoAction::Create, 139 140 &input.collection, 140 141 ) { 141 - return e; 142 + return Ok(e); 142 143 } 143 144 144 145 let did = auth.did; ··· 149 150 if let Some(swap_commit) = &input.swap_commit 150 151 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 151 152 { 152 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 153 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 153 154 } 154 155 155 156 let validation_status = if input.validate == Some(false) { ··· 163 164 require_lexicon, 164 165 ) { 165 166 Ok(status) => Some(status), 166 - Err(err_response) => return *err_response, 167 + Err(err_response) => return Ok(*err_response), 167 168 } 168 169 }; 169 170 let rkey = input.rkey.unwrap_or_else(Rkey::generate); ··· 171 172 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 172 173 let commit_bytes = match tracking_store.get(&current_root_cid).await { 173 174 Ok(Some(b)) => b, 174 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 175 + _ => { 176 + return Ok( 177 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 178 + ); 179 + } 175 180 }; 176 181 let commit = match Commit::from_cbor(&commit_bytes) { 177 182 Ok(c) => c, 178 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 183 + _ => { 184 + return Ok( 185 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 186 + ); 187 + } 179 188 }; 180 189 let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 181 190 let initial_mst_root = commit.data; ··· 197 206 Ok(c) => c, 198 207 Err(e) => { 199 208 error!("Failed to check backlink conflicts: {}", e); 200 - return ApiError::InternalError(None).into_response(); 209 + return Ok(ApiError::InternalError(None).into_response()); 201 210 } 202 211 }; 203 212 ··· 250 259 let record_ipld = crate::util::json_to_ipld(&input.record); 251 260 let mut record_bytes = Vec::new(); 252 261 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 253 - return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); 262 + return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); 254 263 } 255 264 let record_cid = match tracking_store.put(&record_bytes).await { 256 265 Ok(c) => c, 257 266 _ => { 258 - return ApiError::InternalError(Some("Failed to save record block".into())) 259 - .into_response(); 267 + return Ok( 268 + ApiError::InternalError(Some("Failed to save record block".into())).into_response(), 269 + ); 260 270 } 261 271 }; 262 272 let key = format!("{}/{}", input.collection, rkey); ··· 271 281 272 282 let new_mst = match mst.add(&key, record_cid).await { 273 283 Ok(m) => m, 274 - _ => return ApiError::InternalError(Some("Failed to add to MST".into())).into_response(), 284 + _ => { 285 + return Ok(ApiError::InternalError(Some("Failed to add to MST".into())).into_response()); 286 + } 275 287 }; 276 288 let new_mst_root = match new_mst.persist().await { 277 289 Ok(c) => c, 278 - _ => return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 290 + _ => { 291 + return Ok( 292 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 293 + ); 294 + } 279 295 }; 280 296 281 297 ops.push(RecordOp::Create { ··· 290 306 .await 291 307 .is_err() 292 308 { 293 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 294 - .into_response(); 309 + return Ok( 310 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 311 + .into_response(), 312 + ); 295 313 } 296 314 297 315 let mut relevant_blocks = new_mst_blocks.clone(); ··· 333 351 { 334 352 Ok(res) => res, 335 353 Err(e) if e.contains("ConcurrentModification") => { 336 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 354 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 337 355 } 338 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 356 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 339 357 }; 340 358 341 359 for conflict_uri in conflict_uris_to_cleanup { ··· 375 393 error!("Failed to add backlinks for {}: {}", created_uri, e); 376 394 } 377 395 378 - ( 396 + Ok(( 379 397 StatusCode::OK, 380 398 Json(CreateRecordOutput { 381 399 uri: created_uri, ··· 387 405 validation_status: validation_status.map(|s| s.to_string()), 388 406 }), 389 407 ) 390 - .into_response() 408 + .into_response()) 391 409 } 392 410 #[derive(Deserialize)] 393 411 #[allow(dead_code)] ··· 414 432 } 415 433 pub async fn put_record( 416 434 State(state): State<AppState>, 417 - auth: BearerAuth, 435 + auth: RequiredAuth, 418 436 Json(input): Json<PutRecordInput>, 419 - ) -> Response { 420 - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { 437 + ) -> Result<Response, crate::api::error::ApiError> { 438 + let user = auth.0.require_user()?.require_active()?; 439 + let auth = match prepare_repo_write(&state, user, &input.repo).await { 421 440 Ok(res) => res, 422 - Err(err_res) => return err_res, 441 + Err(err_res) => return Ok(err_res), 423 442 }; 424 443 425 444 if let Err(e) = crate::auth::scope_check::check_repo_scope( ··· 428 447 crate::oauth::RepoAction::Create, 429 448 &input.collection, 430 449 ) { 431 - return e; 450 + return Ok(e); 432 451 } 433 452 if let Err(e) = crate::auth::scope_check::check_repo_scope( 434 453 auth.is_oauth, ··· 436 455 crate::oauth::RepoAction::Update, 437 456 &input.collection, 438 457 ) { 439 - return e; 458 + return Ok(e); 440 459 } 441 460 442 461 let did = auth.did; ··· 447 466 if let Some(swap_commit) = &input.swap_commit 448 467 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 449 468 { 450 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 469 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 451 470 } 452 471 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 453 472 let commit_bytes = match tracking_store.get(&current_root_cid).await { 454 473 Ok(Some(b)) => b, 455 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 474 + _ => { 475 + return Ok( 476 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 477 + ); 478 + } 456 479 }; 457 480 let commit = match Commit::from_cbor(&commit_bytes) { 458 481 Ok(c) => c, 459 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 482 + _ => { 483 + return Ok( 484 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 485 + ); 486 + } 460 487 }; 461 488 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 462 489 let key = format!("{}/{}", input.collection, input.rkey); ··· 471 498 require_lexicon, 472 499 ) { 473 500 Ok(status) => Some(status), 474 - Err(err_response) => return *err_response, 501 + Err(err_response) => return Ok(*err_response), 475 502 } 476 503 }; 477 504 if let Some(swap_record_str) = &input.swap_record { 478 505 let expected_cid = Cid::from_str(swap_record_str).ok(); 479 506 let actual_cid = mst.get(&key).await.ok().flatten(); 480 507 if expected_cid != actual_cid { 481 - return ApiError::InvalidSwap(Some( 508 + return Ok(ApiError::InvalidSwap(Some( 482 509 "Record has been modified or does not exist".into(), 483 510 )) 484 - .into_response(); 511 + .into_response()); 485 512 } 486 513 } 487 514 let existing_cid = mst.get(&key).await.ok().flatten(); 488 515 let record_ipld = crate::util::json_to_ipld(&input.record); 489 516 let mut record_bytes = Vec::new(); 490 517 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 491 - return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); 518 + return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); 492 519 } 493 520 let record_cid = match tracking_store.put(&record_bytes).await { 494 521 Ok(c) => c, 495 522 _ => { 496 - return ApiError::InternalError(Some("Failed to save record block".into())) 497 - .into_response(); 523 + return Ok( 524 + ApiError::InternalError(Some("Failed to save record block".into())).into_response(), 525 + ); 498 526 } 499 527 }; 500 528 if existing_cid == Some(record_cid) { 501 - return ( 529 + return Ok(( 502 530 StatusCode::OK, 503 531 Json(PutRecordOutput { 504 532 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 507 535 validation_status: validation_status.map(|s| s.to_string()), 508 536 }), 509 537 ) 510 - .into_response(); 538 + .into_response()); 511 539 } 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(); 540 + let new_mst = 541 + if existing_cid.is_some() { 542 + match mst.update(&key, record_cid).await { 543 + Ok(m) => m, 544 + Err(_) => { 545 + return Ok(ApiError::InternalError(Some("Failed to update MST".into())) 546 + .into_response()); 547 + } 518 548 } 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(); 549 + } else { 550 + match mst.add(&key, record_cid).await { 551 + Ok(m) => m, 552 + Err(_) => { 553 + return Ok(ApiError::InternalError(Some("Failed to add to MST".into())) 554 + .into_response()); 555 + } 526 556 } 527 - } 528 - }; 557 + }; 529 558 let new_mst_root = match new_mst.persist().await { 530 559 Ok(c) => c, 531 560 Err(_) => { 532 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 561 + return Ok( 562 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 563 + ); 533 564 } 534 565 }; 535 566 let op = if existing_cid.is_some() { ··· 553 584 .await 554 585 .is_err() 555 586 { 556 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 557 - .into_response(); 587 + return Ok( 588 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 589 + .into_response(), 590 + ); 558 591 } 559 592 if mst 560 593 .blocks_for_path(&key, &mut old_mst_blocks) 561 594 .await 562 595 .is_err() 563 596 { 564 - return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 565 - .into_response(); 597 + return Ok( 598 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 599 + .into_response(), 600 + ); 566 601 } 567 602 let mut relevant_blocks = new_mst_blocks.clone(); 568 603 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 604 639 { 605 640 Ok(res) => res, 606 641 Err(e) if e.contains("ConcurrentModification") => { 607 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 642 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 608 643 } 609 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 644 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 610 645 }; 611 646 612 647 if let Some(ref controller) = controller_did { ··· 628 663 .await; 629 664 } 630 665 631 - ( 666 + Ok(( 632 667 StatusCode::OK, 633 668 Json(PutRecordOutput { 634 669 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 640 675 validation_status: validation_status.map(|s| s.to_string()), 641 676 }), 642 677 ) 643 - .into_response() 678 + .into_response()) 644 679 }
+49 -46
crates/tranquil-pds/src/api/server/account_status.rs
··· 40 40 41 41 pub async fn check_account_status( 42 42 State(state): State<AppState>, 43 - auth: crate::auth::BearerAuthAllowDeactivated, 44 - ) -> Response { 45 - let did = auth.0.did; 46 - let user_id = match state.user_repo.get_id_by_did(&did).await { 47 - Ok(Some(id)) => id, 48 - _ => { 49 - return ApiError::InternalError(None).into_response(); 50 - } 51 - }; 43 + auth: crate::auth::RequiredAuth, 44 + ) -> Result<Response, ApiError> { 45 + let user = auth.0.require_user()?.require_not_takendown()?; 46 + let did = &user.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: crate::auth::RequiredAuth, 310 + ) -> Result<Response, ApiError> { 310 311 info!("[MIGRATION] activateAccount called"); 311 - let auth_user = auth.0; 312 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 312 313 info!( 313 314 "[MIGRATION] activateAccount: Authenticated user did={}", 314 315 auth_user.did ··· 321 322 crate::oauth::scopes::AccountAction::Manage, 322 323 ) { 323 324 info!("[MIGRATION] activateAccount: Scope check failed"); 324 - return e; 325 + return Ok(e); 325 326 } 326 327 327 - let did = auth_user.did; 328 + let did = auth_user.did.clone(); 328 329 329 330 info!( 330 331 "[MIGRATION] activateAccount: Validating DID document for did={}", ··· 344 345 did, 345 346 did_validation_start.elapsed() 346 347 ); 347 - return e.into_response(); 348 + return Err(e); 348 349 } 349 350 info!( 350 351 "[MIGRATION] activateAccount: DID document validation SUCCESS for {} (took {:?})", ··· 450 451 ); 451 452 } 452 453 info!("[MIGRATION] activateAccount: SUCCESS for did={}", did); 453 - EmptyResponse::ok().into_response() 454 + Ok(EmptyResponse::ok().into_response()) 454 455 } 455 456 Err(e) => { 456 457 error!( 457 458 "[MIGRATION] activateAccount: DB error activating account: {:?}", 458 459 e 459 460 ); 460 - ApiError::InternalError(None).into_response() 461 + Err(ApiError::InternalError(None)) 461 462 } 462 463 } 463 464 } ··· 470 471 471 472 pub async fn deactivate_account( 472 473 State(state): State<AppState>, 473 - auth: crate::auth::BearerAuth, 474 + auth: crate::auth::RequiredAuth, 474 475 Json(input): Json<DeactivateAccountInput>, 475 - ) -> Response { 476 - let auth_user = auth.0; 476 + ) -> Result<Response, ApiError> { 477 + let auth_user = auth.0.require_user()?.require_active()?; 477 478 478 479 if let Err(e) = crate::auth::scope_check::check_account_scope( 479 480 auth_user.is_oauth, ··· 481 482 crate::oauth::scopes::AccountAttr::Repo, 482 483 crate::oauth::scopes::AccountAction::Manage, 483 484 ) { 484 - return e; 485 + return Ok(e); 485 486 } 486 487 487 488 let delete_after: Option<chrono::DateTime<chrono::Utc>> = input ··· 490 491 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) 491 492 .map(|dt| dt.with_timezone(&chrono::Utc)); 492 493 493 - let did = auth_user.did; 494 + let did = auth_user.did.clone(); 494 495 495 496 let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten(); 496 497 ··· 511 512 { 512 513 warn!("Failed to sequence account deactivated event: {}", e); 513 514 } 514 - EmptyResponse::ok().into_response() 515 + Ok(EmptyResponse::ok().into_response()) 515 516 } 516 - Ok(false) => EmptyResponse::ok().into_response(), 517 + Ok(false) => Ok(EmptyResponse::ok().into_response()), 517 518 Err(e) => { 518 519 error!("DB error deactivating account: {:?}", e); 519 - ApiError::InternalError(None).into_response() 520 + Err(ApiError::InternalError(None)) 520 521 } 521 522 } 522 523 } 523 524 524 525 pub async fn request_account_delete( 525 526 State(state): State<AppState>, 526 - auth: crate::auth::BearerAuthAllowDeactivated, 527 - ) -> Response { 528 - let did = &auth.0.did; 527 + auth: crate::auth::RequiredAuth, 528 + ) -> Result<Response, ApiError> { 529 + let user = auth.0.require_user()?.require_not_takendown()?; 530 + let did = &user.did; 529 531 530 532 if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await { 531 - return crate::api::server::reauth::legacy_mfa_required_response( 533 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 532 534 &*state.user_repo, 533 535 &*state.session_repo, 534 536 did, 535 537 ) 536 - .await; 538 + .await); 537 539 } 538 540 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 - }; 541 + let user_id = state 542 + .user_repo 543 + .get_id_by_did(did) 544 + .await 545 + .ok() 546 + .flatten() 547 + .ok_or(ApiError::InternalError(None))?; 545 548 let confirmation_token = Uuid::new_v4().to_string(); 546 549 let expires_at = Utc::now() + Duration::minutes(15); 547 - if let Err(e) = state 550 + state 548 551 .infra_repo 549 552 .create_deletion_request(&confirmation_token, did, expires_at) 550 553 .await 551 - { 552 - error!("DB error creating deletion token: {:?}", e); 553 - return ApiError::InternalError(None).into_response(); 554 - } 554 + .map_err(|e| { 555 + error!("DB error creating deletion token: {:?}", e); 556 + ApiError::InternalError(None) 557 + })?; 555 558 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 556 559 if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion( 557 560 state.user_repo.as_ref(), ··· 565 568 warn!("Failed to enqueue account deletion notification: {:?}", e); 566 569 } 567 570 info!("Account deletion requested for user {}", did); 568 - EmptyResponse::ok().into_response() 571 + Ok(EmptyResponse::ok().into_response()) 569 572 } 570 573 571 574 #[derive(Deserialize)]
+44 -43
crates/tranquil-pds/src/api/server/migration.rs
··· 1 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuth; 2 + use crate::auth::RequiredAuth; 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: RequiredAuth, 40 40 Json(input): Json<UpdateDidDocumentInput>, 41 - ) -> Response { 42 - let auth_user = auth.0; 41 + ) -> Result<Response, ApiError> { 42 + let auth_user = auth.0.require_user()?.require_active()?; 43 43 44 44 if !auth_user.did.starts_with("did:web:") { 45 - return ApiError::InvalidRequest( 45 + return Err(ApiError::InvalidRequest( 46 46 "DID document updates are only available for did:web accounts".into(), 47 - ) 48 - .into_response(); 47 + )); 49 48 } 50 49 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) => { 50 + let user = state 51 + .user_repo 52 + .get_user_for_did_doc(&auth_user.did) 53 + .await 54 + .map_err(|e| { 55 55 tracing::error!("DB error getting user: {:?}", e); 56 - return ApiError::InternalError(None).into_response(); 57 - } 58 - }; 56 + ApiError::InternalError(None) 57 + })? 58 + .ok_or(ApiError::AccountNotFound)?; 59 59 60 - if user.deactivated_at.is_some() { 61 - return ApiError::AccountDeactivated.into_response(); 62 - } 63 - 64 60 if let Some(ref methods) = input.verification_methods { 65 61 if methods.is_empty() { 66 - return ApiError::InvalidRequest("verification_methods cannot be empty".into()) 67 - .into_response(); 62 + return Err(ApiError::InvalidRequest( 63 + "verification_methods cannot be empty".into(), 64 + )); 68 65 } 69 66 let validation_error = methods.iter().find_map(|method| { 70 67 if method.id.is_empty() { ··· 80 77 } 81 78 }); 82 79 if let Some(err) = validation_error { 83 - return ApiError::InvalidRequest(err.into()).into_response(); 80 + return Err(ApiError::InvalidRequest(err.into())); 84 81 } 85 82 } 86 83 87 84 if let Some(ref handles) = input.also_known_as 88 85 && handles.iter().any(|h| !h.starts_with("at://")) 89 86 { 90 - return ApiError::InvalidRequest("alsoKnownAs entries must be at:// URIs".into()) 91 - .into_response(); 87 + return Err(ApiError::InvalidRequest( 88 + "alsoKnownAs entries must be at:// URIs".into(), 89 + )); 92 90 } 93 91 94 92 if let Some(ref endpoint) = input.service_endpoint { 95 93 let endpoint = endpoint.trim(); 96 94 if !endpoint.starts_with("https://") { 97 - return ApiError::InvalidRequest("serviceEndpoint must start with https://".into()) 98 - .into_response(); 95 + return Err(ApiError::InvalidRequest( 96 + "serviceEndpoint must start with https://".into(), 97 + )); 99 98 } 100 99 } 101 100 ··· 106 105 107 106 let also_known_as: Option<Vec<String>> = input.also_known_as.clone(); 108 107 109 - if let Err(e) = state 108 + state 110 109 .user_repo 111 110 .upsert_did_web_overrides(user.id, verification_methods_json, also_known_as) 112 111 .await 113 - { 114 - tracing::error!("DB error upserting did_web_overrides: {:?}", e); 115 - return ApiError::InternalError(None).into_response(); 116 - } 112 + .map_err(|e| { 113 + tracing::error!("DB error upserting did_web_overrides: {:?}", e); 114 + ApiError::InternalError(None) 115 + })?; 117 116 118 117 if let Some(ref endpoint) = input.service_endpoint { 119 118 let endpoint_clean = endpoint.trim().trim_end_matches('/'); 120 - if let Err(e) = state 119 + state 121 120 .user_repo 122 121 .update_migrated_to_pds(&auth_user.did, endpoint_clean) 123 122 .await 124 - { 125 - tracing::error!("DB error updating service endpoint: {:?}", e); 126 - return ApiError::InternalError(None).into_response(); 127 - } 123 + .map_err(|e| { 124 + tracing::error!("DB error updating service endpoint: {:?}", e); 125 + ApiError::InternalError(None) 126 + })?; 128 127 } 129 128 130 129 let did_doc = build_did_document(&state, &auth_user.did).await; 131 130 132 131 tracing::info!("Updated DID document for {}", &auth_user.did); 133 132 134 - ( 133 + Ok(( 135 134 StatusCode::OK, 136 135 Json(UpdateDidDocumentOutput { 137 136 success: true, 138 137 did_document: did_doc, 139 138 }), 140 139 ) 141 - .into_response() 140 + .into_response()) 142 141 } 143 142 144 - pub async fn get_did_document(State(state): State<AppState>, auth: BearerAuth) -> Response { 145 - let auth_user = auth.0; 143 + pub async fn get_did_document( 144 + State(state): State<AppState>, 145 + auth: RequiredAuth, 146 + ) -> Result<Response, ApiError> { 147 + let auth_user = auth.0.require_user()?.require_active()?; 146 148 147 149 if !auth_user.did.starts_with("did:web:") { 148 - return ApiError::InvalidRequest( 150 + return Err(ApiError::InvalidRequest( 149 151 "This endpoint is only available for did:web accounts".into(), 150 - ) 151 - .into_response(); 152 + )); 152 153 } 153 154 154 155 let did_doc = build_did_document(&state, &auth_user.did).await; 155 156 156 - (StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response() 157 + Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response()) 157 158 } 158 159 159 160 async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value {
+9 -8
crates/tranquil-pds/src/api/temp.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::{BearerAuth, OptionalBearerAuth}; 2 + use crate::auth::{OptionalAuth, RequiredAuth}; 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 24 + pub async fn check_signup_queue(auth: OptionalAuth) -> Response { 25 + if let Some(entity) = auth.0 26 + && let Some(user) = entity.as_user() 26 27 && user.is_oauth 27 28 { 28 29 return ApiError::Forbidden.into_response(); ··· 49 50 50 51 pub async fn dereference_scope( 51 52 State(state): State<AppState>, 52 - auth: BearerAuth, 53 + auth: RequiredAuth, 53 54 Json(input): Json<DereferenceScopeInput>, 54 - ) -> Response { 55 - let _ = auth; 55 + ) -> Result<Response, ApiError> { 56 + let _user = auth.0.require_user()?.require_active()?; 56 57 57 58 let scope_parts: Vec<&str> = input.scope.split_whitespace().collect(); 58 59 let mut resolved_scopes: Vec<String> = Vec::new(); ··· 118 119 } 119 120 } 120 121 121 - Json(DereferenceScopeOutput { 122 + Ok(Json(DereferenceScopeOutput { 122 123 scope: resolved_scopes.join(" "), 123 124 }) 124 - .into_response() 125 + .into_response()) 125 126 }
+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 }
+169 -451
crates/tranquil-pds/src/auth/extractor.rs
··· 7 7 8 8 use super::{ 9 9 AccountStatus, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, is_service_token, 10 - validate_bearer_token, validate_bearer_token_allow_deactivated, 11 - validate_bearer_token_allow_takendown, 10 + validate_bearer_token_for_service_auth, 12 11 }; 13 12 use crate::api::error::ApiError; 14 13 use crate::state::AppState; 15 14 use crate::types::Did; 16 15 use crate::util::build_full_url; 17 16 18 - pub struct BearerAuth(pub AuthenticatedUser); 19 - 20 17 #[derive(Debug)] 21 18 pub enum AuthError { 22 19 ··· 61 58 } 62 59 } 63 60 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) 61 + pub struct ExtractedToken { 62 + pub token: String, 63 + pub is_dpop: bool, 83 64 } 84 65 85 66 pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option<String> { ··· 102 83 Some(token.to_string()) 103 84 } 104 85 105 - pub struct ExtractedToken { 106 - pub token: String, 107 - pub is_dpop: bool, 108 - } 109 - 110 86 pub fn extract_auth_token_from_header(auth_header: Option<&str>) -> Option<ExtractedToken> { 111 87 let header = auth_header?; 112 88 let header = header.trim(); ··· 136 112 None 137 113 } 138 114 139 - #[derive(Default)] 140 - struct StatusCheckFlags { 141 - allow_deactivated: bool, 142 - allow_takendown: bool, 115 + pub enum AuthenticatedEntity { 116 + User(AuthenticatedUser), 117 + Service { 118 + did: Did, 119 + claims: ServiceTokenClaims, 120 + }, 143 121 } 144 122 123 + impl AuthenticatedEntity { 124 + pub fn did(&self) -> &Did { 125 + match self { 126 + Self::User(user) => &user.did, 127 + Self::Service { did, .. } => did, 128 + } 129 + } 130 + 131 + pub fn as_user(&self) -> Option<&AuthenticatedUser> { 132 + match self { 133 + Self::User(user) => Some(user), 134 + Self::Service { .. } => None, 135 + } 136 + } 137 + 138 + pub fn as_service(&self) -> Option<(&Did, &ServiceTokenClaims)> { 139 + match self { 140 + Self::User(_) => None, 141 + Self::Service { did, claims } => Some((did, claims)), 142 + } 143 + } 144 + 145 + pub fn require_user(&self) -> Result<&AuthenticatedUser, ApiError> { 146 + match self { 147 + Self::User(user) => Ok(user), 148 + Self::Service { .. } => Err(ApiError::AuthenticationFailed(Some( 149 + "User authentication required".to_string(), 150 + ))), 151 + } 152 + } 153 + 154 + pub fn require_service(&self) -> Result<(&Did, &ServiceTokenClaims), ApiError> { 155 + match self { 156 + Self::User(_) => Err(ApiError::AuthenticationFailed(Some( 157 + "Service authentication required".to_string(), 158 + ))), 159 + Self::Service { did, claims } => Ok((did, claims)), 160 + } 161 + } 162 + 163 + pub fn require_service_lxm( 164 + &self, 165 + expected_lxm: &str, 166 + ) -> Result<(&Did, &ServiceTokenClaims), ApiError> { 167 + let (did, claims) = self.require_service()?; 168 + match &claims.lxm { 169 + Some(lxm) if lxm == "*" || lxm == expected_lxm => Ok((did, claims)), 170 + Some(lxm) => Err(ApiError::AuthorizationError(format!( 171 + "Token lxm '{}' does not permit '{}'", 172 + lxm, expected_lxm 173 + ))), 174 + None => Err(ApiError::AuthorizationError( 175 + "Token missing lxm claim".to_string(), 176 + )), 177 + } 178 + } 179 + 180 + pub fn into_user(self) -> Result<AuthenticatedUser, ApiError> { 181 + match self { 182 + Self::User(user) => Ok(user), 183 + Self::Service { .. } => Err(ApiError::AuthenticationFailed(Some( 184 + "User authentication required".to_string(), 185 + ))), 186 + } 187 + } 188 + } 189 + 190 + impl AuthenticatedUser { 191 + pub fn require_active(&self) -> Result<&Self, ApiError> { 192 + if self.status.is_deactivated() { 193 + return Err(ApiError::AccountDeactivated); 194 + } 195 + if self.status.is_takendown() { 196 + return Err(ApiError::AccountTakedown); 197 + } 198 + Ok(self) 199 + } 200 + 201 + pub fn require_not_takendown(&self) -> Result<&Self, ApiError> { 202 + if self.status.is_takendown() { 203 + return Err(ApiError::AccountTakedown); 204 + } 205 + Ok(self) 206 + } 207 + 208 + pub fn require_admin(&self) -> Result<&Self, ApiError> { 209 + if !self.is_admin { 210 + return Err(ApiError::AdminRequired); 211 + } 212 + Ok(self) 213 + } 214 + } 215 + 145 216 async fn verify_oauth_token_and_build_user( 146 217 state: &AppState, 147 218 token: &str, 148 219 dpop_proof: Option<&str>, 149 220 method: &str, 150 221 uri: &str, 151 - flags: StatusCheckFlags, 152 222 ) -> Result<AuthenticatedUser, AuthError> { 153 223 match crate::oauth::verify::verify_oauth_access_token( 154 224 state.oauth_repo.as_ref(), ··· 171 241 user_info.takedown_ref.as_deref(), 172 242 user_info.deactivated_at, 173 243 ); 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 244 Ok(AuthenticatedUser { 181 245 did: result.did, 182 246 key_bytes: user_info.key_bytes.and_then(|kb| { ··· 198 262 } 199 263 } 200 264 201 - impl FromRequestParts<AppState> for BearerAuth { 202 - type Rejection = AuthError; 265 + async fn verify_service_token(token: &str) -> Result<(Did, ServiceTokenClaims), AuthError> { 266 + let verifier = ServiceTokenVerifier::new(); 267 + let claims = verifier 268 + .verify_service_token(token, None) 269 + .await 270 + .map_err(|e| { 271 + error!("Service token verification failed: {:?}", e); 272 + AuthError::AuthenticationFailed 273 + })?; 203 274 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)?; 275 + let did: Did = claims 276 + .iss 277 + .parse() 278 + .map_err(|_| AuthError::AuthenticationFailed)?; 214 279 215 - let extracted = 216 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 280 + debug!("Service token verified for DID: {}", did); 217 281 218 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 219 - let method = parts.method.as_str(); 220 - let uri = build_full_url(&parts.uri.to_string()); 221 - 222 - match validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await { 223 - Ok(user) if !user.is_oauth => { 224 - return if user.status.is_deactivated() { 225 - Err(AuthError::AccountDeactivated) 226 - } else if user.status.is_takendown() { 227 - Err(AuthError::AccountTakedown) 228 - } else { 229 - Ok(BearerAuth(user)) 230 - }; 231 - } 232 - Ok(_) => {} 233 - Err(super::TokenValidationError::AccountDeactivated) => { 234 - return Err(AuthError::AccountDeactivated); 235 - } 236 - Err(super::TokenValidationError::AccountTakedown) => { 237 - return Err(AuthError::AccountTakedown); 238 - } 239 - Err(super::TokenValidationError::TokenExpired) => { 240 - info!("JWT access token expired in BearerAuth, returning ExpiredToken"); 241 - return Err(AuthError::TokenExpired); 242 - } 243 - Err(_) => {} 244 - } 245 - 246 - verify_oauth_token_and_build_user( 247 - state, 248 - &extracted.token, 249 - dpop_proof, 250 - method, 251 - &uri, 252 - StatusCheckFlags::default(), 253 - ) 254 - .await 255 - .map(BearerAuth) 256 - } 282 + Ok((did, claims)) 257 283 } 258 284 259 - pub struct BearerAuthAllowDeactivated(pub AuthenticatedUser); 285 + async fn extract_auth_internal( 286 + parts: &mut Parts, 287 + state: &AppState, 288 + ) -> Result<AuthenticatedEntity, AuthError> { 289 + let auth_header = parts 290 + .headers 291 + .get(AUTHORIZATION) 292 + .ok_or(AuthError::MissingToken)? 293 + .to_str() 294 + .map_err(|_| AuthError::InvalidFormat)?; 260 295 261 - impl FromRequestParts<AppState> for BearerAuthAllowDeactivated { 262 - type Rejection = AuthError; 296 + let extracted = 297 + extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 263 298 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)?; 299 + if is_service_token(&extracted.token) { 300 + let (did, claims) = verify_service_token(&extracted.token).await?; 301 + return Ok(AuthenticatedEntity::Service { did, claims }); 302 + } 274 303 275 - let extracted = 276 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 304 + let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 305 + let method = parts.method.as_str(); 306 + let uri = build_full_url(&parts.uri.to_string()); 277 307 278 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 279 - let method = parts.method.as_str(); 280 - let uri = build_full_url(&parts.uri.to_string()); 281 - 282 - match validate_bearer_token_allow_deactivated(state.user_repo.as_ref(), &extracted.token) 283 - .await 284 - { 285 - Ok(user) if !user.is_oauth => { 286 - return if user.status.is_takendown() { 287 - Err(AuthError::AccountTakedown) 288 - } else { 289 - Ok(BearerAuthAllowDeactivated(user)) 290 - }; 291 - } 292 - Ok(_) => {} 293 - Err(super::TokenValidationError::AccountTakedown) => { 294 - return Err(AuthError::AccountTakedown); 295 - } 296 - Err(super::TokenValidationError::TokenExpired) => { 297 - return Err(AuthError::TokenExpired); 298 - } 299 - Err(_) => {} 308 + match validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &extracted.token).await { 309 + Ok(user) if !user.is_oauth => { 310 + return Ok(AuthenticatedEntity::User(user)); 300 311 } 301 - 302 - verify_oauth_token_and_build_user( 303 - state, 304 - &extracted.token, 305 - dpop_proof, 306 - method, 307 - &uri, 308 - StatusCheckFlags { 309 - allow_deactivated: true, 310 - allow_takendown: false, 311 - }, 312 - ) 313 - .await 314 - .map(BearerAuthAllowDeactivated) 312 + Ok(_) => {} 313 + Err(super::TokenValidationError::TokenExpired) => { 314 + info!("JWT access token expired, returning ExpiredToken"); 315 + return Err(AuthError::TokenExpired); 316 + } 317 + Err(_) => {} 315 318 } 316 - } 317 319 318 - pub struct BearerAuthAllowTakendown(pub AuthenticatedUser); 320 + let user = verify_oauth_token_and_build_user(state, &extracted.token, dpop_proof, method, &uri) 321 + .await?; 319 322 320 - impl FromRequestParts<AppState> for BearerAuthAllowTakendown { 321 - type Rejection = AuthError; 322 - 323 - async fn from_request_parts( 324 - parts: &mut Parts, 325 - state: &AppState, 326 - ) -> Result<Self, Self::Rejection> { 327 - let auth_header = parts 328 - .headers 329 - .get(AUTHORIZATION) 330 - .ok_or(AuthError::MissingToken)? 331 - .to_str() 332 - .map_err(|_| AuthError::InvalidFormat)?; 333 - 334 - let extracted = 335 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 336 - 337 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 338 - let method = parts.method.as_str(); 339 - let uri = build_full_url(&parts.uri.to_string()); 340 - 341 - match validate_bearer_token_allow_takendown(state.user_repo.as_ref(), &extracted.token) 342 - .await 343 - { 344 - Ok(user) if !user.is_oauth => { 345 - return if user.status.is_deactivated() { 346 - Err(AuthError::AccountDeactivated) 347 - } else { 348 - Ok(BearerAuthAllowTakendown(user)) 349 - }; 350 - } 351 - Ok(_) => {} 352 - Err(super::TokenValidationError::AccountDeactivated) => { 353 - return Err(AuthError::AccountDeactivated); 354 - } 355 - Err(super::TokenValidationError::TokenExpired) => { 356 - return Err(AuthError::TokenExpired); 357 - } 358 - Err(_) => {} 359 - } 360 - 361 - verify_oauth_token_and_build_user( 362 - state, 363 - &extracted.token, 364 - dpop_proof, 365 - method, 366 - &uri, 367 - StatusCheckFlags { 368 - allow_deactivated: false, 369 - allow_takendown: true, 370 - }, 371 - ) 372 - .await 373 - .map(BearerAuthAllowTakendown) 374 - } 323 + Ok(AuthenticatedEntity::User(user)) 375 324 } 376 325 377 - pub struct BearerAuthAdmin(pub AuthenticatedUser); 326 + pub struct RequiredAuth(pub AuthenticatedEntity); 378 327 379 - impl FromRequestParts<AppState> for BearerAuthAdmin { 328 + impl FromRequestParts<AppState> for RequiredAuth { 380 329 type Rejection = AuthError; 381 330 382 331 async fn from_request_parts( 383 332 parts: &mut Parts, 384 333 state: &AppState, 385 334 ) -> Result<Self, Self::Rejection> { 386 - let auth_header = parts 387 - .headers 388 - .get(AUTHORIZATION) 389 - .ok_or(AuthError::MissingToken)? 390 - .to_str() 391 - .map_err(|_| AuthError::InvalidFormat)?; 392 - 393 - let extracted = 394 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 395 - 396 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 397 - let method = parts.method.as_str(); 398 - let uri = build_full_url(&parts.uri.to_string()); 399 - 400 - match validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await { 401 - Ok(user) if !user.is_oauth => { 402 - if user.status.is_deactivated() { 403 - return Err(AuthError::AccountDeactivated); 404 - } 405 - if user.status.is_takendown() { 406 - return Err(AuthError::AccountTakedown); 407 - } 408 - if !user.is_admin { 409 - return Err(AuthError::AdminRequired); 410 - } 411 - return Ok(BearerAuthAdmin(user)); 412 - } 413 - Ok(_) => {} 414 - Err(super::TokenValidationError::AccountDeactivated) => { 415 - return Err(AuthError::AccountDeactivated); 416 - } 417 - Err(super::TokenValidationError::AccountTakedown) => { 418 - return Err(AuthError::AccountTakedown); 419 - } 420 - Err(super::TokenValidationError::TokenExpired) => { 421 - return Err(AuthError::TokenExpired); 422 - } 423 - Err(_) => {} 424 - } 425 - 426 - let user = verify_oauth_token_and_build_user( 427 - state, 428 - &extracted.token, 429 - dpop_proof, 430 - method, 431 - &uri, 432 - StatusCheckFlags::default(), 433 - ) 434 - .await?; 435 - 436 - if !user.is_admin { 437 - return Err(AuthError::AdminRequired); 438 - } 439 - Ok(BearerAuthAdmin(user)) 335 + extract_auth_internal(parts, state).await.map(RequiredAuth) 440 336 } 441 337 } 442 338 443 - pub struct OptionalBearerAuth(pub Option<AuthenticatedUser>); 339 + pub struct OptionalAuth(pub Option<AuthenticatedEntity>); 444 340 445 - impl FromRequestParts<AppState> for OptionalBearerAuth { 446 - type Rejection = AuthError; 341 + impl FromRequestParts<AppState> for OptionalAuth { 342 + type Rejection = std::convert::Infallible; 447 343 448 344 async fn from_request_parts( 449 345 parts: &mut Parts, 450 346 state: &AppState, 451 347 ) -> Result<Self, Self::Rejection> { 452 - let auth_header = match parts.headers.get(AUTHORIZATION) { 453 - Some(h) => match h.to_str() { 454 - Ok(s) => s, 455 - Err(_) => return Ok(OptionalBearerAuth(None)), 456 - }, 457 - None => return Ok(OptionalBearerAuth(None)), 458 - }; 459 - 460 - let extracted = match extract_auth_token_from_header(Some(auth_header)) { 461 - Some(e) => e, 462 - None => return Ok(OptionalBearerAuth(None)), 463 - }; 464 - 465 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 466 - let method = parts.method.as_str(); 467 - let uri = build_full_url(&parts.uri.to_string()); 468 - 469 - if let Ok(user) = validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await 470 - && !user.is_oauth 471 - { 472 - return if user.status.is_deactivated() || user.status.is_takendown() { 473 - Ok(OptionalBearerAuth(None)) 474 - } else { 475 - Ok(OptionalBearerAuth(Some(user))) 476 - }; 477 - } 478 - 479 - Ok(OptionalBearerAuth( 480 - verify_oauth_token_and_build_user( 481 - state, 482 - &extracted.token, 483 - dpop_proof, 484 - method, 485 - &uri, 486 - StatusCheckFlags::default(), 487 - ) 488 - .await 489 - .ok(), 490 - )) 348 + Ok(OptionalAuth(extract_auth_internal(parts, state).await.ok())) 491 349 } 492 350 } 493 351 494 - pub struct ServiceAuth { 495 - pub claims: ServiceTokenClaims, 496 - pub did: Did, 497 - } 352 + #[cfg(test)] 353 + fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> { 354 + let auth_header = auth_header.trim(); 498 355 499 - impl FromRequestParts<AppState> for ServiceAuth { 500 - type Rejection = AuthError; 501 - 502 - async fn from_request_parts( 503 - parts: &mut Parts, 504 - _state: &AppState, 505 - ) -> Result<Self, Self::Rejection> { 506 - let auth_header = parts 507 - .headers 508 - .get(AUTHORIZATION) 509 - .ok_or(AuthError::MissingToken)? 510 - .to_str() 511 - .map_err(|_| AuthError::InvalidFormat)?; 512 - 513 - let extracted = 514 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 515 - 516 - if !is_service_token(&extracted.token) { 517 - return Err(AuthError::InvalidFormat); 518 - } 519 - 520 - let verifier = ServiceTokenVerifier::new(); 521 - let claims = verifier 522 - .verify_service_token(&extracted.token, None) 523 - .await 524 - .map_err(|e| { 525 - error!("Service token verification failed: {:?}", e); 526 - AuthError::AuthenticationFailed 527 - })?; 528 - 529 - let did: Did = claims 530 - .iss 531 - .parse() 532 - .map_err(|_| AuthError::AuthenticationFailed)?; 533 - 534 - debug!("Service token verified for DID: {}", did); 535 - 536 - Ok(ServiceAuth { claims, did }) 356 + if auth_header.len() < 8 { 357 + return Err(AuthError::InvalidFormat); 537 358 } 538 - } 539 359 540 - pub struct OptionalServiceAuth(pub Option<ServiceTokenClaims>); 541 - 542 - impl FromRequestParts<AppState> for OptionalServiceAuth { 543 - type Rejection = std::convert::Infallible; 544 - 545 - async fn from_request_parts( 546 - parts: &mut Parts, 547 - _state: &AppState, 548 - ) -> Result<Self, Self::Rejection> { 549 - let auth_header = match parts.headers.get(AUTHORIZATION) { 550 - Some(h) => match h.to_str() { 551 - Ok(s) => s, 552 - Err(_) => return Ok(OptionalServiceAuth(None)), 553 - }, 554 - None => return Ok(OptionalServiceAuth(None)), 555 - }; 556 - 557 - let extracted = match extract_auth_token_from_header(Some(auth_header)) { 558 - Some(e) => e, 559 - None => return Ok(OptionalServiceAuth(None)), 560 - }; 561 - 562 - if !is_service_token(&extracted.token) { 563 - return Ok(OptionalServiceAuth(None)); 564 - } 565 - 566 - let verifier = ServiceTokenVerifier::new(); 567 - match verifier.verify_service_token(&extracted.token, None).await { 568 - Ok(claims) => { 569 - debug!("Service token verified for DID: {}", claims.iss); 570 - Ok(OptionalServiceAuth(Some(claims))) 571 - } 572 - Err(e) => { 573 - debug!("Service token verification failed (optional): {:?}", e); 574 - Ok(OptionalServiceAuth(None)) 575 - } 576 - } 360 + let prefix = &auth_header[..7]; 361 + if !prefix.eq_ignore_ascii_case("bearer ") { 362 + return Err(AuthError::InvalidFormat); 577 363 } 578 - } 579 364 580 - pub enum BlobAuthResult { 581 - Service { did: Did }, 582 - User(AuthenticatedUser), 583 - } 584 - 585 - pub struct BlobAuth(pub BlobAuthResult); 586 - 587 - impl FromRequestParts<AppState> for BlobAuth { 588 - type Rejection = AuthError; 589 - 590 - async fn from_request_parts( 591 - parts: &mut Parts, 592 - state: &AppState, 593 - ) -> Result<Self, Self::Rejection> { 594 - let auth_header = parts 595 - .headers 596 - .get(AUTHORIZATION) 597 - .ok_or(AuthError::MissingToken)? 598 - .to_str() 599 - .map_err(|_| AuthError::InvalidFormat)?; 600 - 601 - let extracted = 602 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 603 - 604 - if is_service_token(&extracted.token) { 605 - debug!("Verifying service token for blob upload"); 606 - let verifier = ServiceTokenVerifier::new(); 607 - let claims = verifier 608 - .verify_service_token(&extracted.token, Some("com.atproto.repo.uploadBlob")) 609 - .await 610 - .map_err(|e| { 611 - error!("Service token verification failed: {:?}", e); 612 - AuthError::AuthenticationFailed 613 - })?; 614 - 615 - let did: Did = claims 616 - .iss 617 - .parse() 618 - .map_err(|_| AuthError::AuthenticationFailed)?; 619 - 620 - debug!("Service token verified for DID: {}", did); 621 - return Ok(BlobAuth(BlobAuthResult::Service { did })); 622 - } 623 - 624 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 625 - let uri = build_full_url("/xrpc/com.atproto.repo.uploadBlob"); 626 - 627 - if let Ok(user) = 628 - validate_bearer_token_allow_deactivated(state.user_repo.as_ref(), &extracted.token) 629 - .await 630 - && !user.is_oauth 631 - { 632 - return if user.status.is_takendown() { 633 - Err(AuthError::AccountTakedown) 634 - } else { 635 - Ok(BlobAuth(BlobAuthResult::User(user))) 636 - }; 637 - } 638 - 639 - verify_oauth_token_and_build_user( 640 - state, 641 - &extracted.token, 642 - dpop_proof, 643 - "POST", 644 - &uri, 645 - StatusCheckFlags { 646 - allow_deactivated: true, 647 - allow_takendown: false, 648 - }, 649 - ) 650 - .await 651 - .map(|user| BlobAuth(BlobAuthResult::User(user))) 365 + let token = auth_header[7..].trim(); 366 + if token.is_empty() { 367 + return Err(AuthError::InvalidFormat); 652 368 } 369 + 370 + Ok(token) 653 371 } 654 372 655 373 #[cfg(test)]
+1 -2
crates/tranquil-pds/src/auth/mod.rs
··· 16 16 pub mod webauthn; 17 17 18 18 pub use extractor::{ 19 - AuthError, BearerAuth, BearerAuthAdmin, BearerAuthAllowDeactivated, BlobAuth, BlobAuthResult, 20 - ExtractedToken, OptionalBearerAuth, OptionalServiceAuth, ServiceAuth, 19 + AuthError, AuthenticatedEntity, ExtractedToken, OptionalAuth, RequiredAuth, 21 20 extract_auth_token_from_header, extract_bearer_token_from_header, 22 21 }; 23 22 pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token};
crates/tranquil-pds/src/lib.rs

This file has not been changed.

+38 -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::RequiredAuth, 3649 3648 ) -> Response { 3650 - let did = &auth.0.did; 3649 + let user = match auth.0.require_user() { 3650 + Ok(u) => match u.require_active() { 3651 + Ok(u) => u, 3652 + Err(_) => { 3653 + return ( 3654 + StatusCode::FORBIDDEN, 3655 + Json(serde_json::json!({ 3656 + "error": "access_denied", 3657 + "error_description": "Account is deactivated" 3658 + })), 3659 + ) 3660 + .into_response(); 3661 + } 3662 + }, 3663 + Err(_) => { 3664 + return ( 3665 + StatusCode::UNAUTHORIZED, 3666 + Json(serde_json::json!({ 3667 + "error": "invalid_token", 3668 + "error_description": "Authentication required" 3669 + })), 3670 + ) 3671 + .into_response(); 3672 + } 3673 + }; 3674 + let did = &user.did; 3651 3675 3652 3676 let existing_device = extract_device_cookie(&headers); 3653 3677 ··· 3670 3694 }; 3671 3695 let device_typed = DeviceIdType::from(new_id.0.clone()); 3672 3696 3673 - if let Err(e) = state.oauth_repo.create_device(&device_typed, &device_data).await { 3697 + if let Err(e) = state 3698 + .oauth_repo 3699 + .create_device(&device_typed, &device_data) 3700 + .await 3701 + { 3674 3702 tracing::error!(error = ?e, "Failed to create device"); 3675 3703 return ( 3676 3704 StatusCode::INTERNAL_SERVER_ERROR, ··· 3682 3710 .into_response(); 3683 3711 } 3684 3712 3685 - if let Err(e) = state.oauth_repo.upsert_account_device(did, &device_typed).await { 3713 + if let Err(e) = state 3714 + .oauth_repo 3715 + .upsert_account_device(did, &device_typed) 3716 + .await 3717 + { 3686 3718 tracing::error!(error = ?e, "Failed to link device to account"); 3687 3719 return ( 3688 3720 StatusCode::INTERNAL_SERVER_ERROR,
+30 -7
crates/tranquil-pds/src/oauth/endpoints/delegation.rs
··· 1 - use crate::auth::BearerAuth; 1 + use crate::auth::RequiredAuth; 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: RequiredAuth, 467 467 Json(form): Json<DelegationTokenAuthSubmit>, 468 468 ) -> Response { 469 - let controller_did = auth.0.did; 469 + let user = match auth.0.require_user() { 470 + Ok(u) => match u.require_active() { 471 + Ok(u) => u, 472 + Err(_) => { 473 + return Json(DelegationAuthResponse { 474 + success: false, 475 + needs_totp: None, 476 + redirect_uri: None, 477 + error: Some("Account is deactivated".to_string()), 478 + }) 479 + .into_response(); 480 + } 481 + }, 482 + Err(_) => { 483 + return Json(DelegationAuthResponse { 484 + success: false, 485 + needs_totp: None, 486 + redirect_uri: None, 487 + error: Some("Authentication required".to_string()), 488 + }) 489 + .into_response(); 490 + } 491 + }; 492 + let controller_did = &user.did; 470 493 471 494 let delegated_did: Did = match form.delegated_did.parse() { 472 495 Ok(d) => d, ··· 510 533 511 534 let grant = match state 512 535 .delegation_repo 513 - .get_delegation(&delegated_did, &controller_did) 536 + .get_delegation(&delegated_did, controller_did) 514 537 .await 515 538 { 516 539 Ok(Some(g)) => g, ··· 551 574 552 575 if state 553 576 .oauth_repo 554 - .set_controller_did(&request_id, &controller_did) 577 + .set_controller_did(&request_id, controller_did) 555 578 .await 556 579 .is_err() 557 580 { ··· 574 597 .delegation_repo 575 598 .log_delegation_action( 576 599 &delegated_did, 577 - &controller_did, 578 - Some(&controller_did), 600 + controller_did, 601 + Some(controller_did), 579 602 DelegationActionType::TokenIssued, 580 603 Some(serde_json::json!({ 581 604 "client_id": request.client_id,
crates/tranquil-pds/src/oauth/verify.rs

This file has not been changed.

crates/tranquil-pds/tests/auth_extractor.rs

This file has not been changed.

crates/tranquil-pds/tests/common/mod.rs

This file has not been changed.

crates/tranquil-pds/tests/oauth_security.rs

This file has not been changed.

crates/tranquil-scopes/Cargo.toml

This file has not been changed.

+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 }
crates/tranquil-storage/src/lib.rs

This file has not been changed.

frontend/src/lib/api.ts

This file has not been changed.

frontend/src/lib/auth.svelte.ts

This file has not been changed.

frontend/src/lib/migration/atproto-client.ts

This file has not been changed.

frontend/src/lib/migration/flow.svelte.ts

This file has not been changed.

frontend/src/lib/migration/offline-flow.svelte.ts

This file has not been changed.

frontend/src/lib/oauth.ts

This file has not been changed.

frontend/src/locales/en.json

This file has not been changed.

frontend/src/locales/fi.json

This file has not been changed.

frontend/src/locales/ja.json

This file has not been changed.

frontend/src/locales/ko.json

This file has not been changed.

frontend/src/locales/sv.json

This file has not been changed.

frontend/src/locales/zh.json

This file has not been changed.

frontend/src/routes/Migration.svelte

This file has not been changed.

frontend/src/routes/OAuthAccounts.svelte

This file has not been changed.

frontend/src/routes/OAuthConsent.svelte

This file has not been changed.

+21 -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::RequiredAuth; 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: RequiredAuth) -> Response { 36 + let user = match auth.0.require_user() { 37 + Ok(u) => u, 38 + Err(e) => return e.into_response(), 39 + }; 40 + if let Err(e) = user.require_not_takendown() { 41 + return e.into_response(); 42 + } 43 + let has_full_access = user.permissions().has_full_access(); 44 + let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&user.did).await { 42 45 Ok(Some(id)) => id, 43 46 _ => { 44 47 return ApiError::InternalError(Some("User not found".into())).into_response(); ··· 93 96 } 94 97 pub async fn put_preferences( 95 98 State(state): State<AppState>, 96 - auth: BearerAuthAllowDeactivated, 99 + auth: RequiredAuth, 97 100 Json(input): Json<PutPreferencesInput>, 98 101 ) -> Response { 99 - let auth_user = auth.0; 100 - let has_full_access = auth_user.permissions().has_full_access(); 101 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth_user.did).await { 102 + let user = match auth.0.require_user() { 103 + Ok(u) => u, 104 + Err(e) => return e.into_response(), 105 + }; 106 + if let Err(e) = user.require_not_takendown() { 107 + return e.into_response(); 108 + } 109 + let has_full_access = user.permissions().has_full_access(); 110 + let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&user.did).await { 102 111 Ok(Some(id)) => id, 103 112 _ => { 104 113 return ApiError::InternalError(Some("User not found".into())).into_response();
+22 -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::RequiredAuth; 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: RequiredAuth, 22 22 Json(input): Json<DeleteAccountInput>, 23 - ) -> Response { 23 + ) -> Result<Response, ApiError> { 24 + auth.0.require_user()?.require_active()?.require_admin()?; 25 + 24 26 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) => { 27 + let (user_id, handle) = state 28 + .user_repo 29 + .get_id_and_handle_by_did(did) 30 + .await 31 + .map_err(|e| { 31 32 error!("DB error in delete_account: {:?}", e); 32 - return ApiError::InternalError(None).into_response(); 33 - } 34 - }; 35 - if let Err(e) = state 33 + ApiError::InternalError(None) 34 + })? 35 + .ok_or(ApiError::AccountNotFound) 36 + .map(|row| (row.id, row.handle))?; 37 + 38 + state 36 39 .user_repo 37 40 .admin_delete_account_complete(user_id, did) 38 41 .await 39 - { 40 - error!("Failed to delete account {}: {:?}", did, e); 41 - return ApiError::InternalError(Some("Failed to delete account".into())).into_response(); 42 - } 42 + .map_err(|e| { 43 + error!("Failed to delete account {}: {:?}", did, e); 44 + ApiError::InternalError(Some("Failed to delete account".into())) 45 + })?; 46 + 43 47 if let Err(e) = 44 48 crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await 45 49 { ··· 49 53 ); 50 54 } 51 55 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 52 - EmptyResponse::ok().into_response() 56 + Ok(EmptyResponse::ok().into_response()) 53 57 }
+18 -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::RequiredAuth; 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: RequiredAuth, 32 32 AtpJson(input): AtpJson<SendEmailInput>, 33 - ) -> Response { 33 + ) -> Result<Response, ApiError> { 34 + auth.0.require_user()?.require_active()?.require_admin()?; 35 + 34 36 let content = input.content.trim(); 35 37 if content.is_empty() { 36 - return ApiError::InvalidRequest("content is required".into()).into_response(); 38 + return Err(ApiError::InvalidRequest("content is required".into())); 37 39 } 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) => { 40 + let user = state 41 + .user_repo 42 + .get_by_did(&input.recipient_did) 43 + .await 44 + .map_err(|e| { 44 45 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 - }; 46 + ApiError::InternalError(None) 47 + })? 48 + .ok_or(ApiError::AccountNotFound)?; 49 + 50 + let email = user.email.ok_or(ApiError::NoEmail)?; 54 51 let (user_id, handle) = (user.id, user.handle); 55 52 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 56 53 let subject = input ··· 76 73 handle, 77 74 input.recipient_did 78 75 ); 79 - (StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response() 76 + Ok((StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()) 80 77 } 81 78 Err(e) => { 82 79 warn!("Failed to enqueue admin email: {:?}", e); 83 - (StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response() 80 + Ok((StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response()) 84 81 } 85 82 } 86 83 }
+22 -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::RequiredAuth; 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: RequiredAuth, 71 71 Query(params): Query<GetAccountInfoParams>, 72 - ) -> Response { 73 - let account = match state 72 + ) -> Result<Response, ApiError> { 73 + auth.0.require_user()?.require_active()?.require_admin()?; 74 + 75 + let account = state 74 76 .infra_repo 75 77 .get_admin_account_info_by_did(&params.did) 76 78 .await 77 - { 78 - Ok(Some(a)) => a, 79 - Ok(None) => return ApiError::AccountNotFound.into_response(), 80 - Err(e) => { 79 + .map_err(|e| { 81 80 error!("DB error in get_account_info: {:?}", e); 82 - return ApiError::InternalError(None).into_response(); 83 - } 84 - }; 81 + ApiError::InternalError(None) 82 + })? 83 + .ok_or(ApiError::AccountNotFound)?; 85 84 86 85 let invited_by = get_invited_by(&state, account.id).await; 87 86 let invites = get_invites_for_user(&state, account.id).await; 88 87 89 - ( 88 + Ok(( 90 89 StatusCode::OK, 91 90 Json(AccountInfo { 92 91 did: account.did, ··· 105 104 invites, 106 105 }), 107 106 ) 108 - .into_response() 107 + .into_response()) 109 108 } 110 109 111 110 async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> { ··· 200 199 201 200 pub async fn get_account_infos( 202 201 State(state): State<AppState>, 203 - _auth: BearerAuthAdmin, 202 + auth: RequiredAuth, 204 203 RawQuery(raw_query): RawQuery, 205 - ) -> Response { 204 + ) -> Result<Response, ApiError> { 205 + auth.0.require_user()?.require_active()?.require_admin()?; 206 + 206 207 let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids") 207 208 .into_iter() 208 209 .filter(|d| !d.is_empty()) 209 210 .collect(); 210 211 211 212 if dids.is_empty() { 212 - return ApiError::InvalidRequest("dids is required".into()).into_response(); 213 + return Err(ApiError::InvalidRequest("dids is required".into())); 213 214 } 214 215 215 216 let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect(); 216 - let accounts = match state 217 + let accounts = state 217 218 .infra_repo 218 219 .get_admin_account_infos_by_dids(&dids_typed) 219 220 .await 220 - { 221 - Ok(accounts) => accounts, 222 - Err(e) => { 221 + .map_err(|e| { 223 222 error!("Failed to fetch account infos: {:?}", e); 224 - return ApiError::InternalError(None).into_response(); 225 - } 226 - }; 223 + ApiError::InternalError(None) 224 + })?; 227 225 228 226 let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect(); 229 227 ··· 316 314 }) 317 315 .collect(); 318 316 319 - (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response() 317 + Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()) 320 318 }
+41 -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::RequiredAuth; 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: RequiredAuth, 54 54 Query(params): Query<SearchAccountsParams>, 55 - ) -> Response { 55 + ) -> Result<Response, ApiError> { 56 + auth.0.require_user()?.require_active()?.require_admin()?; 57 + 56 58 let limit = params.limit.clamp(1, 100); 57 59 let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); 58 60 let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); 59 61 let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|c| c.parse().ok()); 60 - let result = state 62 + let rows = state 61 63 .user_repo 62 64 .search_accounts( 63 65 cursor_did.as_ref(), ··· 65 67 handle_filter.as_deref(), 66 68 limit + 1, 67 69 ) 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()) 70 + .await 71 + .map_err(|e| { 72 + error!("DB error in search_accounts: {:?}", e); 73 + ApiError::InternalError(None) 74 + })?; 75 + 76 + let has_more = rows.len() > limit as usize; 77 + let accounts: Vec<AccountView> = rows 78 + .into_iter() 79 + .take(limit as usize) 80 + .map(|row| AccountView { 81 + did: row.did.clone(), 82 + handle: row.handle, 83 + email: row.email, 84 + indexed_at: row.created_at.to_rfc3339(), 85 + email_confirmed_at: if row.email_verified { 86 + Some(row.created_at.to_rfc3339()) 91 87 } else { 92 88 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 - } 89 + }, 90 + deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()), 91 + invites_disabled: row.invites_disabled, 92 + }) 93 + .collect(); 94 + let next_cursor = if has_more { 95 + accounts.last().map(|a| a.did.to_string()) 96 + } else { 97 + None 98 + }; 99 + Ok(( 100 + StatusCode::OK, 101 + Json(SearchAccountsOutput { 102 + cursor: next_cursor, 103 + accounts, 104 + }), 105 + ) 106 + .into_response()) 108 107 }
+45 -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::RequiredAuth; 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: RequiredAuth, 23 23 Json(input): Json<UpdateAccountEmailInput>, 24 - ) -> Response { 24 + ) -> Result<Response, ApiError> { 25 + auth.0.require_user()?.require_active()?.require_admin()?; 26 + 25 27 let account = input.account.trim(); 26 28 let email = input.email.trim(); 27 29 if account.is_empty() || email.is_empty() { 28 - return ApiError::InvalidRequest("account and email are required".into()).into_response(); 30 + return Err(ApiError::InvalidRequest( 31 + "account and email are required".into(), 32 + )); 29 33 } 30 - let account_did: Did = match account.parse() { 31 - Ok(d) => d, 32 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 33 - }; 34 + let account_did: Did = account 35 + .parse() 36 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 37 + 34 38 match state 35 39 .user_repo 36 40 .admin_update_email(&account_did, email) 37 41 .await 38 42 { 39 - Ok(0) => ApiError::AccountNotFound.into_response(), 40 - Ok(_) => EmptyResponse::ok().into_response(), 43 + Ok(0) => Err(ApiError::AccountNotFound), 44 + Ok(_) => Ok(EmptyResponse::ok().into_response()), 41 45 Err(e) => { 42 46 error!("DB error updating email: {:?}", e); 43 - ApiError::InternalError(None).into_response() 47 + Err(ApiError::InternalError(None)) 44 48 } 45 49 } 46 50 } ··· 53 57 54 58 pub async fn update_account_handle( 55 59 State(state): State<AppState>, 56 - _auth: BearerAuthAdmin, 60 + auth: RequiredAuth, 57 61 Json(input): Json<UpdateAccountHandleInput>, 58 - ) -> Response { 62 + ) -> Result<Response, ApiError> { 63 + auth.0.require_user()?.require_active()?.require_admin()?; 64 + 59 65 let did = &input.did; 60 66 let input_handle = input.handle.trim(); 61 67 if input_handle.is_empty() { 62 - return ApiError::InvalidRequest("handle is required".into()).into_response(); 68 + return Err(ApiError::InvalidRequest("handle is required".into())); 63 69 } 64 70 if !input_handle 65 71 .chars() 66 72 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 67 73 { 68 - return ApiError::InvalidHandle(None).into_response(); 74 + return Err(ApiError::InvalidHandle(None)); 69 75 } 70 76 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 71 77 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 75 81 input_handle.to_string() 76 82 }; 77 83 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 - }; 84 + let user_id = state 85 + .user_repo 86 + .get_id_by_did(did) 87 + .await 88 + .ok() 89 + .flatten() 90 + .ok_or(ApiError::AccountNotFound)?; 82 91 let handle_for_check = Handle::new_unchecked(&handle); 83 92 if let Ok(true) = state 84 93 .user_repo 85 94 .check_handle_exists(&handle_for_check, user_id) 86 95 .await 87 96 { 88 - return ApiError::HandleTaken.into_response(); 97 + return Err(ApiError::HandleTaken); 89 98 } 90 99 match state 91 100 .user_repo 92 101 .admin_update_handle(did, &handle_for_check) 93 102 .await 94 103 { 95 - Ok(0) => ApiError::AccountNotFound.into_response(), 104 + Ok(0) => Err(ApiError::AccountNotFound), 96 105 Ok(_) => { 97 106 if let Some(old) = old_handle { 98 107 let _ = state.cache.delete(&format!("handle:{}", old)).await; ··· 115 124 { 116 125 warn!("Failed to update PLC handle for admin handle update: {}", e); 117 126 } 118 - EmptyResponse::ok().into_response() 127 + Ok(EmptyResponse::ok().into_response()) 119 128 } 120 129 Err(e) => { 121 130 error!("DB error updating handle: {:?}", e); 122 - ApiError::InternalError(None).into_response() 131 + Err(ApiError::InternalError(None)) 123 132 } 124 133 } 125 134 } ··· 132 141 133 142 pub async fn update_account_password( 134 143 State(state): State<AppState>, 135 - _auth: BearerAuthAdmin, 144 + auth: RequiredAuth, 136 145 Json(input): Json<UpdateAccountPasswordInput>, 137 - ) -> Response { 146 + ) -> Result<Response, ApiError> { 147 + auth.0.require_user()?.require_active()?.require_admin()?; 148 + 138 149 let did = &input.did; 139 150 let password = input.password.trim(); 140 151 if password.is_empty() { 141 - return ApiError::InvalidRequest("password is required".into()).into_response(); 152 + return Err(ApiError::InvalidRequest("password is required".into())); 142 153 } 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 - }; 154 + let password_hash = bcrypt::hash(password, bcrypt::DEFAULT_COST).map_err(|e| { 155 + error!("Failed to hash password: {:?}", e); 156 + ApiError::InternalError(None) 157 + })?; 158 + 150 159 match state 151 160 .user_repo 152 161 .admin_update_password(did, &password_hash) 153 162 .await 154 163 { 155 - Ok(0) => ApiError::AccountNotFound.into_response(), 156 - Ok(_) => EmptyResponse::ok().into_response(), 164 + Ok(0) => Err(ApiError::AccountNotFound), 165 + Ok(_) => Ok(EmptyResponse::ok().into_response()), 157 166 Err(e) => { 158 167 error!("DB error updating password: {:?}", e); 159 - ApiError::InternalError(None).into_response() 168 + Err(ApiError::InternalError(None)) 160 169 } 161 170 } 162 171 }
+4 -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::RequiredAuth; 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: RequiredAuth, 82 82 Json(req): Json<UpdateServerConfigRequest>, 83 83 ) -> Result<Json<UpdateServerConfigResponse>, ApiError> { 84 + auth.0.require_user()?.require_active()?.require_admin()?; 85 + 84 86 if let Some(server_name) = req.server_name { 85 87 let trimmed = server_name.trim(); 86 88 if trimmed.is_empty() || trimmed.len() > 100 {
+40 -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::RequiredAuth; 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: RequiredAuth, 25 25 Json(input): Json<DisableInviteCodesInput>, 26 - ) -> Response { 26 + ) -> Result<Response, ApiError> { 27 + auth.0.require_user()?.require_active()?.require_admin()?; 28 + 27 29 if let Some(codes) = &input.codes 28 30 && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await 29 31 { ··· 40 42 error!("DB error disabling invite codes by account: {:?}", e); 41 43 } 42 44 } 43 - EmptyResponse::ok().into_response() 45 + Ok(EmptyResponse::ok().into_response()) 44 46 } 45 47 46 48 #[derive(Deserialize)] ··· 78 80 79 81 pub async fn get_invite_codes( 80 82 State(state): State<AppState>, 81 - _auth: BearerAuthAdmin, 83 + auth: RequiredAuth, 82 84 Query(params): Query<GetInviteCodesParams>, 83 - ) -> Response { 85 + ) -> Result<Response, ApiError> { 86 + auth.0.require_user()?.require_active()?.require_admin()?; 87 + 84 88 let limit = params.limit.unwrap_or(100).clamp(1, 500); 85 89 let sort_order = match params.sort.as_deref() { 86 90 Some("usage") => InviteCodeSortOrder::Usage, 87 91 _ => InviteCodeSortOrder::Recent, 88 92 }; 89 93 90 - let codes_rows = match state 94 + let codes_rows = state 91 95 .infra_repo 92 96 .list_invite_codes(params.cursor.as_deref(), limit, sort_order) 93 97 .await 94 - { 95 - Ok(rows) => rows, 96 - Err(e) => { 98 + .map_err(|e| { 97 99 error!("DB error fetching invite codes: {:?}", e); 98 - return ApiError::InternalError(None).into_response(); 99 - } 100 - }; 100 + ApiError::InternalError(None) 101 + })?; 101 102 102 103 let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect(); 103 104 let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect(); ··· 155 156 } else { 156 157 None 157 158 }; 158 - ( 159 + Ok(( 159 160 StatusCode::OK, 160 161 Json(GetInviteCodesOutput { 161 162 cursor: next_cursor, 162 163 codes, 163 164 }), 164 165 ) 165 - .into_response() 166 + .into_response()) 166 167 } 167 168 168 169 #[derive(Deserialize)] ··· 172 173 173 174 pub async fn disable_account_invites( 174 175 State(state): State<AppState>, 175 - _auth: BearerAuthAdmin, 176 + auth: RequiredAuth, 176 177 Json(input): Json<DisableAccountInvitesInput>, 177 - ) -> Response { 178 + ) -> Result<Response, ApiError> { 179 + auth.0.require_user()?.require_active()?.require_admin()?; 180 + 178 181 let account = input.account.trim(); 179 182 if account.is_empty() { 180 - return ApiError::InvalidRequest("account is required".into()).into_response(); 183 + return Err(ApiError::InvalidRequest("account is required".into())); 181 184 } 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 - }; 185 + let account_did: tranquil_types::Did = account 186 + .parse() 187 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 188 + 186 189 match state 187 190 .user_repo 188 191 .set_invites_disabled(&account_did, true) 189 192 .await 190 193 { 191 - Ok(true) => EmptyResponse::ok().into_response(), 192 - Ok(false) => ApiError::AccountNotFound.into_response(), 194 + Ok(true) => Ok(EmptyResponse::ok().into_response()), 195 + Ok(false) => Err(ApiError::AccountNotFound), 193 196 Err(e) => { 194 197 error!("DB error disabling account invites: {:?}", e); 195 - ApiError::InternalError(None).into_response() 198 + Err(ApiError::InternalError(None)) 196 199 } 197 200 } 198 201 } ··· 204 207 205 208 pub async fn enable_account_invites( 206 209 State(state): State<AppState>, 207 - _auth: BearerAuthAdmin, 210 + auth: RequiredAuth, 208 211 Json(input): Json<EnableAccountInvitesInput>, 209 - ) -> Response { 212 + ) -> Result<Response, ApiError> { 213 + auth.0.require_user()?.require_active()?.require_admin()?; 214 + 210 215 let account = input.account.trim(); 211 216 if account.is_empty() { 212 - return ApiError::InvalidRequest("account is required".into()).into_response(); 217 + return Err(ApiError::InvalidRequest("account is required".into())); 213 218 } 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 - }; 219 + let account_did: tranquil_types::Did = account 220 + .parse() 221 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 222 + 218 223 match state 219 224 .user_repo 220 225 .set_invites_disabled(&account_did, false) 221 226 .await 222 227 { 223 - Ok(true) => EmptyResponse::ok().into_response(), 224 - Ok(false) => ApiError::AccountNotFound.into_response(), 228 + Ok(true) => Ok(EmptyResponse::ok().into_response()), 229 + Ok(false) => Err(ApiError::AccountNotFound), 225 230 Err(e) => { 226 231 error!("DB error enabling account invites: {:?}", e); 227 - ApiError::InternalError(None).into_response() 232 + Err(ApiError::InternalError(None)) 228 233 } 229 234 } 230 235 }
+10 -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::RequiredAuth; 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: RequiredAuth, 23 + ) -> Result<Response, ApiError> { 24 + auth.0.require_user()?.require_active()?.require_admin()?; 25 + 20 26 let user_count = state.user_repo.count_users().await.unwrap_or(0); 21 27 let repo_count = state.repo_repo.count_repos().await.unwrap_or(0); 22 28 let record_count = state.repo_repo.count_all_records().await.unwrap_or(0); 23 29 let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0); 24 30 25 - Json(ServerStatsResponse { 31 + Ok(Json(ServerStatsResponse { 26 32 user_count, 27 33 repo_count, 28 34 record_count, 29 35 blob_storage_bytes, 30 36 }) 31 - .into_response() 37 + .into_response()) 32 38 }
+77 -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::RequiredAuth; 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: RequiredAuth, 39 39 Query(params): Query<GetSubjectStatusParams>, 40 - ) -> Response { 40 + ) -> Result<Response, ApiError> { 41 + auth.0.require_user()?.require_active()?.require_admin()?; 42 + 41 43 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(); 44 + return Err(ApiError::InvalidRequest( 45 + "Must provide did, uri, or blob".into(), 46 + )); 43 47 } 44 48 if let Some(did_str) = &params.did { 45 - let did: Did = match did_str.parse() { 46 - Ok(d) => d, 47 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 48 - }; 49 + let did: Did = did_str 50 + .parse() 51 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 49 52 match state.user_repo.get_status_by_did(&did).await { 50 53 Ok(Some(status)) => { 51 54 let deactivated = status.deactivated_at.map(|_| StatusAttr { ··· 56 59 applied: true, 57 60 r#ref: Some(r.clone()), 58 61 }); 59 - return ( 62 + return Ok(( 60 63 StatusCode::OK, 61 64 Json(SubjectStatus { 62 65 subject: json!({ ··· 67 70 deactivated, 68 71 }), 69 72 ) 70 - .into_response(); 73 + .into_response()); 71 74 } 72 75 Ok(None) => { 73 - return ApiError::SubjectNotFound.into_response(); 76 + return Err(ApiError::SubjectNotFound); 74 77 } 75 78 Err(e) => { 76 79 error!("DB error in get_subject_status: {:?}", e); 77 - return ApiError::InternalError(None).into_response(); 80 + return Err(ApiError::InternalError(None)); 78 81 } 79 82 } 80 83 } 81 84 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 - }; 85 + let cid: CidLink = uri_str 86 + .parse() 87 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 86 88 match state.repo_repo.get_record_by_cid(&cid).await { 87 89 Ok(Some(record)) => { 88 90 let takedown = record.takedown_ref.as_ref().map(|r| StatusAttr { 89 91 applied: true, 90 92 r#ref: Some(r.clone()), 91 93 }); 92 - return ( 94 + return Ok(( 93 95 StatusCode::OK, 94 96 Json(SubjectStatus { 95 97 subject: json!({ ··· 101 103 deactivated: None, 102 104 }), 103 105 ) 104 - .into_response(); 106 + .into_response()); 105 107 } 106 108 Ok(None) => { 107 - return ApiError::RecordNotFound.into_response(); 109 + return Err(ApiError::RecordNotFound); 108 110 } 109 111 Err(e) => { 110 112 error!("DB error in get_subject_status: {:?}", e); 111 - return ApiError::InternalError(None).into_response(); 113 + return Err(ApiError::InternalError(None)); 112 114 } 113 115 } 114 116 } 115 117 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 - }; 118 + let blob_cid: CidLink = blob_cid_str 119 + .parse() 120 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 121 + let did = params.did.as_ref().ok_or_else(|| { 122 + ApiError::InvalidRequest("Must provide a did to request blob state".into()) 123 + })?; 127 124 match state.blob_repo.get_blob_with_takedown(&blob_cid).await { 128 125 Ok(Some(blob)) => { 129 126 let takedown = blob.takedown_ref.as_ref().map(|r| StatusAttr { 130 127 applied: true, 131 128 r#ref: Some(r.clone()), 132 129 }); 133 - return ( 130 + return Ok(( 134 131 StatusCode::OK, 135 132 Json(SubjectStatus { 136 133 subject: json!({ ··· 142 139 deactivated: None, 143 140 }), 144 141 ) 145 - .into_response(); 142 + .into_response()); 146 143 } 147 144 Ok(None) => { 148 - return ApiError::BlobNotFound(None).into_response(); 145 + return Err(ApiError::BlobNotFound(None)); 149 146 } 150 147 Err(e) => { 151 148 error!("DB error in get_subject_status: {:?}", e); 152 - return ApiError::InternalError(None).into_response(); 149 + return Err(ApiError::InternalError(None)); 153 150 } 154 151 } 155 152 } 156 - ApiError::InvalidRequest("Invalid subject type".into()).into_response() 153 + Err(ApiError::InvalidRequest("Invalid subject type".into())) 157 154 } 158 155 159 156 #[derive(Deserialize)] ··· 172 169 173 170 pub async fn update_subject_status( 174 171 State(state): State<AppState>, 175 - _auth: BearerAuthAdmin, 172 + auth: RequiredAuth, 176 173 Json(input): Json<UpdateSubjectStatusInput>, 177 - ) -> Response { 174 + ) -> Result<Response, ApiError> { 175 + auth.0.require_user()?.require_active()?.require_admin()?; 176 + 178 177 let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 179 178 match subject_type { 180 179 Some("com.atproto.admin.defs#repoRef") => { ··· 187 186 } else { 188 187 None 189 188 }; 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 - } 189 + state 190 + .user_repo 191 + .set_user_takedown(&did, takedown_ref) 192 + .await 193 + .map_err(|e| { 194 + error!("Failed to update user takedown status for {}: {:?}", did, e); 195 + ApiError::InternalError(Some("Failed to update takedown status".into())) 196 + })?; 197 197 } 198 198 if let Some(deactivated) = &input.deactivated { 199 199 let result = if deactivated.applied { ··· 201 201 } else { 202 202 state.user_repo.activate_account(&did).await 203 203 }; 204 - if let Err(e) = result { 204 + result.map_err(|e| { 205 205 error!( 206 206 "Failed to update user deactivation status for {}: {:?}", 207 207 did, e 208 208 ); 209 - return ApiError::InternalError(Some( 210 - "Failed to update deactivation status".into(), 211 - )) 212 - .into_response(); 213 - } 209 + ApiError::InternalError(Some("Failed to update deactivation status".into())) 210 + })?; 214 211 } 215 212 if let Some(takedown) = &input.takedown { 216 213 let status = if takedown.applied { ··· 249 246 if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await { 250 247 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 251 248 } 252 - return ( 249 + return Ok(( 253 250 StatusCode::OK, 254 251 Json(json!({ 255 252 "subject": input.subject, ··· 262 259 })) 263 260 })), 264 261 ) 265 - .into_response(); 262 + .into_response()); 266 263 } 267 264 } 268 265 Some("com.atproto.repo.strongRef") => { 269 266 let uri_str = input.subject.get("uri").and_then(|u| u.as_str()); 270 267 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 - }; 268 + let cid: CidLink = uri_str 269 + .parse() 270 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 278 271 if let Some(takedown) = &input.takedown { 279 272 let takedown_ref = if takedown.applied { 280 273 takedown.r#ref.as_deref() 281 274 } else { 282 275 None 283 276 }; 284 - if let Err(e) = state 277 + state 285 278 .repo_repo 286 279 .set_record_takedown(&cid, takedown_ref) 287 280 .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 - } 281 + .map_err(|e| { 282 + error!( 283 + "Failed to update record takedown status for {}: {:?}", 284 + uri_str, e 285 + ); 286 + ApiError::InternalError(Some("Failed to update takedown status".into())) 287 + })?; 298 288 } 299 - return ( 289 + return Ok(( 300 290 StatusCode::OK, 301 291 Json(json!({ 302 292 "subject": input.subject, ··· 306 296 })) 307 297 })), 308 298 ) 309 - .into_response(); 299 + .into_response()); 310 300 } 311 301 } 312 302 Some("com.atproto.admin.defs#repoBlobRef") => { 313 303 let cid_str = input.subject.get("cid").and_then(|c| c.as_str()); 314 304 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 - }; 305 + let cid: CidLink = cid_str 306 + .parse() 307 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 322 308 if let Some(takedown) = &input.takedown { 323 309 let takedown_ref = if takedown.applied { 324 310 takedown.r#ref.as_deref() 325 311 } else { 326 312 None 327 313 }; 328 - if let Err(e) = state 314 + state 329 315 .blob_repo 330 316 .update_blob_takedown(&cid, takedown_ref) 331 317 .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 - } 318 + .map_err(|e| { 319 + error!( 320 + "Failed to update blob takedown status for {}: {:?}", 321 + cid_str, e 322 + ); 323 + ApiError::InternalError(Some("Failed to update takedown status".into())) 324 + })?; 342 325 } 343 - return ( 326 + return Ok(( 344 327 StatusCode::OK, 345 328 Json(json!({ 346 329 "subject": input.subject, ··· 350 333 })) 351 334 })), 352 335 ) 353 - .into_response(); 336 + .into_response()); 354 337 } 355 338 } 356 339 _ => {} 357 340 } 358 - ApiError::InvalidRequest("Invalid subject type".into()).into_response() 341 + Err(ApiError::InvalidRequest("Invalid subject type".into())) 359 342 }
+99 -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::RequiredAuth; 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: RequiredAuth, 41 + ) -> Result<Response, crate::api::error::ApiError> { 42 + let user = auth.0.require_user()?.require_active()?; 43 + let (user_id, backup_enabled) = match state.backup_repo.get_user_backup_status(&user.did).await 44 + { 45 + Ok(Some(status)) => status, 46 + Ok(None) => { 47 + return Ok(ApiError::AccountNotFound.into_response()); 48 + } 49 + Err(e) => { 50 + error!("DB error fetching user: {:?}", e); 51 + return Ok(ApiError::InternalError(None).into_response()); 52 + } 53 + }; 50 54 51 55 let backups = match state.backup_repo.list_backups_for_user(user_id).await { 52 56 Ok(rows) => rows, 53 57 Err(e) => { 54 58 error!("DB error fetching backups: {:?}", e); 55 - return ApiError::InternalError(None).into_response(); 59 + return Ok(ApiError::InternalError(None).into_response()); 56 60 } 57 61 }; 58 62 ··· 68 72 }) 69 73 .collect(); 70 74 71 - ( 75 + Ok(( 72 76 StatusCode::OK, 73 77 Json(ListBackupsOutput { 74 78 backups: backup_list, 75 79 backup_enabled, 76 80 }), 77 81 ) 78 - .into_response() 82 + .into_response()) 79 83 } 80 84 81 85 #[derive(Deserialize)] ··· 85 89 86 90 pub async fn get_backup( 87 91 State(state): State<AppState>, 88 - auth: BearerAuth, 92 + auth: RequiredAuth, 89 93 Query(query): Query<GetBackupQuery>, 90 - ) -> Response { 94 + ) -> Result<Response, crate::api::error::ApiError> { 95 + let user = auth.0.require_user()?.require_active()?; 91 96 let backup_id = match uuid::Uuid::parse_str(&query.id) { 92 97 Ok(id) => id, 93 98 Err(_) => { 94 - return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); 99 + return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response()); 95 100 } 96 101 }; 97 102 98 103 let backup_info = match state 99 104 .backup_repo 100 - .get_backup_storage_info(backup_id, &auth.0.did) 105 + .get_backup_storage_info(backup_id, &user.did) 101 106 .await 102 107 { 103 108 Ok(Some(b)) => b, 104 109 Ok(None) => { 105 - return ApiError::BackupNotFound.into_response(); 110 + return Ok(ApiError::BackupNotFound.into_response()); 106 111 } 107 112 Err(e) => { 108 113 error!("DB error fetching backup: {:?}", e); 109 - return ApiError::InternalError(None).into_response(); 114 + return Ok(ApiError::InternalError(None).into_response()); 110 115 } 111 116 }; 112 117 113 118 let backup_storage = match state.backup_storage.as_ref() { 114 119 Some(storage) => storage, 115 120 None => { 116 - return ApiError::BackupsDisabled.into_response(); 121 + return Ok(ApiError::BackupsDisabled.into_response()); 117 122 } 118 123 }; 119 124 ··· 121 126 Ok(bytes) => bytes, 122 127 Err(e) => { 123 128 error!("Failed to fetch backup from storage: {:?}", e); 124 - return ApiError::InternalError(Some("Failed to retrieve backup".into())) 125 - .into_response(); 129 + return Ok( 130 + ApiError::InternalError(Some("Failed to retrieve backup".into())).into_response(), 131 + ); 126 132 } 127 133 }; 128 134 129 - ( 135 + Ok(( 130 136 StatusCode::OK, 131 137 [ 132 138 (axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car"), ··· 137 143 ], 138 144 car_bytes, 139 145 ) 140 - .into_response() 146 + .into_response()) 141 147 } 142 148 143 149 #[derive(Serialize)] ··· 149 155 pub block_count: i32, 150 156 } 151 157 152 - pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> Response { 158 + pub async fn create_backup( 159 + State(state): State<AppState>, 160 + auth: RequiredAuth, 161 + ) -> Result<Response, crate::api::error::ApiError> { 162 + let auth_user = auth.0.require_user()?.require_active()?; 153 163 let backup_storage = match state.backup_storage.as_ref() { 154 164 Some(storage) => storage, 155 165 None => { 156 - return ApiError::BackupsDisabled.into_response(); 166 + return Ok(ApiError::BackupsDisabled.into_response()); 157 167 } 158 168 }; 159 169 160 - let user = match state.backup_repo.get_user_for_backup(&auth.0.did).await { 170 + let user = match state.backup_repo.get_user_for_backup(&auth_user.did).await { 161 171 Ok(Some(u)) => u, 162 172 Ok(None) => { 163 - return ApiError::AccountNotFound.into_response(); 173 + return Ok(ApiError::AccountNotFound.into_response()); 164 174 } 165 175 Err(e) => { 166 176 error!("DB error fetching user: {:?}", e); 167 - return ApiError::InternalError(None).into_response(); 177 + return Ok(ApiError::InternalError(None).into_response()); 168 178 } 169 179 }; 170 180 171 181 if user.deactivated_at.is_some() { 172 - return ApiError::AccountDeactivated.into_response(); 182 + return Ok(ApiError::AccountDeactivated.into_response()); 173 183 } 174 184 175 185 let repo_rev = match &user.repo_rev { 176 186 Some(rev) => rev.clone(), 177 187 None => { 178 - return ApiError::RepoNotReady.into_response(); 188 + return Ok(ApiError::RepoNotReady.into_response()); 179 189 } 180 190 }; 181 191 182 192 let head_cid = match Cid::from_str(&user.repo_root_cid) { 183 193 Ok(c) => c, 184 194 Err(_) => { 185 - return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(); 195 + return Ok( 196 + ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(), 197 + ); 186 198 } 187 199 }; 188 200 ··· 197 209 Ok(bytes) => bytes, 198 210 Err(e) => { 199 211 error!("Failed to generate CAR: {:?}", e); 200 - return ApiError::InternalError(Some("Failed to generate backup".into())) 201 - .into_response(); 212 + return Ok( 213 + ApiError::InternalError(Some("Failed to generate backup".into())).into_response(), 214 + ); 202 215 } 203 216 }; 204 217 ··· 212 225 Ok(key) => key, 213 226 Err(e) => { 214 227 error!("Failed to upload backup: {:?}", e); 215 - return ApiError::InternalError(Some("Failed to store backup".into())).into_response(); 228 + return Ok( 229 + ApiError::InternalError(Some("Failed to store backup".into())).into_response(), 230 + ); 216 231 } 217 232 }; 218 233 ··· 238 253 "Failed to rollback orphaned backup from S3" 239 254 ); 240 255 } 241 - return ApiError::InternalError(Some("Failed to record backup".into())).into_response(); 256 + return Ok( 257 + ApiError::InternalError(Some("Failed to record backup".into())).into_response(), 258 + ); 242 259 } 243 260 }; 244 261 ··· 261 278 warn!(did = %user.did, error = %e, "Failed to cleanup old backups after manual backup"); 262 279 } 263 280 264 - ( 281 + Ok(( 265 282 StatusCode::OK, 266 283 Json(CreateBackupOutput { 267 284 id: backup_id.to_string(), ··· 270 287 block_count, 271 288 }), 272 289 ) 273 - .into_response() 290 + .into_response()) 274 291 } 275 292 276 293 async fn cleanup_old_backups( ··· 310 327 311 328 pub async fn delete_backup( 312 329 State(state): State<AppState>, 313 - auth: BearerAuth, 330 + auth: RequiredAuth, 314 331 Query(query): Query<DeleteBackupQuery>, 315 - ) -> Response { 332 + ) -> Result<Response, crate::api::error::ApiError> { 333 + let user = auth.0.require_user()?.require_active()?; 316 334 let backup_id = match uuid::Uuid::parse_str(&query.id) { 317 335 Ok(id) => id, 318 336 Err(_) => { 319 - return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); 337 + return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response()); 320 338 } 321 339 }; 322 340 323 341 let backup = match state 324 342 .backup_repo 325 - .get_backup_for_deletion(backup_id, &auth.0.did) 343 + .get_backup_for_deletion(backup_id, &user.did) 326 344 .await 327 345 { 328 346 Ok(Some(b)) => b, 329 347 Ok(None) => { 330 - return ApiError::BackupNotFound.into_response(); 348 + return Ok(ApiError::BackupNotFound.into_response()); 331 349 } 332 350 Err(e) => { 333 351 error!("DB error fetching backup: {:?}", e); 334 - return ApiError::InternalError(None).into_response(); 352 + return Ok(ApiError::InternalError(None).into_response()); 335 353 } 336 354 }; 337 355 338 356 if backup.deactivated_at.is_some() { 339 - return ApiError::AccountDeactivated.into_response(); 357 + return Ok(ApiError::AccountDeactivated.into_response()); 340 358 } 341 359 342 360 if let Some(backup_storage) = state.backup_storage.as_ref() ··· 351 369 352 370 if let Err(e) = state.backup_repo.delete_backup(backup.id).await { 353 371 error!("DB error deleting backup: {:?}", e); 354 - return ApiError::InternalError(Some("Failed to delete backup".into())).into_response(); 372 + return Ok(ApiError::InternalError(Some("Failed to delete backup".into())).into_response()); 355 373 } 356 374 357 - info!(did = %auth.0.did, backup_id = %backup_id, "Deleted backup"); 375 + info!(did = %user.did, backup_id = %backup_id, "Deleted backup"); 358 376 359 - EmptyResponse::ok().into_response() 377 + Ok(EmptyResponse::ok().into_response()) 360 378 } 361 379 362 380 #[derive(Deserialize)] ··· 367 385 368 386 pub async fn set_backup_enabled( 369 387 State(state): State<AppState>, 370 - auth: BearerAuth, 388 + auth: RequiredAuth, 371 389 Json(input): Json<SetBackupEnabledInput>, 372 - ) -> Response { 390 + ) -> Result<Response, crate::api::error::ApiError> { 391 + let user = auth.0.require_user()?.require_active()?; 373 392 let deactivated_at = match state 374 393 .backup_repo 375 - .get_user_deactivated_status(&auth.0.did) 394 + .get_user_deactivated_status(&user.did) 376 395 .await 377 396 { 378 397 Ok(Some(status)) => status, 379 398 Ok(None) => { 380 - return ApiError::AccountNotFound.into_response(); 399 + return Ok(ApiError::AccountNotFound.into_response()); 381 400 } 382 401 Err(e) => { 383 402 error!("DB error fetching user: {:?}", e); 384 - return ApiError::InternalError(None).into_response(); 403 + return Ok(ApiError::InternalError(None).into_response()); 385 404 } 386 405 }; 387 406 388 407 if deactivated_at.is_some() { 389 - return ApiError::AccountDeactivated.into_response(); 408 + return Ok(ApiError::AccountDeactivated.into_response()); 390 409 } 391 410 392 411 if let Err(e) = state 393 412 .backup_repo 394 - .update_backup_enabled(&auth.0.did, input.enabled) 413 + .update_backup_enabled(&user.did, input.enabled) 395 414 .await 396 415 { 397 416 error!("DB error updating backup_enabled: {:?}", e); 398 - return ApiError::InternalError(Some("Failed to update setting".into())).into_response(); 417 + return Ok( 418 + ApiError::InternalError(Some("Failed to update setting".into())).into_response(), 419 + ); 399 420 } 400 421 401 - info!(did = %auth.0.did, enabled = input.enabled, "Updated backup_enabled setting"); 422 + info!(did = %user.did, enabled = input.enabled, "Updated backup_enabled setting"); 402 423 403 - EnabledResponse::response(input.enabled).into_response() 424 + Ok(EnabledResponse::response(input.enabled).into_response()) 404 425 } 405 426 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 { 427 + pub async fn export_blobs( 428 + State(state): State<AppState>, 429 + auth: RequiredAuth, 430 + ) -> Result<Response, crate::api::error::ApiError> { 431 + let user = auth.0.require_user()?.require_active()?; 432 + let user_id = match state.backup_repo.get_user_id_by_did(&user.did).await { 408 433 Ok(Some(id)) => id, 409 434 Ok(None) => { 410 - return ApiError::AccountNotFound.into_response(); 435 + return Ok(ApiError::AccountNotFound.into_response()); 411 436 } 412 437 Err(e) => { 413 438 error!("DB error fetching user: {:?}", e); 414 - return ApiError::InternalError(None).into_response(); 439 + return Ok(ApiError::InternalError(None).into_response()); 415 440 } 416 441 }; 417 442 ··· 419 444 Ok(rows) => rows, 420 445 Err(e) => { 421 446 error!("DB error fetching blobs: {:?}", e); 422 - return ApiError::InternalError(None).into_response(); 447 + return Ok(ApiError::InternalError(None).into_response()); 423 448 } 424 449 }; 425 450 426 451 if blobs.is_empty() { 427 - return ( 452 + return Ok(( 428 453 StatusCode::OK, 429 454 [ 430 455 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 435 460 ], 436 461 Vec::<u8>::new(), 437 462 ) 438 - .into_response(); 463 + .into_response()); 439 464 } 440 465 441 466 let mut zip_buffer = std::io::Cursor::new(Vec::new()); ··· 513 538 514 539 if let Err(e) = zip.finish() { 515 540 error!("Failed to finish zip: {:?}", e); 516 - return ApiError::InternalError(Some("Failed to create zip file".into())) 517 - .into_response(); 541 + return Ok( 542 + ApiError::InternalError(Some("Failed to create zip file".into())).into_response(), 543 + ); 518 544 } 519 545 } 520 546 521 547 let zip_bytes = zip_buffer.into_inner(); 522 548 523 - info!(did = %auth.0.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); 549 + info!(did = %user.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); 524 550 525 - ( 551 + Ok(( 526 552 StatusCode::OK, 527 553 [ 528 554 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 533 559 ], 534 560 zip_bytes, 535 561 ) 536 - .into_response() 562 + .into_response()) 537 563 } 538 564 539 565 fn mime_to_extension(mime_type: &str) -> &'static str {
+122 -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::RequiredAuth; 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: RequiredAuth, 39 + ) -> Result<Response, ApiError> { 40 + let user = auth.0.require_user()?.require_active()?; 37 41 let controllers = match state 38 42 .delegation_repo 39 - .get_delegations_for_account(&auth.0.did) 43 + .get_delegations_for_account(&user.did) 40 44 .await 41 45 { 42 46 Ok(c) => c, 43 47 Err(e) => { 44 48 tracing::error!("Failed to list controllers: {:?}", e); 45 - return ApiError::InternalError(Some("Failed to list controllers".into())) 46 - .into_response(); 49 + return Ok( 50 + ApiError::InternalError(Some("Failed to list controllers".into())).into_response(), 51 + ); 47 52 } 48 53 }; 49 54 50 - Json(ListControllersResponse { 55 + Ok(Json(ListControllersResponse { 51 56 controllers: controllers 52 57 .into_iter() 53 58 .map(|c| ControllerInfo { ··· 59 64 }) 60 65 .collect(), 61 66 }) 62 - .into_response() 67 + .into_response()) 63 68 } 64 69 65 70 #[derive(Debug, Deserialize)] ··· 70 75 71 76 pub async fn add_controller( 72 77 State(state): State<AppState>, 73 - auth: BearerAuth, 78 + auth: RequiredAuth, 74 79 Json(input): Json<AddControllerInput>, 75 - ) -> Response { 80 + ) -> Result<Response, ApiError> { 81 + let user = auth.0.require_user()?.require_active()?; 76 82 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 77 - return ApiError::InvalidScopes(e).into_response(); 83 + return Ok(ApiError::InvalidScopes(e).into_response()); 78 84 } 79 85 80 86 let controller_exists = state ··· 86 92 .is_some(); 87 93 88 94 if !controller_exists { 89 - return ApiError::ControllerNotFound.into_response(); 95 + return Ok(ApiError::ControllerNotFound.into_response()); 90 96 } 91 97 92 - match state 93 - .delegation_repo 94 - .controls_any_accounts(&auth.0.did) 95 - .await 96 - { 98 + match state.delegation_repo.controls_any_accounts(&user.did).await { 97 99 Ok(true) => { 98 - return ApiError::InvalidDelegation( 100 + return Ok(ApiError::InvalidDelegation( 99 101 "Cannot add controllers to an account that controls other accounts".into(), 100 102 ) 101 - .into_response(); 103 + .into_response()); 102 104 } 103 105 Err(e) => { 104 106 tracing::error!("Failed to check delegation status: {:?}", e); 105 - return ApiError::InternalError(Some("Failed to verify delegation status".into())) 106 - .into_response(); 107 + return Ok( 108 + ApiError::InternalError(Some("Failed to verify delegation status".into())) 109 + .into_response(), 110 + ); 107 111 } 108 112 Ok(false) => {} 109 113 } ··· 114 118 .await 115 119 { 116 120 Ok(true) => { 117 - return ApiError::InvalidDelegation( 121 + return Ok(ApiError::InvalidDelegation( 118 122 "Cannot add a controlled account as a controller".into(), 119 123 ) 120 - .into_response(); 124 + .into_response()); 121 125 } 122 126 Err(e) => { 123 127 tracing::error!("Failed to check controller status: {:?}", e); 124 - return ApiError::InternalError(Some("Failed to verify controller status".into())) 125 - .into_response(); 128 + return Ok( 129 + ApiError::InternalError(Some("Failed to verify controller status".into())) 130 + .into_response(), 131 + ); 126 132 } 127 133 Ok(false) => {} 128 134 } ··· 130 136 match state 131 137 .delegation_repo 132 138 .create_delegation( 133 - &auth.0.did, 139 + &user.did, 134 140 &input.controller_did, 135 141 &input.granted_scopes, 136 - &auth.0.did, 142 + &user.did, 137 143 ) 138 144 .await 139 145 { ··· 141 147 let _ = state 142 148 .delegation_repo 143 149 .log_delegation_action( 144 - &auth.0.did, 145 - &auth.0.did, 150 + &user.did, 151 + &user.did, 146 152 Some(&input.controller_did), 147 153 DelegationActionType::GrantCreated, 148 154 Some(serde_json::json!({ ··· 153 159 ) 154 160 .await; 155 161 156 - ( 162 + Ok(( 157 163 StatusCode::OK, 158 164 Json(serde_json::json!({ 159 165 "success": true 160 166 })), 161 167 ) 162 - .into_response() 168 + .into_response()) 163 169 } 164 170 Err(e) => { 165 171 tracing::error!("Failed to add controller: {:?}", e); 166 - ApiError::InternalError(Some("Failed to add controller".into())).into_response() 172 + Ok(ApiError::InternalError(Some("Failed to add controller".into())).into_response()) 167 173 } 168 174 } 169 175 } ··· 175 181 176 182 pub async fn remove_controller( 177 183 State(state): State<AppState>, 178 - auth: BearerAuth, 184 + auth: RequiredAuth, 179 185 Json(input): Json<RemoveControllerInput>, 180 - ) -> Response { 186 + ) -> Result<Response, ApiError> { 187 + let user = auth.0.require_user()?.require_active()?; 181 188 match state 182 189 .delegation_repo 183 - .revoke_delegation(&auth.0.did, &input.controller_did, &auth.0.did) 190 + .revoke_delegation(&user.did, &input.controller_did, &user.did) 184 191 .await 185 192 { 186 193 Ok(true) => { 187 194 let revoked_app_passwords = state 188 195 .session_repo 189 - .delete_app_passwords_by_controller(&auth.0.did, &input.controller_did) 196 + .delete_app_passwords_by_controller(&user.did, &input.controller_did) 190 197 .await 191 198 .unwrap_or(0) as usize; 192 199 193 200 let revoked_oauth_tokens = state 194 201 .oauth_repo 195 - .revoke_tokens_for_controller(&auth.0.did, &input.controller_did) 202 + .revoke_tokens_for_controller(&user.did, &input.controller_did) 196 203 .await 197 204 .unwrap_or(0); 198 205 199 206 let _ = state 200 207 .delegation_repo 201 208 .log_delegation_action( 202 - &auth.0.did, 203 - &auth.0.did, 209 + &user.did, 210 + &user.did, 204 211 Some(&input.controller_did), 205 212 DelegationActionType::GrantRevoked, 206 213 Some(serde_json::json!({ ··· 212 219 ) 213 220 .await; 214 221 215 - ( 222 + Ok(( 216 223 StatusCode::OK, 217 224 Json(serde_json::json!({ 218 225 "success": true 219 226 })), 220 227 ) 221 - .into_response() 228 + .into_response()) 222 229 } 223 - Ok(false) => ApiError::DelegationNotFound.into_response(), 230 + Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), 224 231 Err(e) => { 225 232 tracing::error!("Failed to remove controller: {:?}", e); 226 - ApiError::InternalError(Some("Failed to remove controller".into())).into_response() 233 + Ok(ApiError::InternalError(Some("Failed to remove controller".into())).into_response()) 227 234 } 228 235 } 229 236 } ··· 236 243 237 244 pub async fn update_controller_scopes( 238 245 State(state): State<AppState>, 239 - auth: BearerAuth, 246 + auth: RequiredAuth, 240 247 Json(input): Json<UpdateControllerScopesInput>, 241 - ) -> Response { 248 + ) -> Result<Response, ApiError> { 249 + let user = auth.0.require_user()?.require_active()?; 242 250 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 243 - return ApiError::InvalidScopes(e).into_response(); 251 + return Ok(ApiError::InvalidScopes(e).into_response()); 244 252 } 245 253 246 254 match state 247 255 .delegation_repo 248 - .update_delegation_scopes(&auth.0.did, &input.controller_did, &input.granted_scopes) 256 + .update_delegation_scopes(&user.did, &input.controller_did, &input.granted_scopes) 249 257 .await 250 258 { 251 259 Ok(true) => { 252 260 let _ = state 253 261 .delegation_repo 254 262 .log_delegation_action( 255 - &auth.0.did, 256 - &auth.0.did, 263 + &user.did, 264 + &user.did, 257 265 Some(&input.controller_did), 258 266 DelegationActionType::ScopesModified, 259 267 Some(serde_json::json!({ ··· 264 272 ) 265 273 .await; 266 274 267 - ( 275 + Ok(( 268 276 StatusCode::OK, 269 277 Json(serde_json::json!({ 270 278 "success": true 271 279 })), 272 280 ) 273 - .into_response() 281 + .into_response()) 274 282 } 275 - Ok(false) => ApiError::DelegationNotFound.into_response(), 283 + Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), 276 284 Err(e) => { 277 285 tracing::error!("Failed to update controller scopes: {:?}", e); 278 - ApiError::InternalError(Some("Failed to update controller scopes".into())) 279 - .into_response() 286 + Ok( 287 + ApiError::InternalError(Some("Failed to update controller scopes".into())) 288 + .into_response(), 289 + ) 280 290 } 281 291 } 282 292 } ··· 295 305 pub accounts: Vec<DelegatedAccountInfo>, 296 306 } 297 307 298 - pub async fn list_controlled_accounts(State(state): State<AppState>, auth: BearerAuth) -> Response { 308 + pub async fn list_controlled_accounts( 309 + State(state): State<AppState>, 310 + auth: RequiredAuth, 311 + ) -> Result<Response, ApiError> { 312 + let user = auth.0.require_user()?.require_active()?; 299 313 let accounts = match state 300 314 .delegation_repo 301 - .get_accounts_controlled_by(&auth.0.did) 315 + .get_accounts_controlled_by(&user.did) 302 316 .await 303 317 { 304 318 Ok(a) => a, 305 319 Err(e) => { 306 320 tracing::error!("Failed to list controlled accounts: {:?}", e); 307 - return ApiError::InternalError(Some("Failed to list controlled accounts".into())) 308 - .into_response(); 321 + return Ok( 322 + ApiError::InternalError(Some("Failed to list controlled accounts".into())) 323 + .into_response(), 324 + ); 309 325 } 310 326 }; 311 327 312 - Json(ListControlledAccountsResponse { 328 + Ok(Json(ListControlledAccountsResponse { 313 329 accounts: accounts 314 330 .into_iter() 315 331 .map(|a| DelegatedAccountInfo { ··· 320 336 }) 321 337 .collect(), 322 338 }) 323 - .into_response() 339 + .into_response()) 324 340 } 325 341 326 342 #[derive(Debug, Deserialize)] ··· 355 371 356 372 pub async fn get_audit_log( 357 373 State(state): State<AppState>, 358 - auth: BearerAuth, 374 + auth: RequiredAuth, 359 375 Query(params): Query<AuditLogParams>, 360 - ) -> Response { 376 + ) -> Result<Response, ApiError> { 377 + let user = auth.0.require_user()?.require_active()?; 361 378 let limit = params.limit.clamp(1, 100); 362 379 let offset = params.offset.max(0); 363 380 364 381 let entries = match state 365 382 .delegation_repo 366 - .get_audit_log_for_account(&auth.0.did, limit, offset) 383 + .get_audit_log_for_account(&user.did, limit, offset) 367 384 .await 368 385 { 369 386 Ok(e) => e, 370 387 Err(e) => { 371 388 tracing::error!("Failed to get audit log: {:?}", e); 372 - return ApiError::InternalError(Some("Failed to get audit log".into())).into_response(); 389 + return Ok( 390 + ApiError::InternalError(Some("Failed to get audit log".into())).into_response(), 391 + ); 373 392 } 374 393 }; 375 394 376 395 let total = state 377 396 .delegation_repo 378 - .count_audit_log_entries(&auth.0.did) 397 + .count_audit_log_entries(&user.did) 379 398 .await 380 399 .unwrap_or_default(); 381 400 382 - Json(GetAuditLogResponse { 401 + Ok(Json(GetAuditLogResponse { 383 402 entries: entries 384 403 .into_iter() 385 404 .map(|e| AuditLogEntry { ··· 394 413 .collect(), 395 414 total, 396 415 }) 397 - .into_response() 416 + .into_response()) 398 417 } 399 418 400 419 #[derive(Debug, Serialize)] ··· 444 463 pub async fn create_delegated_account( 445 464 State(state): State<AppState>, 446 465 headers: HeaderMap, 447 - auth: BearerAuth, 466 + auth: RequiredAuth, 448 467 Json(input): Json<CreateDelegatedAccountInput>, 449 - ) -> Response { 468 + ) -> Result<Response, ApiError> { 469 + let user = auth.0.require_user()?.require_active()?; 450 470 let client_ip = extract_client_ip(&headers); 451 471 if !state 452 472 .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 453 473 .await 454 474 { 455 475 warn!(ip = %client_ip, "Delegated account creation rate limit exceeded"); 456 - return ApiError::RateLimitExceeded(Some( 476 + return Ok(ApiError::RateLimitExceeded(Some( 457 477 "Too many account creation attempts. Please try again later.".into(), 458 478 )) 459 - .into_response(); 479 + .into_response()); 460 480 } 461 481 462 482 if let Err(e) = scopes::validate_delegation_scopes(&input.controller_scopes) { 463 - return ApiError::InvalidScopes(e).into_response(); 483 + return Ok(ApiError::InvalidScopes(e).into_response()); 464 484 } 465 485 466 - match state.delegation_repo.has_any_controllers(&auth.0.did).await { 486 + match state.delegation_repo.has_any_controllers(&user.did).await { 467 487 Ok(true) => { 468 - return ApiError::InvalidDelegation( 488 + return Ok(ApiError::InvalidDelegation( 469 489 "Cannot create delegated accounts from a controlled account".into(), 470 490 ) 471 - .into_response(); 491 + .into_response()); 472 492 } 473 493 Err(e) => { 474 494 tracing::error!("Failed to check controller status: {:?}", e); 475 - return ApiError::InternalError(Some("Failed to verify controller status".into())) 476 - .into_response(); 495 + return Ok( 496 + ApiError::InternalError(Some("Failed to verify controller status".into())) 497 + .into_response(), 498 + ); 477 499 } 478 500 Ok(false) => {} 479 501 } ··· 494 516 match crate::api::validation::validate_short_handle(handle_to_validate) { 495 517 Ok(h) => format!("{}.{}", h, hostname_for_handles), 496 518 Err(e) => { 497 - return ApiError::InvalidRequest(e.to_string()).into_response(); 519 + return Ok(ApiError::InvalidRequest(e.to_string()).into_response()); 498 520 } 499 521 } 500 522 } else { ··· 509 531 if let Some(ref email) = email 510 532 && !crate::api::validation::is_valid_email(email) 511 533 { 512 - return ApiError::InvalidEmail.into_response(); 534 + return Ok(ApiError::InvalidEmail.into_response()); 513 535 } 514 536 515 537 if let Some(ref code) = input.invite_code { ··· 520 542 .unwrap_or(false); 521 543 522 544 if !valid { 523 - return ApiError::InvalidInviteCode.into_response(); 545 + return Ok(ApiError::InvalidInviteCode.into_response()); 524 546 } 525 547 } else { 526 548 let invite_required = std::env::var("INVITE_CODE_REQUIRED") 527 549 .map(|v| v == "true" || v == "1") 528 550 .unwrap_or(false); 529 551 if invite_required { 530 - return ApiError::InviteCodeRequired.into_response(); 552 + return Ok(ApiError::InviteCodeRequired.into_response()); 531 553 } 532 554 } 533 555 ··· 542 564 Ok(k) => k, 543 565 Err(e) => { 544 566 error!("Error creating signing key: {:?}", e); 545 - return ApiError::InternalError(None).into_response(); 567 + return Ok(ApiError::InternalError(None).into_response()); 546 568 } 547 569 }; 548 570 ··· 558 580 Ok(r) => r, 559 581 Err(e) => { 560 582 error!("Error creating PLC genesis operation: {:?}", e); 561 - return ApiError::InternalError(Some("Failed to create PLC operation".into())) 562 - .into_response(); 583 + return Ok( 584 + ApiError::InternalError(Some("Failed to create PLC operation".into())) 585 + .into_response(), 586 + ); 563 587 } 564 588 }; 565 589 ··· 569 593 .await 570 594 { 571 595 error!("Failed to submit PLC genesis operation: {:?}", e); 572 - return ApiError::UpstreamErrorMsg(format!( 596 + return Ok(ApiError::UpstreamErrorMsg(format!( 573 597 "Failed to register DID with PLC directory: {}", 574 598 e 575 599 )) 576 - .into_response(); 600 + .into_response()); 577 601 } 578 602 579 603 let did = Did::new_unchecked(&genesis_result.did); 580 604 let handle = Handle::new_unchecked(&handle); 581 - info!(did = %did, handle = %handle, controller = %&auth.0.did, "Created DID for delegated account"); 605 + info!(did = %did, handle = %handle, controller = %&user.did, "Created DID for delegated account"); 582 606 583 607 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 584 608 Ok(bytes) => bytes, 585 609 Err(e) => { 586 610 error!("Error encrypting signing key: {:?}", e); 587 - return ApiError::InternalError(None).into_response(); 611 + return Ok(ApiError::InternalError(None).into_response()); 588 612 } 589 613 }; 590 614 ··· 593 617 Ok(c) => c, 594 618 Err(e) => { 595 619 error!("Error persisting MST: {:?}", e); 596 - return ApiError::InternalError(None).into_response(); 620 + return Ok(ApiError::InternalError(None).into_response()); 597 621 } 598 622 }; 599 623 let rev = Tid::now(LimitedU32::MIN); ··· 602 626 Ok(result) => result, 603 627 Err(e) => { 604 628 error!("Error creating genesis commit: {:?}", e); 605 - return ApiError::InternalError(None).into_response(); 629 + return Ok(ApiError::InternalError(None).into_response()); 606 630 } 607 631 }; 608 632 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 609 633 Ok(c) => c, 610 634 Err(e) => { 611 635 error!("Error saving genesis commit: {:?}", e); 612 - return ApiError::InternalError(None).into_response(); 636 + return Ok(ApiError::InternalError(None).into_response()); 613 637 } 614 638 }; 615 639 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; ··· 618 642 handle: handle.clone(), 619 643 email: email.clone(), 620 644 did: did.clone(), 621 - controller_did: auth.0.did.clone(), 645 + controller_did: user.did.clone(), 622 646 controller_scopes: input.controller_scopes.clone(), 623 647 encrypted_key_bytes, 624 648 encryption_version: crate::config::ENCRYPTION_VERSION, ··· 635 659 { 636 660 Ok(id) => id, 637 661 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { 638 - return ApiError::HandleNotAvailable(None).into_response(); 662 + return Ok(ApiError::HandleNotAvailable(None).into_response()); 639 663 } 640 664 Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { 641 - return ApiError::EmailTaken.into_response(); 665 + return Ok(ApiError::EmailTaken.into_response()); 642 666 } 643 667 Err(e) => { 644 668 error!("Error creating delegated account: {:?}", e); 645 - return ApiError::InternalError(None).into_response(); 669 + return Ok(ApiError::InternalError(None).into_response()); 646 670 } 647 671 }; 648 672 ··· 678 702 .delegation_repo 679 703 .log_delegation_action( 680 704 &did, 681 - &auth.0.did, 682 - Some(&auth.0.did), 705 + &user.did, 706 + Some(&user.did), 683 707 DelegationActionType::GrantCreated, 684 708 Some(json!({ 685 709 "account_created": true, ··· 690 714 ) 691 715 .await; 692 716 693 - info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created"); 717 + info!(did = %did, handle = %handle, controller = %&user.did, "Delegated account created"); 694 718 695 - Json(CreateDelegatedAccountResponse { did, handle }).into_response() 719 + Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response()) 696 720 }
+87 -99
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::RequiredAuth; 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: RequiredAuth, 522 + ) -> Result<Response, ApiError> { 523 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 524 + let handle = state 525 + .user_repo 526 + .get_handle_by_did(&auth_user.did) 527 + .await 528 + .map_err(|_| ApiError::InternalError(None))? 529 + .ok_or(ApiError::InternalError(None))?; 530 + 531 + let key_bytes = auth_user.key_bytes.clone().ok_or_else(|| { 532 + ApiError::AuthenticationFailed(Some("OAuth tokens cannot get DID credentials".into())) 533 + })?; 534 + 538 535 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 539 536 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 - }; 537 + let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes) 538 + .map_err(|_| ApiError::InternalError(None))?; 544 539 let did_key = signing_key_to_did_key(&signing_key); 545 540 let rotation_keys = if auth_user.did.starts_with("did:web:") { 546 541 vec![] ··· 556 551 }; 557 552 vec![server_rotation_key] 558 553 }; 559 - ( 554 + Ok(( 560 555 StatusCode::OK, 561 556 Json(GetRecommendedDidCredentialsOutput { 562 557 rotation_keys, ··· 570 565 }, 571 566 }), 572 567 ) 573 - .into_response() 568 + .into_response()) 574 569 } 575 570 576 571 #[derive(Deserialize)] ··· 580 575 581 576 pub async fn update_handle( 582 577 State(state): State<AppState>, 583 - auth: BearerAuthAllowDeactivated, 578 + auth: RequiredAuth, 584 579 Json(input): Json<UpdateHandleInput>, 585 - ) -> Response { 586 - let auth_user = auth.0; 580 + ) -> Result<Response, ApiError> { 581 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 587 582 if let Err(e) = crate::auth::scope_check::check_identity_scope( 588 583 auth_user.is_oauth, 589 584 auth_user.scope.as_deref(), 590 585 crate::oauth::scopes::IdentityAttr::Handle, 591 586 ) { 592 - return e; 587 + return Ok(e); 593 588 } 594 - let did = auth_user.did; 589 + let did = auth_user.did.clone(); 595 590 if !state 596 591 .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 597 592 .await 598 593 { 599 - return ApiError::RateLimitExceeded(Some( 594 + return Err(ApiError::RateLimitExceeded(Some( 600 595 "Too many handle updates. Try again later.".into(), 601 - )) 602 - .into_response(); 596 + ))); 603 597 } 604 598 if !state 605 599 .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 606 600 .await 607 601 { 608 - return ApiError::RateLimitExceeded(Some("Daily handle update limit exceeded.".into())) 609 - .into_response(); 602 + return Err(ApiError::RateLimitExceeded(Some( 603 + "Daily handle update limit exceeded.".into(), 604 + ))); 610 605 } 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 - }; 606 + let user_row = state 607 + .user_repo 608 + .get_id_and_handle_by_did(&did) 609 + .await 610 + .map_err(|_| ApiError::InternalError(None))? 611 + .ok_or(ApiError::InternalError(None))?; 616 612 let user_id = user_row.id; 617 613 let current_handle = user_row.handle; 618 614 let new_handle = input.handle.trim().to_ascii_lowercase(); 619 615 if new_handle.is_empty() { 620 - return ApiError::InvalidRequest("handle is required".into()).into_response(); 616 + return Err(ApiError::InvalidRequest("handle is required".into())); 621 617 } 622 618 if !new_handle 623 619 .chars() 624 620 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 625 621 { 626 - return ApiError::InvalidHandle(Some("Handle contains invalid characters".into())) 627 - .into_response(); 622 + return Err(ApiError::InvalidHandle(Some( 623 + "Handle contains invalid characters".into(), 624 + ))); 628 625 } 629 626 if new_handle.split('.').any(|segment| segment.is_empty()) { 630 - return ApiError::InvalidHandle(Some("Handle contains empty segment".into())) 631 - .into_response(); 627 + return Err(ApiError::InvalidHandle(Some( 628 + "Handle contains empty segment".into(), 629 + ))); 632 630 } 633 631 if new_handle 634 632 .split('.') 635 633 .any(|segment| segment.starts_with('-') || segment.ends_with('-')) 636 634 { 637 - return ApiError::InvalidHandle(Some( 635 + return Err(ApiError::InvalidHandle(Some( 638 636 "Handle segment cannot start or end with hyphen".into(), 639 - )) 640 - .into_response(); 637 + ))); 641 638 } 642 639 if crate::moderation::has_explicit_slur(&new_handle) { 643 - return ApiError::InvalidHandle(Some("Inappropriate language in handle".into())) 644 - .into_response(); 640 + return Err(ApiError::InvalidHandle(Some( 641 + "Inappropriate language in handle".into(), 642 + ))); 645 643 } 646 644 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 647 645 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 667 665 { 668 666 warn!("Failed to sequence identity event for handle update: {}", e); 669 667 } 670 - return EmptyResponse::ok().into_response(); 668 + return Ok(EmptyResponse::ok().into_response()); 671 669 } 672 670 if short_part.contains('.') { 673 - return ApiError::InvalidHandle(Some( 671 + return Err(ApiError::InvalidHandle(Some( 674 672 "Nested subdomains are not allowed. Use a simple handle without dots.".into(), 675 - )) 676 - .into_response(); 673 + ))); 677 674 } 678 675 if short_part.len() < 3 { 679 - return ApiError::InvalidHandle(Some("Handle too short".into())).into_response(); 676 + return Err(ApiError::InvalidHandle(Some("Handle too short".into()))); 680 677 } 681 678 if short_part.len() > 18 { 682 - return ApiError::InvalidHandle(Some("Handle too long".into())).into_response(); 679 + return Err(ApiError::InvalidHandle(Some("Handle too long".into()))); 683 680 } 684 681 full_handle 685 682 } else { ··· 691 688 { 692 689 warn!("Failed to sequence identity event for handle update: {}", e); 693 690 } 694 - return EmptyResponse::ok().into_response(); 691 + return Ok(EmptyResponse::ok().into_response()); 695 692 } 696 693 match crate::handle::verify_handle_ownership(&new_handle, &did).await { 697 694 Ok(()) => {} 698 695 Err(crate::handle::HandleResolutionError::NotFound) => { 699 - return ApiError::HandleNotAvailable(None).into_response(); 696 + return Err(ApiError::HandleNotAvailable(None)); 700 697 } 701 698 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { 702 - return ApiError::HandleNotAvailable(Some(format!( 699 + return Err(ApiError::HandleNotAvailable(Some(format!( 703 700 "Handle points to different DID. Expected {}, got {}", 704 701 expected, actual 705 - ))) 706 - .into_response(); 702 + )))); 707 703 } 708 704 Err(e) => { 709 705 warn!("Handle verification failed: {}", e); 710 - return ApiError::HandleNotAvailable(Some(format!( 706 + return Err(ApiError::HandleNotAvailable(Some(format!( 711 707 "Handle verification failed: {}", 712 708 e 713 - ))) 714 - .into_response(); 709 + )))); 715 710 } 716 711 } 717 712 new_handle.clone() 718 713 }; 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 714 + let handle_typed: Handle = handle 715 + .parse() 716 + .map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?; 717 + let handle_exists = state 726 718 .user_repo 727 719 .check_handle_exists(&handle_typed, user_id) 728 720 .await 729 - { 730 - Ok(exists) => exists, 731 - Err(_) => return ApiError::InternalError(None).into_response(), 732 - }; 721 + .map_err(|_| ApiError::InternalError(None))?; 733 722 if handle_exists { 734 - return ApiError::HandleTaken.into_response(); 723 + return Err(ApiError::HandleTaken); 735 724 } 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) => { 725 + state 726 + .user_repo 727 + .update_handle(user_id, &handle_typed) 728 + .await 729 + .map_err(|e| { 758 730 error!("DB error updating handle: {:?}", e); 759 - ApiError::InternalError(None).into_response() 760 - } 731 + ApiError::InternalError(None) 732 + })?; 733 + 734 + if !current_handle.is_empty() { 735 + let _ = state 736 + .cache 737 + .delete(&format!("handle:{}", current_handle)) 738 + .await; 739 + } 740 + let _ = state.cache.delete(&format!("handle:{}", handle)).await; 741 + if let Err(e) = 742 + crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)).await 743 + { 744 + warn!("Failed to sequence identity event for handle update: {}", e); 745 + } 746 + if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await { 747 + warn!("Failed to update PLC handle: {}", e); 761 748 } 749 + Ok(EmptyResponse::ok().into_response()) 762 750 } 763 751 764 752 pub async fn update_plc_handle(
+21 -18
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::RequiredAuth; 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: RequiredAuth, 19 + ) -> Result<Response, ApiError> { 20 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 21 21 if let Err(e) = crate::auth::scope_check::check_identity_scope( 22 22 auth_user.is_oauth, 23 23 auth_user.scope.as_deref(), 24 24 crate::oauth::scopes::IdentityAttr::Wildcard, 25 25 ) { 26 - return e; 26 + return Ok(e); 27 27 } 28 - let user_id = match state.user_repo.get_id_by_did(&auth_user.did).await { 29 - Ok(Some(id)) => id, 30 - Ok(None) => return ApiError::AccountNotFound.into_response(), 31 - Err(e) => { 28 + let user_id = state 29 + .user_repo 30 + .get_id_by_did(&auth_user.did) 31 + .await 32 + .map_err(|e| { 32 33 error!("DB error: {:?}", e); 33 - return ApiError::InternalError(None).into_response(); 34 - } 35 - }; 34 + ApiError::InternalError(None) 35 + })? 36 + .ok_or(ApiError::AccountNotFound)?; 37 + 36 38 let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await; 37 39 let plc_token = generate_plc_token(); 38 40 let expires_at = Utc::now() + Duration::minutes(10); 39 - if let Err(e) = state 41 + state 40 42 .infra_repo 41 43 .insert_plc_token(user_id, &plc_token, expires_at) 42 44 .await 43 - { 44 - error!("Failed to create PLC token: {:?}", e); 45 - return ApiError::InternalError(None).into_response(); 46 - } 45 + .map_err(|e| { 46 + error!("Failed to create PLC token: {:?}", e); 47 + ApiError::InternalError(None) 48 + })?; 49 + 47 50 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 48 51 if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation( 49 52 state.user_repo.as_ref(), ··· 60 63 "PLC operation signature requested for user {}", 61 64 auth_user.did 62 65 ); 63 - EmptyResponse::ok().into_response() 66 + Ok(EmptyResponse::ok().into_response()) 64 67 }
+68 -82
crates/tranquil-pds/src/api/identity/plc/sign.rs
··· 1 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuthAllowDeactivated; 2 + use crate::auth::RequiredAuth; 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: RequiredAuth, 44 44 Json(input): Json<SignPlcOperationInput>, 45 - ) -> Response { 46 - let auth_user = auth.0; 45 + ) -> Result<Response, ApiError> { 46 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 47 47 if let Err(e) = crate::auth::scope_check::check_identity_scope( 48 48 auth_user.is_oauth, 49 49 auth_user.scope.as_deref(), 50 50 crate::oauth::scopes::IdentityAttr::Wildcard, 51 51 ) { 52 - return e; 52 + return Ok(e); 53 53 } 54 54 let did = &auth_user.did; 55 55 if did.starts_with("did:web:") { 56 - return ApiError::InvalidRequest( 56 + return Err(ApiError::InvalidRequest( 57 57 "PLC operations are only valid for did:plc identities".into(), 58 - ) 59 - .into_response(); 58 + )); 60 59 } 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) => { 60 + let token = input.token.as_ref().ok_or_else(|| { 61 + ApiError::InvalidRequest("Email confirmation token required to sign PLC operations".into()) 62 + })?; 63 + 64 + let user_id = state 65 + .user_repo 66 + .get_id_by_did(did) 67 + .await 68 + .map_err(|e| { 74 69 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) => { 70 + ApiError::InternalError(None) 71 + })? 72 + .ok_or(ApiError::AccountNotFound)?; 73 + 74 + let token_expiry = state 75 + .infra_repo 76 + .get_plc_token_expiry(user_id, token) 77 + .await 78 + .map_err(|e| { 84 79 error!("DB error: {:?}", e); 85 - return ApiError::InternalError(None).into_response(); 86 - } 87 - }; 80 + ApiError::InternalError(None) 81 + })? 82 + .ok_or_else(|| ApiError::InvalidToken(Some("Invalid or expired token".into())))?; 83 + 88 84 if Utc::now() > token_expiry { 89 85 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 90 - return ApiError::ExpiredToken(Some("Token has expired".into())).into_response(); 86 + return Err(ApiError::ExpiredToken(Some("Token has expired".into()))); 91 87 } 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) => { 88 + let key_row = state 89 + .user_repo 90 + .get_user_key_by_id(user_id) 91 + .await 92 + .map_err(|e| { 99 93 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) => { 94 + ApiError::InternalError(None) 95 + })? 96 + .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; 97 + 98 + let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 99 + .map_err(|e| { 107 100 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 - }; 101 + ApiError::InternalError(None) 102 + })?; 103 + 104 + let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { 105 + error!("Failed to create signing key: {:?}", e); 106 + ApiError::InternalError(None) 107 + })?; 108 + 118 109 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 119 110 let did_clone = did.clone(); 120 - let last_op = match with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 111 + let last_op = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 121 112 plc_client.get_last_op(&did_clone).await 122 113 }) 123 114 .await 124 - { 125 - Ok(op) => op, 126 - Err(e) => return ApiError::from(e).into_response(), 127 - }; 115 + .map_err(ApiError::from)?; 116 + 128 117 if last_op.is_tombstone() { 129 - return ApiError::from(PlcError::Tombstoned).into_response(); 118 + return Err(ApiError::from(PlcError::Tombstoned)); 130 119 } 131 120 let services = input.services.map(|s| { 132 121 s.into_iter() ··· 141 130 }) 142 131 .collect() 143 132 }); 144 - let unsigned_op = match create_update_op( 133 + let unsigned_op = create_update_op( 145 134 &last_op, 146 135 input.rotation_keys, 147 136 input.verification_methods, 148 137 input.also_known_as, 149 138 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) => { 139 + ) 140 + .map_err(|e| match e { 141 + PlcError::Tombstoned => ApiError::InvalidRequest("Cannot update tombstoned DID".into()), 142 + _ => { 156 143 error!("Failed to create PLC operation: {:?}", e); 157 - return ApiError::InternalError(None).into_response(); 144 + ApiError::InternalError(None) 158 145 } 159 - }; 160 - let signed_op = match sign_operation(&unsigned_op, &signing_key) { 161 - Ok(op) => op, 162 - Err(e) => { 163 - error!("Failed to sign PLC operation: {:?}", e); 164 - return ApiError::InternalError(None).into_response(); 165 - } 166 - }; 146 + })?; 147 + 148 + let signed_op = sign_operation(&unsigned_op, &signing_key).map_err(|e| { 149 + error!("Failed to sign PLC operation: {:?}", e); 150 + ApiError::InternalError(None) 151 + })?; 152 + 167 153 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 168 154 info!("Signed PLC operation for user {}", did); 169 - ( 155 + Ok(( 170 156 StatusCode::OK, 171 157 Json(SignPlcOperationOutput { 172 158 operation: signed_op, 173 159 }), 174 160 ) 175 - .into_response() 161 + .into_response()) 176 162 }
+56 -58
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::RequiredAuth; 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: RequiredAuth, 24 24 Json(input): Json<SubmitPlcOperationInput>, 25 - ) -> Response { 26 - let auth_user = auth.0; 25 + ) -> Result<Response, ApiError> { 26 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 27 27 if let Err(e) = crate::auth::scope_check::check_identity_scope( 28 28 auth_user.is_oauth, 29 29 auth_user.scope.as_deref(), 30 30 crate::oauth::scopes::IdentityAttr::Wildcard, 31 31 ) { 32 - return e; 32 + return Ok(e); 33 33 } 34 34 let did = &auth_user.did; 35 35 if did.starts_with("did:web:") { 36 - return ApiError::InvalidRequest( 36 + return Err(ApiError::InvalidRequest( 37 37 "PLC operations are only valid for did:plc identities".into(), 38 - ) 39 - .into_response(); 38 + )); 40 39 } 41 - if let Err(e) = validate_plc_operation(&input.operation) { 42 - return ApiError::InvalidRequest(format!("Invalid operation: {}", e)).into_response(); 43 - } 40 + validate_plc_operation(&input.operation) 41 + .map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?; 42 + 44 43 let op = &input.operation; 45 44 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 46 45 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) => { 46 + let user = state 47 + .user_repo 48 + .get_id_and_handle_by_did(did) 49 + .await 50 + .map_err(|e| { 51 51 error!("DB error: {:?}", e); 52 - return ApiError::InternalError(None).into_response(); 53 - } 54 - }; 55 - let key_row = match state.user_repo.get_user_key_by_id(user.id).await { 56 - Ok(Some(row)) => row, 57 - Ok(None) => { 58 - return ApiError::InternalError(Some("User signing key not found".into())) 59 - .into_response(); 60 - } 61 - Err(e) => { 52 + ApiError::InternalError(None) 53 + })? 54 + .ok_or(ApiError::AccountNotFound)?; 55 + 56 + let key_row = state 57 + .user_repo 58 + .get_user_key_by_id(user.id) 59 + .await 60 + .map_err(|e| { 62 61 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) => { 62 + ApiError::InternalError(None) 63 + })? 64 + .ok_or_else(|| ApiError::InternalError(Some("User signing key not found".into())))?; 65 + 66 + let key_bytes = crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 67 + .map_err(|e| { 70 68 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 - }; 69 + ApiError::InternalError(None) 70 + })?; 71 + 72 + let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { 73 + error!("Failed to create signing key: {:?}", e); 74 + ApiError::InternalError(None) 75 + })?; 76 + 81 77 let user_did_key = signing_key_to_did_key(&signing_key); 82 78 let server_rotation_key = 83 79 std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); ··· 86 82 .iter() 87 83 .any(|k| k.as_str() == Some(&server_rotation_key)); 88 84 if !has_server_key { 89 - return ApiError::InvalidRequest( 85 + return Err(ApiError::InvalidRequest( 90 86 "Rotation keys do not include server's rotation key".into(), 91 - ) 92 - .into_response(); 87 + )); 93 88 } 94 89 } 95 90 if let Some(services) = op.get("services").and_then(|v| v.as_object()) ··· 98 93 let service_type = pds.get("type").and_then(|v| v.as_str()); 99 94 let endpoint = pds.get("endpoint").and_then(|v| v.as_str()); 100 95 if service_type != Some("AtprotoPersonalDataServer") { 101 - return ApiError::InvalidRequest("Incorrect type on atproto_pds service".into()) 102 - .into_response(); 96 + return Err(ApiError::InvalidRequest( 97 + "Incorrect type on atproto_pds service".into(), 98 + )); 103 99 } 104 100 if endpoint != Some(&public_url) { 105 - return ApiError::InvalidRequest("Incorrect endpoint on atproto_pds service".into()) 106 - .into_response(); 101 + return Err(ApiError::InvalidRequest( 102 + "Incorrect endpoint on atproto_pds service".into(), 103 + )); 107 104 } 108 105 } 109 106 if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object()) 110 107 && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 111 108 && atproto_key != user_did_key 112 109 { 113 - return ApiError::InvalidRequest("Incorrect signing key in verificationMethods".into()) 114 - .into_response(); 110 + return Err(ApiError::InvalidRequest( 111 + "Incorrect signing key in verificationMethods".into(), 112 + )); 115 113 } 116 114 if let Some(also_known_as) = (!user.handle.is_empty()) 117 115 .then(|| op.get("alsoKnownAs").and_then(|v| v.as_array())) ··· 120 118 let expected_handle = format!("at://{}", user.handle); 121 119 let first_aka = also_known_as.first().and_then(|v| v.as_str()); 122 120 if first_aka != Some(&expected_handle) { 123 - return ApiError::InvalidRequest("Incorrect handle in alsoKnownAs".into()) 124 - .into_response(); 121 + return Err(ApiError::InvalidRequest( 122 + "Incorrect handle in alsoKnownAs".into(), 123 + )); 125 124 } 126 125 } 127 126 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 128 127 let operation_clone = input.operation.clone(); 129 128 let did_clone = did.clone(); 130 - if let Err(e) = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 129 + with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 131 130 plc_client 132 131 .send_operation(&did_clone, &operation_clone) 133 132 .await 134 133 }) 135 134 .await 136 - { 137 - return ApiError::from(e).into_response(); 138 - } 135 + .map_err(ApiError::from)?; 136 + 139 137 match state 140 138 .repo_repo 141 139 .insert_identity_event(did, Some(&user.handle)) ··· 157 155 warn!(did = %did, "Failed to refresh DID cache after PLC update"); 158 156 } 159 157 info!(did = %did, "PLC operation submitted successfully"); 160 - EmptyResponse::ok().into_response() 158 + Ok(EmptyResponse::ok().into_response()) 161 159 }
+8 -5
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::RequiredAuth; 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: RequiredAuth, 46 46 Json(input): Json<CreateReportInput>, 47 47 ) -> Response { 48 - let auth_user = auth.0; 48 + let auth_user = match auth.0.require_user() { 49 + Ok(u) => u, 50 + Err(e) => return e.into_response(), 51 + }; 49 52 let did = &auth_user.did; 50 53 51 54 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) 55 + return proxy_to_report_service(&state, auth_user, &service_url, &service_did, &input) 53 56 .await; 54 57 } 55 58 56 - create_report_locally(&state, did, auth_user.is_takendown(), input).await 59 + create_report_locally(&state, did, auth_user.status.is_takendown(), input).await 57 60 } 58 61 59 62 async fn proxy_to_report_service(
+74 -85
crates/tranquil-pds/src/api/notification_prefs.rs
··· 1 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuth; 2 + use crate::auth::RequiredAuth; 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: RequiredAuth, 29 + ) -> Result<Response, ApiError> { 30 + let user = auth.0.require_user()?.require_active()?; 31 + let prefs = state 32 + .user_repo 33 + .get_notification_prefs(&user.did) 34 + .await 35 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 36 + .ok_or(ApiError::AccountNotFound)?; 37 + Ok(Json(NotificationPrefsResponse { 36 38 preferred_channel: prefs.preferred_channel, 37 39 email: prefs.email, 38 40 discord_id: prefs.discord_id, ··· 42 44 signal_number: prefs.signal_number, 43 45 signal_verified: prefs.signal_verified, 44 46 }) 45 - .into_response() 47 + .into_response()) 46 48 } 47 49 48 50 #[derive(Serialize)] ··· 62 64 pub notifications: Vec<NotificationHistoryEntry>, 63 65 } 64 66 65 - pub async fn get_notification_history(State(state): State<AppState>, auth: BearerAuth) -> Response { 66 - let user = auth.0; 67 + pub async fn get_notification_history( 68 + State(state): State<AppState>, 69 + auth: RequiredAuth, 70 + ) -> Result<Response, ApiError> { 71 + let user = auth.0.require_user()?.require_active()?; 67 72 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 - }; 73 + let user_id = state 74 + .user_repo 75 + .get_id_by_did(&user.did) 76 + .await 77 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 78 + .ok_or(ApiError::AccountNotFound)?; 75 79 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 - }; 80 + let rows = state 81 + .infra_repo 82 + .get_notification_history(user_id, 50) 83 + .await 84 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 82 85 83 86 let sensitive_types = [ 84 87 "email_verification", ··· 111 114 }) 112 115 .collect(); 113 116 114 - Json(GetNotificationHistoryResponse { notifications }).into_response() 117 + Ok(Json(GetNotificationHistoryResponse { notifications }).into_response()) 115 118 } 116 119 117 120 #[derive(Deserialize)] ··· 184 187 185 188 pub async fn update_notification_prefs( 186 189 State(state): State<AppState>, 187 - auth: BearerAuth, 190 + auth: RequiredAuth, 188 191 Json(input): Json<UpdateNotificationPrefsInput>, 189 - ) -> Response { 190 - let user = auth.0; 192 + ) -> Result<Response, ApiError> { 193 + let user = auth.0.require_user()?.require_active()?; 191 194 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 - }; 195 + let user_row = state 196 + .user_repo 197 + .get_id_handle_email_by_did(&user.did) 198 + .await 199 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))? 200 + .ok_or(ApiError::AccountNotFound)?; 199 201 200 202 let user_id = user_row.id; 201 203 let handle = user_row.handle; ··· 206 208 if let Some(ref channel) = input.preferred_channel { 207 209 let valid_channels = ["email", "discord", "telegram", "signal"]; 208 210 if !valid_channels.contains(&channel.as_str()) { 209 - return ApiError::InvalidRequest( 211 + return Err(ApiError::InvalidRequest( 210 212 "Invalid channel. Must be one of: email, discord, telegram, signal".into(), 211 - ) 212 - .into_response(); 213 + )); 213 214 } 214 - if let Err(e) = state 215 + state 215 216 .user_repo 216 217 .update_preferred_comms_channel(&user.did, channel) 217 218 .await 218 - { 219 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 220 - } 219 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 221 220 info!(did = %user.did, channel = %channel, "Updated preferred notification channel"); 222 221 } 223 222 224 223 if let Some(ref new_email) = input.email { 225 224 let email_clean = new_email.trim().to_lowercase(); 226 225 if email_clean.is_empty() { 227 - return ApiError::InvalidRequest("Email cannot be empty".into()).into_response(); 226 + return Err(ApiError::InvalidRequest("Email cannot be empty".into())); 228 227 } 229 228 230 229 if !crate::api::validation::is_valid_email(&email_clean) { 231 - return ApiError::InvalidEmail.into_response(); 230 + return Err(ApiError::InvalidEmail); 232 231 } 233 232 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( 233 + if current_email.as_ref().map(|e| e.to_lowercase()) != Some(email_clean.clone()) { 234 + request_channel_verification( 238 235 &state, 239 236 user_id, 240 237 &user.did, ··· 243 240 Some(&handle), 244 241 ) 245 242 .await 246 - { 247 - return ApiError::InternalError(Some(e)).into_response(); 248 - } 243 + .map_err(|e| ApiError::InternalError(Some(e)))?; 249 244 verification_required.push("email".to_string()); 250 245 info!(did = %user.did, "Requested email verification"); 251 246 } ··· 253 248 254 249 if let Some(ref discord_id) = input.discord_id { 255 250 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 - } 251 + state 252 + .user_repo 253 + .clear_discord(user_id) 254 + .await 255 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 260 256 info!(did = %user.did, "Cleared Discord ID"); 261 257 } 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 - } 258 + request_channel_verification(&state, user_id, &user.did, "discord", discord_id, None) 259 + .await 260 + .map_err(|e| ApiError::InternalError(Some(e)))?; 269 261 verification_required.push("discord".to_string()); 270 262 info!(did = %user.did, "Requested Discord verification"); 271 263 } ··· 274 266 if let Some(ref telegram) = input.telegram_username { 275 267 let telegram_clean = telegram.trim_start_matches('@'); 276 268 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 - } 269 + state 270 + .user_repo 271 + .clear_telegram(user_id) 272 + .await 273 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 281 274 info!(did = %user.did, "Cleared Telegram username"); 282 275 } else { 283 - if let Err(e) = request_channel_verification( 276 + request_channel_verification( 284 277 &state, 285 278 user_id, 286 279 &user.did, ··· 289 282 None, 290 283 ) 291 284 .await 292 - { 293 - return ApiError::InternalError(Some(e)).into_response(); 294 - } 285 + .map_err(|e| ApiError::InternalError(Some(e)))?; 295 286 verification_required.push("telegram".to_string()); 296 287 info!(did = %user.did, "Requested Telegram verification"); 297 288 } ··· 299 290 300 291 if let Some(ref signal) = input.signal_number { 301 292 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 - } 293 + state 294 + .user_repo 295 + .clear_signal(user_id) 296 + .await 297 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 306 298 info!(did = %user.did, "Cleared Signal number"); 307 299 } 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 - } 300 + request_channel_verification(&state, user_id, &user.did, "signal", signal, None) 301 + .await 302 + .map_err(|e| ApiError::InternalError(Some(e)))?; 314 303 verification_required.push("signal".to_string()); 315 304 info!(did = %user.did, "Requested Signal verification"); 316 305 } 317 306 } 318 307 319 - Json(UpdateNotificationPrefsResponse { 308 + Ok(Json(UpdateNotificationPrefsResponse { 320 309 success: true, 321 310 verification_required, 322 311 }) 323 - .into_response() 312 + .into_response()) 324 313 }
+129 -130
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::RequiredAuth; 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: RequiredAuth, 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; 47 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 48 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) => { 49 + let user = state 50 + .user_repo 51 + .get_by_did(did) 52 + .await 53 + .map_err(|e| { 55 54 error!("DB error fetching user: {:?}", e); 56 - return ApiError::InternalError(None).into_response(); 57 - } 58 - }; 55 + ApiError::InternalError(None) 56 + })? 57 + .ok_or(ApiError::AccountNotFound)?; 59 58 if user.takedown_ref.is_some() { 60 - return ApiError::AccountTakedown.into_response(); 59 + return Err(ApiError::AccountTakedown); 61 60 } 62 61 let user_id = user.id; 63 62 let (root, blocks) = match parse_car(&body).await { 64 63 Ok((r, b)) => (r, b), 65 64 Err(ImportError::InvalidRootCount) => { 66 - return ApiError::InvalidRequest("Expected exactly one root in CAR file".into()) 67 - .into_response(); 65 + return Err(ApiError::InvalidRequest( 66 + "Expected exactly one root in CAR file".into(), 67 + )); 68 68 } 69 69 Err(ImportError::CarParse(msg)) => { 70 - return ApiError::InvalidRequest(format!("Failed to parse CAR file: {}", msg)) 71 - .into_response(); 70 + return Err(ApiError::InvalidRequest(format!( 71 + "Failed to parse CAR file: {}", 72 + msg 73 + ))); 72 74 } 73 75 Err(e) => { 74 76 error!("CAR parsing error: {:?}", e); 75 - return ApiError::InvalidRequest(format!("Invalid CAR file: {}", e)).into_response(); 77 + return Err(ApiError::InvalidRequest(format!("Invalid CAR file: {}", e))); 76 78 } 77 79 }; 78 80 info!( ··· 82 84 root 83 85 ); 84 86 let Some(root_block) = blocks.get(&root) else { 85 - return ApiError::InvalidRequest("Root block not found in CAR file".into()).into_response(); 87 + return Err(ApiError::InvalidRequest( 88 + "Root block not found in CAR file".into(), 89 + )); 86 90 }; 87 91 let commit_did = match jacquard_repo::commit::Commit::from_cbor(root_block) { 88 92 Ok(commit) => commit.did().to_string(), 89 93 Err(e) => { 90 - return ApiError::InvalidRequest(format!("Invalid commit: {}", e)).into_response(); 94 + return Err(ApiError::InvalidRequest(format!("Invalid commit: {}", e))); 91 95 } 92 96 }; 93 97 if commit_did != *did { 94 - return ApiError::InvalidRepo(format!( 98 + return Err(ApiError::InvalidRepo(format!( 95 99 "CAR file is for DID {} but you are authenticated as {}", 96 100 commit_did, did 97 - )) 98 - .into_response(); 101 + ))); 99 102 } 100 103 let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION") 101 104 .map(|v| v == "true" || v == "1") ··· 117 120 commit_did, 118 121 expected_did, 119 122 }) => { 120 - return ApiError::InvalidRepo(format!( 123 + return Err(ApiError::InvalidRepo(format!( 121 124 "CAR file is for DID {} but you are authenticated as {}", 122 125 commit_did, expected_did 123 - )) 124 - .into_response(); 126 + ))); 125 127 } 126 128 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 127 - return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 128 - .into_response(); 129 + return Err(ApiError::InvalidRequest(format!( 130 + "MST validation failed: {}", 131 + msg 132 + ))); 129 133 } 130 134 Err(e) => { 131 135 error!("CAR structure verification error: {:?}", e); 132 - return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) 133 - .into_response(); 136 + return Err(ApiError::InvalidRequest(format!( 137 + "CAR verification failed: {}", 138 + e 139 + ))); 134 140 } 135 141 } 136 142 } else { ··· 147 153 commit_did, 148 154 expected_did, 149 155 }) => { 150 - return ApiError::InvalidRepo(format!( 156 + return Err(ApiError::InvalidRepo(format!( 151 157 "CAR file is for DID {} but you are authenticated as {}", 152 158 commit_did, expected_did 153 - )) 154 - .into_response(); 159 + ))); 155 160 } 156 161 Err(crate::sync::verify::VerifyError::InvalidSignature) => { 157 - return ApiError::InvalidRequest( 162 + return Err(ApiError::InvalidRequest( 158 163 "CAR file commit signature verification failed".into(), 159 - ) 160 - .into_response(); 164 + )); 161 165 } 162 166 Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => { 163 167 warn!("DID resolution failed during import verification: {}", msg); 164 - return ApiError::InvalidRequest(format!("Failed to verify DID: {}", msg)) 165 - .into_response(); 168 + return Err(ApiError::InvalidRequest(format!( 169 + "Failed to verify DID: {}", 170 + msg 171 + ))); 166 172 } 167 173 Err(crate::sync::verify::VerifyError::NoSigningKey) => { 168 - return ApiError::InvalidRequest( 174 + return Err(ApiError::InvalidRequest( 169 175 "DID document does not contain a signing key".into(), 170 - ) 171 - .into_response(); 176 + )); 172 177 } 173 178 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 174 - return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 175 - .into_response(); 179 + return Err(ApiError::InvalidRequest(format!( 180 + "MST validation failed: {}", 181 + msg 182 + ))); 176 183 } 177 184 Err(e) => { 178 185 error!("CAR verification error: {:?}", e); 179 - return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) 180 - .into_response(); 186 + return Err(ApiError::InvalidRequest(format!( 187 + "CAR verification failed: {}", 188 + e 189 + ))); 181 190 } 182 191 } 183 192 } ··· 227 236 } 228 237 } 229 238 } 230 - let key_row = match state.user_repo.get_user_with_key_by_did(did).await { 231 - Ok(Some(row)) => row, 232 - Ok(None) => { 239 + let key_row = state 240 + .user_repo 241 + .get_user_with_key_by_did(did) 242 + .await 243 + .map_err(|e| { 244 + error!("DB error fetching signing key: {:?}", e); 245 + ApiError::InternalError(None) 246 + })? 247 + .ok_or_else(|| { 233 248 error!("No signing key found for user {}", did); 234 - return ApiError::InternalError(Some("Signing key not found".into())) 235 - .into_response(); 236 - } 237 - Err(e) => { 238 - error!("DB error fetching signing key: {:?}", e); 239 - return ApiError::InternalError(None).into_response(); 240 - } 241 - }; 249 + ApiError::InternalError(Some("Signing key not found".into())) 250 + })?; 242 251 let key_bytes = 243 - match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) { 244 - Ok(k) => k, 245 - Err(e) => { 252 + crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 253 + .map_err(|e| { 246 254 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 - }; 255 + ApiError::InternalError(None) 256 + })?; 257 + let signing_key = SigningKey::from_slice(&key_bytes).map_err(|e| { 258 + error!("Invalid signing key: {:?}", e); 259 + ApiError::InternalError(None) 260 + })?; 257 261 let new_rev = Tid::now(LimitedU32::MIN); 258 262 let new_rev_str = new_rev.to_string(); 259 - let (commit_bytes, _sig) = match create_signed_commit( 263 + let (commit_bytes, _sig) = create_signed_commit( 260 264 did, 261 265 import_result.data_cid, 262 266 &new_rev_str, 263 267 None, 264 268 &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) => { 269 + ) 270 + .map_err(|e| { 271 + error!("Failed to create new commit: {}", e); 272 + ApiError::InternalError(None) 273 + })?; 274 + let new_root_cid: cid::Cid = 275 + state.block_store.put(&commit_bytes).await.map_err(|e| { 275 276 error!("Failed to store new commit block: {:?}", e); 276 - return ApiError::InternalError(None).into_response(); 277 - } 278 - }; 277 + ApiError::InternalError(None) 278 + })?; 279 279 let new_root_cid_link = CidLink::new_unchecked(new_root_cid.to_string()); 280 - if let Err(e) = state 280 + state 281 281 .repo_repo 282 282 .update_repo_root(user_id, &new_root_cid_link, &new_rev_str) 283 283 .await 284 - { 285 - error!("Failed to update repo root: {:?}", e); 286 - return ApiError::InternalError(None).into_response(); 287 - } 284 + .map_err(|e| { 285 + error!("Failed to update repo root: {:?}", e); 286 + ApiError::InternalError(None) 287 + })?; 288 288 let mut all_block_cids: Vec<Vec<u8>> = blocks.keys().map(|c| c.to_bytes()).collect(); 289 289 all_block_cids.push(new_root_cid.to_bytes()); 290 - if let Err(e) = state 290 + state 291 291 .repo_repo 292 292 .insert_user_blocks(user_id, &all_block_cids, &new_rev_str) 293 293 .await 294 - { 295 - error!("Failed to insert user_blocks: {:?}", e); 296 - return ApiError::InternalError(None).into_response(); 297 - } 294 + .map_err(|e| { 295 + error!("Failed to insert user_blocks: {:?}", e); 296 + ApiError::InternalError(None) 297 + })?; 298 298 let new_root_str = new_root_cid.to_string(); 299 299 info!( 300 300 "Created new commit for imported repo: cid={}, rev={}", ··· 324 324 ); 325 325 } 326 326 } 327 - EmptyResponse::ok().into_response() 327 + Ok(EmptyResponse::ok().into_response()) 328 328 } 329 - Err(ImportError::SizeLimitExceeded) => { 330 - ApiError::PayloadTooLarge(format!("Import exceeds block limit of {}", max_blocks)) 331 - .into_response() 332 - } 333 - Err(ImportError::RepoNotFound) => { 334 - ApiError::RepoNotFound(Some("Repository not initialized for this account".into())) 335 - .into_response() 336 - } 337 - Err(ImportError::InvalidCbor(msg)) => { 338 - ApiError::InvalidRequest(format!("Invalid CBOR data: {}", msg)).into_response() 339 - } 340 - Err(ImportError::InvalidCommit(msg)) => { 341 - ApiError::InvalidRequest(format!("Invalid commit structure: {}", msg)).into_response() 342 - } 343 - Err(ImportError::BlockNotFound(cid)) => { 344 - ApiError::InvalidRequest(format!("Referenced block not found in CAR: {}", cid)) 345 - .into_response() 346 - } 347 - Err(ImportError::ConcurrentModification) => ApiError::InvalidSwap(Some( 329 + Err(ImportError::SizeLimitExceeded) => Err(ApiError::PayloadTooLarge(format!( 330 + "Import exceeds block limit of {}", 331 + max_blocks 332 + ))), 333 + Err(ImportError::RepoNotFound) => Err(ApiError::RepoNotFound(Some( 334 + "Repository not initialized for this account".into(), 335 + ))), 336 + Err(ImportError::InvalidCbor(msg)) => Err(ApiError::InvalidRequest(format!( 337 + "Invalid CBOR data: {}", 338 + msg 339 + ))), 340 + Err(ImportError::InvalidCommit(msg)) => Err(ApiError::InvalidRequest(format!( 341 + "Invalid commit structure: {}", 342 + msg 343 + ))), 344 + Err(ImportError::BlockNotFound(cid)) => Err(ApiError::InvalidRequest(format!( 345 + "Referenced block not found in CAR: {}", 346 + cid 347 + ))), 348 + Err(ImportError::ConcurrentModification) => Err(ApiError::InvalidSwap(Some( 348 349 "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(), 350 + ))), 351 + Err(ImportError::VerificationFailed(ve)) => Err(ApiError::InvalidRequest(format!( 352 + "CAR verification failed: {}", 353 + ve 354 + ))), 355 + Err(ImportError::DidMismatch { car_did, auth_did }) => Err(ApiError::InvalidRequest( 356 + format!("CAR is for {} but authenticated as {}", car_did, auth_did), 357 + )), 359 358 Err(e) => { 360 359 error!("Import error: {:?}", e); 361 - ApiError::InternalError(None).into_response() 360 + Err(ApiError::InternalError(None)) 362 361 } 363 362 } 364 363 }
+61 -62
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::RequiredAuth; 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: RequiredAuth, 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; 273 + let auth_user = auth.0.require_user()?.require_active()?; 274 274 let did = auth_user.did.clone(); 275 275 let is_oauth = auth_user.is_oauth; 276 - let scope = auth_user.scope; 276 + let scope = auth_user.scope.clone(); 277 277 let controller_did = auth_user.controller_did.clone(); 278 278 if input.repo.as_str() != did { 279 - return ApiError::InvalidRepo("Repo does not match authenticated user".into()) 280 - .into_response(); 279 + return Err(ApiError::InvalidRepo( 280 + "Repo does not match authenticated user".into(), 281 + )); 281 282 } 282 283 if state 283 284 .user_repo ··· 285 286 .await 286 287 .unwrap_or(false) 287 288 { 288 - return ApiError::AccountMigrated.into_response(); 289 + return Err(ApiError::AccountMigrated); 289 290 } 290 291 let is_verified = state 291 292 .user_repo ··· 298 299 .await 299 300 .unwrap_or(false); 300 301 if !is_verified && !is_delegated { 301 - return ApiError::AccountNotVerified.into_response(); 302 + return Err(ApiError::AccountNotVerified); 302 303 } 303 304 if input.writes.is_empty() { 304 - return ApiError::InvalidRequest("writes array is empty".into()).into_response(); 305 + return Err(ApiError::InvalidRequest("writes array is empty".into())); 305 306 } 306 307 if input.writes.len() > MAX_BATCH_WRITES { 307 - return ApiError::InvalidRequest(format!("Too many writes (max {})", MAX_BATCH_WRITES)) 308 - .into_response(); 308 + return Err(ApiError::InvalidRequest(format!( 309 + "Too many writes (max {})", 310 + MAX_BATCH_WRITES 311 + ))); 309 312 } 310 313 311 314 let has_custom_scope = scope ··· 374 377 }) 375 378 .next() 376 379 { 377 - return err; 380 + return Ok(err); 378 381 } 379 382 } 380 383 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 - }; 384 + let user_id: uuid::Uuid = state 385 + .user_repo 386 + .get_id_by_did(&did) 387 + .await 388 + .ok() 389 + .flatten() 390 + .ok_or_else(|| ApiError::InternalError(Some("User not found".into())))?; 391 + let root_cid_str = state 392 + .repo_repo 393 + .get_repo_root_cid_by_user_id(user_id) 394 + .await 395 + .ok() 396 + .flatten() 397 + .ok_or_else(|| ApiError::InternalError(Some("Repo root not found".into())))?; 398 + let current_root_cid = Cid::from_str(&root_cid_str) 399 + .map_err(|_| ApiError::InternalError(Some("Invalid repo root CID".into())))?; 395 400 if let Some(swap_commit) = &input.swap_commit 396 401 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 397 402 { 398 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 403 + return Err(ApiError::InvalidSwap(Some("Repo has been modified".into()))); 399 404 } 400 405 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 - }; 406 + let commit_bytes = tracking_store 407 + .get(&current_root_cid) 408 + .await 409 + .ok() 410 + .flatten() 411 + .ok_or_else(|| ApiError::InternalError(Some("Commit block not found".into())))?; 412 + let commit = Commit::from_cbor(&commit_bytes) 413 + .map_err(|_| ApiError::InternalError(Some("Failed to parse commit".into())))?; 409 414 let original_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 410 415 let initial_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 411 416 let WriteAccumulator { ··· 424 429 .await 425 430 { 426 431 Ok(acc) => acc, 427 - Err(response) => return response, 432 + Err(response) => return Ok(response), 428 433 }; 429 - let new_mst_root = match mst.persist().await { 430 - Ok(c) => c, 431 - Err(_) => { 432 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 433 - } 434 - }; 434 + let new_mst_root = mst 435 + .persist() 436 + .await 437 + .map_err(|_| ApiError::InternalError(Some("Failed to persist MST".into())))?; 435 438 let (new_mst_blocks, old_mst_blocks) = { 436 439 let mut new_blocks = std::collections::BTreeMap::new(); 437 440 let mut old_blocks = std::collections::BTreeMap::new(); 438 441 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 442 + mst.blocks_for_path(key, &mut new_blocks) 443 + .await 444 + .map_err(|_| { 445 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 446 + })?; 447 + original_mst 446 448 .blocks_for_path(key, &mut old_blocks) 447 449 .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 - } 450 + .map_err(|_| { 451 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 452 + })?; 455 453 } 456 454 (new_blocks, old_blocks) 457 455 }; ··· 503 501 { 504 502 Ok(res) => res, 505 503 Err(e) if e.contains("ConcurrentModification") => { 506 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 504 + return Err(ApiError::InvalidSwap(Some("Repo has been modified".into()))); 507 505 } 508 506 Err(e) => { 509 507 error!("Commit failed: {}", e); 510 - return ApiError::InternalError(Some("Failed to commit changes".into())) 511 - .into_response(); 508 + return Err(ApiError::InternalError(Some( 509 + "Failed to commit changes".into(), 510 + ))); 512 511 } 513 512 }; 514 513 ··· 557 556 .await; 558 557 } 559 558 560 - ( 559 + Ok(( 561 560 StatusCode::OK, 562 561 Json(ApplyWritesOutput { 563 562 commit: CommitInfo { ··· 567 566 results, 568 567 }), 569 568 ) 570 - .into_response() 569 + .into_response()) 571 570 }
+124 -117
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::{RequiredAuth, 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: RequiredAuth, 37 + ) -> Result<Response, ApiError> { 38 + let auth_user = auth.0.require_user()?.require_active()?; 39 + let user = state 40 + .user_repo 41 + .get_by_did(&auth_user.did) 42 + .await 43 + .map_err(|e| { 42 44 error!("DB error getting user: {:?}", e); 43 - return ApiError::InternalError(None).into_response(); 44 - } 45 - }; 45 + ApiError::InternalError(None) 46 + })? 47 + .ok_or(ApiError::AccountNotFound)?; 46 48 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) => { 49 + let rows = state 50 + .session_repo 51 + .list_app_passwords(user.id) 52 + .await 53 + .map_err(|e| { 65 54 error!("DB error listing app passwords: {:?}", e); 66 - ApiError::InternalError(None).into_response() 67 - } 68 - } 55 + ApiError::InternalError(None) 56 + })?; 57 + let passwords: Vec<AppPassword> = rows 58 + .iter() 59 + .map(|row| AppPassword { 60 + name: row.name.clone(), 61 + created_at: row.created_at.to_rfc3339(), 62 + privileged: row.privileged, 63 + scopes: row.scopes.clone(), 64 + created_by_controller: row 65 + .created_by_controller_did 66 + .as_ref() 67 + .map(|d| d.to_string()), 68 + }) 69 + .collect(); 70 + Ok(Json(ListAppPasswordsOutput { passwords }).into_response()) 69 71 } 70 72 71 73 #[derive(Deserialize)] ··· 89 91 pub async fn create_app_password( 90 92 State(state): State<AppState>, 91 93 headers: HeaderMap, 92 - BearerAuth(auth_user): BearerAuth, 94 + auth: RequiredAuth, 93 95 Json(input): Json<CreateAppPasswordInput>, 94 - ) -> Response { 96 + ) -> Result<Response, ApiError> { 97 + let auth_user = auth.0.require_user()?.require_active()?; 95 98 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 96 99 if !state 97 100 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 98 101 .await 99 102 { 100 103 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 101 - return ApiError::RateLimitExceeded(None).into_response(); 104 + return Err(ApiError::RateLimitExceeded(None)); 102 105 } 103 106 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) => { 107 + let user = state 108 + .user_repo 109 + .get_by_did(&auth_user.did) 110 + .await 111 + .map_err(|e| { 108 112 error!("DB error getting user: {:?}", e); 109 - return ApiError::InternalError(None).into_response(); 110 - } 111 - }; 113 + ApiError::InternalError(None) 114 + })? 115 + .ok_or(ApiError::AccountNotFound)?; 112 116 113 117 let name = input.name.trim(); 114 118 if name.is_empty() { 115 - return ApiError::InvalidRequest("name is required".into()).into_response(); 119 + return Err(ApiError::InvalidRequest("name is required".into())); 116 120 } 117 121 118 - match state 122 + if state 119 123 .session_repo 120 124 .get_app_password_by_name(user.id, name) 121 125 .await 122 - { 123 - Ok(Some(_)) => return ApiError::DuplicateAppPassword.into_response(), 124 - Err(e) => { 126 + .map_err(|e| { 125 127 error!("DB error checking app password: {:?}", e); 126 - return ApiError::InternalError(None).into_response(); 127 - } 128 - Ok(None) => {} 128 + ApiError::InternalError(None) 129 + })? 130 + .is_some() 131 + { 132 + return Err(ApiError::DuplicateAppPassword); 129 133 } 130 134 131 135 let (final_scopes, controller_did) = if let Some(ref controller) = auth_user.controller_did { ··· 141 145 let intersected = intersect_scopes(requested, &granted_scopes); 142 146 143 147 if intersected.is_empty() && !granted_scopes.is_empty() { 144 - return ApiError::InsufficientScope(None).into_response(); 148 + return Err(ApiError::InsufficientScope(None)); 145 149 } 146 150 147 151 let scope_result = if intersected.is_empty() { ··· 157 161 let password = generate_app_password(); 158 162 159 163 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 - }; 164 + let password_hash = 165 + tokio::task::spawn_blocking(move || bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST)) 166 + .await 167 + .map_err(|e| { 168 + error!("Failed to spawn blocking task: {:?}", e); 169 + ApiError::InternalError(None) 170 + })? 171 + .map_err(|e| { 172 + error!("Failed to hash password: {:?}", e); 173 + ApiError::InternalError(None) 174 + })?; 175 175 176 176 let privileged = input.privileged.unwrap_or(false); 177 177 let created_at = chrono::Utc::now(); ··· 185 185 created_by_controller_did: controller_did.clone(), 186 186 }; 187 187 188 - match state.session_repo.create_app_password(&create_data).await { 189 - Ok(_) => { 190 - if let Some(ref controller) = controller_did { 191 - let _ = state 192 - .delegation_repo 193 - .log_delegation_action( 194 - &auth_user.did, 195 - controller, 196 - Some(controller), 197 - DelegationActionType::AccountAction, 198 - Some(json!({ 199 - "action": "create_app_password", 200 - "name": name, 201 - "scopes": final_scopes 202 - })), 203 - None, 204 - None, 205 - ) 206 - .await; 207 - } 208 - Json(CreateAppPasswordOutput { 209 - name: name.to_string(), 210 - password, 211 - created_at: created_at.to_rfc3339(), 212 - privileged, 213 - scopes: final_scopes, 214 - }) 215 - .into_response() 216 - } 217 - Err(e) => { 188 + state 189 + .session_repo 190 + .create_app_password(&create_data) 191 + .await 192 + .map_err(|e| { 218 193 error!("DB error creating app password: {:?}", e); 219 - ApiError::InternalError(None).into_response() 220 - } 194 + ApiError::InternalError(None) 195 + })?; 196 + 197 + if let Some(ref controller) = controller_did { 198 + let _ = state 199 + .delegation_repo 200 + .log_delegation_action( 201 + &auth_user.did, 202 + controller, 203 + Some(controller), 204 + DelegationActionType::AccountAction, 205 + Some(json!({ 206 + "action": "create_app_password", 207 + "name": name, 208 + "scopes": final_scopes 209 + })), 210 + None, 211 + None, 212 + ) 213 + .await; 221 214 } 215 + Ok(Json(CreateAppPasswordOutput { 216 + name: name.to_string(), 217 + password, 218 + created_at: created_at.to_rfc3339(), 219 + privileged, 220 + scopes: final_scopes, 221 + }) 222 + .into_response()) 222 223 } 223 224 224 225 #[derive(Deserialize)] ··· 228 229 229 230 pub async fn revoke_app_password( 230 231 State(state): State<AppState>, 231 - BearerAuth(auth_user): BearerAuth, 232 + auth: RequiredAuth, 232 233 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) => { 234 + ) -> Result<Response, ApiError> { 235 + let auth_user = auth.0.require_user()?.require_active()?; 236 + let user = state 237 + .user_repo 238 + .get_by_did(&auth_user.did) 239 + .await 240 + .map_err(|e| { 238 241 error!("DB error getting user: {:?}", e); 239 - return ApiError::InternalError(None).into_response(); 240 - } 241 - }; 242 + ApiError::InternalError(None) 243 + })? 244 + .ok_or(ApiError::AccountNotFound)?; 242 245 243 246 let name = input.name.trim(); 244 247 if name.is_empty() { 245 - return ApiError::InvalidRequest("name is required".into()).into_response(); 248 + return Err(ApiError::InvalidRequest("name is required".into())); 246 249 } 247 250 248 251 let sessions_to_invalidate = state ··· 251 254 .await 252 255 .unwrap_or_default(); 253 256 254 - if let Err(e) = state 257 + state 255 258 .session_repo 256 259 .delete_sessions_by_app_password(&auth_user.did, name) 257 260 .await 258 - { 259 - error!("DB error revoking sessions for app password: {:?}", e); 260 - return ApiError::InternalError(None).into_response(); 261 - } 261 + .map_err(|e| { 262 + error!("DB error revoking sessions for app password: {:?}", e); 263 + ApiError::InternalError(None) 264 + })?; 262 265 263 266 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 264 267 let cache_key = format!("auth:session:{}:{}", &auth_user.did, jti); ··· 269 272 })) 270 273 .await; 271 274 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 - } 275 + state 276 + .session_repo 277 + .delete_app_password(user.id, name) 278 + .await 279 + .map_err(|e| { 280 + error!("DB error revoking app password: {:?}", e); 281 + ApiError::InternalError(None) 282 + })?; 276 283 277 - EmptyResponse::ok().into_response() 284 + Ok(EmptyResponse::ok().into_response()) 278 285 }
+93 -85
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::RequiredAuth; 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: RequiredAuth, 49 49 input: Option<Json<RequestEmailUpdateInput>>, 50 - ) -> Response { 50 + ) -> Result<Response, ApiError> { 51 + let auth_user = auth.0.require_user()?.require_active()?; 51 52 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 52 53 if !state 53 54 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 54 55 .await 55 56 { 56 57 warn!(ip = %client_ip, "Email update rate limit exceeded"); 57 - return ApiError::RateLimitExceeded(None).into_response(); 58 + return Err(ApiError::RateLimitExceeded(None)); 58 59 } 59 60 60 61 if let Err(e) = crate::auth::scope_check::check_account_scope( 61 - auth.0.is_oauth, 62 - auth.0.scope.as_deref(), 62 + auth_user.is_oauth, 63 + auth_user.scope.as_deref(), 63 64 crate::oauth::scopes::AccountAttr::Email, 64 65 crate::oauth::scopes::AccountAction::Manage, 65 66 ) { 66 - return e; 67 + return Ok(e); 67 68 } 68 69 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) => { 70 + let user = state 71 + .user_repo 72 + .get_email_info_by_did(&auth_user.did) 73 + .await 74 + .map_err(|e| { 75 75 error!("DB error: {:?}", e); 76 - return ApiError::InternalError(None).into_response(); 77 - } 78 - }; 76 + ApiError::InternalError(None) 77 + })? 78 + .ok_or(ApiError::AccountNotFound)?; 79 79 80 80 let Some(current_email) = user.email else { 81 - return ApiError::InvalidRequest("account does not have an email address".into()) 82 - .into_response(); 81 + return Err(ApiError::InvalidRequest( 82 + "account does not have an email address".into(), 83 + )); 83 84 }; 84 85 85 86 let token_required = user.email_verified; 86 87 87 88 if token_required { 88 89 let code = crate::auth::verification_token::generate_channel_update_token( 89 - &auth.0.did, 90 + &auth_user.did, 90 91 "email_update", 91 92 &current_email.to_lowercase(), 92 93 ); ··· 103 104 authorized: false, 104 105 }; 105 106 if let Ok(json) = serde_json::to_string(&pending) { 106 - let cache_key = email_update_cache_key(&auth.0.did); 107 + let cache_key = email_update_cache_key(&auth_user.did); 107 108 if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await { 108 109 warn!("Failed to cache pending email update: {:?}", e); 109 110 } ··· 127 128 } 128 129 129 130 info!("Email update requested for user {}", user.id); 130 - TokenRequiredResponse::response(token_required).into_response() 131 + Ok(TokenRequiredResponse::response(token_required).into_response()) 131 132 } 132 133 133 134 #[derive(Deserialize)] ··· 140 141 pub async fn confirm_email( 141 142 State(state): State<AppState>, 142 143 headers: axum::http::HeaderMap, 143 - auth: BearerAuth, 144 + auth: RequiredAuth, 144 145 Json(input): Json<ConfirmEmailInput>, 145 - ) -> Response { 146 + ) -> Result<Response, ApiError> { 147 + let auth_user = auth.0.require_user()?.require_active()?; 146 148 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 147 149 if !state 148 150 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 149 151 .await 150 152 { 151 153 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 152 - return ApiError::RateLimitExceeded(None).into_response(); 154 + return Err(ApiError::RateLimitExceeded(None)); 153 155 } 154 156 155 157 if let Err(e) = crate::auth::scope_check::check_account_scope( 156 - auth.0.is_oauth, 157 - auth.0.scope.as_deref(), 158 + auth_user.is_oauth, 159 + auth_user.scope.as_deref(), 158 160 crate::oauth::scopes::AccountAttr::Email, 159 161 crate::oauth::scopes::AccountAction::Manage, 160 162 ) { 161 - return e; 163 + return Ok(e); 162 164 } 163 165 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) => { 166 + let did = &auth_user.did; 167 + let user = state 168 + .user_repo 169 + .get_email_info_by_did(did) 170 + .await 171 + .map_err(|e| { 171 172 error!("DB error: {:?}", e); 172 - return ApiError::InternalError(None).into_response(); 173 - } 174 - }; 173 + ApiError::InternalError(None) 174 + })? 175 + .ok_or(ApiError::AccountNotFound)?; 175 176 176 177 let Some(ref email) = user.email else { 177 - return ApiError::InvalidEmail.into_response(); 178 + return Err(ApiError::InvalidEmail); 178 179 }; 179 180 let current_email = email.to_lowercase(); 180 181 181 182 let provided_email = input.email.trim().to_lowercase(); 182 183 if provided_email != current_email { 183 - return ApiError::InvalidEmail.into_response(); 184 + return Err(ApiError::InvalidEmail); 184 185 } 185 186 186 187 if user.email_verified { 187 - return EmptyResponse::ok().into_response(); 188 + return Ok(EmptyResponse::ok().into_response()); 188 189 } 189 190 190 191 let confirmation_code = ··· 199 200 match verified { 200 201 Ok(token_data) => { 201 202 if token_data.did != did.as_str() { 202 - return ApiError::InvalidToken(None).into_response(); 203 + return Err(ApiError::InvalidToken(None)); 203 204 } 204 205 } 205 206 Err(crate::auth::verification_token::VerifyError::Expired) => { 206 - return ApiError::ExpiredToken(None).into_response(); 207 + return Err(ApiError::ExpiredToken(None)); 207 208 } 208 209 Err(_) => { 209 - return ApiError::InvalidToken(None).into_response(); 210 + return Err(ApiError::InvalidToken(None)); 210 211 } 211 212 } 212 213 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 - } 214 + state 215 + .user_repo 216 + .set_email_verified(user.id, true) 217 + .await 218 + .map_err(|e| { 219 + error!("DB error confirming email: {:?}", e); 220 + ApiError::InternalError(None) 221 + })?; 217 222 218 223 info!("Email confirmed for user {}", user.id); 219 - EmptyResponse::ok().into_response() 224 + Ok(EmptyResponse::ok().into_response()) 220 225 } 221 226 222 227 #[derive(Deserialize)] ··· 230 235 231 236 pub async fn update_email( 232 237 State(state): State<AppState>, 233 - auth: BearerAuth, 238 + auth: RequiredAuth, 234 239 Json(input): Json<UpdateEmailInput>, 235 - ) -> Response { 236 - let auth_user = auth.0; 240 + ) -> Result<Response, ApiError> { 241 + let auth_user = auth.0.require_user()?.require_active()?; 237 242 238 243 if let Err(e) = crate::auth::scope_check::check_account_scope( 239 244 auth_user.is_oauth, ··· 241 246 crate::oauth::scopes::AccountAttr::Email, 242 247 crate::oauth::scopes::AccountAction::Manage, 243 248 ) { 244 - return e; 249 + return Ok(e); 245 250 } 246 251 247 252 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) => { 253 + let user = state 254 + .user_repo 255 + .get_email_info_by_did(did) 256 + .await 257 + .map_err(|e| { 254 258 error!("DB error: {:?}", e); 255 - return ApiError::InternalError(None).into_response(); 256 - } 257 - }; 259 + ApiError::InternalError(None) 260 + })? 261 + .ok_or(ApiError::AccountNotFound)?; 258 262 259 263 let user_id = user.id; 260 264 let current_email = user.email.clone(); ··· 262 266 let new_email = input.email.trim().to_lowercase(); 263 267 264 268 if !crate::api::validation::is_valid_email(&new_email) { 265 - return ApiError::InvalidRequest( 269 + return Err(ApiError::InvalidRequest( 266 270 "This email address is not supported, please use a different email.".into(), 267 - ) 268 - .into_response(); 271 + )); 269 272 } 270 273 271 274 if let Some(ref current) = current_email 272 275 && new_email == current.to_lowercase() 273 276 { 274 - return EmptyResponse::ok().into_response(); 277 + return Ok(EmptyResponse::ok().into_response()); 275 278 } 276 279 277 280 if email_verified { ··· 290 293 291 294 if !authorized_via_link { 292 295 let Some(ref t) = input.token else { 293 - return ApiError::TokenRequired.into_response(); 296 + return Err(ApiError::TokenRequired); 294 297 }; 295 298 let confirmation_token = 296 299 crate::auth::verification_token::normalize_token_input(t.trim()); ··· 309 312 match verified { 310 313 Ok(token_data) => { 311 314 if token_data.did != did.as_str() { 312 - return ApiError::InvalidToken(None).into_response(); 315 + return Err(ApiError::InvalidToken(None)); 313 316 } 314 317 } 315 318 Err(crate::auth::verification_token::VerifyError::Expired) => { 316 - return ApiError::ExpiredToken(None).into_response(); 319 + return Err(ApiError::ExpiredToken(None)); 317 320 } 318 321 Err(_) => { 319 - return ApiError::InvalidToken(None).into_response(); 322 + return Err(ApiError::InvalidToken(None)); 320 323 } 321 324 } 322 325 } 323 326 } 324 327 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 - } 328 + state 329 + .user_repo 330 + .update_email(user_id, &new_email) 331 + .await 332 + .map_err(|e| { 333 + error!("DB error updating email: {:?}", e); 334 + ApiError::InternalError(None) 335 + })?; 329 336 330 337 let verification_token = 331 338 crate::auth::verification_token::generate_signup_token(did, "email", &new_email); ··· 358 365 } 359 366 360 367 info!("Email updated for user {}", user_id); 361 - EmptyResponse::ok().into_response() 368 + Ok(EmptyResponse::ok().into_response()) 362 369 } 363 370 364 371 #[derive(Deserialize)] ··· 497 504 pub async fn check_email_update_status( 498 505 State(state): State<AppState>, 499 506 headers: axum::http::HeaderMap, 500 - auth: BearerAuth, 501 - ) -> Response { 507 + auth: RequiredAuth, 508 + ) -> Result<Response, ApiError> { 509 + let auth_user = auth.0.require_user()?.require_active()?; 502 510 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 503 511 if !state 504 512 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 505 513 .await 506 514 { 507 - return ApiError::RateLimitExceeded(None).into_response(); 515 + return Err(ApiError::RateLimitExceeded(None)); 508 516 } 509 517 510 518 if let Err(e) = crate::auth::scope_check::check_account_scope( 511 - auth.0.is_oauth, 512 - auth.0.scope.as_deref(), 519 + auth_user.is_oauth, 520 + auth_user.scope.as_deref(), 513 521 crate::oauth::scopes::AccountAttr::Email, 514 522 crate::oauth::scopes::AccountAction::Read, 515 523 ) { 516 - return e; 524 + return Ok(e); 517 525 } 518 526 519 - let cache_key = email_update_cache_key(&auth.0.did); 527 + let cache_key = email_update_cache_key(&auth_user.did); 520 528 let pending_json = match state.cache.get(&cache_key).await { 521 529 Some(json) => json, 522 530 None => { 523 - return Json(json!({ "pending": false, "authorized": false })).into_response(); 531 + return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); 524 532 } 525 533 }; 526 534 527 535 let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) { 528 536 Ok(p) => p, 529 537 Err(_) => { 530 - return Json(json!({ "pending": false, "authorized": false })).into_response(); 538 + return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); 531 539 } 532 540 }; 533 541 534 - Json(json!({ 542 + Ok(Json(json!({ 535 543 "pending": true, 536 544 "authorized": pending.authorized, 537 545 "newEmail": pending.new_email, 538 546 })) 539 - .into_response() 547 + .into_response()) 540 548 } 541 549 542 550 #[derive(Deserialize)]
+46 -45
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::RequiredAuth; 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: RequiredAuth, 48 47 Json(input): Json<CreateInviteCodeInput>, 49 - ) -> Response { 48 + ) -> Result<Response, ApiError> { 49 + let auth_user = auth.0.require_user()?.require_active()?.require_admin()?; 50 50 if input.use_count < 1 { 51 - return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 51 + return Err(ApiError::InvalidRequest( 52 + "useCount must be at least 1".into(), 53 + )); 52 54 } 53 55 54 56 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 - }, 57 + Some(acct) => acct 58 + .parse() 59 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?, 59 60 None => auth_user.did.clone(), 60 61 }; 61 62 let code = gen_invite_code(); ··· 65 66 .create_invite_code(&code, input.use_count, Some(&for_account)) 66 67 .await 67 68 { 68 - Ok(true) => Json(CreateInviteCodeOutput { code }).into_response(), 69 + Ok(true) => Ok(Json(CreateInviteCodeOutput { code }).into_response()), 69 70 Ok(false) => { 70 71 error!("No admin user found to create invite code"); 71 - ApiError::InternalError(None).into_response() 72 + Err(ApiError::InternalError(None)) 72 73 } 73 74 Err(e) => { 74 75 error!("DB error creating invite code: {:?}", e); 75 - ApiError::InternalError(None).into_response() 76 + Err(ApiError::InternalError(None)) 76 77 } 77 78 } 78 79 } ··· 98 99 99 100 pub async fn create_invite_codes( 100 101 State(state): State<AppState>, 101 - BearerAuthAdmin(auth_user): BearerAuthAdmin, 102 + auth: RequiredAuth, 102 103 Json(input): Json<CreateInviteCodesInput>, 103 - ) -> Response { 104 + ) -> Result<Response, ApiError> { 105 + let auth_user = auth.0.require_user()?.require_active()?.require_admin()?; 104 106 if input.use_count < 1 { 105 - return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 107 + return Err(ApiError::InvalidRequest( 108 + "useCount must be at least 1".into(), 109 + )); 106 110 } 107 111 108 112 let code_count = input.code_count.unwrap_or(1).max(1); 109 113 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 - } 114 + Some(accounts) if !accounts.is_empty() => accounts 115 + .iter() 116 + .map(|a| a.parse()) 117 + .collect::<Result<Vec<Did>, _>>() 118 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?, 117 119 _ => vec![auth_user.did.clone()], 118 120 }; 119 121 120 - let admin_user_id = match state.user_repo.get_any_admin_user_id().await { 121 - Ok(Some(id)) => id, 122 - Ok(None) => { 122 + let admin_user_id = state 123 + .user_repo 124 + .get_any_admin_user_id() 125 + .await 126 + .map_err(|e| { 127 + error!("DB error looking up admin user: {:?}", e); 128 + ApiError::InternalError(None) 129 + })? 130 + .ok_or_else(|| { 123 131 error!("No admin user found to create invite codes"); 124 - return ApiError::InternalError(None).into_response(); 125 - } 126 - Err(e) => { 127 - error!("DB error looking up admin user: {:?}", e); 128 - return ApiError::InternalError(None).into_response(); 129 - } 130 - }; 132 + ApiError::InternalError(None) 133 + })?; 131 134 132 135 let result = futures::future::try_join_all(for_accounts.into_iter().map(|account| { 133 136 let infra_repo = state.infra_repo.clone(); ··· 146 149 .await; 147 150 148 151 match result { 149 - Ok(result_codes) => Json(CreateInviteCodesOutput { 152 + Ok(result_codes) => Ok(Json(CreateInviteCodesOutput { 150 153 codes: result_codes, 151 154 }) 152 - .into_response(), 155 + .into_response()), 153 156 Err(e) => { 154 157 error!("DB error creating invite codes: {:?}", e); 155 - ApiError::InternalError(None).into_response() 158 + Err(ApiError::InternalError(None)) 156 159 } 157 160 } 158 161 } ··· 192 195 193 196 pub async fn get_account_invite_codes( 194 197 State(state): State<AppState>, 195 - BearerAuth(auth_user): BearerAuth, 198 + auth: RequiredAuth, 196 199 axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>, 197 - ) -> Response { 200 + ) -> Result<Response, ApiError> { 201 + let auth_user = auth.0.require_user()?.require_active()?; 198 202 let include_used = params.include_used.unwrap_or(true); 199 203 200 - let codes_info = match state 204 + let codes_info = state 201 205 .infra_repo 202 206 .get_invite_codes_for_account(&auth_user.did) 203 207 .await 204 - { 205 - Ok(info) => info, 206 - Err(e) => { 208 + .map_err(|e| { 207 209 error!("DB error fetching invite codes: {:?}", e); 208 - return ApiError::InternalError(None).into_response(); 209 - } 210 - }; 210 + ApiError::InternalError(None) 211 + })?; 211 212 212 213 let filtered_codes: Vec<_> = codes_info 213 214 .into_iter() ··· 254 255 .await; 255 256 256 257 let codes: Vec<InviteCode> = codes.into_iter().flatten().collect(); 257 - Json(GetAccountInviteCodesOutput { codes }).into_response() 258 + Ok(Json(GetAccountInviteCodesOutput { codes }).into_response()) 258 259 }
+114 -146
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; 3 + use crate::auth::RequiredAuth; 4 4 use crate::auth::webauthn::WebAuthnConfig; 5 5 use crate::state::AppState; 6 6 use axum::{ ··· 34 34 35 35 pub async fn start_passkey_registration( 36 36 State(state): State<AppState>, 37 - auth: BearerAuth, 37 + auth: RequiredAuth, 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 auth_user = auth.0.require_user()?.require_active()?; 41 + let webauthn = get_webauthn()?; 44 42 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) => { 43 + let handle = state 44 + .user_repo 45 + .get_handle_by_did(&auth_user.did) 46 + .await 47 + .map_err(|e| { 51 48 error!("DB error fetching user: {:?}", e); 52 - return ApiError::InternalError(None).into_response(); 53 - } 54 - }; 49 + ApiError::InternalError(None) 50 + })? 51 + .ok_or(ApiError::AccountNotFound)?; 55 52 56 - let existing_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { 57 - Ok(passkeys) => passkeys, 58 - Err(e) => { 53 + let existing_passkeys = state 54 + .user_repo 55 + .get_passkeys_for_user(&auth_user.did) 56 + .await 57 + .map_err(|e| { 59 58 error!("DB error fetching existing passkeys: {:?}", e); 60 - return ApiError::InternalError(None).into_response(); 61 - } 62 - }; 59 + ApiError::InternalError(None) 60 + })?; 63 61 64 62 let exclude_credentials: Vec<CredentialID> = existing_passkeys 65 63 .iter() ··· 68 66 69 67 let display_name = input.friendly_name.as_deref().unwrap_or(&handle); 70 68 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) => { 69 + let (ccr, reg_state) = webauthn 70 + .start_registration(&auth_user.did, &handle, display_name, exclude_credentials) 71 + .map_err(|e| { 79 72 error!("Failed to start passkey registration: {}", e); 80 - return ApiError::InternalError(Some("Failed to start registration".into())) 81 - .into_response(); 82 - } 83 - }; 73 + ApiError::InternalError(Some("Failed to start registration".into())) 74 + })?; 84 75 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 - }; 76 + let state_json = serde_json::to_string(&reg_state).map_err(|e| { 77 + error!("Failed to serialize registration state: {:?}", e); 78 + ApiError::InternalError(None) 79 + })?; 92 80 93 - if let Err(e) = state 81 + state 94 82 .user_repo 95 - .save_webauthn_challenge(&auth.0.did, "registration", &state_json) 83 + .save_webauthn_challenge(&auth_user.did, "registration", &state_json) 96 84 .await 97 - { 98 - error!("Failed to save registration state: {:?}", e); 99 - return ApiError::InternalError(None).into_response(); 100 - } 85 + .map_err(|e| { 86 + error!("Failed to save registration state: {:?}", e); 87 + ApiError::InternalError(None) 88 + })?; 101 89 102 90 let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 103 91 104 - info!(did = %auth.0.did, "Passkey registration started"); 92 + info!(did = %auth_user.did, "Passkey registration started"); 105 93 106 - Json(StartRegistrationResponse { options }).into_response() 94 + Ok(Json(StartRegistrationResponse { options }).into_response()) 107 95 } 108 96 109 97 #[derive(Deserialize)] ··· 122 110 123 111 pub async fn finish_passkey_registration( 124 112 State(state): State<AppState>, 125 - auth: BearerAuth, 113 + auth: RequiredAuth, 126 114 Json(input): Json<FinishRegistrationInput>, 127 - ) -> Response { 128 - let webauthn = match get_webauthn() { 129 - Ok(w) => w, 130 - Err(e) => return e.into_response(), 131 - }; 115 + ) -> Result<Response, ApiError> { 116 + let auth_user = auth.0.require_user()?.require_active()?; 117 + let webauthn = get_webauthn()?; 132 118 133 - let reg_state_json = match state 119 + let reg_state_json = state 134 120 .user_repo 135 - .load_webauthn_challenge(&auth.0.did, "registration") 121 + .load_webauthn_challenge(&auth_user.did, "registration") 136 122 .await 137 - { 138 - Ok(Some(json)) => json, 139 - Ok(None) => { 140 - return ApiError::NoRegistrationInProgress.into_response(); 141 - } 142 - Err(e) => { 123 + .map_err(|e| { 143 124 error!("DB error loading registration state: {:?}", e); 144 - return ApiError::InternalError(None).into_response(); 145 - } 146 - }; 125 + ApiError::InternalError(None) 126 + })? 127 + .ok_or(ApiError::NoRegistrationInProgress)?; 147 128 148 - let reg_state: SecurityKeyRegistration = match serde_json::from_str(&reg_state_json) { 149 - Ok(s) => s, 150 - Err(e) => { 129 + let reg_state: SecurityKeyRegistration = 130 + serde_json::from_str(&reg_state_json).map_err(|e| { 151 131 error!("Failed to deserialize registration state: {:?}", e); 152 - return ApiError::InternalError(None).into_response(); 153 - } 154 - }; 132 + ApiError::InternalError(None) 133 + })?; 155 134 156 - let credential: RegisterPublicKeyCredential = match serde_json::from_value(input.credential) { 157 - Ok(c) => c, 158 - Err(e) => { 135 + let credential: RegisterPublicKeyCredential = serde_json::from_value(input.credential) 136 + .map_err(|e| { 159 137 warn!("Failed to parse credential: {:?}", e); 160 - return ApiError::InvalidCredential.into_response(); 161 - } 162 - }; 138 + ApiError::InvalidCredential 139 + })?; 163 140 164 - let passkey = match webauthn.finish_registration(&credential, &reg_state) { 165 - Ok(pk) => pk, 166 - Err(e) => { 141 + let passkey = webauthn 142 + .finish_registration(&credential, &reg_state) 143 + .map_err(|e| { 167 144 warn!("Failed to finish passkey registration: {}", e); 168 - return ApiError::RegistrationFailed.into_response(); 169 - } 170 - }; 145 + ApiError::RegistrationFailed 146 + })?; 171 147 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 - }; 148 + let public_key = serde_json::to_vec(&passkey).map_err(|e| { 149 + error!("Failed to serialize passkey: {:?}", e); 150 + ApiError::InternalError(None) 151 + })?; 179 152 180 - let passkey_id = match state 153 + let passkey_id = state 181 154 .user_repo 182 155 .save_passkey( 183 - &auth.0.did, 156 + &auth_user.did, 184 157 passkey.cred_id(), 185 158 &public_key, 186 159 input.friendly_name.as_deref(), 187 160 ) 188 161 .await 189 - { 190 - Ok(id) => id, 191 - Err(e) => { 162 + .map_err(|e| { 192 163 error!("Failed to save passkey: {:?}", e); 193 - return ApiError::InternalError(None).into_response(); 194 - } 195 - }; 164 + ApiError::InternalError(None) 165 + })?; 196 166 197 167 if let Err(e) = state 198 168 .user_repo 199 - .delete_webauthn_challenge(&auth.0.did, "registration") 169 + .delete_webauthn_challenge(&auth_user.did, "registration") 200 170 .await 201 171 { 202 172 warn!("Failed to delete registration state: {:?}", e); ··· 207 177 passkey.cred_id(), 208 178 ); 209 179 210 - info!(did = %auth.0.did, passkey_id = %passkey_id, "Passkey registered"); 180 + info!(did = %auth_user.did, passkey_id = %passkey_id, "Passkey registered"); 211 181 212 - Json(FinishRegistrationResponse { 182 + Ok(Json(FinishRegistrationResponse { 213 183 id: passkey_id.to_string(), 214 184 credential_id: credential_id_base64, 215 185 }) 216 - .into_response() 186 + .into_response()) 217 187 } 218 188 219 189 #[derive(Serialize)] ··· 232 202 pub passkeys: Vec<PasskeyInfo>, 233 203 } 234 204 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) => { 205 + pub async fn list_passkeys( 206 + State(state): State<AppState>, 207 + auth: RequiredAuth, 208 + ) -> Result<Response, ApiError> { 209 + let auth_user = auth.0.require_user()?.require_active()?; 210 + let passkeys = state 211 + .user_repo 212 + .get_passkeys_for_user(&auth_user.did) 213 + .await 214 + .map_err(|e| { 239 215 error!("DB error fetching passkeys: {:?}", e); 240 - return ApiError::InternalError(None).into_response(); 241 - } 242 - }; 216 + ApiError::InternalError(None) 217 + })?; 243 218 244 219 let passkey_infos: Vec<PasskeyInfo> = passkeys 245 220 .into_iter() ··· 252 227 }) 253 228 .collect(); 254 229 255 - Json(ListPasskeysResponse { 230 + Ok(Json(ListPasskeysResponse { 256 231 passkeys: passkey_infos, 257 232 }) 258 - .into_response() 233 + .into_response()) 259 234 } 260 235 261 236 #[derive(Deserialize)] ··· 266 241 267 242 pub async fn delete_passkey( 268 243 State(state): State<AppState>, 269 - auth: BearerAuth, 244 + auth: RequiredAuth, 270 245 Json(input): Json<DeletePasskeyInput>, 271 - ) -> Response { 272 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 246 + ) -> Result<Response, ApiError> { 247 + let auth_user = auth.0.require_user()?.require_active()?; 248 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth_user.did) 273 249 .await 274 250 { 275 - return crate::api::server::reauth::legacy_mfa_required_response( 251 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 276 252 &*state.user_repo, 277 253 &*state.session_repo, 278 - &auth.0.did, 254 + &auth_user.did, 279 255 ) 280 - .await; 256 + .await); 281 257 } 282 258 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( 259 + if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth_user.did).await 260 + { 261 + return Ok(crate::api::server::reauth::reauth_required_response( 285 262 &*state.user_repo, 286 263 &*state.session_repo, 287 - &auth.0.did, 264 + &auth_user.did, 288 265 ) 289 - .await; 266 + .await); 290 267 } 291 268 292 - let id: uuid::Uuid = match input.id.parse() { 293 - Ok(id) => id, 294 - Err(_) => { 295 - return ApiError::InvalidId.into_response(); 296 - } 297 - }; 269 + let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 298 270 299 - match state.user_repo.delete_passkey(id, &auth.0.did).await { 271 + match state.user_repo.delete_passkey(id, &auth_user.did).await { 300 272 Ok(true) => { 301 - info!(did = %auth.0.did, passkey_id = %id, "Passkey deleted"); 302 - EmptyResponse::ok().into_response() 273 + info!(did = %auth_user.did, passkey_id = %id, "Passkey deleted"); 274 + Ok(EmptyResponse::ok().into_response()) 303 275 } 304 - Ok(false) => ApiError::PasskeyNotFound.into_response(), 276 + Ok(false) => Err(ApiError::PasskeyNotFound), 305 277 Err(e) => { 306 278 error!("DB error deleting passkey: {:?}", e); 307 - ApiError::InternalError(None).into_response() 279 + Err(ApiError::InternalError(None)) 308 280 } 309 281 } 310 282 } ··· 318 290 319 291 pub async fn update_passkey( 320 292 State(state): State<AppState>, 321 - auth: BearerAuth, 293 + auth: RequiredAuth, 322 294 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 - }; 295 + ) -> Result<Response, ApiError> { 296 + let auth_user = auth.0.require_user()?.require_active()?; 297 + let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 330 298 331 299 match state 332 300 .user_repo 333 - .update_passkey_name(id, &auth.0.did, &input.friendly_name) 301 + .update_passkey_name(id, &auth_user.did, &input.friendly_name) 334 302 .await 335 303 { 336 304 Ok(true) => { 337 - info!(did = %auth.0.did, passkey_id = %id, "Passkey renamed"); 338 - EmptyResponse::ok().into_response() 305 + info!(did = %auth_user.did, passkey_id = %id, "Passkey renamed"); 306 + Ok(EmptyResponse::ok().into_response()) 339 307 } 340 - Ok(false) => ApiError::PasskeyNotFound.into_response(), 308 + Ok(false) => Err(ApiError::PasskeyNotFound), 341 309 Err(e) => { 342 310 error!("DB error updating passkey: {:?}", e); 343 - ApiError::InternalError(None).into_response() 311 + Err(ApiError::InternalError(None)) 344 312 } 345 313 } 346 314 }
+131 -124
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::RequiredAuth; 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: RequiredAuth, 231 231 Json(input): Json<ChangePasswordInput>, 232 - ) -> Response { 233 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 232 + ) -> Result<Response, ApiError> { 233 + let auth_user = auth.0.require_user()?.require_active()?; 234 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth_user.did) 234 235 .await 235 236 { 236 - return crate::api::server::reauth::legacy_mfa_required_response( 237 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 237 238 &*state.user_repo, 238 239 &*state.session_repo, 239 - &auth.0.did, 240 + &auth_user.did, 240 241 ) 241 - .await; 242 + .await); 242 243 } 243 244 244 245 let current_password = &input.current_password; 245 246 let new_password = &input.new_password; 246 247 if current_password.is_empty() { 247 - return ApiError::InvalidRequest("currentPassword is required".into()).into_response(); 248 + return Err(ApiError::InvalidRequest( 249 + "currentPassword is required".into(), 250 + )); 248 251 } 249 252 if new_password.is_empty() { 250 - return ApiError::InvalidRequest("newPassword is required".into()).into_response(); 253 + return Err(ApiError::InvalidRequest("newPassword is required".into())); 251 254 } 252 255 if let Err(e) = validate_password(new_password) { 253 - return ApiError::InvalidRequest(e.to_string()).into_response(); 256 + return Err(ApiError::InvalidRequest(e.to_string())); 254 257 } 255 - let user = match state 258 + let user = state 256 259 .user_repo 257 - .get_id_and_password_hash_by_did(&auth.0.did) 260 + .get_id_and_password_hash_by_did(&auth_user.did) 258 261 .await 259 - { 260 - Ok(Some(u)) => u, 261 - Ok(None) => { 262 - return ApiError::AccountNotFound.into_response(); 263 - } 264 - Err(e) => { 262 + .map_err(|e| { 265 263 error!("DB error in change_password: {:?}", e); 266 - return ApiError::InternalError(None).into_response(); 267 - } 268 - }; 264 + ApiError::InternalError(None) 265 + })? 266 + .ok_or(ApiError::AccountNotFound)?; 267 + 269 268 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 - }; 269 + let valid = verify(current_password, &password_hash).map_err(|e| { 270 + error!("Password verification error: {:?}", e); 271 + ApiError::InternalError(None) 272 + })?; 277 273 if !valid { 278 - return ApiError::InvalidPassword("Current password is incorrect".into()).into_response(); 274 + return Err(ApiError::InvalidPassword( 275 + "Current password is incorrect".into(), 276 + )); 279 277 } 280 278 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 279 + let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)) 280 + .await 281 + .map_err(|e| { 282 + error!("Failed to spawn blocking task: {:?}", e); 283 + ApiError::InternalError(None) 284 + })? 285 + .map_err(|e| { 286 + error!("Failed to hash password: {:?}", e); 287 + ApiError::InternalError(None) 288 + })?; 289 + 290 + state 294 291 .user_repo 295 292 .update_password_hash(user_id, &new_hash) 296 293 .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() 294 + .map_err(|e| { 295 + error!("DB error updating password: {:?}", e); 296 + ApiError::InternalError(None) 297 + })?; 298 + 299 + info!(did = %&auth_user.did, "Password changed successfully"); 300 + Ok(EmptyResponse::ok().into_response()) 303 301 } 304 302 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(), 303 + pub async fn get_password_status( 304 + State(state): State<AppState>, 305 + auth: RequiredAuth, 306 + ) -> Result<Response, ApiError> { 307 + let auth_user = auth.0.require_user()?.require_active()?; 308 + match state.user_repo.has_password_by_did(&auth_user.did).await { 309 + Ok(Some(has)) => Ok(HasPasswordResponse::response(has).into_response()), 310 + Ok(None) => Err(ApiError::AccountNotFound), 309 311 Err(e) => { 310 312 error!("DB error: {:?}", e); 311 - ApiError::InternalError(None).into_response() 313 + Err(ApiError::InternalError(None)) 312 314 } 313 315 } 314 316 } 315 317 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 + pub async fn remove_password( 319 + State(state): State<AppState>, 320 + auth: RequiredAuth, 321 + ) -> Result<Response, ApiError> { 322 + let auth_user = auth.0.require_user()?.require_active()?; 323 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth_user.did) 318 324 .await 319 325 { 320 - return crate::api::server::reauth::legacy_mfa_required_response( 326 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 321 327 &*state.user_repo, 322 328 &*state.session_repo, 323 - &auth.0.did, 329 + &auth_user.did, 324 330 ) 325 - .await; 331 + .await); 326 332 } 327 333 328 334 if crate::api::server::reauth::check_reauth_required_cached( 329 335 &*state.session_repo, 330 336 &state.cache, 331 - &auth.0.did, 337 + &auth_user.did, 332 338 ) 333 339 .await 334 340 { 335 - return crate::api::server::reauth::reauth_required_response( 341 + return Ok(crate::api::server::reauth::reauth_required_response( 336 342 &*state.user_repo, 337 343 &*state.session_repo, 338 - &auth.0.did, 344 + &auth_user.did, 339 345 ) 340 - .await; 346 + .await); 341 347 } 342 348 343 349 let has_passkeys = state 344 350 .user_repo 345 - .has_passkeys(&auth.0.did) 351 + .has_passkeys(&auth_user.did) 346 352 .await 347 353 .unwrap_or(false); 348 354 if !has_passkeys { 349 - return ApiError::InvalidRequest( 355 + return Err(ApiError::InvalidRequest( 350 356 "You must have at least one passkey registered before removing your password".into(), 351 - ) 352 - .into_response(); 357 + )); 353 358 } 354 359 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) => { 360 + let user = state 361 + .user_repo 362 + .get_password_info_by_did(&auth_user.did) 363 + .await 364 + .map_err(|e| { 361 365 error!("DB error: {:?}", e); 362 - return ApiError::InternalError(None).into_response(); 363 - } 364 - }; 366 + ApiError::InternalError(None) 367 + })? 368 + .ok_or(ApiError::AccountNotFound)?; 365 369 366 370 if user.password_hash.is_none() { 367 - return ApiError::InvalidRequest("Account already has no password".into()).into_response(); 371 + return Err(ApiError::InvalidRequest( 372 + "Account already has no password".into(), 373 + )); 368 374 } 369 375 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 - } 376 + state 377 + .user_repo 378 + .remove_user_password(user.id) 379 + .await 380 + .map_err(|e| { 381 + error!("DB error removing password: {:?}", e); 382 + ApiError::InternalError(None) 383 + })?; 374 384 375 - info!(did = %&auth.0.did, "Password removed - account is now passkey-only"); 376 - SuccessResponse::ok().into_response() 385 + info!(did = %&auth_user.did, "Password removed - account is now passkey-only"); 386 + Ok(SuccessResponse::ok().into_response()) 377 387 } 378 388 379 389 #[derive(Deserialize)] ··· 384 394 385 395 pub async fn set_password( 386 396 State(state): State<AppState>, 387 - auth: BearerAuth, 397 + auth: RequiredAuth, 388 398 Json(input): Json<SetPasswordInput>, 389 - ) -> Response { 399 + ) -> Result<Response, ApiError> { 400 + let auth_user = auth.0.require_user()?.require_active()?; 390 401 let has_password = state 391 402 .user_repo 392 - .has_password_by_did(&auth.0.did) 403 + .has_password_by_did(&auth_user.did) 393 404 .await 394 405 .ok() 395 406 .flatten() 396 407 .unwrap_or(false); 397 408 let has_passkeys = state 398 409 .user_repo 399 - .has_passkeys(&auth.0.did) 410 + .has_passkeys(&auth_user.did) 400 411 .await 401 412 .unwrap_or(false); 402 413 let has_totp = state 403 414 .user_repo 404 - .has_totp_enabled(&auth.0.did) 415 + .has_totp_enabled(&auth_user.did) 405 416 .await 406 417 .unwrap_or(false); 407 418 ··· 411 422 && crate::api::server::reauth::check_reauth_required_cached( 412 423 &*state.session_repo, 413 424 &state.cache, 414 - &auth.0.did, 425 + &auth_user.did, 415 426 ) 416 427 .await 417 428 { 418 - return crate::api::server::reauth::reauth_required_response( 429 + return Ok(crate::api::server::reauth::reauth_required_response( 419 430 &*state.user_repo, 420 431 &*state.session_repo, 421 - &auth.0.did, 432 + &auth_user.did, 422 433 ) 423 - .await; 434 + .await); 424 435 } 425 436 426 437 let new_password = &input.new_password; 427 438 if new_password.is_empty() { 428 - return ApiError::InvalidRequest("newPassword is required".into()).into_response(); 439 + return Err(ApiError::InvalidRequest("newPassword is required".into())); 429 440 } 430 441 if let Err(e) = validate_password(new_password) { 431 - return ApiError::InvalidRequest(e.to_string()).into_response(); 442 + return Err(ApiError::InvalidRequest(e.to_string())); 432 443 } 433 444 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) => { 445 + let user = state 446 + .user_repo 447 + .get_password_info_by_did(&auth_user.did) 448 + .await 449 + .map_err(|e| { 440 450 error!("DB error: {:?}", e); 441 - return ApiError::InternalError(None).into_response(); 442 - } 443 - }; 451 + ApiError::InternalError(None) 452 + })? 453 + .ok_or(ApiError::AccountNotFound)?; 444 454 445 455 if user.password_hash.is_some() { 446 - return ApiError::InvalidRequest( 456 + return Err(ApiError::InvalidRequest( 447 457 "Account already has a password. Use changePassword instead.".into(), 448 - ) 449 - .into_response(); 458 + )); 450 459 } 451 460 452 461 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 - }; 462 + let new_hash = tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)) 463 + .await 464 + .map_err(|e| { 465 + error!("Failed to spawn blocking task: {:?}", e); 466 + ApiError::InternalError(None) 467 + })? 468 + .map_err(|e| { 469 + error!("Failed to hash password: {:?}", e); 470 + ApiError::InternalError(None) 471 + })?; 465 472 466 - if let Err(e) = state 473 + state 467 474 .user_repo 468 475 .set_new_user_password(user.id, &new_hash) 469 476 .await 470 - { 471 - error!("DB error setting password: {:?}", e); 472 - return ApiError::InternalError(None).into_response(); 473 - } 477 + .map_err(|e| { 478 + error!("DB error setting password: {:?}", e); 479 + ApiError::InternalError(None) 480 + })?; 474 481 475 - info!(did = %&auth.0.did, "Password set for passkey-only account"); 476 - SuccessResponse::ok().into_response() 482 + info!(did = %&auth_user.did, "Password set for passkey-only account"); 483 + Ok(SuccessResponse::ok().into_response()) 477 484 }
+137 -147
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::RequiredAuth; 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: RequiredAuth, 30 + ) -> Result<Response, ApiError> { 31 + let auth_user = auth.0.require_user()?.require_active()?; 32 + let last_reauth_at = state 33 + .session_repo 34 + .get_last_reauth_at(&auth_user.did) 35 + .await 36 + .map_err(|e| { 31 37 error!("DB error: {:?}", e); 32 - return ApiError::InternalError(None).into_response(); 33 - } 34 - }; 38 + ApiError::InternalError(None) 39 + })?; 35 40 36 41 let reauth_required = is_reauth_required(last_reauth_at); 37 42 let available_methods = 38 - get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.0.did).await; 43 + get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth_user.did).await; 39 44 40 - Json(ReauthStatusResponse { 45 + Ok(Json(ReauthStatusResponse { 41 46 last_reauth_at, 42 47 reauth_required, 43 48 available_methods, 44 49 }) 45 - .into_response() 50 + .into_response()) 46 51 } 47 52 48 53 #[derive(Deserialize)] ··· 59 64 60 65 pub async fn reauth_password( 61 66 State(state): State<AppState>, 62 - auth: BearerAuth, 67 + auth: RequiredAuth, 63 68 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) => { 69 + ) -> Result<Response, ApiError> { 70 + let auth_user = auth.0.require_user()?.require_active()?; 71 + let password_hash = state 72 + .user_repo 73 + .get_password_hash_by_did(&auth_user.did) 74 + .await 75 + .map_err(|e| { 71 76 error!("DB error: {:?}", e); 72 - return ApiError::InternalError(None).into_response(); 73 - } 74 - }; 77 + ApiError::InternalError(None) 78 + })? 79 + .ok_or(ApiError::AccountNotFound)?; 75 80 76 81 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 77 82 78 83 if !password_valid { 79 84 let app_password_hashes = state 80 85 .session_repo 81 - .get_app_password_hashes_by_did(&auth.0.did) 86 + .get_app_password_hashes_by_did(&auth_user.did) 82 87 .await 83 88 .unwrap_or_default(); 84 89 ··· 87 92 }); 88 93 89 94 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(); 95 + warn!(did = %&auth_user.did, "Re-auth failed: invalid password"); 96 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 92 97 } 93 98 } 94 99 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) => { 100 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth_user.did) 101 + .await 102 + .map_err(|e| { 101 103 error!("DB error updating reauth: {:?}", e); 102 - ApiError::InternalError(None).into_response() 103 - } 104 - } 104 + ApiError::InternalError(None) 105 + })?; 106 + 107 + info!(did = %&auth_user.did, "Re-auth successful via password"); 108 + Ok(Json(ReauthResponse { reauthed_at }).into_response()) 105 109 } 106 110 107 111 #[derive(Deserialize)] ··· 112 116 113 117 pub async fn reauth_totp( 114 118 State(state): State<AppState>, 115 - auth: BearerAuth, 119 + auth: RequiredAuth, 116 120 Json(input): Json<TotpReauthInput>, 117 - ) -> Response { 121 + ) -> Result<Response, ApiError> { 122 + let auth_user = auth.0.require_user()?.require_active()?; 118 123 if !state 119 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 124 + .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 120 125 .await 121 126 { 122 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 123 - return ApiError::RateLimitExceeded(Some( 127 + warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 128 + return Err(ApiError::RateLimitExceeded(Some( 124 129 "Too many verification attempts. Please try again in a few minutes.".into(), 125 - )) 126 - .into_response(); 130 + ))); 127 131 } 128 132 129 - let valid = 130 - crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.0.did, &input.code) 131 - .await; 133 + let valid = crate::api::server::totp::verify_totp_or_backup_for_user( 134 + &state, 135 + &auth_user.did, 136 + &input.code, 137 + ) 138 + .await; 132 139 133 140 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(); 141 + warn!(did = %&auth_user.did, "Re-auth failed: invalid TOTP code"); 142 + return Err(ApiError::InvalidCode(Some( 143 + "Invalid TOTP or backup code".into(), 144 + ))); 136 145 } 137 146 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) => { 147 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth_user.did) 148 + .await 149 + .map_err(|e| { 144 150 error!("DB error updating reauth: {:?}", e); 145 - ApiError::InternalError(None).into_response() 146 - } 147 - } 151 + ApiError::InternalError(None) 152 + })?; 153 + 154 + info!(did = %&auth_user.did, "Re-auth successful via TOTP"); 155 + Ok(Json(ReauthResponse { reauthed_at }).into_response()) 148 156 } 149 157 150 158 #[derive(Serialize)] ··· 153 161 pub options: serde_json::Value, 154 162 } 155 163 156 - pub async fn reauth_passkey_start(State(state): State<AppState>, auth: BearerAuth) -> Response { 164 + pub async fn reauth_passkey_start( 165 + State(state): State<AppState>, 166 + auth: RequiredAuth, 167 + ) -> Result<Response, ApiError> { 168 + let auth_user = auth.0.require_user()?.require_active()?; 157 169 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 158 170 159 - let stored_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { 160 - Ok(pks) => pks, 161 - Err(e) => { 171 + let stored_passkeys = state 172 + .user_repo 173 + .get_passkeys_for_user(&auth_user.did) 174 + .await 175 + .map_err(|e| { 162 176 error!("Failed to get passkeys: {:?}", e); 163 - return ApiError::InternalError(None).into_response(); 164 - } 165 - }; 177 + ApiError::InternalError(None) 178 + })?; 166 179 167 180 if stored_passkeys.is_empty() { 168 - return ApiError::NoPasskeys.into_response(); 181 + return Err(ApiError::NoPasskeys); 169 182 } 170 183 171 184 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys ··· 174 187 .collect(); 175 188 176 189 if passkeys.is_empty() { 177 - return ApiError::InternalError(Some("Failed to load passkeys".into())).into_response(); 190 + return Err(ApiError::InternalError(Some( 191 + "Failed to load passkeys".into(), 192 + ))); 178 193 } 179 194 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 - }; 195 + let webauthn = crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname).map_err(|e| { 196 + error!("Failed to create WebAuthn config: {:?}", e); 197 + ApiError::InternalError(None) 198 + })?; 187 199 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 - }; 200 + let (rcr, auth_state) = webauthn.start_authentication(passkeys).map_err(|e| { 201 + error!("Failed to start passkey authentication: {:?}", e); 202 + ApiError::InternalError(None) 203 + })?; 195 204 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 - }; 205 + let state_json = serde_json::to_string(&auth_state).map_err(|e| { 206 + error!("Failed to serialize authentication state: {:?}", e); 207 + ApiError::InternalError(None) 208 + })?; 203 209 204 - if let Err(e) = state 210 + state 205 211 .user_repo 206 - .save_webauthn_challenge(&auth.0.did, "authentication", &state_json) 212 + .save_webauthn_challenge(&auth_user.did, "authentication", &state_json) 207 213 .await 208 - { 209 - error!("Failed to save authentication state: {:?}", e); 210 - return ApiError::InternalError(None).into_response(); 211 - } 214 + .map_err(|e| { 215 + error!("Failed to save authentication state: {:?}", e); 216 + ApiError::InternalError(None) 217 + })?; 212 218 213 219 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 214 - Json(PasskeyReauthStartResponse { options }).into_response() 220 + Ok(Json(PasskeyReauthStartResponse { options }).into_response()) 215 221 } 216 222 217 223 #[derive(Deserialize)] ··· 222 228 223 229 pub async fn reauth_passkey_finish( 224 230 State(state): State<AppState>, 225 - auth: BearerAuth, 231 + auth: RequiredAuth, 226 232 Json(input): Json<PasskeyReauthFinishInput>, 227 - ) -> Response { 233 + ) -> Result<Response, ApiError> { 234 + let auth_user = auth.0.require_user()?.require_active()?; 228 235 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 229 236 230 - let auth_state_json = match state 237 + let auth_state_json = state 231 238 .user_repo 232 - .load_webauthn_challenge(&auth.0.did, "authentication") 239 + .load_webauthn_challenge(&auth_user.did, "authentication") 233 240 .await 234 - { 235 - Ok(Some(json)) => json, 236 - Ok(None) => { 237 - return ApiError::NoChallengeInProgress.into_response(); 238 - } 239 - Err(e) => { 241 + .map_err(|e| { 240 242 error!("Failed to load authentication state: {:?}", e); 241 - return ApiError::InternalError(None).into_response(); 242 - } 243 - }; 243 + ApiError::InternalError(None) 244 + })? 245 + .ok_or(ApiError::NoChallengeInProgress)?; 244 246 245 247 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 - }; 248 + serde_json::from_str(&auth_state_json).map_err(|e| { 249 + error!("Failed to deserialize authentication state: {:?}", e); 250 + ApiError::InternalError(None) 251 + })?; 253 252 254 253 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 - }; 254 + serde_json::from_value(input.credential).map_err(|e| { 255 + warn!("Failed to parse credential: {:?}", e); 256 + ApiError::InvalidCredential 257 + })?; 262 258 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 - }; 259 + let webauthn = crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname).map_err(|e| { 260 + error!("Failed to create WebAuthn config: {:?}", e); 261 + ApiError::InternalError(None) 262 + })?; 270 263 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 - }; 264 + let auth_result = webauthn 265 + .finish_authentication(&credential, &auth_state) 266 + .map_err(|e| { 267 + warn!(did = %&auth_user.did, "Passkey re-auth failed: {:?}", e); 268 + ApiError::AuthenticationFailed(Some("Passkey authentication failed".into())) 269 + })?; 279 270 280 271 let cred_id_bytes = auth_result.cred_id().as_ref(); 281 272 match state ··· 284 275 .await 285 276 { 286 277 Ok(false) => { 287 - warn!(did = %&auth.0.did, "Passkey counter anomaly detected - possible cloned key"); 278 + warn!(did = %&auth_user.did, "Passkey counter anomaly detected - possible cloned key"); 288 279 let _ = state 289 280 .user_repo 290 - .delete_webauthn_challenge(&auth.0.did, "authentication") 281 + .delete_webauthn_challenge(&auth_user.did, "authentication") 291 282 .await; 292 - return ApiError::PasskeyCounterAnomaly.into_response(); 283 + return Err(ApiError::PasskeyCounterAnomaly); 293 284 } 294 285 Err(e) => { 295 286 error!("Failed to update passkey counter: {:?}", e); ··· 299 290 300 291 let _ = state 301 292 .user_repo 302 - .delete_webauthn_challenge(&auth.0.did, "authentication") 293 + .delete_webauthn_challenge(&auth_user.did, "authentication") 303 294 .await; 304 295 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) => { 296 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth_user.did) 297 + .await 298 + .map_err(|e| { 311 299 error!("DB error updating reauth: {:?}", e); 312 - ApiError::InternalError(None).into_response() 313 - } 314 - } 300 + ApiError::InternalError(None) 301 + })?; 302 + 303 + info!(did = %&auth_user.did, "Re-auth successful via passkey"); 304 + Ok(Json(ReauthResponse { reauthed_at }).into_response()) 315 305 } 316 306 317 307 pub async fn update_last_reauth_cached(
+174 -169
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::RequiredAuth; 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 { 282 + auth: RequiredAuth, 283 + ) -> Result<Response, ApiError> { 284 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 284 285 let permissions = auth_user.permissions(); 285 286 let can_read_email = permissions.allows_email_read(); 286 287 ··· 337 338 if let Some(doc) = did_doc { 338 339 response["didDoc"] = doc; 339 340 } 340 - Json(response).into_response() 341 + Ok(Json(response).into_response()) 341 342 } 342 - Ok(None) => ApiError::AuthenticationFailed(None).into_response(), 343 + Ok(None) => Err(ApiError::AuthenticationFailed(None)), 343 344 Err(e) => { 344 345 error!("Database error in get_session: {:?}", e); 345 - ApiError::InternalError(None).into_response() 346 + Err(ApiError::InternalError(None)) 346 347 } 347 348 } 348 349 } ··· 350 351 pub async fn delete_session( 351 352 State(state): State<AppState>, 352 353 headers: axum::http::HeaderMap, 353 - _auth: BearerAuth, 354 - ) -> Response { 355 - let extracted = match crate::auth::extract_auth_token_from_header( 354 + auth: RequiredAuth, 355 + ) -> Result<Response, ApiError> { 356 + auth.0.require_user()?.require_active()?; 357 + let extracted = crate::auth::extract_auth_token_from_header( 356 358 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 - }; 359 + ) 360 + .ok_or(ApiError::AuthenticationRequired)?; 361 + let jti = crate::auth::get_jti_from_token(&extracted.token) 362 + .map_err(|_| ApiError::AuthenticationFailed(None))?; 365 363 let did = crate::auth::get_did_from_token(&extracted.token).ok(); 366 364 match state.session_repo.delete_session_by_access_jti(&jti).await { 367 365 Ok(rows) if rows > 0 => { ··· 369 367 let session_cache_key = format!("auth:session:{}:{}", did, jti); 370 368 let _ = state.cache.delete(&session_cache_key).await; 371 369 } 372 - EmptyResponse::ok().into_response() 370 + Ok(EmptyResponse::ok().into_response()) 373 371 } 374 - Ok(_) => ApiError::AuthenticationFailed(None).into_response(), 375 - Err(_) => ApiError::AuthenticationFailed(None).into_response(), 372 + Ok(_) => Err(ApiError::AuthenticationFailed(None)), 373 + Err(_) => Err(ApiError::AuthenticationFailed(None)), 376 374 } 377 375 } 378 376 ··· 796 794 pub async fn list_sessions( 797 795 State(state): State<AppState>, 798 796 headers: HeaderMap, 799 - auth: BearerAuth, 800 - ) -> Response { 797 + auth: RequiredAuth, 798 + ) -> Result<Response, ApiError> { 799 + let auth_user = auth.0.require_user()?.require_active()?; 801 800 let current_jti = headers 802 801 .get("authorization") 803 802 .and_then(|v| v.to_str().ok()) 804 803 .and_then(|v| v.strip_prefix("Bearer ")) 805 804 .and_then(|token| crate::auth::get_jti_from_token(token).ok()); 806 805 807 - let jwt_rows = match state.session_repo.list_sessions_by_did(&auth.0.did).await { 808 - Ok(rows) => rows, 809 - Err(e) => { 806 + let jwt_rows = state 807 + .session_repo 808 + .list_sessions_by_did(&auth_user.did) 809 + .await 810 + .map_err(|e| { 810 811 error!("DB error fetching JWT sessions: {:?}", e); 811 - return ApiError::InternalError(None).into_response(); 812 - } 813 - }; 812 + ApiError::InternalError(None) 813 + })?; 814 814 815 - let oauth_rows = match state.oauth_repo.list_sessions_by_did(&auth.0.did).await { 816 - Ok(rows) => rows, 817 - Err(e) => { 815 + let oauth_rows = state 816 + .oauth_repo 817 + .list_sessions_by_did(&auth_user.did) 818 + .await 819 + .map_err(|e| { 818 820 error!("DB error fetching OAuth sessions: {:?}", e); 819 - return ApiError::InternalError(None).into_response(); 820 - } 821 - }; 821 + ApiError::InternalError(None) 822 + })?; 822 823 823 824 let jwt_sessions = jwt_rows.into_iter().map(|row| SessionInfo { 824 825 id: format!("jwt:{}", row.id), ··· 829 830 is_current: current_jti.as_ref() == Some(&row.access_jti), 830 831 }); 831 832 832 - let is_oauth = auth.0.is_oauth; 833 + let is_oauth = auth_user.is_oauth; 833 834 let oauth_sessions = oauth_rows.into_iter().map(|row| { 834 835 let client_name = extract_client_name(&row.client_id); 835 836 let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str()); ··· 846 847 let mut sessions: Vec<SessionInfo> = jwt_sessions.chain(oauth_sessions).collect(); 847 848 sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); 848 849 849 - (StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response() 850 + Ok((StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response()) 850 851 } 851 852 852 853 fn extract_client_name(client_id: &str) -> String { ··· 867 868 868 869 pub async fn revoke_session( 869 870 State(state): State<AppState>, 870 - auth: BearerAuth, 871 + auth: RequiredAuth, 871 872 Json(input): Json<RevokeSessionInput>, 872 - ) -> Response { 873 + ) -> Result<Response, ApiError> { 874 + let auth_user = auth.0.require_user()?.require_active()?; 873 875 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 876 + let session_id: i32 = jwt_id 877 + .parse() 878 + .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; 879 + let access_jti = state 878 880 .session_repo 879 - .get_session_access_jti_by_id(session_id, &auth.0.did) 881 + .get_session_access_jti_by_id(session_id, &auth_user.did) 880 882 .await 881 - { 882 - Ok(Some(jti)) => jti, 883 - Ok(None) => { 884 - return ApiError::SessionNotFound.into_response(); 885 - } 886 - Err(e) => { 883 + .map_err(|e| { 887 884 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); 885 + ApiError::InternalError(None) 886 + })? 887 + .ok_or(ApiError::SessionNotFound)?; 888 + state 889 + .session_repo 890 + .delete_session_by_id(session_id) 891 + .await 892 + .map_err(|e| { 893 + error!("DB error deleting session: {:?}", e); 894 + ApiError::InternalError(None) 895 + })?; 896 + let cache_key = format!("auth:session:{}:{}", &auth_user.did, access_jti); 896 897 if let Err(e) = state.cache.delete(&cache_key).await { 897 898 warn!("Failed to invalidate session cache: {:?}", e); 898 899 } 899 - info!(did = %&auth.0.did, session_id = %session_id, "JWT session revoked"); 900 + info!(did = %&auth_user.did, session_id = %session_id, "JWT session revoked"); 900 901 } 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 902 + let session_id: i32 = oauth_id 903 + .parse() 904 + .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; 905 + let deleted = state 905 906 .oauth_repo 906 - .delete_session_by_id(session_id, &auth.0.did) 907 + .delete_session_by_id(session_id, &auth_user.did) 907 908 .await 908 - { 909 - Ok(0) => { 910 - return ApiError::SessionNotFound.into_response(); 911 - } 912 - Err(e) => { 909 + .map_err(|e| { 913 910 error!("DB error deleting OAuth session: {:?}", e); 914 - return ApiError::InternalError(None).into_response(); 915 - } 916 - _ => {} 911 + ApiError::InternalError(None) 912 + })?; 913 + if deleted == 0 { 914 + return Err(ApiError::SessionNotFound); 917 915 } 918 - info!(did = %&auth.0.did, session_id = %session_id, "OAuth session revoked"); 916 + info!(did = %&auth_user.did, session_id = %session_id, "OAuth session revoked"); 919 917 } else { 920 - return ApiError::InvalidRequest("Invalid session ID format".into()).into_response(); 918 + return Err(ApiError::InvalidRequest("Invalid session ID format".into())); 921 919 } 922 - EmptyResponse::ok().into_response() 920 + Ok(EmptyResponse::ok().into_response()) 923 921 } 924 922 925 923 pub async fn revoke_all_sessions( 926 924 State(state): State<AppState>, 927 925 headers: HeaderMap, 928 - auth: BearerAuth, 929 - ) -> Response { 930 - let current_jti = crate::auth::extract_auth_token_from_header( 926 + auth: RequiredAuth, 927 + ) -> Result<Response, ApiError> { 928 + let auth_user = auth.0.require_user()?.require_active()?; 929 + let jti = crate::auth::extract_auth_token_from_header( 931 930 headers.get("authorization").and_then(|v| v.to_str().ok()), 932 931 ) 933 - .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()); 932 + .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()) 933 + .ok_or(ApiError::InvalidToken(None))?; 934 934 935 - let Some(ref jti) = current_jti else { 936 - return ApiError::InvalidToken(None).into_response(); 937 - }; 938 - 939 - if auth.0.is_oauth { 940 - if let Err(e) = state.session_repo.delete_sessions_by_did(&auth.0.did).await { 941 - error!("DB error revoking JWT sessions: {:?}", e); 942 - return ApiError::InternalError(None).into_response(); 943 - } 935 + if auth_user.is_oauth { 936 + state 937 + .session_repo 938 + .delete_sessions_by_did(&auth_user.did) 939 + .await 940 + .map_err(|e| { 941 + error!("DB error revoking JWT sessions: {:?}", e); 942 + ApiError::InternalError(None) 943 + })?; 944 944 let jti_typed = TokenId::from(jti.clone()); 945 - if let Err(e) = state 945 + state 946 946 .oauth_repo 947 - .delete_sessions_by_did_except(&auth.0.did, &jti_typed) 947 + .delete_sessions_by_did_except(&auth_user.did, &jti_typed) 948 948 .await 949 - { 950 - error!("DB error revoking OAuth sessions: {:?}", e); 951 - return ApiError::InternalError(None).into_response(); 952 - } 949 + .map_err(|e| { 950 + error!("DB error revoking OAuth sessions: {:?}", e); 951 + ApiError::InternalError(None) 952 + })?; 953 953 } else { 954 - if let Err(e) = state 954 + state 955 955 .session_repo 956 - .delete_sessions_by_did_except_jti(&auth.0.did, jti) 956 + .delete_sessions_by_did_except_jti(&auth_user.did, &jti) 957 + .await 958 + .map_err(|e| { 959 + error!("DB error revoking JWT sessions: {:?}", e); 960 + ApiError::InternalError(None) 961 + })?; 962 + state 963 + .oauth_repo 964 + .delete_sessions_by_did(&auth_user.did) 957 965 .await 958 - { 959 - error!("DB error revoking JWT sessions: {:?}", e); 960 - return ApiError::InternalError(None).into_response(); 961 - } 962 - if let Err(e) = state.oauth_repo.delete_sessions_by_did(&auth.0.did).await { 963 - error!("DB error revoking OAuth sessions: {:?}", e); 964 - return ApiError::InternalError(None).into_response(); 965 - } 966 + .map_err(|e| { 967 + error!("DB error revoking OAuth sessions: {:?}", e); 968 + ApiError::InternalError(None) 969 + })?; 966 970 } 967 971 968 - info!(did = %&auth.0.did, "All other sessions revoked"); 969 - SuccessResponse::ok().into_response() 972 + info!(did = %&auth_user.did, "All other sessions revoked"); 973 + Ok(SuccessResponse::ok().into_response()) 970 974 } 971 975 972 976 #[derive(Serialize)] ··· 978 982 979 983 pub async fn get_legacy_login_preference( 980 984 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) => { 985 + auth: RequiredAuth, 986 + ) -> Result<Response, ApiError> { 987 + let auth_user = auth.0.require_user()?.require_active()?; 988 + let pref = state 989 + .user_repo 990 + .get_legacy_login_pref(&auth_user.did) 991 + .await 992 + .map_err(|e| { 991 993 error!("DB error: {:?}", e); 992 - ApiError::InternalError(None).into_response() 993 - } 994 - } 994 + ApiError::InternalError(None) 995 + })? 996 + .ok_or(ApiError::AccountNotFound)?; 997 + Ok(Json(LegacyLoginPreferenceOutput { 998 + allow_legacy_login: pref.allow_legacy_login, 999 + has_mfa: pref.has_mfa, 1000 + }) 1001 + .into_response()) 995 1002 } 996 1003 997 1004 #[derive(Deserialize)] ··· 1002 1009 1003 1010 pub async fn update_legacy_login_preference( 1004 1011 State(state): State<AppState>, 1005 - auth: BearerAuth, 1012 + auth: RequiredAuth, 1006 1013 Json(input): Json<UpdateLegacyLoginInput>, 1007 - ) -> Response { 1008 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 1014 + ) -> Result<Response, ApiError> { 1015 + let auth_user = auth.0.require_user()?.require_active()?; 1016 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth_user.did) 1009 1017 .await 1010 1018 { 1011 - return crate::api::server::reauth::legacy_mfa_required_response( 1019 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 1012 1020 &*state.user_repo, 1013 1021 &*state.session_repo, 1014 - &auth.0.did, 1022 + &auth_user.did, 1015 1023 ) 1016 - .await; 1024 + .await); 1017 1025 } 1018 1026 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( 1027 + if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth_user.did).await 1028 + { 1029 + return Ok(crate::api::server::reauth::reauth_required_response( 1021 1030 &*state.user_repo, 1022 1031 &*state.session_repo, 1023 - &auth.0.did, 1032 + &auth_user.did, 1024 1033 ) 1025 - .await; 1034 + .await); 1026 1035 } 1027 1036 1028 - match state 1037 + let updated = state 1029 1038 .user_repo 1030 - .update_legacy_login(&auth.0.did, input.allow_legacy_login) 1039 + .update_legacy_login(&auth_user.did, input.allow_legacy_login) 1031 1040 .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) => { 1041 + .map_err(|e| { 1046 1042 error!("DB error: {:?}", e); 1047 - ApiError::InternalError(None).into_response() 1048 - } 1043 + ApiError::InternalError(None) 1044 + })?; 1045 + if !updated { 1046 + return Err(ApiError::AccountNotFound); 1049 1047 } 1048 + info!( 1049 + did = %&auth_user.did, 1050 + allow_legacy_login = input.allow_legacy_login, 1051 + "Legacy login preference updated" 1052 + ); 1053 + Ok(Json(json!({ 1054 + "allowLegacyLogin": input.allow_legacy_login 1055 + })) 1056 + .into_response()) 1050 1057 } 1051 1058 1052 1059 use crate::comms::VALID_LOCALES; ··· 1059 1066 1060 1067 pub async fn update_locale( 1061 1068 State(state): State<AppState>, 1062 - auth: BearerAuth, 1069 + auth: RequiredAuth, 1063 1070 Json(input): Json<UpdateLocaleInput>, 1064 - ) -> Response { 1071 + ) -> Result<Response, ApiError> { 1072 + let auth_user = auth.0.require_user()?.require_active()?; 1065 1073 if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) { 1066 - return ApiError::InvalidRequest(format!( 1074 + return Err(ApiError::InvalidRequest(format!( 1067 1075 "Invalid locale. Valid options: {}", 1068 1076 VALID_LOCALES.join(", ") 1069 - )) 1070 - .into_response(); 1077 + ))); 1071 1078 } 1072 1079 1073 - match state 1080 + let updated = state 1074 1081 .user_repo 1075 - .update_locale(&auth.0.did, &input.preferred_locale) 1082 + .update_locale(&auth_user.did, &input.preferred_locale) 1076 1083 .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) => { 1084 + .map_err(|e| { 1091 1085 error!("DB error updating locale: {:?}", e); 1092 - ApiError::InternalError(None).into_response() 1093 - } 1086 + ApiError::InternalError(None) 1087 + })?; 1088 + if !updated { 1089 + return Err(ApiError::AccountNotFound); 1094 1090 } 1091 + info!( 1092 + did = %&auth_user.did, 1093 + locale = %input.preferred_locale, 1094 + "User locale preference updated" 1095 + ); 1096 + Ok(Json(json!({ 1097 + "preferredLocale": input.preferred_locale 1098 + })) 1099 + .into_response()) 1095 1100 }
+166 -160
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::RequiredAuth; 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: RequiredAuth, 32 + ) -> Result<Response, ApiError> { 33 + let auth_user = auth.0.require_user()?.require_active()?; 34 + match state.user_repo.get_totp_record(&auth_user.did).await { 35 + Ok(Some(record)) if record.verified => return Err(ApiError::TotpAlreadyEnabled), 32 36 Ok(_) => {} 33 37 Err(e) => { 34 38 error!("DB error checking TOTP: {:?}", e); 35 - return ApiError::InternalError(None).into_response(); 39 + return Err(ApiError::InternalError(None)); 36 40 } 37 41 } 38 42 39 43 let secret = generate_totp_secret(); 40 44 41 - let handle = match state.user_repo.get_handle_by_did(&auth.0.did).await { 42 - Ok(Some(h)) => h, 43 - Ok(None) => return ApiError::AccountNotFound.into_response(), 44 - Err(e) => { 45 + let handle = state 46 + .user_repo 47 + .get_handle_by_did(&auth_user.did) 48 + .await 49 + .map_err(|e| { 45 50 error!("DB error fetching handle: {:?}", e); 46 - return ApiError::InternalError(None).into_response(); 47 - } 48 - }; 51 + ApiError::InternalError(None) 52 + })? 53 + .ok_or(ApiError::AccountNotFound)?; 49 54 50 55 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 51 56 let uri = generate_totp_uri(&secret, &handle, &hostname); 52 57 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 - }; 58 + let qr_code = generate_qr_png_base64(&secret, &handle, &hostname).map_err(|e| { 59 + error!("Failed to generate QR code: {:?}", e); 60 + ApiError::InternalError(Some("Failed to generate QR code".into())) 61 + })?; 61 62 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 - }; 63 + let encrypted_secret = encrypt_totp_secret(&secret).map_err(|e| { 64 + error!("Failed to encrypt TOTP secret: {:?}", e); 65 + ApiError::InternalError(None) 66 + })?; 69 67 70 - if let Err(e) = state 68 + state 71 69 .user_repo 72 - .upsert_totp_secret(&auth.0.did, &encrypted_secret, ENCRYPTION_VERSION) 70 + .upsert_totp_secret(&auth_user.did, &encrypted_secret, ENCRYPTION_VERSION) 73 71 .await 74 - { 75 - error!("Failed to store TOTP secret: {:?}", e); 76 - return ApiError::InternalError(None).into_response(); 77 - } 72 + .map_err(|e| { 73 + error!("Failed to store TOTP secret: {:?}", e); 74 + ApiError::InternalError(None) 75 + })?; 78 76 79 77 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 80 78 81 - info!(did = %&auth.0.did, "TOTP secret created (pending verification)"); 79 + info!(did = %&auth_user.did, "TOTP secret created (pending verification)"); 82 80 83 - Json(CreateTotpSecretResponse { 81 + Ok(Json(CreateTotpSecretResponse { 84 82 secret: secret_base32, 85 83 uri, 86 84 qr_base64: qr_code, 87 85 }) 88 - .into_response() 86 + .into_response()) 89 87 } 90 88 91 89 #[derive(Deserialize)] ··· 101 99 102 100 pub async fn enable_totp( 103 101 State(state): State<AppState>, 104 - auth: BearerAuth, 102 + auth: RequiredAuth, 105 103 Json(input): Json<EnableTotpInput>, 106 - ) -> Response { 104 + ) -> Result<Response, ApiError> { 105 + let auth_user = auth.0.require_user()?.require_active()?; 107 106 if !state 108 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 107 + .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 109 108 .await 110 109 { 111 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 112 - return ApiError::RateLimitExceeded(None).into_response(); 110 + warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 111 + return Err(ApiError::RateLimitExceeded(None)); 113 112 } 114 113 115 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 114 + let totp_record = match state.user_repo.get_totp_record(&auth_user.did).await { 116 115 Ok(Some(row)) => row, 117 - Ok(None) => return ApiError::TotpNotEnabled.into_response(), 116 + Ok(None) => return Err(ApiError::TotpNotEnabled), 118 117 Err(e) => { 119 118 error!("DB error fetching TOTP: {:?}", e); 120 - return ApiError::InternalError(None).into_response(); 119 + return Err(ApiError::InternalError(None)); 121 120 } 122 121 }; 123 122 124 123 if totp_record.verified { 125 - return ApiError::TotpAlreadyEnabled.into_response(); 124 + return Err(ApiError::TotpAlreadyEnabled); 126 125 } 127 126 128 - let secret = match decrypt_totp_secret( 127 + let secret = decrypt_totp_secret( 129 128 &totp_record.secret_encrypted, 130 129 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 - }; 130 + ) 131 + .map_err(|e| { 132 + error!("Failed to decrypt TOTP secret: {:?}", e); 133 + ApiError::InternalError(None) 134 + })?; 138 135 139 136 let code = input.code.trim(); 140 137 if !verify_totp_code(&secret, code) { 141 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 138 + return Err(ApiError::InvalidCode(Some( 139 + "Invalid verification code".into(), 140 + ))); 142 141 } 143 142 144 143 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) => { 144 + let backup_hashes: Vec<_> = backup_codes 145 + .iter() 146 + .map(|c| hash_backup_code(c)) 147 + .collect::<Result<Vec<_>, _>>() 148 + .map_err(|e| { 150 149 error!("Failed to hash backup code: {:?}", e); 151 - return ApiError::InternalError(None).into_response(); 152 - } 153 - }; 150 + ApiError::InternalError(None) 151 + })?; 154 152 155 - if let Err(e) = state 153 + state 156 154 .user_repo 157 - .enable_totp_with_backup_codes(&auth.0.did, &backup_hashes) 155 + .enable_totp_with_backup_codes(&auth_user.did, &backup_hashes) 158 156 .await 159 - { 160 - error!("Failed to enable TOTP: {:?}", e); 161 - return ApiError::InternalError(None).into_response(); 162 - } 157 + .map_err(|e| { 158 + error!("Failed to enable TOTP: {:?}", e); 159 + ApiError::InternalError(None) 160 + })?; 163 161 164 - info!(did = %&auth.0.did, "TOTP enabled with {} backup codes", backup_codes.len()); 162 + info!(did = %&auth_user.did, "TOTP enabled with {} backup codes", backup_codes.len()); 165 163 166 - Json(EnableTotpResponse { backup_codes }).into_response() 164 + Ok(Json(EnableTotpResponse { backup_codes }).into_response()) 167 165 } 168 166 169 167 #[derive(Deserialize)] ··· 174 172 175 173 pub async fn disable_totp( 176 174 State(state): State<AppState>, 177 - auth: BearerAuth, 175 + auth: RequiredAuth, 178 176 Json(input): Json<DisableTotpInput>, 179 - ) -> Response { 180 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 177 + ) -> Result<Response, ApiError> { 178 + let auth_user = auth.0.require_user()?.require_active()?; 179 + if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth_user.did) 181 180 .await 182 181 { 183 - return crate::api::server::reauth::legacy_mfa_required_response( 182 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 184 183 &*state.user_repo, 185 184 &*state.session_repo, 186 - &auth.0.did, 185 + &auth_user.did, 187 186 ) 188 - .await; 187 + .await); 189 188 } 190 189 191 190 if !state 192 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 191 + .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 193 192 .await 194 193 { 195 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 196 - return ApiError::RateLimitExceeded(None).into_response(); 194 + warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 195 + return Err(ApiError::RateLimitExceeded(None)); 197 196 } 198 197 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) => { 198 + let password_hash = state 199 + .user_repo 200 + .get_password_hash_by_did(&auth_user.did) 201 + .await 202 + .map_err(|e| { 203 203 error!("DB error fetching user: {:?}", e); 204 - return ApiError::InternalError(None).into_response(); 205 - } 206 - }; 204 + ApiError::InternalError(None) 205 + })? 206 + .ok_or(ApiError::AccountNotFound)?; 207 207 208 208 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 209 209 if !password_valid { 210 - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 210 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 211 211 } 212 212 213 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 213 + let totp_record = match state.user_repo.get_totp_record(&auth_user.did).await { 214 214 Ok(Some(row)) if row.verified => row, 215 - Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 215 + Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 216 216 Err(e) => { 217 217 error!("DB error fetching TOTP: {:?}", e); 218 - return ApiError::InternalError(None).into_response(); 218 + return Err(ApiError::InternalError(None)); 219 219 } 220 220 }; 221 221 222 222 let code = input.code.trim(); 223 223 let code_valid = if is_backup_code_format(code) { 224 - verify_backup_code_for_user(&state, &auth.0.did, code).await 224 + verify_backup_code_for_user(&state, &auth_user.did, code).await 225 225 } else { 226 - let secret = match decrypt_totp_secret( 226 + let secret = decrypt_totp_secret( 227 227 &totp_record.secret_encrypted, 228 228 totp_record.encryption_version, 229 - ) { 230 - Ok(s) => s, 231 - Err(e) => { 232 - error!("Failed to decrypt TOTP secret: {:?}", e); 233 - return ApiError::InternalError(None).into_response(); 234 - } 235 - }; 229 + ) 230 + .map_err(|e| { 231 + error!("Failed to decrypt TOTP secret: {:?}", e); 232 + ApiError::InternalError(None) 233 + })?; 236 234 verify_totp_code(&secret, code) 237 235 }; 238 236 239 237 if !code_valid { 240 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 238 + return Err(ApiError::InvalidCode(Some( 239 + "Invalid verification code".into(), 240 + ))); 241 241 } 242 242 243 - if let Err(e) = state 243 + state 244 244 .user_repo 245 - .delete_totp_and_backup_codes(&auth.0.did) 245 + .delete_totp_and_backup_codes(&auth_user.did) 246 246 .await 247 - { 248 - error!("Failed to delete TOTP: {:?}", e); 249 - return ApiError::InternalError(None).into_response(); 250 - } 247 + .map_err(|e| { 248 + error!("Failed to delete TOTP: {:?}", e); 249 + ApiError::InternalError(None) 250 + })?; 251 251 252 - info!(did = %&auth.0.did, "TOTP disabled"); 252 + info!(did = %&auth_user.did, "TOTP disabled"); 253 253 254 - EmptyResponse::ok().into_response() 254 + Ok(EmptyResponse::ok().into_response()) 255 255 } 256 256 257 257 #[derive(Serialize)] ··· 262 262 pub backup_codes_remaining: i64, 263 263 } 264 264 265 - pub async fn get_totp_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 266 - let enabled = match state.user_repo.get_totp_record(&auth.0.did).await { 265 + pub async fn get_totp_status( 266 + State(state): State<AppState>, 267 + auth: RequiredAuth, 268 + ) -> Result<Response, ApiError> { 269 + let auth_user = auth.0.require_user()?.require_active()?; 270 + let enabled = match state.user_repo.get_totp_record(&auth_user.did).await { 267 271 Ok(Some(row)) => row.verified, 268 272 Ok(None) => false, 269 273 Err(e) => { 270 274 error!("DB error fetching TOTP status: {:?}", e); 271 - return ApiError::InternalError(None).into_response(); 275 + return Err(ApiError::InternalError(None)); 272 276 } 273 277 }; 274 278 275 - let backup_count = match state.user_repo.count_unused_backup_codes(&auth.0.did).await { 276 - Ok(count) => count, 277 - Err(e) => { 279 + let backup_count = state 280 + .user_repo 281 + .count_unused_backup_codes(&auth_user.did) 282 + .await 283 + .map_err(|e| { 278 284 error!("DB error counting backup codes: {:?}", e); 279 - return ApiError::InternalError(None).into_response(); 280 - } 281 - }; 285 + ApiError::InternalError(None) 286 + })?; 282 287 283 - Json(GetTotpStatusResponse { 288 + Ok(Json(GetTotpStatusResponse { 284 289 enabled, 285 290 has_backup_codes: backup_count > 0, 286 291 backup_codes_remaining: backup_count, 287 292 }) 288 - .into_response() 293 + .into_response()) 289 294 } 290 295 291 296 #[derive(Deserialize)] ··· 302 307 303 308 pub async fn regenerate_backup_codes( 304 309 State(state): State<AppState>, 305 - auth: BearerAuth, 310 + auth: RequiredAuth, 306 311 Json(input): Json<RegenerateBackupCodesInput>, 307 - ) -> Response { 312 + ) -> Result<Response, ApiError> { 313 + let auth_user = auth.0.require_user()?.require_active()?; 308 314 if !state 309 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 315 + .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 310 316 .await 311 317 { 312 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 313 - return ApiError::RateLimitExceeded(None).into_response(); 318 + warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 319 + return Err(ApiError::RateLimitExceeded(None)); 314 320 } 315 321 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) => { 322 + let password_hash = state 323 + .user_repo 324 + .get_password_hash_by_did(&auth_user.did) 325 + .await 326 + .map_err(|e| { 320 327 error!("DB error fetching user: {:?}", e); 321 - return ApiError::InternalError(None).into_response(); 322 - } 323 - }; 328 + ApiError::InternalError(None) 329 + })? 330 + .ok_or(ApiError::AccountNotFound)?; 324 331 325 332 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 326 333 if !password_valid { 327 - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 334 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 328 335 } 329 336 330 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 337 + let totp_record = match state.user_repo.get_totp_record(&auth_user.did).await { 331 338 Ok(Some(row)) if row.verified => row, 332 - Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 339 + Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 333 340 Err(e) => { 334 341 error!("DB error fetching TOTP: {:?}", e); 335 - return ApiError::InternalError(None).into_response(); 342 + return Err(ApiError::InternalError(None)); 336 343 } 337 344 }; 338 345 339 - let secret = match decrypt_totp_secret( 346 + let secret = decrypt_totp_secret( 340 347 &totp_record.secret_encrypted, 341 348 totp_record.encryption_version, 342 - ) { 343 - Ok(s) => s, 344 - Err(e) => { 345 - error!("Failed to decrypt TOTP secret: {:?}", e); 346 - return ApiError::InternalError(None).into_response(); 347 - } 348 - }; 349 + ) 350 + .map_err(|e| { 351 + error!("Failed to decrypt TOTP secret: {:?}", e); 352 + ApiError::InternalError(None) 353 + })?; 349 354 350 355 let code = input.code.trim(); 351 356 if !verify_totp_code(&secret, code) { 352 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 357 + return Err(ApiError::InvalidCode(Some( 358 + "Invalid verification code".into(), 359 + ))); 353 360 } 354 361 355 362 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) => { 363 + let backup_hashes: Vec<_> = backup_codes 364 + .iter() 365 + .map(|c| hash_backup_code(c)) 366 + .collect::<Result<Vec<_>, _>>() 367 + .map_err(|e| { 361 368 error!("Failed to hash backup code: {:?}", e); 362 - return ApiError::InternalError(None).into_response(); 363 - } 364 - }; 369 + ApiError::InternalError(None) 370 + })?; 365 371 366 - if let Err(e) = state 372 + state 367 373 .user_repo 368 - .replace_backup_codes(&auth.0.did, &backup_hashes) 374 + .replace_backup_codes(&auth_user.did, &backup_hashes) 369 375 .await 370 - { 371 - error!("Failed to regenerate backup codes: {:?}", e); 372 - return ApiError::InternalError(None).into_response(); 373 - } 376 + .map_err(|e| { 377 + error!("Failed to regenerate backup codes: {:?}", e); 378 + ApiError::InternalError(None) 379 + })?; 374 380 375 - info!(did = %&auth.0.did, "Backup codes regenerated"); 381 + info!(did = %&auth_user.did, "Backup codes regenerated"); 376 382 377 - Json(RegenerateBackupCodesResponse { backup_codes }).into_response() 383 + Ok(Json(RegenerateBackupCodesResponse { backup_codes }).into_response()) 378 384 } 379 385 380 386 async fn verify_backup_code_for_user(
+60 -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::RequiredAuth; 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: RequiredAuth, 77 + ) -> Result<Response, ApiError> { 78 + let auth_user = auth.0.require_user()?.require_active()?; 79 + let rows = state 80 + .oauth_repo 81 + .list_trusted_devices(&auth_user.did) 82 + .await 83 + .map_err(|e| { 96 84 error!("DB error: {:?}", e); 97 - ApiError::InternalError(None).into_response() 98 - } 99 - } 85 + ApiError::InternalError(None) 86 + })?; 87 + 88 + let devices = rows 89 + .into_iter() 90 + .map(|row| { 91 + let trust_state = DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until); 92 + TrustedDevice { 93 + id: row.id, 94 + user_agent: row.user_agent, 95 + friendly_name: row.friendly_name, 96 + trusted_at: row.trusted_at, 97 + trusted_until: row.trusted_until, 98 + last_seen_at: row.last_seen_at, 99 + trust_state, 100 + } 101 + }) 102 + .collect(); 103 + 104 + Ok(Json(ListTrustedDevicesResponse { devices }).into_response()) 100 105 } 101 106 102 107 #[derive(Deserialize)] ··· 107 112 108 113 pub async fn revoke_trusted_device( 109 114 State(state): State<AppState>, 110 - auth: BearerAuth, 115 + auth: RequiredAuth, 111 116 Json(input): Json<RevokeTrustedDeviceInput>, 112 - ) -> Response { 117 + ) -> Result<Response, ApiError> { 118 + let auth_user = auth.0.require_user()?.require_active()?; 113 119 let device_id = DeviceId::from(input.device_id.clone()); 114 120 match state 115 121 .oauth_repo 116 - .device_belongs_to_user(&device_id, &auth.0.did) 122 + .device_belongs_to_user(&device_id, &auth_user.did) 117 123 .await 118 124 { 119 125 Ok(true) => {} 120 126 Ok(false) => { 121 - return ApiError::DeviceNotFound.into_response(); 127 + return Err(ApiError::DeviceNotFound); 122 128 } 123 129 Err(e) => { 124 130 error!("DB error: {:?}", e); 125 - return ApiError::InternalError(None).into_response(); 131 + return Err(ApiError::InternalError(None)); 126 132 } 127 133 } 128 134 129 - match state.oauth_repo.revoke_device_trust(&device_id).await { 130 - Ok(()) => { 131 - info!(did = %&auth.0.did, device_id = %input.device_id, "Trusted device revoked"); 132 - SuccessResponse::ok().into_response() 133 - } 134 - Err(e) => { 135 + state 136 + .oauth_repo 137 + .revoke_device_trust(&device_id) 138 + .await 139 + .map_err(|e| { 135 140 error!("DB error: {:?}", e); 136 - ApiError::InternalError(None).into_response() 137 - } 138 - } 141 + ApiError::InternalError(None) 142 + })?; 143 + 144 + info!(did = %&auth_user.did, device_id = %input.device_id, "Trusted device revoked"); 145 + Ok(SuccessResponse::ok().into_response()) 139 146 } 140 147 141 148 #[derive(Deserialize)] ··· 147 154 148 155 pub async fn update_trusted_device( 149 156 State(state): State<AppState>, 150 - auth: BearerAuth, 157 + auth: RequiredAuth, 151 158 Json(input): Json<UpdateTrustedDeviceInput>, 152 - ) -> Response { 159 + ) -> Result<Response, ApiError> { 160 + let auth_user = auth.0.require_user()?.require_active()?; 153 161 let device_id = DeviceId::from(input.device_id.clone()); 154 162 match state 155 163 .oauth_repo 156 - .device_belongs_to_user(&device_id, &auth.0.did) 164 + .device_belongs_to_user(&device_id, &auth_user.did) 157 165 .await 158 166 { 159 167 Ok(true) => {} 160 168 Ok(false) => { 161 - return ApiError::DeviceNotFound.into_response(); 169 + return Err(ApiError::DeviceNotFound); 162 170 } 163 171 Err(e) => { 164 172 error!("DB error: {:?}", e); 165 - return ApiError::InternalError(None).into_response(); 173 + return Err(ApiError::InternalError(None)); 166 174 } 167 175 } 168 176 169 - match state 177 + state 170 178 .oauth_repo 171 179 .update_device_friendly_name(&device_id, input.friendly_name.as_deref()) 172 180 .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) => { 181 + .map_err(|e| { 179 182 error!("DB error: {:?}", e); 180 - ApiError::InternalError(None).into_response() 181 - } 182 - } 183 + ApiError::InternalError(None) 184 + })?; 185 + 186 + info!(did = %auth_user.did, device_id = %input.device_id, "Trusted device updated"); 187 + Ok(SuccessResponse::ok().into_response()) 183 188 } 184 189 185 190 pub async fn get_device_trust_state(
+4 -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::RequiredAuth, 648 648 ) -> Result<Json<LinkedAccountsResponse>, ApiError> { 649 + let auth = auth.0.require_user()?.require_active()?; 649 650 let identities = state 650 651 .sso_repo 651 652 .get_external_identities_by_did(&auth.did) ··· 679 680 680 681 pub async fn unlink_account( 681 682 State(state): State<AppState>, 682 - crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth, 683 + auth: crate::auth::RequiredAuth, 683 684 Json(input): Json<UnlinkAccountRequest>, 684 685 ) -> Result<Json<UnlinkAccountResponse>, ApiError> { 686 + let auth = auth.0.require_user()?.require_active()?; 685 687 if !state 686 688 .check_rate_limit(RateLimitKind::SsoUnlink, auth.did.as_str()) 687 689 .await

History

6 rounds 1 comment
sign up or login to add to the discussion
3 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
fix: match ref pds permission-levels for some endpoints
expand 0 comments
pull request successfully merged
3 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
fix: match ref pds permission-levels for some endpoints
expand 0 comments
2 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
expand 0 comments
lewis.moe submitted #2
2 commits
expand
fix: oauth consolidation, include-scope improvements
fix: consolidate auth extractors & standardize usage
expand 1 comment

so three things:

crates/tranquil-pds/src/auth/auth_extractor.rs does not seem to be used at all anywhere?

crates/tranquil-pds/src/api/temp.rs feels like it just ... shouldnt excist based on the name

and i still dont really like these extractors. the separation of inter service auth is weird to me. inter-service auth is a form of user auth. it shouldnt be separated out from the other types. the AuthExtractor should just be AuthExtractor(pub AuthenticatedUser).

i also discovered https://docs.rs/axum/0.8.8/axum/extract/trait.OptionalFromRequestParts.html which we should be able to reduce optional vs not optional with just an AuthExtractor vs Option.

principly id want whether or not its required that the account is active or not and whether its an admin account or not to both also be type safe configurations on the extractor. probably with generics of some sort. but i cant think of a specific design i like right now so. if you come up with one feel free to do it. otherwise we can do it later

1 commit
expand
fix: oauth consolidation, include-scope improvements
expand 0 comments
lewis.moe submitted #0
1 commit
expand
fix: oauth consolidation, include-scope improvements
expand 0 comments