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 use crate::api::error::ApiError; 2 - use crate::auth::{BearerAuthAllowDeactivated, BlobAuth, BlobAuthResult}; 3 use crate::delegation::DelegationActionType; 4 use crate::state::AppState; 5 use crate::types::{CidLink, Did}; ··· 44 pub async fn upload_blob( 45 State(state): State<AppState>, 46 headers: axum::http::HeaderMap, 47 - auth: BlobAuth, 48 body: Body, 49 - ) -> Response { 50 - let (did, controller_did): (Did, Option<Did>) = match auth.0 { 51 - BlobAuthResult::Service { did } => (did, None), 52 - BlobAuthResult::User(auth_user) => { 53 let mime_type_for_check = headers 54 .get("content-type") 55 .and_then(|h| h.to_str().ok()) ··· 59 auth_user.scope.as_deref(), 60 mime_type_for_check, 61 ) { 62 - return e; 63 } 64 let ctrl_did = auth_user.controller_did.clone(); 65 - (auth_user.did, ctrl_did) 66 } 67 }; 68 ··· 72 .await 73 .unwrap_or(false) 74 { 75 - return ApiError::Forbidden.into_response(); 76 } 77 78 let client_mime_hint = headers ··· 80 .and_then(|h| h.to_str().ok()) 81 .unwrap_or("application/octet-stream"); 82 83 - let user_id = match state.user_repo.get_id_by_did(&did).await { 84 - Ok(Some(id)) => id, 85 - _ => { 86 - return ApiError::InternalError(None).into_response(); 87 - } 88 - }; 89 90 let temp_key = format!("temp/{}", uuid::Uuid::new_v4()); 91 let max_size = get_max_blob_size() as u64; ··· 98 99 info!("Starting streaming blob upload to temp key: {}", temp_key); 100 101 - let upload_result = match state.blob_store.put_stream(&temp_key, pinned_stream).await { 102 - Ok(result) => result, 103 - Err(e) => { 104 error!("Failed to stream blob to storage: {:?}", e); 105 - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 106 - } 107 - }; 108 109 let size = upload_result.size; 110 if size > max_size { 111 let _ = state.blob_store.delete(&temp_key).await; 112 - return ApiError::InvalidRequest(format!( 113 "Blob size {} exceeds maximum of {} bytes", 114 size, max_size 115 - )) 116 - .into_response(); 117 } 118 119 let mime_type = match state.blob_store.get_head(&temp_key, 8192).await { ··· 129 Err(e) => { 130 let _ = state.blob_store.delete(&temp_key).await; 131 error!("Failed to create multihash for blob: {:?}", e); 132 - return ApiError::InternalError(Some("Failed to hash blob".into())).into_response(); 133 } 134 }; 135 let cid = Cid::new_v1(0x55, multihash); ··· 152 Err(e) => { 153 let _ = state.blob_store.delete(&temp_key).await; 154 error!("Failed to insert blob record: {:?}", e); 155 - return ApiError::InternalError(None).into_response(); 156 } 157 }; 158 159 if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await { 160 let _ = state.blob_store.delete(&temp_key).await; 161 error!("Failed to copy blob to final location: {:?}", e); 162 - return ApiError::InternalError(Some("Failed to store blob".into())).into_response(); 163 } 164 165 let _ = state.blob_store.delete(&temp_key).await; ··· 183 .await; 184 } 185 186 - Json(json!({ 187 "blob": { 188 "$type": "blob", 189 "ref": { ··· 193 "size": size 194 } 195 })) 196 - .into_response() 197 } 198 199 #[derive(Deserialize)] ··· 218 219 pub async fn list_missing_blobs( 220 State(state): State<AppState>, 221 - auth: BearerAuthAllowDeactivated, 222 Query(params): Query<ListMissingBlobsParams>, 223 - ) -> Response { 224 - let auth_user = auth.0; 225 let did = &auth_user.did; 226 - let user = match state.user_repo.get_by_did(did).await { 227 - Ok(Some(u)) => u, 228 - Ok(None) => return ApiError::InternalError(None).into_response(), 229 - Err(e) => { 230 error!("DB error fetching user: {:?}", e); 231 - return ApiError::InternalError(None).into_response(); 232 - } 233 - }; 234 let limit = params.limit.unwrap_or(500).clamp(1, 1000); 235 let cursor = params.cursor.as_deref(); 236 - let missing = match state 237 .blob_repo 238 .list_missing_blobs(user.id, cursor, limit + 1) 239 .await 240 - { 241 - Ok(m) => m, 242 - Err(e) => { 243 error!("DB error fetching missing blobs: {:?}", e); 244 - return ApiError::InternalError(None).into_response(); 245 - } 246 - }; 247 let has_more = missing.len() > limit as usize; 248 let blobs: Vec<RecordBlob> = missing 249 .into_iter() ··· 258 } else { 259 None 260 }; 261 - ( 262 StatusCode::OK, 263 Json(ListMissingBlobsOutput { 264 cursor: next_cursor, 265 blobs, 266 }), 267 ) 268 - .into_response() 269 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::{AuthenticatedEntity, RequiredAuth}; 3 use crate::delegation::DelegationActionType; 4 use crate::state::AppState; 5 use crate::types::{CidLink, Did}; ··· 44 pub async fn upload_blob( 45 State(state): State<AppState>, 46 headers: axum::http::HeaderMap, 47 + auth: RequiredAuth, 48 body: Body, 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 + } 72 let mime_type_for_check = headers 73 .get("content-type") 74 .and_then(|h| h.to_str().ok()) ··· 78 auth_user.scope.as_deref(), 79 mime_type_for_check, 80 ) { 81 + return Ok(e); 82 } 83 let ctrl_did = auth_user.controller_did.clone(); 84 + (auth_user.did.clone(), ctrl_did) 85 } 86 }; 87 ··· 91 .await 92 .unwrap_or(false) 93 { 94 + return Err(ApiError::Forbidden); 95 } 96 97 let client_mime_hint = headers ··· 99 .and_then(|h| h.to_str().ok()) 100 .unwrap_or("application/octet-stream"); 101 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))?; 109 110 let temp_key = format!("temp/{}", uuid::Uuid::new_v4()); 111 let max_size = get_max_blob_size() as u64; ··· 118 119 info!("Starting streaming blob upload to temp key: {}", temp_key); 120 121 + let upload_result = state 122 + .blob_store 123 + .put_stream(&temp_key, pinned_stream) 124 + .await 125 + .map_err(|e| { 126 error!("Failed to stream blob to storage: {:?}", e); 127 + ApiError::InternalError(Some("Failed to store blob".into())) 128 + })?; 129 130 let size = upload_result.size; 131 if size > max_size { 132 let _ = state.blob_store.delete(&temp_key).await; 133 + return Err(ApiError::InvalidRequest(format!( 134 "Blob size {} exceeds maximum of {} bytes", 135 size, max_size 136 + ))); 137 } 138 139 let mime_type = match state.blob_store.get_head(&temp_key, 8192).await { ··· 149 Err(e) => { 150 let _ = state.blob_store.delete(&temp_key).await; 151 error!("Failed to create multihash for blob: {:?}", e); 152 + return Err(ApiError::InternalError(Some("Failed to hash blob".into()))); 153 } 154 }; 155 let cid = Cid::new_v1(0x55, multihash); ··· 172 Err(e) => { 173 let _ = state.blob_store.delete(&temp_key).await; 174 error!("Failed to insert blob record: {:?}", e); 175 + return Err(ApiError::InternalError(None)); 176 } 177 }; 178 179 if was_inserted && let Err(e) = state.blob_store.copy(&temp_key, &storage_key).await { 180 let _ = state.blob_store.delete(&temp_key).await; 181 error!("Failed to copy blob to final location: {:?}", e); 182 + return Err(ApiError::InternalError(Some("Failed to store blob".into()))); 183 } 184 185 let _ = state.blob_store.delete(&temp_key).await; ··· 203 .await; 204 } 205 206 + Ok(Json(json!({ 207 "blob": { 208 "$type": "blob", 209 "ref": { ··· 213 "size": size 214 } 215 })) 216 + .into_response()) 217 } 218 219 #[derive(Deserialize)] ··· 238 239 pub async fn list_missing_blobs( 240 State(state): State<AppState>, 241 + auth: RequiredAuth, 242 Query(params): Query<ListMissingBlobsParams>, 243 + ) -> Result<Response, ApiError> { 244 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 245 + 246 let did = &auth_user.did; 247 + let user = state 248 + .user_repo 249 + .get_by_did(did) 250 + .await 251 + .map_err(|e| { 252 error!("DB error fetching user: {:?}", e); 253 + ApiError::InternalError(None) 254 + })? 255 + .ok_or(ApiError::InternalError(None))?; 256 + 257 let limit = params.limit.unwrap_or(500).clamp(1, 1000); 258 let cursor = params.cursor.as_deref(); 259 + let missing = state 260 .blob_repo 261 .list_missing_blobs(user.id, cursor, limit + 1) 262 .await 263 + .map_err(|e| { 264 error!("DB error fetching missing blobs: {:?}", e); 265 + ApiError::InternalError(None) 266 + })?; 267 + 268 let has_more = missing.len() > limit as usize; 269 let blobs: Vec<RecordBlob> = missing 270 .into_iter() ··· 279 } else { 280 None 281 }; 282 + Ok(( 283 StatusCode::OK, 284 Json(ListMissingBlobsOutput { 285 cursor: next_cursor, 286 blobs, 287 }), 288 ) 289 + .into_response()) 290 }
+41 -23
crates/tranquil-pds/src/api/repo/record/delete.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log}; 3 use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; 4 - use crate::auth::BearerAuth; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 40 41 pub async fn delete_record( 42 State(state): State<AppState>, 43 - auth: BearerAuth, 44 Json(input): Json<DeleteRecordInput>, 45 - ) -> Response { 46 - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { 47 Ok(res) => res, 48 - Err(err_res) => return err_res, 49 }; 50 51 if let Err(e) = crate::auth::scope_check::check_repo_scope( ··· 54 crate::oauth::RepoAction::Delete, 55 &input.collection, 56 ) { 57 - return e; 58 } 59 60 let did = auth.did; ··· 65 if let Some(swap_commit) = &input.swap_commit 66 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 67 { 68 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 69 } 70 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 71 let commit_bytes = match tracking_store.get(&current_root_cid).await { 72 Ok(Some(b)) => b, 73 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 74 }; 75 let commit = match Commit::from_cbor(&commit_bytes) { 76 Ok(c) => c, 77 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 78 }; 79 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 80 let key = format!("{}/{}", input.collection, input.rkey); ··· 82 let expected_cid = Cid::from_str(swap_record_str).ok(); 83 let actual_cid = mst.get(&key).await.ok().flatten(); 84 if expected_cid != actual_cid { 85 - return ApiError::InvalidSwap(Some( 86 "Record has been modified or does not exist".into(), 87 )) 88 - .into_response(); 89 } 90 } 91 let prev_record_cid = mst.get(&key).await.ok().flatten(); 92 if prev_record_cid.is_none() { 93 - return (StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response(); 94 } 95 let new_mst = match mst.delete(&key).await { 96 Ok(m) => m, 97 Err(e) => { 98 error!("Failed to delete from MST: {:?}", e); 99 - return ApiError::InternalError(Some(format!("Failed to delete from MST: {:?}", e))) 100 - .into_response(); 101 } 102 }; 103 let new_mst_root = match new_mst.persist().await { 104 Ok(c) => c, 105 Err(e) => { 106 error!("Failed to persist MST: {:?}", e); 107 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 108 } 109 }; 110 let collection_for_audit = input.collection.to_string(); ··· 121 .await 122 .is_err() 123 { 124 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 125 - .into_response(); 126 } 127 if mst 128 .blocks_for_path(&key, &mut old_mst_blocks) 129 .await 130 .is_err() 131 { 132 - return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 133 - .into_response(); 134 } 135 let mut relevant_blocks = new_mst_blocks.clone(); 136 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 169 { 170 Ok(res) => res, 171 Err(e) if e.contains("ConcurrentModification") => { 172 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 173 } 174 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 175 }; 176 177 if let Some(ref controller) = controller_did { ··· 202 error!("Failed to remove backlinks for {}: {}", deleted_uri, e); 203 } 204 205 - ( 206 StatusCode::OK, 207 Json(DeleteRecordOutput { 208 commit: Some(CommitInfo { ··· 211 }), 212 }), 213 ) 214 - .into_response() 215 } 216 217 use crate::types::Did;
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log}; 3 use crate::api::repo::record::write::{CommitInfo, prepare_repo_write}; 4 + use crate::auth::RequiredAuth; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 40 41 pub async fn delete_record( 42 State(state): State<AppState>, 43 + auth: RequiredAuth, 44 Json(input): Json<DeleteRecordInput>, 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 { 48 Ok(res) => res, 49 + Err(err_res) => return Ok(err_res), 50 }; 51 52 if let Err(e) = crate::auth::scope_check::check_repo_scope( ··· 55 crate::oauth::RepoAction::Delete, 56 &input.collection, 57 ) { 58 + return Ok(e); 59 } 60 61 let did = auth.did; ··· 66 if let Some(swap_commit) = &input.swap_commit 67 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 68 { 69 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 70 } 71 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 72 let commit_bytes = match tracking_store.get(&current_root_cid).await { 73 Ok(Some(b)) => b, 74 + _ => { 75 + return Ok( 76 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 77 + ); 78 + } 79 }; 80 let commit = match Commit::from_cbor(&commit_bytes) { 81 Ok(c) => c, 82 + _ => { 83 + return Ok( 84 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 85 + ); 86 + } 87 }; 88 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 89 let key = format!("{}/{}", input.collection, input.rkey); ··· 91 let expected_cid = Cid::from_str(swap_record_str).ok(); 92 let actual_cid = mst.get(&key).await.ok().flatten(); 93 if expected_cid != actual_cid { 94 + return Ok(ApiError::InvalidSwap(Some( 95 "Record has been modified or does not exist".into(), 96 )) 97 + .into_response()); 98 } 99 } 100 let prev_record_cid = mst.get(&key).await.ok().flatten(); 101 if prev_record_cid.is_none() { 102 + return Ok((StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response()); 103 } 104 let new_mst = match mst.delete(&key).await { 105 Ok(m) => m, 106 Err(e) => { 107 error!("Failed to delete from MST: {:?}", e); 108 + return Ok(ApiError::InternalError(Some(format!( 109 + "Failed to delete from MST: {:?}", 110 + e 111 + ))) 112 + .into_response()); 113 } 114 }; 115 let new_mst_root = match new_mst.persist().await { 116 Ok(c) => c, 117 Err(e) => { 118 error!("Failed to persist MST: {:?}", e); 119 + return Ok( 120 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 121 + ); 122 } 123 }; 124 let collection_for_audit = input.collection.to_string(); ··· 135 .await 136 .is_err() 137 { 138 + return Ok( 139 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 140 + .into_response(), 141 + ); 142 } 143 if mst 144 .blocks_for_path(&key, &mut old_mst_blocks) 145 .await 146 .is_err() 147 { 148 + return Ok( 149 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 150 + .into_response(), 151 + ); 152 } 153 let mut relevant_blocks = new_mst_blocks.clone(); 154 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 187 { 188 Ok(res) => res, 189 Err(e) if e.contains("ConcurrentModification") => { 190 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 191 } 192 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 193 }; 194 195 if let Some(ref controller) = controller_did { ··· 220 error!("Failed to remove backlinks for {}: {}", deleted_uri, e); 221 } 222 223 + Ok(( 224 StatusCode::OK, 225 Json(DeleteRecordOutput { 226 commit: Some(CommitInfo { ··· 229 }), 230 }), 231 ) 232 + .into_response()) 233 } 234 235 use crate::types::Did;
+100 -65
crates/tranquil-pds/src/api/repo/record/write.rs
··· 3 use crate::api::repo::record::utils::{ 4 CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, 5 }; 6 - use crate::auth::{AuthenticatedUser, BearerAuth}; 7 use crate::delegation::DelegationActionType; 8 use crate::repo::tracking::TrackingBlockStore; 9 use crate::state::AppState; ··· 34 35 pub async fn prepare_repo_write( 36 state: &AppState, 37 - auth_user: AuthenticatedUser, 38 repo: &AtIdentifier, 39 ) -> Result<RepoWriteAuth, Response> { 40 if repo.as_str() != auth_user.did.as_str() { ··· 91 user_id, 92 current_root_cid, 93 is_oauth: auth_user.is_oauth, 94 - scope: auth_user.scope, 95 controller_did: auth_user.controller_did.clone(), 96 }) 97 } ··· 124 } 125 pub async fn create_record( 126 State(state): State<AppState>, 127 - auth: BearerAuth, 128 Json(input): Json<CreateRecordInput>, 129 - ) -> Response { 130 - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { 131 Ok(res) => res, 132 - Err(err_res) => return err_res, 133 }; 134 135 if let Err(e) = crate::auth::scope_check::check_repo_scope( ··· 138 crate::oauth::RepoAction::Create, 139 &input.collection, 140 ) { 141 - return e; 142 } 143 144 let did = auth.did; ··· 149 if let Some(swap_commit) = &input.swap_commit 150 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 151 { 152 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 153 } 154 155 let validation_status = if input.validate == Some(false) { ··· 163 require_lexicon, 164 ) { 165 Ok(status) => Some(status), 166 - Err(err_response) => return *err_response, 167 } 168 }; 169 let rkey = input.rkey.unwrap_or_else(Rkey::generate); ··· 171 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 172 let commit_bytes = match tracking_store.get(&current_root_cid).await { 173 Ok(Some(b)) => b, 174 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 175 }; 176 let commit = match Commit::from_cbor(&commit_bytes) { 177 Ok(c) => c, 178 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 179 }; 180 let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 181 let initial_mst_root = commit.data; ··· 197 Ok(c) => c, 198 Err(e) => { 199 error!("Failed to check backlink conflicts: {}", e); 200 - return ApiError::InternalError(None).into_response(); 201 } 202 }; 203 ··· 250 let record_ipld = crate::util::json_to_ipld(&input.record); 251 let mut record_bytes = Vec::new(); 252 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 253 - return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); 254 } 255 let record_cid = match tracking_store.put(&record_bytes).await { 256 Ok(c) => c, 257 _ => { 258 - return ApiError::InternalError(Some("Failed to save record block".into())) 259 - .into_response(); 260 } 261 }; 262 let key = format!("{}/{}", input.collection, rkey); ··· 271 272 let new_mst = match mst.add(&key, record_cid).await { 273 Ok(m) => m, 274 - _ => return ApiError::InternalError(Some("Failed to add to MST".into())).into_response(), 275 }; 276 let new_mst_root = match new_mst.persist().await { 277 Ok(c) => c, 278 - _ => return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 279 }; 280 281 ops.push(RecordOp::Create { ··· 290 .await 291 .is_err() 292 { 293 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 294 - .into_response(); 295 } 296 297 let mut relevant_blocks = new_mst_blocks.clone(); ··· 333 { 334 Ok(res) => res, 335 Err(e) if e.contains("ConcurrentModification") => { 336 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 337 } 338 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 339 }; 340 341 for conflict_uri in conflict_uris_to_cleanup { ··· 375 error!("Failed to add backlinks for {}: {}", created_uri, e); 376 } 377 378 - ( 379 StatusCode::OK, 380 Json(CreateRecordOutput { 381 uri: created_uri, ··· 387 validation_status: validation_status.map(|s| s.to_string()), 388 }), 389 ) 390 - .into_response() 391 } 392 #[derive(Deserialize)] 393 #[allow(dead_code)] ··· 414 } 415 pub async fn put_record( 416 State(state): State<AppState>, 417 - auth: BearerAuth, 418 Json(input): Json<PutRecordInput>, 419 - ) -> Response { 420 - let auth = match prepare_repo_write(&state, auth.0, &input.repo).await { 421 Ok(res) => res, 422 - Err(err_res) => return err_res, 423 }; 424 425 if let Err(e) = crate::auth::scope_check::check_repo_scope( ··· 428 crate::oauth::RepoAction::Create, 429 &input.collection, 430 ) { 431 - return e; 432 } 433 if let Err(e) = crate::auth::scope_check::check_repo_scope( 434 auth.is_oauth, ··· 436 crate::oauth::RepoAction::Update, 437 &input.collection, 438 ) { 439 - return e; 440 } 441 442 let did = auth.did; ··· 447 if let Some(swap_commit) = &input.swap_commit 448 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 449 { 450 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 451 } 452 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 453 let commit_bytes = match tracking_store.get(&current_root_cid).await { 454 Ok(Some(b)) => b, 455 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 456 }; 457 let commit = match Commit::from_cbor(&commit_bytes) { 458 Ok(c) => c, 459 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 460 }; 461 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 462 let key = format!("{}/{}", input.collection, input.rkey); ··· 471 require_lexicon, 472 ) { 473 Ok(status) => Some(status), 474 - Err(err_response) => return *err_response, 475 } 476 }; 477 if let Some(swap_record_str) = &input.swap_record { 478 let expected_cid = Cid::from_str(swap_record_str).ok(); 479 let actual_cid = mst.get(&key).await.ok().flatten(); 480 if expected_cid != actual_cid { 481 - return ApiError::InvalidSwap(Some( 482 "Record has been modified or does not exist".into(), 483 )) 484 - .into_response(); 485 } 486 } 487 let existing_cid = mst.get(&key).await.ok().flatten(); 488 let record_ipld = crate::util::json_to_ipld(&input.record); 489 let mut record_bytes = Vec::new(); 490 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 491 - return ApiError::InvalidRecord("Failed to serialize record".into()).into_response(); 492 } 493 let record_cid = match tracking_store.put(&record_bytes).await { 494 Ok(c) => c, 495 _ => { 496 - return ApiError::InternalError(Some("Failed to save record block".into())) 497 - .into_response(); 498 } 499 }; 500 if existing_cid == Some(record_cid) { 501 - return ( 502 StatusCode::OK, 503 Json(PutRecordOutput { 504 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 507 validation_status: validation_status.map(|s| s.to_string()), 508 }), 509 ) 510 - .into_response(); 511 } 512 - let new_mst = if existing_cid.is_some() { 513 - match mst.update(&key, record_cid).await { 514 - Ok(m) => m, 515 - Err(_) => { 516 - return ApiError::InternalError(Some("Failed to update MST".into())) 517 - .into_response(); 518 } 519 - } 520 - } else { 521 - match mst.add(&key, record_cid).await { 522 - Ok(m) => m, 523 - Err(_) => { 524 - return ApiError::InternalError(Some("Failed to add to MST".into())) 525 - .into_response(); 526 } 527 - } 528 - }; 529 let new_mst_root = match new_mst.persist().await { 530 Ok(c) => c, 531 Err(_) => { 532 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 533 } 534 }; 535 let op = if existing_cid.is_some() { ··· 553 .await 554 .is_err() 555 { 556 - return ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 557 - .into_response(); 558 } 559 if mst 560 .blocks_for_path(&key, &mut old_mst_blocks) 561 .await 562 .is_err() 563 { 564 - return ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 565 - .into_response(); 566 } 567 let mut relevant_blocks = new_mst_blocks.clone(); 568 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 604 { 605 Ok(res) => res, 606 Err(e) if e.contains("ConcurrentModification") => { 607 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 608 } 609 - Err(e) => return ApiError::InternalError(Some(e)).into_response(), 610 }; 611 612 if let Some(ref controller) = controller_did { ··· 628 .await; 629 } 630 631 - ( 632 StatusCode::OK, 633 Json(PutRecordOutput { 634 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 640 validation_status: validation_status.map(|s| s.to_string()), 641 }), 642 ) 643 - .into_response() 644 }
··· 3 use crate::api::repo::record::utils::{ 4 CommitParams, RecordOp, commit_and_log, extract_backlinks, extract_blob_cids, 5 }; 6 + use crate::auth::RequiredAuth; 7 use crate::delegation::DelegationActionType; 8 use crate::repo::tracking::TrackingBlockStore; 9 use crate::state::AppState; ··· 34 35 pub async fn prepare_repo_write( 36 state: &AppState, 37 + auth_user: &crate::auth::AuthenticatedUser, 38 repo: &AtIdentifier, 39 ) -> Result<RepoWriteAuth, Response> { 40 if repo.as_str() != auth_user.did.as_str() { ··· 91 user_id, 92 current_root_cid, 93 is_oauth: auth_user.is_oauth, 94 + scope: auth_user.scope.clone(), 95 controller_did: auth_user.controller_did.clone(), 96 }) 97 } ··· 124 } 125 pub async fn create_record( 126 State(state): State<AppState>, 127 + auth: RequiredAuth, 128 Json(input): Json<CreateRecordInput>, 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 { 132 Ok(res) => res, 133 + Err(err_res) => return Ok(err_res), 134 }; 135 136 if let Err(e) = crate::auth::scope_check::check_repo_scope( ··· 139 crate::oauth::RepoAction::Create, 140 &input.collection, 141 ) { 142 + return Ok(e); 143 } 144 145 let did = auth.did; ··· 150 if let Some(swap_commit) = &input.swap_commit 151 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 152 { 153 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 154 } 155 156 let validation_status = if input.validate == Some(false) { ··· 164 require_lexicon, 165 ) { 166 Ok(status) => Some(status), 167 + Err(err_response) => return Ok(*err_response), 168 } 169 }; 170 let rkey = input.rkey.unwrap_or_else(Rkey::generate); ··· 172 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 173 let commit_bytes = match tracking_store.get(&current_root_cid).await { 174 Ok(Some(b)) => b, 175 + _ => { 176 + return Ok( 177 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 178 + ); 179 + } 180 }; 181 let commit = match Commit::from_cbor(&commit_bytes) { 182 Ok(c) => c, 183 + _ => { 184 + return Ok( 185 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 186 + ); 187 + } 188 }; 189 let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 190 let initial_mst_root = commit.data; ··· 206 Ok(c) => c, 207 Err(e) => { 208 error!("Failed to check backlink conflicts: {}", e); 209 + return Ok(ApiError::InternalError(None).into_response()); 210 } 211 }; 212 ··· 259 let record_ipld = crate::util::json_to_ipld(&input.record); 260 let mut record_bytes = Vec::new(); 261 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 262 + return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); 263 } 264 let record_cid = match tracking_store.put(&record_bytes).await { 265 Ok(c) => c, 266 _ => { 267 + return Ok( 268 + ApiError::InternalError(Some("Failed to save record block".into())).into_response(), 269 + ); 270 } 271 }; 272 let key = format!("{}/{}", input.collection, rkey); ··· 281 282 let new_mst = match mst.add(&key, record_cid).await { 283 Ok(m) => m, 284 + _ => { 285 + return Ok(ApiError::InternalError(Some("Failed to add to MST".into())).into_response()); 286 + } 287 }; 288 let new_mst_root = match new_mst.persist().await { 289 Ok(c) => c, 290 + _ => { 291 + return Ok( 292 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 293 + ); 294 + } 295 }; 296 297 ops.push(RecordOp::Create { ··· 306 .await 307 .is_err() 308 { 309 + return Ok( 310 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 311 + .into_response(), 312 + ); 313 } 314 315 let mut relevant_blocks = new_mst_blocks.clone(); ··· 351 { 352 Ok(res) => res, 353 Err(e) if e.contains("ConcurrentModification") => { 354 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 355 } 356 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 357 }; 358 359 for conflict_uri in conflict_uris_to_cleanup { ··· 393 error!("Failed to add backlinks for {}: {}", created_uri, e); 394 } 395 396 + Ok(( 397 StatusCode::OK, 398 Json(CreateRecordOutput { 399 uri: created_uri, ··· 405 validation_status: validation_status.map(|s| s.to_string()), 406 }), 407 ) 408 + .into_response()) 409 } 410 #[derive(Deserialize)] 411 #[allow(dead_code)] ··· 432 } 433 pub async fn put_record( 434 State(state): State<AppState>, 435 + auth: RequiredAuth, 436 Json(input): Json<PutRecordInput>, 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 { 440 Ok(res) => res, 441 + Err(err_res) => return Ok(err_res), 442 }; 443 444 if let Err(e) = crate::auth::scope_check::check_repo_scope( ··· 447 crate::oauth::RepoAction::Create, 448 &input.collection, 449 ) { 450 + return Ok(e); 451 } 452 if let Err(e) = crate::auth::scope_check::check_repo_scope( 453 auth.is_oauth, ··· 455 crate::oauth::RepoAction::Update, 456 &input.collection, 457 ) { 458 + return Ok(e); 459 } 460 461 let did = auth.did; ··· 466 if let Some(swap_commit) = &input.swap_commit 467 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 468 { 469 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 470 } 471 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 472 let commit_bytes = match tracking_store.get(&current_root_cid).await { 473 Ok(Some(b)) => b, 474 + _ => { 475 + return Ok( 476 + ApiError::InternalError(Some("Commit block not found".into())).into_response(), 477 + ); 478 + } 479 }; 480 let commit = match Commit::from_cbor(&commit_bytes) { 481 Ok(c) => c, 482 + _ => { 483 + return Ok( 484 + ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 485 + ); 486 + } 487 }; 488 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 489 let key = format!("{}/{}", input.collection, input.rkey); ··· 498 require_lexicon, 499 ) { 500 Ok(status) => Some(status), 501 + Err(err_response) => return Ok(*err_response), 502 } 503 }; 504 if let Some(swap_record_str) = &input.swap_record { 505 let expected_cid = Cid::from_str(swap_record_str).ok(); 506 let actual_cid = mst.get(&key).await.ok().flatten(); 507 if expected_cid != actual_cid { 508 + return Ok(ApiError::InvalidSwap(Some( 509 "Record has been modified or does not exist".into(), 510 )) 511 + .into_response()); 512 } 513 } 514 let existing_cid = mst.get(&key).await.ok().flatten(); 515 let record_ipld = crate::util::json_to_ipld(&input.record); 516 let mut record_bytes = Vec::new(); 517 if serde_ipld_dagcbor::to_writer(&mut record_bytes, &record_ipld).is_err() { 518 + return Ok(ApiError::InvalidRecord("Failed to serialize record".into()).into_response()); 519 } 520 let record_cid = match tracking_store.put(&record_bytes).await { 521 Ok(c) => c, 522 _ => { 523 + return Ok( 524 + ApiError::InternalError(Some("Failed to save record block".into())).into_response(), 525 + ); 526 } 527 }; 528 if existing_cid == Some(record_cid) { 529 + return Ok(( 530 StatusCode::OK, 531 Json(PutRecordOutput { 532 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 535 validation_status: validation_status.map(|s| s.to_string()), 536 }), 537 ) 538 + .into_response()); 539 } 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 + } 548 } 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 + } 556 } 557 + }; 558 let new_mst_root = match new_mst.persist().await { 559 Ok(c) => c, 560 Err(_) => { 561 + return Ok( 562 + ApiError::InternalError(Some("Failed to persist MST".into())).into_response(), 563 + ); 564 } 565 }; 566 let op = if existing_cid.is_some() { ··· 584 .await 585 .is_err() 586 { 587 + return Ok( 588 + ApiError::InternalError(Some("Failed to get new MST blocks for path".into())) 589 + .into_response(), 590 + ); 591 } 592 if mst 593 .blocks_for_path(&key, &mut old_mst_blocks) 594 .await 595 .is_err() 596 { 597 + return Ok( 598 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 599 + .into_response(), 600 + ); 601 } 602 let mut relevant_blocks = new_mst_blocks.clone(); 603 relevant_blocks.extend(old_mst_blocks.iter().map(|(k, v)| (*k, v.clone()))); ··· 639 { 640 Ok(res) => res, 641 Err(e) if e.contains("ConcurrentModification") => { 642 + return Ok(ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response()); 643 } 644 + Err(e) => return Ok(ApiError::InternalError(Some(e)).into_response()), 645 }; 646 647 if let Some(ref controller) = controller_did { ··· 663 .await; 664 } 665 666 + Ok(( 667 StatusCode::OK, 668 Json(PutRecordOutput { 669 uri: AtUri::from_parts(&did, &input.collection, &input.rkey), ··· 675 validation_status: validation_status.map(|s| s.to_string()), 676 }), 677 ) 678 + .into_response()) 679 }
+49 -46
crates/tranquil-pds/src/api/server/account_status.rs
··· 40 41 pub async fn check_account_status( 42 State(state): State<AppState>, 43 - auth: crate::auth::BearerAuthAllowDeactivated, 44 - ) -> Response { 45 - let did = auth.0.did; 46 - let user_id = match state.user_repo.get_id_by_did(&did).await { 47 - Ok(Some(id)) => id, 48 - _ => { 49 - return ApiError::InternalError(None).into_response(); 50 - } 51 - }; 52 let is_active = state 53 .user_repo 54 - .is_account_active_by_did(&did) 55 .await 56 .ok() 57 .flatten() ··· 95 .await 96 .unwrap_or(0); 97 let valid_did = 98 - is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), &did).await; 99 - ( 100 StatusCode::OK, 101 Json(CheckAccountStatusOutput { 102 activated: is_active, ··· 110 imported_blobs, 111 }), 112 ) 113 - .into_response() 114 } 115 116 async fn is_valid_did_for_service( ··· 305 306 pub async fn activate_account( 307 State(state): State<AppState>, 308 - auth: crate::auth::BearerAuthAllowDeactivated, 309 - ) -> Response { 310 info!("[MIGRATION] activateAccount called"); 311 - let auth_user = auth.0; 312 info!( 313 "[MIGRATION] activateAccount: Authenticated user did={}", 314 auth_user.did ··· 321 crate::oauth::scopes::AccountAction::Manage, 322 ) { 323 info!("[MIGRATION] activateAccount: Scope check failed"); 324 - return e; 325 } 326 327 - let did = auth_user.did; 328 329 info!( 330 "[MIGRATION] activateAccount: Validating DID document for did={}", ··· 344 did, 345 did_validation_start.elapsed() 346 ); 347 - return e.into_response(); 348 } 349 info!( 350 "[MIGRATION] activateAccount: DID document validation SUCCESS for {} (took {:?})", ··· 450 ); 451 } 452 info!("[MIGRATION] activateAccount: SUCCESS for did={}", did); 453 - EmptyResponse::ok().into_response() 454 } 455 Err(e) => { 456 error!( 457 "[MIGRATION] activateAccount: DB error activating account: {:?}", 458 e 459 ); 460 - ApiError::InternalError(None).into_response() 461 } 462 } 463 } ··· 470 471 pub async fn deactivate_account( 472 State(state): State<AppState>, 473 - auth: crate::auth::BearerAuth, 474 Json(input): Json<DeactivateAccountInput>, 475 - ) -> Response { 476 - let auth_user = auth.0; 477 478 if let Err(e) = crate::auth::scope_check::check_account_scope( 479 auth_user.is_oauth, ··· 481 crate::oauth::scopes::AccountAttr::Repo, 482 crate::oauth::scopes::AccountAction::Manage, 483 ) { 484 - return e; 485 } 486 487 let delete_after: Option<chrono::DateTime<chrono::Utc>> = input ··· 490 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) 491 .map(|dt| dt.with_timezone(&chrono::Utc)); 492 493 - let did = auth_user.did; 494 495 let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten(); 496 ··· 511 { 512 warn!("Failed to sequence account deactivated event: {}", e); 513 } 514 - EmptyResponse::ok().into_response() 515 } 516 - Ok(false) => EmptyResponse::ok().into_response(), 517 Err(e) => { 518 error!("DB error deactivating account: {:?}", e); 519 - ApiError::InternalError(None).into_response() 520 } 521 } 522 } 523 524 pub async fn request_account_delete( 525 State(state): State<AppState>, 526 - auth: crate::auth::BearerAuthAllowDeactivated, 527 - ) -> Response { 528 - let did = &auth.0.did; 529 530 if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await { 531 - return crate::api::server::reauth::legacy_mfa_required_response( 532 &*state.user_repo, 533 &*state.session_repo, 534 did, 535 ) 536 - .await; 537 } 538 539 - let user_id = match state.user_repo.get_id_by_did(did).await { 540 - Ok(Some(id)) => id, 541 - _ => { 542 - return ApiError::InternalError(None).into_response(); 543 - } 544 - }; 545 let confirmation_token = Uuid::new_v4().to_string(); 546 let expires_at = Utc::now() + Duration::minutes(15); 547 - if let Err(e) = state 548 .infra_repo 549 .create_deletion_request(&confirmation_token, did, expires_at) 550 .await 551 - { 552 - error!("DB error creating deletion token: {:?}", e); 553 - return ApiError::InternalError(None).into_response(); 554 - } 555 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 556 if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion( 557 state.user_repo.as_ref(), ··· 565 warn!("Failed to enqueue account deletion notification: {:?}", e); 566 } 567 info!("Account deletion requested for user {}", did); 568 - EmptyResponse::ok().into_response() 569 } 570 571 #[derive(Deserialize)]
··· 40 41 pub async fn check_account_status( 42 State(state): State<AppState>, 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))?; 53 let is_active = state 54 .user_repo 55 + .is_account_active_by_did(did) 56 .await 57 .ok() 58 .flatten() ··· 96 .await 97 .unwrap_or(0); 98 let valid_did = 99 + is_valid_did_for_service(state.user_repo.as_ref(), state.cache.clone(), did).await; 100 + Ok(( 101 StatusCode::OK, 102 Json(CheckAccountStatusOutput { 103 activated: is_active, ··· 111 imported_blobs, 112 }), 113 ) 114 + .into_response()) 115 } 116 117 async fn is_valid_did_for_service( ··· 306 307 pub async fn activate_account( 308 State(state): State<AppState>, 309 + auth: crate::auth::RequiredAuth, 310 + ) -> Result<Response, ApiError> { 311 info!("[MIGRATION] activateAccount called"); 312 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 313 info!( 314 "[MIGRATION] activateAccount: Authenticated user did={}", 315 auth_user.did ··· 322 crate::oauth::scopes::AccountAction::Manage, 323 ) { 324 info!("[MIGRATION] activateAccount: Scope check failed"); 325 + return Ok(e); 326 } 327 328 + let did = auth_user.did.clone(); 329 330 info!( 331 "[MIGRATION] activateAccount: Validating DID document for did={}", ··· 345 did, 346 did_validation_start.elapsed() 347 ); 348 + return Err(e); 349 } 350 info!( 351 "[MIGRATION] activateAccount: DID document validation SUCCESS for {} (took {:?})", ··· 451 ); 452 } 453 info!("[MIGRATION] activateAccount: SUCCESS for did={}", did); 454 + Ok(EmptyResponse::ok().into_response()) 455 } 456 Err(e) => { 457 error!( 458 "[MIGRATION] activateAccount: DB error activating account: {:?}", 459 e 460 ); 461 + Err(ApiError::InternalError(None)) 462 } 463 } 464 } ··· 471 472 pub async fn deactivate_account( 473 State(state): State<AppState>, 474 + auth: crate::auth::RequiredAuth, 475 Json(input): Json<DeactivateAccountInput>, 476 + ) -> Result<Response, ApiError> { 477 + let auth_user = auth.0.require_user()?.require_active()?; 478 479 if let Err(e) = crate::auth::scope_check::check_account_scope( 480 auth_user.is_oauth, ··· 482 crate::oauth::scopes::AccountAttr::Repo, 483 crate::oauth::scopes::AccountAction::Manage, 484 ) { 485 + return Ok(e); 486 } 487 488 let delete_after: Option<chrono::DateTime<chrono::Utc>> = input ··· 491 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) 492 .map(|dt| dt.with_timezone(&chrono::Utc)); 493 494 + let did = auth_user.did.clone(); 495 496 let handle = state.user_repo.get_handle_by_did(&did).await.ok().flatten(); 497 ··· 512 { 513 warn!("Failed to sequence account deactivated event: {}", e); 514 } 515 + Ok(EmptyResponse::ok().into_response()) 516 } 517 + Ok(false) => Ok(EmptyResponse::ok().into_response()), 518 Err(e) => { 519 error!("DB error deactivating account: {:?}", e); 520 + Err(ApiError::InternalError(None)) 521 } 522 } 523 } 524 525 pub async fn request_account_delete( 526 State(state): State<AppState>, 527 + auth: crate::auth::RequiredAuth, 528 + ) -> Result<Response, ApiError> { 529 + let user = auth.0.require_user()?.require_not_takendown()?; 530 + let did = &user.did; 531 532 if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, did).await { 533 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 534 &*state.user_repo, 535 &*state.session_repo, 536 did, 537 ) 538 + .await); 539 } 540 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))?; 548 let confirmation_token = Uuid::new_v4().to_string(); 549 let expires_at = Utc::now() + Duration::minutes(15); 550 + state 551 .infra_repo 552 .create_deletion_request(&confirmation_token, did, expires_at) 553 .await 554 + .map_err(|e| { 555 + error!("DB error creating deletion token: {:?}", e); 556 + ApiError::InternalError(None) 557 + })?; 558 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 559 if let Err(e) = crate::comms::comms_repo::enqueue_account_deletion( 560 state.user_repo.as_ref(), ··· 568 warn!("Failed to enqueue account deletion notification: {:?}", e); 569 } 570 info!("Account deletion requested for user {}", did); 571 + Ok(EmptyResponse::ok().into_response()) 572 } 573 574 #[derive(Deserialize)]
+44 -43
crates/tranquil-pds/src/api/server/migration.rs
··· 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuth; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 36 37 pub async fn update_did_document( 38 State(state): State<AppState>, 39 - auth: BearerAuth, 40 Json(input): Json<UpdateDidDocumentInput>, 41 - ) -> Response { 42 - let auth_user = auth.0; 43 44 if !auth_user.did.starts_with("did:web:") { 45 - return ApiError::InvalidRequest( 46 "DID document updates are only available for did:web accounts".into(), 47 - ) 48 - .into_response(); 49 } 50 51 - let user = match state.user_repo.get_user_for_did_doc(&auth_user.did).await { 52 - Ok(Some(u)) => u, 53 - Ok(None) => return ApiError::AccountNotFound.into_response(), 54 - Err(e) => { 55 tracing::error!("DB error getting user: {:?}", e); 56 - return ApiError::InternalError(None).into_response(); 57 - } 58 - }; 59 60 - if user.deactivated_at.is_some() { 61 - return ApiError::AccountDeactivated.into_response(); 62 - } 63 - 64 if let Some(ref methods) = input.verification_methods { 65 if methods.is_empty() { 66 - return ApiError::InvalidRequest("verification_methods cannot be empty".into()) 67 - .into_response(); 68 } 69 let validation_error = methods.iter().find_map(|method| { 70 if method.id.is_empty() { ··· 80 } 81 }); 82 if let Some(err) = validation_error { 83 - return ApiError::InvalidRequest(err.into()).into_response(); 84 } 85 } 86 87 if let Some(ref handles) = input.also_known_as 88 && handles.iter().any(|h| !h.starts_with("at://")) 89 { 90 - return ApiError::InvalidRequest("alsoKnownAs entries must be at:// URIs".into()) 91 - .into_response(); 92 } 93 94 if let Some(ref endpoint) = input.service_endpoint { 95 let endpoint = endpoint.trim(); 96 if !endpoint.starts_with("https://") { 97 - return ApiError::InvalidRequest("serviceEndpoint must start with https://".into()) 98 - .into_response(); 99 } 100 } 101 ··· 106 107 let also_known_as: Option<Vec<String>> = input.also_known_as.clone(); 108 109 - if let Err(e) = state 110 .user_repo 111 .upsert_did_web_overrides(user.id, verification_methods_json, also_known_as) 112 .await 113 - { 114 - tracing::error!("DB error upserting did_web_overrides: {:?}", e); 115 - return ApiError::InternalError(None).into_response(); 116 - } 117 118 if let Some(ref endpoint) = input.service_endpoint { 119 let endpoint_clean = endpoint.trim().trim_end_matches('/'); 120 - if let Err(e) = state 121 .user_repo 122 .update_migrated_to_pds(&auth_user.did, endpoint_clean) 123 .await 124 - { 125 - tracing::error!("DB error updating service endpoint: {:?}", e); 126 - return ApiError::InternalError(None).into_response(); 127 - } 128 } 129 130 let did_doc = build_did_document(&state, &auth_user.did).await; 131 132 tracing::info!("Updated DID document for {}", &auth_user.did); 133 134 - ( 135 StatusCode::OK, 136 Json(UpdateDidDocumentOutput { 137 success: true, 138 did_document: did_doc, 139 }), 140 ) 141 - .into_response() 142 } 143 144 - pub async fn get_did_document(State(state): State<AppState>, auth: BearerAuth) -> Response { 145 - let auth_user = auth.0; 146 147 if !auth_user.did.starts_with("did:web:") { 148 - return ApiError::InvalidRequest( 149 "This endpoint is only available for did:web accounts".into(), 150 - ) 151 - .into_response(); 152 } 153 154 let did_doc = build_did_document(&state, &auth_user.did).await; 155 156 - (StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response() 157 } 158 159 async fn build_did_document(state: &AppState, did: &crate::types::Did) -> serde_json::Value {
··· 1 use crate::api::ApiError; 2 + use crate::auth::RequiredAuth; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 36 37 pub async fn update_did_document( 38 State(state): State<AppState>, 39 + auth: RequiredAuth, 40 Json(input): Json<UpdateDidDocumentInput>, 41 + ) -> Result<Response, ApiError> { 42 + let auth_user = auth.0.require_user()?.require_active()?; 43 44 if !auth_user.did.starts_with("did:web:") { 45 + return Err(ApiError::InvalidRequest( 46 "DID document updates are only available for did:web accounts".into(), 47 + )); 48 } 49 50 + let user = state 51 + .user_repo 52 + .get_user_for_did_doc(&auth_user.did) 53 + .await 54 + .map_err(|e| { 55 tracing::error!("DB error getting user: {:?}", e); 56 + ApiError::InternalError(None) 57 + })? 58 + .ok_or(ApiError::AccountNotFound)?; 59 60 if let Some(ref methods) = input.verification_methods { 61 if methods.is_empty() { 62 + return Err(ApiError::InvalidRequest( 63 + "verification_methods cannot be empty".into(), 64 + )); 65 } 66 let validation_error = methods.iter().find_map(|method| { 67 if method.id.is_empty() { ··· 77 } 78 }); 79 if let Some(err) = validation_error { 80 + return Err(ApiError::InvalidRequest(err.into())); 81 } 82 } 83 84 if let Some(ref handles) = input.also_known_as 85 && handles.iter().any(|h| !h.starts_with("at://")) 86 { 87 + return Err(ApiError::InvalidRequest( 88 + "alsoKnownAs entries must be at:// URIs".into(), 89 + )); 90 } 91 92 if let Some(ref endpoint) = input.service_endpoint { 93 let endpoint = endpoint.trim(); 94 if !endpoint.starts_with("https://") { 95 + return Err(ApiError::InvalidRequest( 96 + "serviceEndpoint must start with https://".into(), 97 + )); 98 } 99 } 100 ··· 105 106 let also_known_as: Option<Vec<String>> = input.also_known_as.clone(); 107 108 + state 109 .user_repo 110 .upsert_did_web_overrides(user.id, verification_methods_json, also_known_as) 111 .await 112 + .map_err(|e| { 113 + tracing::error!("DB error upserting did_web_overrides: {:?}", e); 114 + ApiError::InternalError(None) 115 + })?; 116 117 if let Some(ref endpoint) = input.service_endpoint { 118 let endpoint_clean = endpoint.trim().trim_end_matches('/'); 119 + state 120 .user_repo 121 .update_migrated_to_pds(&auth_user.did, endpoint_clean) 122 .await 123 + .map_err(|e| { 124 + tracing::error!("DB error updating service endpoint: {:?}", e); 125 + ApiError::InternalError(None) 126 + })?; 127 } 128 129 let did_doc = build_did_document(&state, &auth_user.did).await; 130 131 tracing::info!("Updated DID document for {}", &auth_user.did); 132 133 + Ok(( 134 StatusCode::OK, 135 Json(UpdateDidDocumentOutput { 136 success: true, 137 did_document: did_doc, 138 }), 139 ) 140 + .into_response()) 141 } 142 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()?; 148 149 if !auth_user.did.starts_with("did:web:") { 150 + return Err(ApiError::InvalidRequest( 151 "This endpoint is only available for did:web accounts".into(), 152 + )); 153 } 154 155 let did_doc = build_did_document(&state, &auth_user.did).await; 156 157 + Ok((StatusCode::OK, Json(json!({ "didDocument": did_doc }))).into_response()) 158 } 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 use crate::api::error::ApiError; 2 - use crate::auth::{BearerAuth, OptionalBearerAuth}; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 21 pub estimated_time_ms: Option<i64>, 22 } 23 24 - pub async fn check_signup_queue(auth: OptionalBearerAuth) -> Response { 25 - if let Some(user) = auth.0 26 && user.is_oauth 27 { 28 return ApiError::Forbidden.into_response(); ··· 49 50 pub async fn dereference_scope( 51 State(state): State<AppState>, 52 - auth: BearerAuth, 53 Json(input): Json<DereferenceScopeInput>, 54 - ) -> Response { 55 - let _ = auth; 56 57 let scope_parts: Vec<&str> = input.scope.split_whitespace().collect(); 58 let mut resolved_scopes: Vec<String> = Vec::new(); ··· 118 } 119 } 120 121 - Json(DereferenceScopeOutput { 122 scope: resolved_scopes.join(" "), 123 }) 124 - .into_response() 125 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::{OptionalAuth, RequiredAuth}; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 21 pub estimated_time_ms: Option<i64>, 22 } 23 24 + pub async fn check_signup_queue(auth: OptionalAuth) -> Response { 25 + if let Some(entity) = auth.0 26 + && let Some(user) = entity.as_user() 27 && user.is_oauth 28 { 29 return ApiError::Forbidden.into_response(); ··· 50 51 pub async fn dereference_scope( 52 State(state): State<AppState>, 53 + auth: RequiredAuth, 54 Json(input): Json<DereferenceScopeInput>, 55 + ) -> Result<Response, ApiError> { 56 + let _user = auth.0.require_user()?.require_active()?; 57 58 let scope_parts: Vec<&str> = input.scope.split_whitespace().collect(); 59 let mut resolved_scopes: Vec<String> = Vec::new(); ··· 119 } 120 } 121 122 + Ok(Json(DereferenceScopeOutput { 123 scope: resolved_scopes.join(" "), 124 }) 125 + .into_response()) 126 }
+1 -1
crates/tranquil-pds/src/auth/auth_extractor.rs
··· 161 .await 162 .unwrap(); 163 164 - assert_eq!(res.status(), StatusCode::OK, "OAuth token should work with BearerAuth extractor"); 165 let body: Value = res.json().await.unwrap(); 166 assert_eq!(body["did"].as_str().unwrap(), did); 167 }
··· 161 .await 162 .unwrap(); 163 164 + assert_eq!(res.status(), StatusCode::OK, "OAuth token should work with RequiredAuth extractor"); 165 let body: Value = res.json().await.unwrap(); 166 assert_eq!(body["did"].as_str().unwrap(), did); 167 }
+169 -451
crates/tranquil-pds/src/auth/extractor.rs
··· 7 8 use super::{ 9 AccountStatus, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, is_service_token, 10 - validate_bearer_token, validate_bearer_token_allow_deactivated, 11 - validate_bearer_token_allow_takendown, 12 }; 13 use crate::api::error::ApiError; 14 use crate::state::AppState; 15 use crate::types::Did; 16 use crate::util::build_full_url; 17 18 - pub struct BearerAuth(pub AuthenticatedUser); 19 - 20 #[derive(Debug)] 21 pub enum AuthError { 22 ··· 61 } 62 } 63 64 - #[cfg(test)] 65 - fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> { 66 - let auth_header = auth_header.trim(); 67 - 68 - if auth_header.len() < 8 { 69 - return Err(AuthError::InvalidFormat); 70 - } 71 - 72 - let prefix = &auth_header[..7]; 73 - if !prefix.eq_ignore_ascii_case("bearer ") { 74 - return Err(AuthError::InvalidFormat); 75 - } 76 - 77 - let token = auth_header[7..].trim(); 78 - if token.is_empty() { 79 - return Err(AuthError::InvalidFormat); 80 - } 81 - 82 - Ok(token) 83 } 84 85 pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option<String> { ··· 102 Some(token.to_string()) 103 } 104 105 - pub struct ExtractedToken { 106 - pub token: String, 107 - pub is_dpop: bool, 108 - } 109 - 110 pub fn extract_auth_token_from_header(auth_header: Option<&str>) -> Option<ExtractedToken> { 111 let header = auth_header?; 112 let header = header.trim(); ··· 136 None 137 } 138 139 - #[derive(Default)] 140 - struct StatusCheckFlags { 141 - allow_deactivated: bool, 142 - allow_takendown: bool, 143 } 144 145 async fn verify_oauth_token_and_build_user( 146 state: &AppState, 147 token: &str, 148 dpop_proof: Option<&str>, 149 method: &str, 150 uri: &str, 151 - flags: StatusCheckFlags, 152 ) -> Result<AuthenticatedUser, AuthError> { 153 match crate::oauth::verify::verify_oauth_access_token( 154 state.oauth_repo.as_ref(), ··· 171 user_info.takedown_ref.as_deref(), 172 user_info.deactivated_at, 173 ); 174 - if !flags.allow_deactivated && status.is_deactivated() { 175 - return Err(AuthError::AccountDeactivated); 176 - } 177 - if !flags.allow_takendown && status.is_takendown() { 178 - return Err(AuthError::AccountTakedown); 179 - } 180 Ok(AuthenticatedUser { 181 did: result.did, 182 key_bytes: user_info.key_bytes.and_then(|kb| { ··· 198 } 199 } 200 201 - impl FromRequestParts<AppState> for BearerAuth { 202 - type Rejection = AuthError; 203 204 - async fn from_request_parts( 205 - parts: &mut Parts, 206 - state: &AppState, 207 - ) -> Result<Self, Self::Rejection> { 208 - let auth_header = parts 209 - .headers 210 - .get(AUTHORIZATION) 211 - .ok_or(AuthError::MissingToken)? 212 - .to_str() 213 - .map_err(|_| AuthError::InvalidFormat)?; 214 215 - let extracted = 216 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 217 218 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 219 - let method = parts.method.as_str(); 220 - let uri = build_full_url(&parts.uri.to_string()); 221 - 222 - match validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await { 223 - Ok(user) if !user.is_oauth => { 224 - return if user.status.is_deactivated() { 225 - Err(AuthError::AccountDeactivated) 226 - } else if user.status.is_takendown() { 227 - Err(AuthError::AccountTakedown) 228 - } else { 229 - Ok(BearerAuth(user)) 230 - }; 231 - } 232 - Ok(_) => {} 233 - Err(super::TokenValidationError::AccountDeactivated) => { 234 - return Err(AuthError::AccountDeactivated); 235 - } 236 - Err(super::TokenValidationError::AccountTakedown) => { 237 - return Err(AuthError::AccountTakedown); 238 - } 239 - Err(super::TokenValidationError::TokenExpired) => { 240 - info!("JWT access token expired in BearerAuth, returning ExpiredToken"); 241 - return Err(AuthError::TokenExpired); 242 - } 243 - Err(_) => {} 244 - } 245 - 246 - verify_oauth_token_and_build_user( 247 - state, 248 - &extracted.token, 249 - dpop_proof, 250 - method, 251 - &uri, 252 - StatusCheckFlags::default(), 253 - ) 254 - .await 255 - .map(BearerAuth) 256 - } 257 } 258 259 - pub struct BearerAuthAllowDeactivated(pub AuthenticatedUser); 260 261 - impl FromRequestParts<AppState> for BearerAuthAllowDeactivated { 262 - type Rejection = AuthError; 263 264 - async fn from_request_parts( 265 - parts: &mut Parts, 266 - state: &AppState, 267 - ) -> Result<Self, Self::Rejection> { 268 - let auth_header = parts 269 - .headers 270 - .get(AUTHORIZATION) 271 - .ok_or(AuthError::MissingToken)? 272 - .to_str() 273 - .map_err(|_| AuthError::InvalidFormat)?; 274 275 - let extracted = 276 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 277 278 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 279 - let method = parts.method.as_str(); 280 - let uri = build_full_url(&parts.uri.to_string()); 281 - 282 - match validate_bearer_token_allow_deactivated(state.user_repo.as_ref(), &extracted.token) 283 - .await 284 - { 285 - Ok(user) if !user.is_oauth => { 286 - return if user.status.is_takendown() { 287 - Err(AuthError::AccountTakedown) 288 - } else { 289 - Ok(BearerAuthAllowDeactivated(user)) 290 - }; 291 - } 292 - Ok(_) => {} 293 - Err(super::TokenValidationError::AccountTakedown) => { 294 - return Err(AuthError::AccountTakedown); 295 - } 296 - Err(super::TokenValidationError::TokenExpired) => { 297 - return Err(AuthError::TokenExpired); 298 - } 299 - Err(_) => {} 300 } 301 - 302 - verify_oauth_token_and_build_user( 303 - state, 304 - &extracted.token, 305 - dpop_proof, 306 - method, 307 - &uri, 308 - StatusCheckFlags { 309 - allow_deactivated: true, 310 - allow_takendown: false, 311 - }, 312 - ) 313 - .await 314 - .map(BearerAuthAllowDeactivated) 315 } 316 - } 317 318 - pub struct BearerAuthAllowTakendown(pub AuthenticatedUser); 319 320 - impl FromRequestParts<AppState> for BearerAuthAllowTakendown { 321 - type Rejection = AuthError; 322 - 323 - async fn from_request_parts( 324 - parts: &mut Parts, 325 - state: &AppState, 326 - ) -> Result<Self, Self::Rejection> { 327 - let auth_header = parts 328 - .headers 329 - .get(AUTHORIZATION) 330 - .ok_or(AuthError::MissingToken)? 331 - .to_str() 332 - .map_err(|_| AuthError::InvalidFormat)?; 333 - 334 - let extracted = 335 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 336 - 337 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 338 - let method = parts.method.as_str(); 339 - let uri = build_full_url(&parts.uri.to_string()); 340 - 341 - match validate_bearer_token_allow_takendown(state.user_repo.as_ref(), &extracted.token) 342 - .await 343 - { 344 - Ok(user) if !user.is_oauth => { 345 - return if user.status.is_deactivated() { 346 - Err(AuthError::AccountDeactivated) 347 - } else { 348 - Ok(BearerAuthAllowTakendown(user)) 349 - }; 350 - } 351 - Ok(_) => {} 352 - Err(super::TokenValidationError::AccountDeactivated) => { 353 - return Err(AuthError::AccountDeactivated); 354 - } 355 - Err(super::TokenValidationError::TokenExpired) => { 356 - return Err(AuthError::TokenExpired); 357 - } 358 - Err(_) => {} 359 - } 360 - 361 - verify_oauth_token_and_build_user( 362 - state, 363 - &extracted.token, 364 - dpop_proof, 365 - method, 366 - &uri, 367 - StatusCheckFlags { 368 - allow_deactivated: false, 369 - allow_takendown: true, 370 - }, 371 - ) 372 - .await 373 - .map(BearerAuthAllowTakendown) 374 - } 375 } 376 377 - pub struct BearerAuthAdmin(pub AuthenticatedUser); 378 379 - impl FromRequestParts<AppState> for BearerAuthAdmin { 380 type Rejection = AuthError; 381 382 async fn from_request_parts( 383 parts: &mut Parts, 384 state: &AppState, 385 ) -> Result<Self, Self::Rejection> { 386 - let auth_header = parts 387 - .headers 388 - .get(AUTHORIZATION) 389 - .ok_or(AuthError::MissingToken)? 390 - .to_str() 391 - .map_err(|_| AuthError::InvalidFormat)?; 392 - 393 - let extracted = 394 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 395 - 396 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 397 - let method = parts.method.as_str(); 398 - let uri = build_full_url(&parts.uri.to_string()); 399 - 400 - match validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await { 401 - Ok(user) if !user.is_oauth => { 402 - if user.status.is_deactivated() { 403 - return Err(AuthError::AccountDeactivated); 404 - } 405 - if user.status.is_takendown() { 406 - return Err(AuthError::AccountTakedown); 407 - } 408 - if !user.is_admin { 409 - return Err(AuthError::AdminRequired); 410 - } 411 - return Ok(BearerAuthAdmin(user)); 412 - } 413 - Ok(_) => {} 414 - Err(super::TokenValidationError::AccountDeactivated) => { 415 - return Err(AuthError::AccountDeactivated); 416 - } 417 - Err(super::TokenValidationError::AccountTakedown) => { 418 - return Err(AuthError::AccountTakedown); 419 - } 420 - Err(super::TokenValidationError::TokenExpired) => { 421 - return Err(AuthError::TokenExpired); 422 - } 423 - Err(_) => {} 424 - } 425 - 426 - let user = verify_oauth_token_and_build_user( 427 - state, 428 - &extracted.token, 429 - dpop_proof, 430 - method, 431 - &uri, 432 - StatusCheckFlags::default(), 433 - ) 434 - .await?; 435 - 436 - if !user.is_admin { 437 - return Err(AuthError::AdminRequired); 438 - } 439 - Ok(BearerAuthAdmin(user)) 440 } 441 } 442 443 - pub struct OptionalBearerAuth(pub Option<AuthenticatedUser>); 444 445 - impl FromRequestParts<AppState> for OptionalBearerAuth { 446 - type Rejection = AuthError; 447 448 async fn from_request_parts( 449 parts: &mut Parts, 450 state: &AppState, 451 ) -> Result<Self, Self::Rejection> { 452 - let auth_header = match parts.headers.get(AUTHORIZATION) { 453 - Some(h) => match h.to_str() { 454 - Ok(s) => s, 455 - Err(_) => return Ok(OptionalBearerAuth(None)), 456 - }, 457 - None => return Ok(OptionalBearerAuth(None)), 458 - }; 459 - 460 - let extracted = match extract_auth_token_from_header(Some(auth_header)) { 461 - Some(e) => e, 462 - None => return Ok(OptionalBearerAuth(None)), 463 - }; 464 - 465 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 466 - let method = parts.method.as_str(); 467 - let uri = build_full_url(&parts.uri.to_string()); 468 - 469 - if let Ok(user) = validate_bearer_token(state.user_repo.as_ref(), &extracted.token).await 470 - && !user.is_oauth 471 - { 472 - return if user.status.is_deactivated() || user.status.is_takendown() { 473 - Ok(OptionalBearerAuth(None)) 474 - } else { 475 - Ok(OptionalBearerAuth(Some(user))) 476 - }; 477 - } 478 - 479 - Ok(OptionalBearerAuth( 480 - verify_oauth_token_and_build_user( 481 - state, 482 - &extracted.token, 483 - dpop_proof, 484 - method, 485 - &uri, 486 - StatusCheckFlags::default(), 487 - ) 488 - .await 489 - .ok(), 490 - )) 491 } 492 } 493 494 - pub struct ServiceAuth { 495 - pub claims: ServiceTokenClaims, 496 - pub did: Did, 497 - } 498 499 - impl FromRequestParts<AppState> for ServiceAuth { 500 - type Rejection = AuthError; 501 - 502 - async fn from_request_parts( 503 - parts: &mut Parts, 504 - _state: &AppState, 505 - ) -> Result<Self, Self::Rejection> { 506 - let auth_header = parts 507 - .headers 508 - .get(AUTHORIZATION) 509 - .ok_or(AuthError::MissingToken)? 510 - .to_str() 511 - .map_err(|_| AuthError::InvalidFormat)?; 512 - 513 - let extracted = 514 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 515 - 516 - if !is_service_token(&extracted.token) { 517 - return Err(AuthError::InvalidFormat); 518 - } 519 - 520 - let verifier = ServiceTokenVerifier::new(); 521 - let claims = verifier 522 - .verify_service_token(&extracted.token, None) 523 - .await 524 - .map_err(|e| { 525 - error!("Service token verification failed: {:?}", e); 526 - AuthError::AuthenticationFailed 527 - })?; 528 - 529 - let did: Did = claims 530 - .iss 531 - .parse() 532 - .map_err(|_| AuthError::AuthenticationFailed)?; 533 - 534 - debug!("Service token verified for DID: {}", did); 535 - 536 - Ok(ServiceAuth { claims, did }) 537 } 538 - } 539 540 - pub struct OptionalServiceAuth(pub Option<ServiceTokenClaims>); 541 - 542 - impl FromRequestParts<AppState> for OptionalServiceAuth { 543 - type Rejection = std::convert::Infallible; 544 - 545 - async fn from_request_parts( 546 - parts: &mut Parts, 547 - _state: &AppState, 548 - ) -> Result<Self, Self::Rejection> { 549 - let auth_header = match parts.headers.get(AUTHORIZATION) { 550 - Some(h) => match h.to_str() { 551 - Ok(s) => s, 552 - Err(_) => return Ok(OptionalServiceAuth(None)), 553 - }, 554 - None => return Ok(OptionalServiceAuth(None)), 555 - }; 556 - 557 - let extracted = match extract_auth_token_from_header(Some(auth_header)) { 558 - Some(e) => e, 559 - None => return Ok(OptionalServiceAuth(None)), 560 - }; 561 - 562 - if !is_service_token(&extracted.token) { 563 - return Ok(OptionalServiceAuth(None)); 564 - } 565 - 566 - let verifier = ServiceTokenVerifier::new(); 567 - match verifier.verify_service_token(&extracted.token, None).await { 568 - Ok(claims) => { 569 - debug!("Service token verified for DID: {}", claims.iss); 570 - Ok(OptionalServiceAuth(Some(claims))) 571 - } 572 - Err(e) => { 573 - debug!("Service token verification failed (optional): {:?}", e); 574 - Ok(OptionalServiceAuth(None)) 575 - } 576 - } 577 } 578 - } 579 580 - pub enum BlobAuthResult { 581 - Service { did: Did }, 582 - User(AuthenticatedUser), 583 - } 584 - 585 - pub struct BlobAuth(pub BlobAuthResult); 586 - 587 - impl FromRequestParts<AppState> for BlobAuth { 588 - type Rejection = AuthError; 589 - 590 - async fn from_request_parts( 591 - parts: &mut Parts, 592 - state: &AppState, 593 - ) -> Result<Self, Self::Rejection> { 594 - let auth_header = parts 595 - .headers 596 - .get(AUTHORIZATION) 597 - .ok_or(AuthError::MissingToken)? 598 - .to_str() 599 - .map_err(|_| AuthError::InvalidFormat)?; 600 - 601 - let extracted = 602 - extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 603 - 604 - if is_service_token(&extracted.token) { 605 - debug!("Verifying service token for blob upload"); 606 - let verifier = ServiceTokenVerifier::new(); 607 - let claims = verifier 608 - .verify_service_token(&extracted.token, Some("com.atproto.repo.uploadBlob")) 609 - .await 610 - .map_err(|e| { 611 - error!("Service token verification failed: {:?}", e); 612 - AuthError::AuthenticationFailed 613 - })?; 614 - 615 - let did: Did = claims 616 - .iss 617 - .parse() 618 - .map_err(|_| AuthError::AuthenticationFailed)?; 619 - 620 - debug!("Service token verified for DID: {}", did); 621 - return Ok(BlobAuth(BlobAuthResult::Service { did })); 622 - } 623 - 624 - let dpop_proof = parts.headers.get("DPoP").and_then(|h| h.to_str().ok()); 625 - let uri = build_full_url("/xrpc/com.atproto.repo.uploadBlob"); 626 - 627 - if let Ok(user) = 628 - validate_bearer_token_allow_deactivated(state.user_repo.as_ref(), &extracted.token) 629 - .await 630 - && !user.is_oauth 631 - { 632 - return if user.status.is_takendown() { 633 - Err(AuthError::AccountTakedown) 634 - } else { 635 - Ok(BlobAuth(BlobAuthResult::User(user))) 636 - }; 637 - } 638 - 639 - verify_oauth_token_and_build_user( 640 - state, 641 - &extracted.token, 642 - dpop_proof, 643 - "POST", 644 - &uri, 645 - StatusCheckFlags { 646 - allow_deactivated: true, 647 - allow_takendown: false, 648 - }, 649 - ) 650 - .await 651 - .map(|user| BlobAuth(BlobAuthResult::User(user))) 652 } 653 } 654 655 #[cfg(test)]
··· 7 8 use super::{ 9 AccountStatus, AuthenticatedUser, ServiceTokenClaims, ServiceTokenVerifier, is_service_token, 10 + validate_bearer_token_for_service_auth, 11 }; 12 use crate::api::error::ApiError; 13 use crate::state::AppState; 14 use crate::types::Did; 15 use crate::util::build_full_url; 16 17 #[derive(Debug)] 18 pub enum AuthError { 19 ··· 58 } 59 } 60 61 + pub struct ExtractedToken { 62 + pub token: String, 63 + pub is_dpop: bool, 64 } 65 66 pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option<String> { ··· 83 Some(token.to_string()) 84 } 85 86 pub fn extract_auth_token_from_header(auth_header: Option<&str>) -> Option<ExtractedToken> { 87 let header = auth_header?; 88 let header = header.trim(); ··· 112 None 113 } 114 115 + pub enum AuthenticatedEntity { 116 + User(AuthenticatedUser), 117 + Service { 118 + did: Did, 119 + claims: ServiceTokenClaims, 120 + }, 121 } 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 + 216 async fn verify_oauth_token_and_build_user( 217 state: &AppState, 218 token: &str, 219 dpop_proof: Option<&str>, 220 method: &str, 221 uri: &str, 222 ) -> Result<AuthenticatedUser, AuthError> { 223 match crate::oauth::verify::verify_oauth_access_token( 224 state.oauth_repo.as_ref(), ··· 241 user_info.takedown_ref.as_deref(), 242 user_info.deactivated_at, 243 ); 244 Ok(AuthenticatedUser { 245 did: result.did, 246 key_bytes: user_info.key_bytes.and_then(|kb| { ··· 262 } 263 } 264 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 + })?; 274 275 + let did: Did = claims 276 + .iss 277 + .parse() 278 + .map_err(|_| AuthError::AuthenticationFailed)?; 279 280 + debug!("Service token verified for DID: {}", did); 281 282 + Ok((did, claims)) 283 } 284 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)?; 295 296 + let extracted = 297 + extract_auth_token_from_header(Some(auth_header)).ok_or(AuthError::InvalidFormat)?; 298 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 + } 303 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()); 307 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)); 311 } 312 + Ok(_) => {} 313 + Err(super::TokenValidationError::TokenExpired) => { 314 + info!("JWT access token expired, returning ExpiredToken"); 315 + return Err(AuthError::TokenExpired); 316 + } 317 + Err(_) => {} 318 } 319 320 + let user = verify_oauth_token_and_build_user(state, &extracted.token, dpop_proof, method, &uri) 321 + .await?; 322 323 + Ok(AuthenticatedEntity::User(user)) 324 } 325 326 + pub struct RequiredAuth(pub AuthenticatedEntity); 327 328 + impl FromRequestParts<AppState> for RequiredAuth { 329 type Rejection = AuthError; 330 331 async fn from_request_parts( 332 parts: &mut Parts, 333 state: &AppState, 334 ) -> Result<Self, Self::Rejection> { 335 + extract_auth_internal(parts, state).await.map(RequiredAuth) 336 } 337 } 338 339 + pub struct OptionalAuth(pub Option<AuthenticatedEntity>); 340 341 + impl FromRequestParts<AppState> for OptionalAuth { 342 + type Rejection = std::convert::Infallible; 343 344 async fn from_request_parts( 345 parts: &mut Parts, 346 state: &AppState, 347 ) -> Result<Self, Self::Rejection> { 348 + Ok(OptionalAuth(extract_auth_internal(parts, state).await.ok())) 349 } 350 } 351 352 + #[cfg(test)] 353 + fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> { 354 + let auth_header = auth_header.trim(); 355 356 + if auth_header.len() < 8 { 357 + return Err(AuthError::InvalidFormat); 358 } 359 360 + let prefix = &auth_header[..7]; 361 + if !prefix.eq_ignore_ascii_case("bearer ") { 362 + return Err(AuthError::InvalidFormat); 363 } 364 365 + let token = auth_header[7..].trim(); 366 + if token.is_empty() { 367 + return Err(AuthError::InvalidFormat); 368 } 369 + 370 + Ok(token) 371 } 372 373 #[cfg(test)]
+1 -2
crates/tranquil-pds/src/auth/mod.rs
··· 16 pub mod webauthn; 17 18 pub use extractor::{ 19 - AuthError, BearerAuth, BearerAuthAdmin, BearerAuthAllowDeactivated, BlobAuth, BlobAuthResult, 20 - ExtractedToken, OptionalBearerAuth, OptionalServiceAuth, ServiceAuth, 21 extract_auth_token_from_header, extract_bearer_token_from_header, 22 }; 23 pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token};
··· 16 pub mod webauthn; 17 18 pub use extractor::{ 19 + AuthError, AuthenticatedEntity, ExtractedToken, OptionalAuth, RequiredAuth, 20 extract_auth_token_from_header, extract_bearer_token_from_header, 21 }; 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 use crate::comms::{channel_display_name, comms_repo::enqueue_2fa_code}; 2 use crate::oauth::{ 3 AuthFlowState, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, SessionId, 4 - db::should_show_consent, 5 - scopes::expand_include_scopes, 6 }; 7 use crate::state::{AppState, RateLimitKind}; 8 use crate::types::{Did, Handle, PlainPassword}; ··· 3645 pub async fn establish_session( 3646 State(state): State<AppState>, 3647 headers: HeaderMap, 3648 - auth: crate::auth::BearerAuth, 3649 ) -> Response { 3650 - let did = &auth.0.did; 3651 3652 let existing_device = extract_device_cookie(&headers); 3653 ··· 3670 }; 3671 let device_typed = DeviceIdType::from(new_id.0.clone()); 3672 3673 - if let Err(e) = state.oauth_repo.create_device(&device_typed, &device_data).await { 3674 tracing::error!(error = ?e, "Failed to create device"); 3675 return ( 3676 StatusCode::INTERNAL_SERVER_ERROR, ··· 3682 .into_response(); 3683 } 3684 3685 - if let Err(e) = state.oauth_repo.upsert_account_device(did, &device_typed).await { 3686 tracing::error!(error = ?e, "Failed to link device to account"); 3687 return ( 3688 StatusCode::INTERNAL_SERVER_ERROR,
··· 1 use crate::comms::{channel_display_name, comms_repo::enqueue_2fa_code}; 2 use crate::oauth::{ 3 AuthFlowState, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, SessionId, 4 + db::should_show_consent, scopes::expand_include_scopes, 5 }; 6 use crate::state::{AppState, RateLimitKind}; 7 use crate::types::{Did, Handle, PlainPassword}; ··· 3644 pub async fn establish_session( 3645 State(state): State<AppState>, 3646 headers: HeaderMap, 3647 + auth: crate::auth::RequiredAuth, 3648 ) -> Response { 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; 3675 3676 let existing_device = extract_device_cookie(&headers); 3677 ··· 3694 }; 3695 let device_typed = DeviceIdType::from(new_id.0.clone()); 3696 3697 + if let Err(e) = state 3698 + .oauth_repo 3699 + .create_device(&device_typed, &device_data) 3700 + .await 3701 + { 3702 tracing::error!(error = ?e, "Failed to create device"); 3703 return ( 3704 StatusCode::INTERNAL_SERVER_ERROR, ··· 3710 .into_response(); 3711 } 3712 3713 + if let Err(e) = state 3714 + .oauth_repo 3715 + .upsert_account_device(did, &device_typed) 3716 + .await 3717 + { 3718 tracing::error!(error = ?e, "Failed to link device to account"); 3719 return ( 3720 StatusCode::INTERNAL_SERVER_ERROR,
+30 -7
crates/tranquil-pds/src/oauth/endpoints/delegation.rs
··· 1 - use crate::auth::BearerAuth; 2 use crate::delegation::DelegationActionType; 3 use crate::state::{AppState, RateLimitKind}; 4 use crate::types::PlainPassword; ··· 463 pub async fn delegation_auth_token( 464 State(state): State<AppState>, 465 headers: HeaderMap, 466 - auth: BearerAuth, 467 Json(form): Json<DelegationTokenAuthSubmit>, 468 ) -> Response { 469 - let controller_did = auth.0.did; 470 471 let delegated_did: Did = match form.delegated_did.parse() { 472 Ok(d) => d, ··· 510 511 let grant = match state 512 .delegation_repo 513 - .get_delegation(&delegated_did, &controller_did) 514 .await 515 { 516 Ok(Some(g)) => g, ··· 551 552 if state 553 .oauth_repo 554 - .set_controller_did(&request_id, &controller_did) 555 .await 556 .is_err() 557 { ··· 574 .delegation_repo 575 .log_delegation_action( 576 &delegated_did, 577 - &controller_did, 578 - Some(&controller_did), 579 DelegationActionType::TokenIssued, 580 Some(serde_json::json!({ 581 "client_id": request.client_id,
··· 1 + use crate::auth::RequiredAuth; 2 use crate::delegation::DelegationActionType; 3 use crate::state::{AppState, RateLimitKind}; 4 use crate::types::PlainPassword; ··· 463 pub async fn delegation_auth_token( 464 State(state): State<AppState>, 465 headers: HeaderMap, 466 + auth: RequiredAuth, 467 Json(form): Json<DelegationTokenAuthSubmit>, 468 ) -> Response { 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; 493 494 let delegated_did: Did = match form.delegated_did.parse() { 495 Ok(d) => d, ··· 533 534 let grant = match state 535 .delegation_repo 536 + .get_delegation(&delegated_did, controller_did) 537 .await 538 { 539 Ok(Some(g)) => g, ··· 574 575 if state 576 .oauth_repo 577 + .set_controller_did(&request_id, controller_did) 578 .await 579 .is_err() 580 { ··· 597 .delegation_repo 598 .log_delegation_action( 599 &delegated_did, 600 + controller_did, 601 + Some(controller_did), 602 DelegationActionType::TokenIssued, 603 Some(serde_json::json!({ 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 return Err(format!("Invalid NSID format: {}", nsid)); 150 } 151 152 - let authority = parts[..2].iter().rev().cloned().collect::<Vec<_>>().join("."); 153 debug!(nsid, authority = %authority, "Resolving lexicon DID authority via DNS"); 154 155 let did = resolve_lexicon_did_authority(&authority).await?; ··· 279 ) -> String { 280 let mut scopes: Vec<String> = Vec::new(); 281 282 - permissions.iter().for_each(|perm| match perm.resource.as_str() { 283 - "repo" => { 284 - if let Some(collections) = &perm.collection { 285 - let actions: Vec<&str> = perm 286 - .action 287 - .as_ref() 288 - .map(|a| a.iter().map(String::as_str).collect()) 289 - .unwrap_or_else(|| DEFAULT_ACTIONS.to_vec()); 290 291 - collections 292 - .iter() 293 - .filter(|coll| is_under_authority(coll, namespace_authority)) 294 - .for_each(|coll| { 295 - actions.iter().for_each(|action| { 296 - scopes.push(format!("repo:{}?action={}", coll, action)); 297 }); 298 - }); 299 } 300 - } 301 - "rpc" => { 302 - if let Some(lxms) = &perm.lxm { 303 - let perm_aud = perm.aud.as_deref().or(default_aud); 304 305 - lxms.iter().for_each(|lxm| { 306 - let scope = match perm_aud { 307 - Some(aud) => format!("rpc:{}?aud={}", lxm, aud), 308 - None => format!("rpc:{}", lxm), 309 - }; 310 - scopes.push(scope); 311 - }); 312 } 313 - } 314 - _ => {} 315 - }); 316 317 scopes.join(" ") 318 } ··· 334 335 #[test] 336 fn test_parse_include_scope_with_multiple_params() { 337 - let (nsid, aud) = parse_include_scope("io.atcr.authFullApp?foo=bar&aud=did:web:example.com&baz=qux"); 338 assert_eq!(nsid, "io.atcr.authFullApp"); 339 assert_eq!(aud, Some("did:web:example.com")); 340 } ··· 443 aud: None, 444 }]; 445 446 - let expanded = build_expanded_scopes(&permissions, Some("did:web:api.example.com"), "io.atcr"); 447 assert!(expanded.contains("rpc:io.atcr.getManifest?aud=did:web:api.example.com")); 448 } 449 ··· 583 cache_key.to_string(), 584 CachedLexicon { 585 expanded_scope: "old_value".to_string(), 586 - cached_at: std::time::Instant::now() - std::time::Duration::from_secs(CACHE_TTL_SECS + 1), 587 }, 588 ); 589 } ··· 601 fn test_nsid_authority_extraction_for_dns() { 602 let nsid = "io.atcr.authFullApp"; 603 let parts: Vec<&str> = nsid.split('.').collect(); 604 - let authority = parts[..2].iter().rev().cloned().collect::<Vec<_>>().join("."); 605 assert_eq!(authority, "atcr.io"); 606 607 let nsid2 = "app.bsky.feed.post"; 608 let parts2: Vec<&str> = nsid2.split('.').collect(); 609 - let authority2 = parts2[..2].iter().rev().cloned().collect::<Vec<_>>().join("."); 610 assert_eq!(authority2, "bsky.app"); 611 } 612 }
··· 149 return Err(format!("Invalid NSID format: {}", nsid)); 150 } 151 152 + let authority = parts[..2] 153 + .iter() 154 + .rev() 155 + .cloned() 156 + .collect::<Vec<_>>() 157 + .join("."); 158 debug!(nsid, authority = %authority, "Resolving lexicon DID authority via DNS"); 159 160 let did = resolve_lexicon_did_authority(&authority).await?; ··· 284 ) -> String { 285 let mut scopes: Vec<String> = Vec::new(); 286 287 + permissions 288 + .iter() 289 + .for_each(|perm| match perm.resource.as_str() { 290 + "repo" => { 291 + if let Some(collections) = &perm.collection { 292 + let actions: Vec<&str> = perm 293 + .action 294 + .as_ref() 295 + .map(|a| a.iter().map(String::as_str).collect()) 296 + .unwrap_or_else(|| DEFAULT_ACTIONS.to_vec()); 297 298 + collections 299 + .iter() 300 + .filter(|coll| is_under_authority(coll, namespace_authority)) 301 + .for_each(|coll| { 302 + actions.iter().for_each(|action| { 303 + scopes.push(format!("repo:{}?action={}", coll, action)); 304 + }); 305 }); 306 + } 307 } 308 + "rpc" => { 309 + if let Some(lxms) = &perm.lxm { 310 + let perm_aud = perm.aud.as_deref().or(default_aud); 311 312 + lxms.iter().for_each(|lxm| { 313 + let scope = match perm_aud { 314 + Some(aud) => format!("rpc:{}?aud={}", lxm, aud), 315 + None => format!("rpc:{}", lxm), 316 + }; 317 + scopes.push(scope); 318 + }); 319 + } 320 } 321 + _ => {} 322 + }); 323 324 scopes.join(" ") 325 } ··· 341 342 #[test] 343 fn test_parse_include_scope_with_multiple_params() { 344 + let (nsid, aud) = 345 + parse_include_scope("io.atcr.authFullApp?foo=bar&aud=did:web:example.com&baz=qux"); 346 assert_eq!(nsid, "io.atcr.authFullApp"); 347 assert_eq!(aud, Some("did:web:example.com")); 348 } ··· 451 aud: None, 452 }]; 453 454 + let expanded = 455 + build_expanded_scopes(&permissions, Some("did:web:api.example.com"), "io.atcr"); 456 assert!(expanded.contains("rpc:io.atcr.getManifest?aud=did:web:api.example.com")); 457 } 458 ··· 592 cache_key.to_string(), 593 CachedLexicon { 594 expanded_scope: "old_value".to_string(), 595 + cached_at: std::time::Instant::now() 596 + - std::time::Duration::from_secs(CACHE_TTL_SECS + 1), 597 }, 598 ); 599 } ··· 611 fn test_nsid_authority_extraction_for_dns() { 612 let nsid = "io.atcr.authFullApp"; 613 let parts: Vec<&str> = nsid.split('.').collect(); 614 + let authority = parts[..2] 615 + .iter() 616 + .rev() 617 + .cloned() 618 + .collect::<Vec<_>>() 619 + .join("."); 620 assert_eq!(authority, "atcr.io"); 621 622 let nsid2 = "app.bsky.feed.post"; 623 let parts2: Vec<&str> = nsid2.split('.').collect(); 624 + let authority2 = parts2[..2] 625 + .iter() 626 + .rev() 627 + .cloned() 628 + .collect::<Vec<_>>() 629 + .join("."); 630 assert_eq!(authority2, "bsky.app"); 631 } 632 }
-1
crates/tranquil-scopes/src/permissions.rs
··· 556 "app.bsky.feed.getAuthorFeed" 557 )); 558 } 559 - 560 }
··· 556 "app.bsky.feed.getAuthorFeed" 557 )); 558 } 559 }
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 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAllowDeactivated; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 32 pub struct GetPreferencesOutput { 33 pub preferences: Vec<Value>, 34 } 35 - pub async fn get_preferences( 36 - State(state): State<AppState>, 37 - auth: BearerAuthAllowDeactivated, 38 - ) -> Response { 39 - let auth_user = auth.0; 40 - let has_full_access = auth_user.permissions().has_full_access(); 41 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth_user.did).await { 42 Ok(Some(id)) => id, 43 _ => { 44 return ApiError::InternalError(Some("User not found".into())).into_response(); ··· 93 } 94 pub async fn put_preferences( 95 State(state): State<AppState>, 96 - auth: BearerAuthAllowDeactivated, 97 Json(input): Json<PutPreferencesInput>, 98 ) -> Response { 99 - let auth_user = auth.0; 100 - let has_full_access = auth_user.permissions().has_full_access(); 101 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth_user.did).await { 102 Ok(Some(id)) => id, 103 _ => { 104 return ApiError::InternalError(Some("User not found".into())).into_response();
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::RequiredAuth; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 32 pub struct GetPreferencesOutput { 33 pub preferences: Vec<Value>, 34 } 35 + pub async fn get_preferences(State(state): State<AppState>, auth: 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 { 45 Ok(Some(id)) => id, 46 _ => { 47 return ApiError::InternalError(Some("User not found".into())).into_response(); ··· 96 } 97 pub async fn put_preferences( 98 State(state): State<AppState>, 99 + auth: RequiredAuth, 100 Json(input): Json<PutPreferencesInput>, 101 ) -> Response { 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 { 111 Ok(Some(id)) => id, 112 _ => { 113 return ApiError::InternalError(Some("User not found".into())).into_response();
+22 -18
crates/tranquil-pds/src/api/admin/account/delete.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAdmin; 4 use crate::state::AppState; 5 use crate::types::Did; 6 use axum::{ ··· 18 19 pub async fn delete_account( 20 State(state): State<AppState>, 21 - _auth: BearerAuthAdmin, 22 Json(input): Json<DeleteAccountInput>, 23 - ) -> Response { 24 let did = &input.did; 25 - let (user_id, handle) = match state.user_repo.get_id_and_handle_by_did(did).await { 26 - Ok(Some(row)) => (row.id, row.handle), 27 - Ok(None) => { 28 - return ApiError::AccountNotFound.into_response(); 29 - } 30 - Err(e) => { 31 error!("DB error in delete_account: {:?}", e); 32 - return ApiError::InternalError(None).into_response(); 33 - } 34 - }; 35 - if let Err(e) = state 36 .user_repo 37 .admin_delete_account_complete(user_id, did) 38 .await 39 - { 40 - error!("Failed to delete account {}: {:?}", did, e); 41 - return ApiError::InternalError(Some("Failed to delete account".into())).into_response(); 42 - } 43 if let Err(e) = 44 crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await 45 { ··· 49 ); 50 } 51 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 52 - EmptyResponse::ok().into_response() 53 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::RequiredAuth; 4 use crate::state::AppState; 5 use crate::types::Did; 6 use axum::{ ··· 18 19 pub async fn delete_account( 20 State(state): State<AppState>, 21 + auth: RequiredAuth, 22 Json(input): Json<DeleteAccountInput>, 23 + ) -> Result<Response, ApiError> { 24 + auth.0.require_user()?.require_active()?.require_admin()?; 25 + 26 let did = &input.did; 27 + let (user_id, handle) = state 28 + .user_repo 29 + .get_id_and_handle_by_did(did) 30 + .await 31 + .map_err(|e| { 32 error!("DB error in delete_account: {:?}", e); 33 + ApiError::InternalError(None) 34 + })? 35 + .ok_or(ApiError::AccountNotFound) 36 + .map(|row| (row.id, row.handle))?; 37 + 38 + state 39 .user_repo 40 .admin_delete_account_complete(user_id, did) 41 .await 42 + .map_err(|e| { 43 + error!("Failed to delete account {}: {:?}", did, e); 44 + ApiError::InternalError(Some("Failed to delete account".into())) 45 + })?; 46 + 47 if let Err(e) = 48 crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await 49 { ··· 53 ); 54 } 55 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 56 + Ok(EmptyResponse::ok().into_response()) 57 }
+18 -21
crates/tranquil-pds/src/api/admin/account/email.rs
··· 1 use crate::api::error::{ApiError, AtpJson}; 2 - use crate::auth::BearerAuthAdmin; 3 use crate::state::AppState; 4 use crate::types::Did; 5 use axum::{ ··· 28 29 pub async fn send_email( 30 State(state): State<AppState>, 31 - _auth: BearerAuthAdmin, 32 AtpJson(input): AtpJson<SendEmailInput>, 33 - ) -> Response { 34 let content = input.content.trim(); 35 if content.is_empty() { 36 - return ApiError::InvalidRequest("content is required".into()).into_response(); 37 } 38 - let user = match state.user_repo.get_by_did(&input.recipient_did).await { 39 - Ok(Some(row)) => row, 40 - Ok(None) => { 41 - return ApiError::AccountNotFound.into_response(); 42 - } 43 - Err(e) => { 44 error!("DB error in send_email: {:?}", e); 45 - return ApiError::InternalError(None).into_response(); 46 - } 47 - }; 48 - let email = match user.email { 49 - Some(e) => e, 50 - None => { 51 - return ApiError::NoEmail.into_response(); 52 - } 53 - }; 54 let (user_id, handle) = (user.id, user.handle); 55 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 56 let subject = input ··· 76 handle, 77 input.recipient_did 78 ); 79 - (StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response() 80 } 81 Err(e) => { 82 warn!("Failed to enqueue admin email: {:?}", e); 83 - (StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response() 84 } 85 } 86 }
··· 1 use crate::api::error::{ApiError, AtpJson}; 2 + use crate::auth::RequiredAuth; 3 use crate::state::AppState; 4 use crate::types::Did; 5 use axum::{ ··· 28 29 pub async fn send_email( 30 State(state): State<AppState>, 31 + auth: RequiredAuth, 32 AtpJson(input): AtpJson<SendEmailInput>, 33 + ) -> Result<Response, ApiError> { 34 + auth.0.require_user()?.require_active()?.require_admin()?; 35 + 36 let content = input.content.trim(); 37 if content.is_empty() { 38 + return Err(ApiError::InvalidRequest("content is required".into())); 39 } 40 + let user = state 41 + .user_repo 42 + .get_by_did(&input.recipient_did) 43 + .await 44 + .map_err(|e| { 45 error!("DB error in send_email: {:?}", e); 46 + ApiError::InternalError(None) 47 + })? 48 + .ok_or(ApiError::AccountNotFound)?; 49 + 50 + let email = user.email.ok_or(ApiError::NoEmail)?; 51 let (user_id, handle) = (user.id, user.handle); 52 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 53 let subject = input ··· 73 handle, 74 input.recipient_did 75 ); 76 + Ok((StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response()) 77 } 78 Err(e) => { 79 warn!("Failed to enqueue admin email: {:?}", e); 80 + Ok((StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response()) 81 } 82 } 83 }
+22 -24
crates/tranquil-pds/src/api/admin/account/info.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; 5 use axum::{ ··· 67 68 pub async fn get_account_info( 69 State(state): State<AppState>, 70 - _auth: BearerAuthAdmin, 71 Query(params): Query<GetAccountInfoParams>, 72 - ) -> Response { 73 - let account = match state 74 .infra_repo 75 .get_admin_account_info_by_did(&params.did) 76 .await 77 - { 78 - Ok(Some(a)) => a, 79 - Ok(None) => return ApiError::AccountNotFound.into_response(), 80 - Err(e) => { 81 error!("DB error in get_account_info: {:?}", e); 82 - return ApiError::InternalError(None).into_response(); 83 - } 84 - }; 85 86 let invited_by = get_invited_by(&state, account.id).await; 87 let invites = get_invites_for_user(&state, account.id).await; 88 89 - ( 90 StatusCode::OK, 91 Json(AccountInfo { 92 did: account.did, ··· 105 invites, 106 }), 107 ) 108 - .into_response() 109 } 110 111 async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> { ··· 200 201 pub async fn get_account_infos( 202 State(state): State<AppState>, 203 - _auth: BearerAuthAdmin, 204 RawQuery(raw_query): RawQuery, 205 - ) -> Response { 206 let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids") 207 .into_iter() 208 .filter(|d| !d.is_empty()) 209 .collect(); 210 211 if dids.is_empty() { 212 - return ApiError::InvalidRequest("dids is required".into()).into_response(); 213 } 214 215 let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect(); 216 - let accounts = match state 217 .infra_repo 218 .get_admin_account_infos_by_dids(&dids_typed) 219 .await 220 - { 221 - Ok(accounts) => accounts, 222 - Err(e) => { 223 error!("Failed to fetch account infos: {:?}", e); 224 - return ApiError::InternalError(None).into_response(); 225 - } 226 - }; 227 228 let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect(); 229 ··· 316 }) 317 .collect(); 318 319 - (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response() 320 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::RequiredAuth; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; 5 use axum::{ ··· 67 68 pub async fn get_account_info( 69 State(state): State<AppState>, 70 + auth: RequiredAuth, 71 Query(params): Query<GetAccountInfoParams>, 72 + ) -> Result<Response, ApiError> { 73 + auth.0.require_user()?.require_active()?.require_admin()?; 74 + 75 + let account = state 76 .infra_repo 77 .get_admin_account_info_by_did(&params.did) 78 .await 79 + .map_err(|e| { 80 error!("DB error in get_account_info: {:?}", e); 81 + ApiError::InternalError(None) 82 + })? 83 + .ok_or(ApiError::AccountNotFound)?; 84 85 let invited_by = get_invited_by(&state, account.id).await; 86 let invites = get_invites_for_user(&state, account.id).await; 87 88 + Ok(( 89 StatusCode::OK, 90 Json(AccountInfo { 91 did: account.did, ··· 104 invites, 105 }), 106 ) 107 + .into_response()) 108 } 109 110 async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> { ··· 199 200 pub async fn get_account_infos( 201 State(state): State<AppState>, 202 + auth: RequiredAuth, 203 RawQuery(raw_query): RawQuery, 204 + ) -> Result<Response, ApiError> { 205 + auth.0.require_user()?.require_active()?.require_admin()?; 206 + 207 let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids") 208 .into_iter() 209 .filter(|d| !d.is_empty()) 210 .collect(); 211 212 if dids.is_empty() { 213 + return Err(ApiError::InvalidRequest("dids is required".into())); 214 } 215 216 let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect(); 217 + let accounts = state 218 .infra_repo 219 .get_admin_account_infos_by_dids(&dids_typed) 220 .await 221 + .map_err(|e| { 222 error!("Failed to fetch account infos: {:?}", e); 223 + ApiError::InternalError(None) 224 + })?; 225 226 let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect(); 227 ··· 314 }) 315 .collect(); 316 317 + Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()) 318 }
+41 -42
crates/tranquil-pds/src/api/admin/account/search.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; 5 use axum::{ ··· 50 51 pub async fn search_accounts( 52 State(state): State<AppState>, 53 - _auth: BearerAuthAdmin, 54 Query(params): Query<SearchAccountsParams>, 55 - ) -> Response { 56 let limit = params.limit.clamp(1, 100); 57 let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); 58 let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); 59 let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|c| c.parse().ok()); 60 - let result = state 61 .user_repo 62 .search_accounts( 63 cursor_did.as_ref(), ··· 65 handle_filter.as_deref(), 66 limit + 1, 67 ) 68 - .await; 69 - match result { 70 - Ok(rows) => { 71 - let has_more = rows.len() > limit as usize; 72 - let accounts: Vec<AccountView> = rows 73 - .into_iter() 74 - .take(limit as usize) 75 - .map(|row| AccountView { 76 - did: row.did.clone(), 77 - handle: row.handle, 78 - email: row.email, 79 - indexed_at: row.created_at.to_rfc3339(), 80 - email_confirmed_at: if row.email_verified { 81 - Some(row.created_at.to_rfc3339()) 82 - } else { 83 - None 84 - }, 85 - deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()), 86 - invites_disabled: row.invites_disabled, 87 - }) 88 - .collect(); 89 - let next_cursor = if has_more { 90 - accounts.last().map(|a| a.did.to_string()) 91 } else { 92 None 93 - }; 94 - ( 95 - StatusCode::OK, 96 - Json(SearchAccountsOutput { 97 - cursor: next_cursor, 98 - accounts, 99 - }), 100 - ) 101 - .into_response() 102 - } 103 - Err(e) => { 104 - error!("DB error in search_accounts: {:?}", e); 105 - ApiError::InternalError(None).into_response() 106 - } 107 - } 108 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::RequiredAuth; 3 use crate::state::AppState; 4 use crate::types::{Did, Handle}; 5 use axum::{ ··· 50 51 pub async fn search_accounts( 52 State(state): State<AppState>, 53 + auth: RequiredAuth, 54 Query(params): Query<SearchAccountsParams>, 55 + ) -> Result<Response, ApiError> { 56 + auth.0.require_user()?.require_active()?.require_admin()?; 57 + 58 let limit = params.limit.clamp(1, 100); 59 let email_filter = params.email.as_deref().map(|e| format!("%{}%", e)); 60 let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h)); 61 let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|c| c.parse().ok()); 62 + let rows = state 63 .user_repo 64 .search_accounts( 65 cursor_did.as_ref(), ··· 67 handle_filter.as_deref(), 68 limit + 1, 69 ) 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()) 87 } else { 88 None 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()) 107 }
+45 -36
crates/tranquil-pds/src/api/admin/account/update.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAdmin; 4 use crate::state::AppState; 5 use crate::types::{Did, Handle, PlainPassword}; 6 use axum::{ ··· 19 20 pub async fn update_account_email( 21 State(state): State<AppState>, 22 - _auth: BearerAuthAdmin, 23 Json(input): Json<UpdateAccountEmailInput>, 24 - ) -> Response { 25 let account = input.account.trim(); 26 let email = input.email.trim(); 27 if account.is_empty() || email.is_empty() { 28 - return ApiError::InvalidRequest("account and email are required".into()).into_response(); 29 } 30 - let account_did: Did = match account.parse() { 31 - Ok(d) => d, 32 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 33 - }; 34 match state 35 .user_repo 36 .admin_update_email(&account_did, email) 37 .await 38 { 39 - Ok(0) => ApiError::AccountNotFound.into_response(), 40 - Ok(_) => EmptyResponse::ok().into_response(), 41 Err(e) => { 42 error!("DB error updating email: {:?}", e); 43 - ApiError::InternalError(None).into_response() 44 } 45 } 46 } ··· 53 54 pub async fn update_account_handle( 55 State(state): State<AppState>, 56 - _auth: BearerAuthAdmin, 57 Json(input): Json<UpdateAccountHandleInput>, 58 - ) -> Response { 59 let did = &input.did; 60 let input_handle = input.handle.trim(); 61 if input_handle.is_empty() { 62 - return ApiError::InvalidRequest("handle is required".into()).into_response(); 63 } 64 if !input_handle 65 .chars() 66 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 67 { 68 - return ApiError::InvalidHandle(None).into_response(); 69 } 70 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 71 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 75 input_handle.to_string() 76 }; 77 let old_handle = state.user_repo.get_handle_by_did(did).await.ok().flatten(); 78 - let user_id = match state.user_repo.get_id_by_did(did).await { 79 - Ok(Some(id)) => id, 80 - _ => return ApiError::AccountNotFound.into_response(), 81 - }; 82 let handle_for_check = Handle::new_unchecked(&handle); 83 if let Ok(true) = state 84 .user_repo 85 .check_handle_exists(&handle_for_check, user_id) 86 .await 87 { 88 - return ApiError::HandleTaken.into_response(); 89 } 90 match state 91 .user_repo 92 .admin_update_handle(did, &handle_for_check) 93 .await 94 { 95 - Ok(0) => ApiError::AccountNotFound.into_response(), 96 Ok(_) => { 97 if let Some(old) = old_handle { 98 let _ = state.cache.delete(&format!("handle:{}", old)).await; ··· 115 { 116 warn!("Failed to update PLC handle for admin handle update: {}", e); 117 } 118 - EmptyResponse::ok().into_response() 119 } 120 Err(e) => { 121 error!("DB error updating handle: {:?}", e); 122 - ApiError::InternalError(None).into_response() 123 } 124 } 125 } ··· 132 133 pub async fn update_account_password( 134 State(state): State<AppState>, 135 - _auth: BearerAuthAdmin, 136 Json(input): Json<UpdateAccountPasswordInput>, 137 - ) -> Response { 138 let did = &input.did; 139 let password = input.password.trim(); 140 if password.is_empty() { 141 - return ApiError::InvalidRequest("password is required".into()).into_response(); 142 } 143 - let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) { 144 - Ok(h) => h, 145 - Err(e) => { 146 - error!("Failed to hash password: {:?}", e); 147 - return ApiError::InternalError(None).into_response(); 148 - } 149 - }; 150 match state 151 .user_repo 152 .admin_update_password(did, &password_hash) 153 .await 154 { 155 - Ok(0) => ApiError::AccountNotFound.into_response(), 156 - Ok(_) => EmptyResponse::ok().into_response(), 157 Err(e) => { 158 error!("DB error updating password: {:?}", e); 159 - ApiError::InternalError(None).into_response() 160 } 161 } 162 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::RequiredAuth; 4 use crate::state::AppState; 5 use crate::types::{Did, Handle, PlainPassword}; 6 use axum::{ ··· 19 20 pub async fn update_account_email( 21 State(state): State<AppState>, 22 + auth: RequiredAuth, 23 Json(input): Json<UpdateAccountEmailInput>, 24 + ) -> Result<Response, ApiError> { 25 + auth.0.require_user()?.require_active()?.require_admin()?; 26 + 27 let account = input.account.trim(); 28 let email = input.email.trim(); 29 if account.is_empty() || email.is_empty() { 30 + return Err(ApiError::InvalidRequest( 31 + "account and email are required".into(), 32 + )); 33 } 34 + let account_did: Did = account 35 + .parse() 36 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 37 + 38 match state 39 .user_repo 40 .admin_update_email(&account_did, email) 41 .await 42 { 43 + Ok(0) => Err(ApiError::AccountNotFound), 44 + Ok(_) => Ok(EmptyResponse::ok().into_response()), 45 Err(e) => { 46 error!("DB error updating email: {:?}", e); 47 + Err(ApiError::InternalError(None)) 48 } 49 } 50 } ··· 57 58 pub async fn update_account_handle( 59 State(state): State<AppState>, 60 + auth: RequiredAuth, 61 Json(input): Json<UpdateAccountHandleInput>, 62 + ) -> Result<Response, ApiError> { 63 + auth.0.require_user()?.require_active()?.require_admin()?; 64 + 65 let did = &input.did; 66 let input_handle = input.handle.trim(); 67 if input_handle.is_empty() { 68 + return Err(ApiError::InvalidRequest("handle is required".into())); 69 } 70 if !input_handle 71 .chars() 72 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 73 { 74 + return Err(ApiError::InvalidHandle(None)); 75 } 76 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 77 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 81 input_handle.to_string() 82 }; 83 let old_handle = state.user_repo.get_handle_by_did(did).await.ok().flatten(); 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)?; 91 let handle_for_check = Handle::new_unchecked(&handle); 92 if let Ok(true) = state 93 .user_repo 94 .check_handle_exists(&handle_for_check, user_id) 95 .await 96 { 97 + return Err(ApiError::HandleTaken); 98 } 99 match state 100 .user_repo 101 .admin_update_handle(did, &handle_for_check) 102 .await 103 { 104 + Ok(0) => Err(ApiError::AccountNotFound), 105 Ok(_) => { 106 if let Some(old) = old_handle { 107 let _ = state.cache.delete(&format!("handle:{}", old)).await; ··· 124 { 125 warn!("Failed to update PLC handle for admin handle update: {}", e); 126 } 127 + Ok(EmptyResponse::ok().into_response()) 128 } 129 Err(e) => { 130 error!("DB error updating handle: {:?}", e); 131 + Err(ApiError::InternalError(None)) 132 } 133 } 134 } ··· 141 142 pub async fn update_account_password( 143 State(state): State<AppState>, 144 + auth: RequiredAuth, 145 Json(input): Json<UpdateAccountPasswordInput>, 146 + ) -> Result<Response, ApiError> { 147 + auth.0.require_user()?.require_active()?.require_admin()?; 148 + 149 let did = &input.did; 150 let password = input.password.trim(); 151 if password.is_empty() { 152 + return Err(ApiError::InvalidRequest("password is required".into())); 153 } 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 + 159 match state 160 .user_repo 161 .admin_update_password(did, &password_hash) 162 .await 163 { 164 + Ok(0) => Err(ApiError::AccountNotFound), 165 + Ok(_) => Ok(EmptyResponse::ok().into_response()), 166 Err(e) => { 167 error!("DB error updating password: {:?}", e); 168 + Err(ApiError::InternalError(None)) 169 } 170 } 171 }
+4 -2
crates/tranquil-pds/src/api/admin/config.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 3 use crate::state::AppState; 4 use axum::{Json, extract::State}; 5 use serde::{Deserialize, Serialize}; ··· 78 79 pub async fn update_server_config( 80 State(state): State<AppState>, 81 - _admin: BearerAuthAdmin, 82 Json(req): Json<UpdateServerConfigRequest>, 83 ) -> Result<Json<UpdateServerConfigResponse>, ApiError> { 84 if let Some(server_name) = req.server_name { 85 let trimmed = server_name.trim(); 86 if trimmed.is_empty() || trimmed.len() > 100 {
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::RequiredAuth; 3 use crate::state::AppState; 4 use axum::{Json, extract::State}; 5 use serde::{Deserialize, Serialize}; ··· 78 79 pub async fn update_server_config( 80 State(state): State<AppState>, 81 + auth: RequiredAuth, 82 Json(req): Json<UpdateServerConfigRequest>, 83 ) -> Result<Json<UpdateServerConfigResponse>, ApiError> { 84 + auth.0.require_user()?.require_active()?.require_admin()?; 85 + 86 if let Some(server_name) = req.server_name { 87 let trimmed = server_name.trim(); 88 if trimmed.is_empty() || trimmed.len() > 100 {
+40 -35
crates/tranquil-pds/src/api/admin/invite.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAdmin; 4 use crate::state::AppState; 5 use axum::{ 6 Json, ··· 21 22 pub async fn disable_invite_codes( 23 State(state): State<AppState>, 24 - _auth: BearerAuthAdmin, 25 Json(input): Json<DisableInviteCodesInput>, 26 - ) -> Response { 27 if let Some(codes) = &input.codes 28 && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await 29 { ··· 40 error!("DB error disabling invite codes by account: {:?}", e); 41 } 42 } 43 - EmptyResponse::ok().into_response() 44 } 45 46 #[derive(Deserialize)] ··· 78 79 pub async fn get_invite_codes( 80 State(state): State<AppState>, 81 - _auth: BearerAuthAdmin, 82 Query(params): Query<GetInviteCodesParams>, 83 - ) -> Response { 84 let limit = params.limit.unwrap_or(100).clamp(1, 500); 85 let sort_order = match params.sort.as_deref() { 86 Some("usage") => InviteCodeSortOrder::Usage, 87 _ => InviteCodeSortOrder::Recent, 88 }; 89 90 - let codes_rows = match state 91 .infra_repo 92 .list_invite_codes(params.cursor.as_deref(), limit, sort_order) 93 .await 94 - { 95 - Ok(rows) => rows, 96 - Err(e) => { 97 error!("DB error fetching invite codes: {:?}", e); 98 - return ApiError::InternalError(None).into_response(); 99 - } 100 - }; 101 102 let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect(); 103 let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect(); ··· 155 } else { 156 None 157 }; 158 - ( 159 StatusCode::OK, 160 Json(GetInviteCodesOutput { 161 cursor: next_cursor, 162 codes, 163 }), 164 ) 165 - .into_response() 166 } 167 168 #[derive(Deserialize)] ··· 172 173 pub async fn disable_account_invites( 174 State(state): State<AppState>, 175 - _auth: BearerAuthAdmin, 176 Json(input): Json<DisableAccountInvitesInput>, 177 - ) -> Response { 178 let account = input.account.trim(); 179 if account.is_empty() { 180 - return ApiError::InvalidRequest("account is required".into()).into_response(); 181 } 182 - let account_did: tranquil_types::Did = match account.parse() { 183 - Ok(d) => d, 184 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 185 - }; 186 match state 187 .user_repo 188 .set_invites_disabled(&account_did, true) 189 .await 190 { 191 - Ok(true) => EmptyResponse::ok().into_response(), 192 - Ok(false) => ApiError::AccountNotFound.into_response(), 193 Err(e) => { 194 error!("DB error disabling account invites: {:?}", e); 195 - ApiError::InternalError(None).into_response() 196 } 197 } 198 } ··· 204 205 pub async fn enable_account_invites( 206 State(state): State<AppState>, 207 - _auth: BearerAuthAdmin, 208 Json(input): Json<EnableAccountInvitesInput>, 209 - ) -> Response { 210 let account = input.account.trim(); 211 if account.is_empty() { 212 - return ApiError::InvalidRequest("account is required".into()).into_response(); 213 } 214 - let account_did: tranquil_types::Did = match account.parse() { 215 - Ok(d) => d, 216 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 217 - }; 218 match state 219 .user_repo 220 .set_invites_disabled(&account_did, false) 221 .await 222 { 223 - Ok(true) => EmptyResponse::ok().into_response(), 224 - Ok(false) => ApiError::AccountNotFound.into_response(), 225 Err(e) => { 226 error!("DB error enabling account invites: {:?}", e); 227 - ApiError::InternalError(None).into_response() 228 } 229 } 230 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::RequiredAuth; 4 use crate::state::AppState; 5 use axum::{ 6 Json, ··· 21 22 pub async fn disable_invite_codes( 23 State(state): State<AppState>, 24 + auth: RequiredAuth, 25 Json(input): Json<DisableInviteCodesInput>, 26 + ) -> Result<Response, ApiError> { 27 + auth.0.require_user()?.require_active()?.require_admin()?; 28 + 29 if let Some(codes) = &input.codes 30 && let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await 31 { ··· 42 error!("DB error disabling invite codes by account: {:?}", e); 43 } 44 } 45 + Ok(EmptyResponse::ok().into_response()) 46 } 47 48 #[derive(Deserialize)] ··· 80 81 pub async fn get_invite_codes( 82 State(state): State<AppState>, 83 + auth: RequiredAuth, 84 Query(params): Query<GetInviteCodesParams>, 85 + ) -> Result<Response, ApiError> { 86 + auth.0.require_user()?.require_active()?.require_admin()?; 87 + 88 let limit = params.limit.unwrap_or(100).clamp(1, 500); 89 let sort_order = match params.sort.as_deref() { 90 Some("usage") => InviteCodeSortOrder::Usage, 91 _ => InviteCodeSortOrder::Recent, 92 }; 93 94 + let codes_rows = state 95 .infra_repo 96 .list_invite_codes(params.cursor.as_deref(), limit, sort_order) 97 .await 98 + .map_err(|e| { 99 error!("DB error fetching invite codes: {:?}", e); 100 + ApiError::InternalError(None) 101 + })?; 102 103 let user_ids: Vec<uuid::Uuid> = codes_rows.iter().map(|r| r.created_by_user).collect(); 104 let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect(); ··· 156 } else { 157 None 158 }; 159 + Ok(( 160 StatusCode::OK, 161 Json(GetInviteCodesOutput { 162 cursor: next_cursor, 163 codes, 164 }), 165 ) 166 + .into_response()) 167 } 168 169 #[derive(Deserialize)] ··· 173 174 pub async fn disable_account_invites( 175 State(state): State<AppState>, 176 + auth: RequiredAuth, 177 Json(input): Json<DisableAccountInvitesInput>, 178 + ) -> Result<Response, ApiError> { 179 + auth.0.require_user()?.require_active()?.require_admin()?; 180 + 181 let account = input.account.trim(); 182 if account.is_empty() { 183 + return Err(ApiError::InvalidRequest("account is required".into())); 184 } 185 + let account_did: tranquil_types::Did = account 186 + .parse() 187 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 188 + 189 match state 190 .user_repo 191 .set_invites_disabled(&account_did, true) 192 .await 193 { 194 + Ok(true) => Ok(EmptyResponse::ok().into_response()), 195 + Ok(false) => Err(ApiError::AccountNotFound), 196 Err(e) => { 197 error!("DB error disabling account invites: {:?}", e); 198 + Err(ApiError::InternalError(None)) 199 } 200 } 201 } ··· 207 208 pub async fn enable_account_invites( 209 State(state): State<AppState>, 210 + auth: RequiredAuth, 211 Json(input): Json<EnableAccountInvitesInput>, 212 + ) -> Result<Response, ApiError> { 213 + auth.0.require_user()?.require_active()?.require_admin()?; 214 + 215 let account = input.account.trim(); 216 if account.is_empty() { 217 + return Err(ApiError::InvalidRequest("account is required".into())); 218 } 219 + let account_did: tranquil_types::Did = account 220 + .parse() 221 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 222 + 223 match state 224 .user_repo 225 .set_invites_disabled(&account_did, false) 226 .await 227 { 228 + Ok(true) => Ok(EmptyResponse::ok().into_response()), 229 + Ok(false) => Err(ApiError::AccountNotFound), 230 Err(e) => { 231 error!("DB error enabling account invites: {:?}", e); 232 + Err(ApiError::InternalError(None)) 233 } 234 } 235 }
+10 -4
crates/tranquil-pds/src/api/admin/server_stats.rs
··· 1 - use crate::auth::BearerAuthAdmin; 2 use crate::state::AppState; 3 use axum::{ 4 Json, ··· 16 pub blob_storage_bytes: i64, 17 } 18 19 - pub async fn get_server_stats(State(state): State<AppState>, _auth: BearerAuthAdmin) -> Response { 20 let user_count = state.user_repo.count_users().await.unwrap_or(0); 21 let repo_count = state.repo_repo.count_repos().await.unwrap_or(0); 22 let record_count = state.repo_repo.count_all_records().await.unwrap_or(0); 23 let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0); 24 25 - Json(ServerStatsResponse { 26 user_count, 27 repo_count, 28 record_count, 29 blob_storage_bytes, 30 }) 31 - .into_response() 32 }
··· 1 + use crate::api::error::ApiError; 2 + use crate::auth::RequiredAuth; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 17 pub blob_storage_bytes: i64, 18 } 19 20 + pub async fn get_server_stats( 21 + State(state): State<AppState>, 22 + auth: RequiredAuth, 23 + ) -> Result<Response, ApiError> { 24 + auth.0.require_user()?.require_active()?.require_admin()?; 25 + 26 let user_count = state.user_repo.count_users().await.unwrap_or(0); 27 let repo_count = state.repo_repo.count_repos().await.unwrap_or(0); 28 let record_count = state.repo_repo.count_all_records().await.unwrap_or(0); 29 let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0); 30 31 + Ok(Json(ServerStatsResponse { 32 user_count, 33 repo_count, 34 record_count, 35 blob_storage_bytes, 36 }) 37 + .into_response()) 38 }
+77 -94
crates/tranquil-pds/src/api/admin/status.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuthAdmin; 3 use crate::state::AppState; 4 use crate::types::{CidLink, Did}; 5 use axum::{ ··· 35 36 pub async fn get_subject_status( 37 State(state): State<AppState>, 38 - _auth: BearerAuthAdmin, 39 Query(params): Query<GetSubjectStatusParams>, 40 - ) -> Response { 41 if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { 42 - return ApiError::InvalidRequest("Must provide did, uri, or blob".into()).into_response(); 43 } 44 if let Some(did_str) = &params.did { 45 - let did: Did = match did_str.parse() { 46 - Ok(d) => d, 47 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 48 - }; 49 match state.user_repo.get_status_by_did(&did).await { 50 Ok(Some(status)) => { 51 let deactivated = status.deactivated_at.map(|_| StatusAttr { ··· 56 applied: true, 57 r#ref: Some(r.clone()), 58 }); 59 - return ( 60 StatusCode::OK, 61 Json(SubjectStatus { 62 subject: json!({ ··· 67 deactivated, 68 }), 69 ) 70 - .into_response(); 71 } 72 Ok(None) => { 73 - return ApiError::SubjectNotFound.into_response(); 74 } 75 Err(e) => { 76 error!("DB error in get_subject_status: {:?}", e); 77 - return ApiError::InternalError(None).into_response(); 78 } 79 } 80 } 81 if let Some(uri_str) = &params.uri { 82 - let cid: CidLink = match uri_str.parse() { 83 - Ok(c) => c, 84 - Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(), 85 - }; 86 match state.repo_repo.get_record_by_cid(&cid).await { 87 Ok(Some(record)) => { 88 let takedown = record.takedown_ref.as_ref().map(|r| StatusAttr { 89 applied: true, 90 r#ref: Some(r.clone()), 91 }); 92 - return ( 93 StatusCode::OK, 94 Json(SubjectStatus { 95 subject: json!({ ··· 101 deactivated: None, 102 }), 103 ) 104 - .into_response(); 105 } 106 Ok(None) => { 107 - return ApiError::RecordNotFound.into_response(); 108 } 109 Err(e) => { 110 error!("DB error in get_subject_status: {:?}", e); 111 - return ApiError::InternalError(None).into_response(); 112 } 113 } 114 } 115 if let Some(blob_cid_str) = &params.blob { 116 - let blob_cid: CidLink = match blob_cid_str.parse() { 117 - Ok(c) => c, 118 - Err(_) => return ApiError::InvalidRequest("Invalid CID format".into()).into_response(), 119 - }; 120 - let did = match &params.did { 121 - Some(d) => d, 122 - None => { 123 - return ApiError::InvalidRequest("Must provide a did to request blob state".into()) 124 - .into_response(); 125 - } 126 - }; 127 match state.blob_repo.get_blob_with_takedown(&blob_cid).await { 128 Ok(Some(blob)) => { 129 let takedown = blob.takedown_ref.as_ref().map(|r| StatusAttr { 130 applied: true, 131 r#ref: Some(r.clone()), 132 }); 133 - return ( 134 StatusCode::OK, 135 Json(SubjectStatus { 136 subject: json!({ ··· 142 deactivated: None, 143 }), 144 ) 145 - .into_response(); 146 } 147 Ok(None) => { 148 - return ApiError::BlobNotFound(None).into_response(); 149 } 150 Err(e) => { 151 error!("DB error in get_subject_status: {:?}", e); 152 - return ApiError::InternalError(None).into_response(); 153 } 154 } 155 } 156 - ApiError::InvalidRequest("Invalid subject type".into()).into_response() 157 } 158 159 #[derive(Deserialize)] ··· 172 173 pub async fn update_subject_status( 174 State(state): State<AppState>, 175 - _auth: BearerAuthAdmin, 176 Json(input): Json<UpdateSubjectStatusInput>, 177 - ) -> Response { 178 let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 179 match subject_type { 180 Some("com.atproto.admin.defs#repoRef") => { ··· 187 } else { 188 None 189 }; 190 - if let Err(e) = state.user_repo.set_user_takedown(&did, takedown_ref).await { 191 - error!("Failed to update user takedown status for {}: {:?}", did, e); 192 - return ApiError::InternalError(Some( 193 - "Failed to update takedown status".into(), 194 - )) 195 - .into_response(); 196 - } 197 } 198 if let Some(deactivated) = &input.deactivated { 199 let result = if deactivated.applied { ··· 201 } else { 202 state.user_repo.activate_account(&did).await 203 }; 204 - if let Err(e) = result { 205 error!( 206 "Failed to update user deactivation status for {}: {:?}", 207 did, e 208 ); 209 - return ApiError::InternalError(Some( 210 - "Failed to update deactivation status".into(), 211 - )) 212 - .into_response(); 213 - } 214 } 215 if let Some(takedown) = &input.takedown { 216 let status = if takedown.applied { ··· 249 if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await { 250 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 251 } 252 - return ( 253 StatusCode::OK, 254 Json(json!({ 255 "subject": input.subject, ··· 262 })) 263 })), 264 ) 265 - .into_response(); 266 } 267 } 268 Some("com.atproto.repo.strongRef") => { 269 let uri_str = input.subject.get("uri").and_then(|u| u.as_str()); 270 if let Some(uri_str) = uri_str { 271 - let cid: CidLink = match uri_str.parse() { 272 - Ok(c) => c, 273 - Err(_) => { 274 - return ApiError::InvalidRequest("Invalid CID format".into()) 275 - .into_response(); 276 - } 277 - }; 278 if let Some(takedown) = &input.takedown { 279 let takedown_ref = if takedown.applied { 280 takedown.r#ref.as_deref() 281 } else { 282 None 283 }; 284 - if let Err(e) = state 285 .repo_repo 286 .set_record_takedown(&cid, takedown_ref) 287 .await 288 - { 289 - error!( 290 - "Failed to update record takedown status for {}: {:?}", 291 - uri_str, e 292 - ); 293 - return ApiError::InternalError(Some( 294 - "Failed to update takedown status".into(), 295 - )) 296 - .into_response(); 297 - } 298 } 299 - return ( 300 StatusCode::OK, 301 Json(json!({ 302 "subject": input.subject, ··· 306 })) 307 })), 308 ) 309 - .into_response(); 310 } 311 } 312 Some("com.atproto.admin.defs#repoBlobRef") => { 313 let cid_str = input.subject.get("cid").and_then(|c| c.as_str()); 314 if let Some(cid_str) = cid_str { 315 - let cid: CidLink = match cid_str.parse() { 316 - Ok(c) => c, 317 - Err(_) => { 318 - return ApiError::InvalidRequest("Invalid CID format".into()) 319 - .into_response(); 320 - } 321 - }; 322 if let Some(takedown) = &input.takedown { 323 let takedown_ref = if takedown.applied { 324 takedown.r#ref.as_deref() 325 } else { 326 None 327 }; 328 - if let Err(e) = state 329 .blob_repo 330 .update_blob_takedown(&cid, takedown_ref) 331 .await 332 - { 333 - error!( 334 - "Failed to update blob takedown status for {}: {:?}", 335 - cid_str, e 336 - ); 337 - return ApiError::InternalError(Some( 338 - "Failed to update takedown status".into(), 339 - )) 340 - .into_response(); 341 - } 342 } 343 - return ( 344 StatusCode::OK, 345 Json(json!({ 346 "subject": input.subject, ··· 350 })) 351 })), 352 ) 353 - .into_response(); 354 } 355 } 356 _ => {} 357 } 358 - ApiError::InvalidRequest("Invalid subject type".into()).into_response() 359 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::RequiredAuth; 3 use crate::state::AppState; 4 use crate::types::{CidLink, Did}; 5 use axum::{ ··· 35 36 pub async fn get_subject_status( 37 State(state): State<AppState>, 38 + auth: RequiredAuth, 39 Query(params): Query<GetSubjectStatusParams>, 40 + ) -> Result<Response, ApiError> { 41 + auth.0.require_user()?.require_active()?.require_admin()?; 42 + 43 if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { 44 + return Err(ApiError::InvalidRequest( 45 + "Must provide did, uri, or blob".into(), 46 + )); 47 } 48 if let Some(did_str) = &params.did { 49 + let did: Did = did_str 50 + .parse() 51 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?; 52 match state.user_repo.get_status_by_did(&did).await { 53 Ok(Some(status)) => { 54 let deactivated = status.deactivated_at.map(|_| StatusAttr { ··· 59 applied: true, 60 r#ref: Some(r.clone()), 61 }); 62 + return Ok(( 63 StatusCode::OK, 64 Json(SubjectStatus { 65 subject: json!({ ··· 70 deactivated, 71 }), 72 ) 73 + .into_response()); 74 } 75 Ok(None) => { 76 + return Err(ApiError::SubjectNotFound); 77 } 78 Err(e) => { 79 error!("DB error in get_subject_status: {:?}", e); 80 + return Err(ApiError::InternalError(None)); 81 } 82 } 83 } 84 if let Some(uri_str) = &params.uri { 85 + let cid: CidLink = uri_str 86 + .parse() 87 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 88 match state.repo_repo.get_record_by_cid(&cid).await { 89 Ok(Some(record)) => { 90 let takedown = record.takedown_ref.as_ref().map(|r| StatusAttr { 91 applied: true, 92 r#ref: Some(r.clone()), 93 }); 94 + return Ok(( 95 StatusCode::OK, 96 Json(SubjectStatus { 97 subject: json!({ ··· 103 deactivated: None, 104 }), 105 ) 106 + .into_response()); 107 } 108 Ok(None) => { 109 + return Err(ApiError::RecordNotFound); 110 } 111 Err(e) => { 112 error!("DB error in get_subject_status: {:?}", e); 113 + return Err(ApiError::InternalError(None)); 114 } 115 } 116 } 117 if let Some(blob_cid_str) = &params.blob { 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 + })?; 124 match state.blob_repo.get_blob_with_takedown(&blob_cid).await { 125 Ok(Some(blob)) => { 126 let takedown = blob.takedown_ref.as_ref().map(|r| StatusAttr { 127 applied: true, 128 r#ref: Some(r.clone()), 129 }); 130 + return Ok(( 131 StatusCode::OK, 132 Json(SubjectStatus { 133 subject: json!({ ··· 139 deactivated: None, 140 }), 141 ) 142 + .into_response()); 143 } 144 Ok(None) => { 145 + return Err(ApiError::BlobNotFound(None)); 146 } 147 Err(e) => { 148 error!("DB error in get_subject_status: {:?}", e); 149 + return Err(ApiError::InternalError(None)); 150 } 151 } 152 } 153 + Err(ApiError::InvalidRequest("Invalid subject type".into())) 154 } 155 156 #[derive(Deserialize)] ··· 169 170 pub async fn update_subject_status( 171 State(state): State<AppState>, 172 + auth: RequiredAuth, 173 Json(input): Json<UpdateSubjectStatusInput>, 174 + ) -> Result<Response, ApiError> { 175 + auth.0.require_user()?.require_active()?.require_admin()?; 176 + 177 let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 178 match subject_type { 179 Some("com.atproto.admin.defs#repoRef") => { ··· 186 } else { 187 None 188 }; 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 } 198 if let Some(deactivated) = &input.deactivated { 199 let result = if deactivated.applied { ··· 201 } else { 202 state.user_repo.activate_account(&did).await 203 }; 204 + result.map_err(|e| { 205 error!( 206 "Failed to update user deactivation status for {}: {:?}", 207 did, e 208 ); 209 + ApiError::InternalError(Some("Failed to update deactivation status".into())) 210 + })?; 211 } 212 if let Some(takedown) = &input.takedown { 213 let status = if takedown.applied { ··· 246 if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await { 247 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 248 } 249 + return Ok(( 250 StatusCode::OK, 251 Json(json!({ 252 "subject": input.subject, ··· 259 })) 260 })), 261 ) 262 + .into_response()); 263 } 264 } 265 Some("com.atproto.repo.strongRef") => { 266 let uri_str = input.subject.get("uri").and_then(|u| u.as_str()); 267 if let Some(uri_str) = uri_str { 268 + let cid: CidLink = uri_str 269 + .parse() 270 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 271 if let Some(takedown) = &input.takedown { 272 let takedown_ref = if takedown.applied { 273 takedown.r#ref.as_deref() 274 } else { 275 None 276 }; 277 + state 278 .repo_repo 279 .set_record_takedown(&cid, takedown_ref) 280 .await 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 + })?; 288 } 289 + return Ok(( 290 StatusCode::OK, 291 Json(json!({ 292 "subject": input.subject, ··· 296 })) 297 })), 298 ) 299 + .into_response()); 300 } 301 } 302 Some("com.atproto.admin.defs#repoBlobRef") => { 303 let cid_str = input.subject.get("cid").and_then(|c| c.as_str()); 304 if let Some(cid_str) = cid_str { 305 + let cid: CidLink = cid_str 306 + .parse() 307 + .map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?; 308 if let Some(takedown) = &input.takedown { 309 let takedown_ref = if takedown.applied { 310 takedown.r#ref.as_deref() 311 } else { 312 None 313 }; 314 + state 315 .blob_repo 316 .update_blob_takedown(&cid, takedown_ref) 317 .await 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 + })?; 325 } 326 + return Ok(( 327 StatusCode::OK, 328 Json(json!({ 329 "subject": input.subject, ··· 333 })) 334 })), 335 ) 336 + .into_response()); 337 } 338 } 339 _ => {} 340 } 341 + Err(ApiError::InvalidRequest("Invalid subject type".into())) 342 }
+99 -73
crates/tranquil-pds/src/api/backup.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, EnabledResponse}; 3 - use crate::auth::BearerAuth; 4 use crate::scheduled::generate_full_backup; 5 use crate::state::AppState; 6 use crate::storage::{BackupStorage, backup_retention_count}; ··· 35 pub backup_enabled: bool, 36 } 37 38 - pub async fn list_backups(State(state): State<AppState>, auth: BearerAuth) -> Response { 39 - let (user_id, backup_enabled) = 40 - match state.backup_repo.get_user_backup_status(&auth.0.did).await { 41 - Ok(Some(status)) => status, 42 - Ok(None) => { 43 - return ApiError::AccountNotFound.into_response(); 44 - } 45 - Err(e) => { 46 - error!("DB error fetching user: {:?}", e); 47 - return ApiError::InternalError(None).into_response(); 48 - } 49 - }; 50 51 let backups = match state.backup_repo.list_backups_for_user(user_id).await { 52 Ok(rows) => rows, 53 Err(e) => { 54 error!("DB error fetching backups: {:?}", e); 55 - return ApiError::InternalError(None).into_response(); 56 } 57 }; 58 ··· 68 }) 69 .collect(); 70 71 - ( 72 StatusCode::OK, 73 Json(ListBackupsOutput { 74 backups: backup_list, 75 backup_enabled, 76 }), 77 ) 78 - .into_response() 79 } 80 81 #[derive(Deserialize)] ··· 85 86 pub async fn get_backup( 87 State(state): State<AppState>, 88 - auth: BearerAuth, 89 Query(query): Query<GetBackupQuery>, 90 - ) -> Response { 91 let backup_id = match uuid::Uuid::parse_str(&query.id) { 92 Ok(id) => id, 93 Err(_) => { 94 - return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); 95 } 96 }; 97 98 let backup_info = match state 99 .backup_repo 100 - .get_backup_storage_info(backup_id, &auth.0.did) 101 .await 102 { 103 Ok(Some(b)) => b, 104 Ok(None) => { 105 - return ApiError::BackupNotFound.into_response(); 106 } 107 Err(e) => { 108 error!("DB error fetching backup: {:?}", e); 109 - return ApiError::InternalError(None).into_response(); 110 } 111 }; 112 113 let backup_storage = match state.backup_storage.as_ref() { 114 Some(storage) => storage, 115 None => { 116 - return ApiError::BackupsDisabled.into_response(); 117 } 118 }; 119 ··· 121 Ok(bytes) => bytes, 122 Err(e) => { 123 error!("Failed to fetch backup from storage: {:?}", e); 124 - return ApiError::InternalError(Some("Failed to retrieve backup".into())) 125 - .into_response(); 126 } 127 }; 128 129 - ( 130 StatusCode::OK, 131 [ 132 (axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car"), ··· 137 ], 138 car_bytes, 139 ) 140 - .into_response() 141 } 142 143 #[derive(Serialize)] ··· 149 pub block_count: i32, 150 } 151 152 - pub async fn create_backup(State(state): State<AppState>, auth: BearerAuth) -> Response { 153 let backup_storage = match state.backup_storage.as_ref() { 154 Some(storage) => storage, 155 None => { 156 - return ApiError::BackupsDisabled.into_response(); 157 } 158 }; 159 160 - let user = match state.backup_repo.get_user_for_backup(&auth.0.did).await { 161 Ok(Some(u)) => u, 162 Ok(None) => { 163 - return ApiError::AccountNotFound.into_response(); 164 } 165 Err(e) => { 166 error!("DB error fetching user: {:?}", e); 167 - return ApiError::InternalError(None).into_response(); 168 } 169 }; 170 171 if user.deactivated_at.is_some() { 172 - return ApiError::AccountDeactivated.into_response(); 173 } 174 175 let repo_rev = match &user.repo_rev { 176 Some(rev) => rev.clone(), 177 None => { 178 - return ApiError::RepoNotReady.into_response(); 179 } 180 }; 181 182 let head_cid = match Cid::from_str(&user.repo_root_cid) { 183 Ok(c) => c, 184 Err(_) => { 185 - return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(); 186 } 187 }; 188 ··· 197 Ok(bytes) => bytes, 198 Err(e) => { 199 error!("Failed to generate CAR: {:?}", e); 200 - return ApiError::InternalError(Some("Failed to generate backup".into())) 201 - .into_response(); 202 } 203 }; 204 ··· 212 Ok(key) => key, 213 Err(e) => { 214 error!("Failed to upload backup: {:?}", e); 215 - return ApiError::InternalError(Some("Failed to store backup".into())).into_response(); 216 } 217 }; 218 ··· 238 "Failed to rollback orphaned backup from S3" 239 ); 240 } 241 - return ApiError::InternalError(Some("Failed to record backup".into())).into_response(); 242 } 243 }; 244 ··· 261 warn!(did = %user.did, error = %e, "Failed to cleanup old backups after manual backup"); 262 } 263 264 - ( 265 StatusCode::OK, 266 Json(CreateBackupOutput { 267 id: backup_id.to_string(), ··· 270 block_count, 271 }), 272 ) 273 - .into_response() 274 } 275 276 async fn cleanup_old_backups( ··· 310 311 pub async fn delete_backup( 312 State(state): State<AppState>, 313 - auth: BearerAuth, 314 Query(query): Query<DeleteBackupQuery>, 315 - ) -> Response { 316 let backup_id = match uuid::Uuid::parse_str(&query.id) { 317 Ok(id) => id, 318 Err(_) => { 319 - return ApiError::InvalidRequest("Invalid backup ID".into()).into_response(); 320 } 321 }; 322 323 let backup = match state 324 .backup_repo 325 - .get_backup_for_deletion(backup_id, &auth.0.did) 326 .await 327 { 328 Ok(Some(b)) => b, 329 Ok(None) => { 330 - return ApiError::BackupNotFound.into_response(); 331 } 332 Err(e) => { 333 error!("DB error fetching backup: {:?}", e); 334 - return ApiError::InternalError(None).into_response(); 335 } 336 }; 337 338 if backup.deactivated_at.is_some() { 339 - return ApiError::AccountDeactivated.into_response(); 340 } 341 342 if let Some(backup_storage) = state.backup_storage.as_ref() ··· 351 352 if let Err(e) = state.backup_repo.delete_backup(backup.id).await { 353 error!("DB error deleting backup: {:?}", e); 354 - return ApiError::InternalError(Some("Failed to delete backup".into())).into_response(); 355 } 356 357 - info!(did = %auth.0.did, backup_id = %backup_id, "Deleted backup"); 358 359 - EmptyResponse::ok().into_response() 360 } 361 362 #[derive(Deserialize)] ··· 367 368 pub async fn set_backup_enabled( 369 State(state): State<AppState>, 370 - auth: BearerAuth, 371 Json(input): Json<SetBackupEnabledInput>, 372 - ) -> Response { 373 let deactivated_at = match state 374 .backup_repo 375 - .get_user_deactivated_status(&auth.0.did) 376 .await 377 { 378 Ok(Some(status)) => status, 379 Ok(None) => { 380 - return ApiError::AccountNotFound.into_response(); 381 } 382 Err(e) => { 383 error!("DB error fetching user: {:?}", e); 384 - return ApiError::InternalError(None).into_response(); 385 } 386 }; 387 388 if deactivated_at.is_some() { 389 - return ApiError::AccountDeactivated.into_response(); 390 } 391 392 if let Err(e) = state 393 .backup_repo 394 - .update_backup_enabled(&auth.0.did, input.enabled) 395 .await 396 { 397 error!("DB error updating backup_enabled: {:?}", e); 398 - return ApiError::InternalError(Some("Failed to update setting".into())).into_response(); 399 } 400 401 - info!(did = %auth.0.did, enabled = input.enabled, "Updated backup_enabled setting"); 402 403 - EnabledResponse::response(input.enabled).into_response() 404 } 405 406 - pub async fn export_blobs(State(state): State<AppState>, auth: BearerAuth) -> Response { 407 - let user_id = match state.backup_repo.get_user_id_by_did(&auth.0.did).await { 408 Ok(Some(id)) => id, 409 Ok(None) => { 410 - return ApiError::AccountNotFound.into_response(); 411 } 412 Err(e) => { 413 error!("DB error fetching user: {:?}", e); 414 - return ApiError::InternalError(None).into_response(); 415 } 416 }; 417 ··· 419 Ok(rows) => rows, 420 Err(e) => { 421 error!("DB error fetching blobs: {:?}", e); 422 - return ApiError::InternalError(None).into_response(); 423 } 424 }; 425 426 if blobs.is_empty() { 427 - return ( 428 StatusCode::OK, 429 [ 430 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 435 ], 436 Vec::<u8>::new(), 437 ) 438 - .into_response(); 439 } 440 441 let mut zip_buffer = std::io::Cursor::new(Vec::new()); ··· 513 514 if let Err(e) = zip.finish() { 515 error!("Failed to finish zip: {:?}", e); 516 - return ApiError::InternalError(Some("Failed to create zip file".into())) 517 - .into_response(); 518 } 519 } 520 521 let zip_bytes = zip_buffer.into_inner(); 522 523 - info!(did = %auth.0.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); 524 525 - ( 526 StatusCode::OK, 527 [ 528 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 533 ], 534 zip_bytes, 535 ) 536 - .into_response() 537 } 538 539 fn mime_to_extension(mime_type: &str) -> &'static str {
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, EnabledResponse}; 3 + use crate::auth::RequiredAuth; 4 use crate::scheduled::generate_full_backup; 5 use crate::state::AppState; 6 use crate::storage::{BackupStorage, backup_retention_count}; ··· 35 pub backup_enabled: bool, 36 } 37 38 + pub async fn list_backups( 39 + State(state): State<AppState>, 40 + auth: 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 + }; 54 55 let backups = match state.backup_repo.list_backups_for_user(user_id).await { 56 Ok(rows) => rows, 57 Err(e) => { 58 error!("DB error fetching backups: {:?}", e); 59 + return Ok(ApiError::InternalError(None).into_response()); 60 } 61 }; 62 ··· 72 }) 73 .collect(); 74 75 + Ok(( 76 StatusCode::OK, 77 Json(ListBackupsOutput { 78 backups: backup_list, 79 backup_enabled, 80 }), 81 ) 82 + .into_response()) 83 } 84 85 #[derive(Deserialize)] ··· 89 90 pub async fn get_backup( 91 State(state): State<AppState>, 92 + auth: RequiredAuth, 93 Query(query): Query<GetBackupQuery>, 94 + ) -> Result<Response, crate::api::error::ApiError> { 95 + let user = auth.0.require_user()?.require_active()?; 96 let backup_id = match uuid::Uuid::parse_str(&query.id) { 97 Ok(id) => id, 98 Err(_) => { 99 + return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response()); 100 } 101 }; 102 103 let backup_info = match state 104 .backup_repo 105 + .get_backup_storage_info(backup_id, &user.did) 106 .await 107 { 108 Ok(Some(b)) => b, 109 Ok(None) => { 110 + return Ok(ApiError::BackupNotFound.into_response()); 111 } 112 Err(e) => { 113 error!("DB error fetching backup: {:?}", e); 114 + return Ok(ApiError::InternalError(None).into_response()); 115 } 116 }; 117 118 let backup_storage = match state.backup_storage.as_ref() { 119 Some(storage) => storage, 120 None => { 121 + return Ok(ApiError::BackupsDisabled.into_response()); 122 } 123 }; 124 ··· 126 Ok(bytes) => bytes, 127 Err(e) => { 128 error!("Failed to fetch backup from storage: {:?}", e); 129 + return Ok( 130 + ApiError::InternalError(Some("Failed to retrieve backup".into())).into_response(), 131 + ); 132 } 133 }; 134 135 + Ok(( 136 StatusCode::OK, 137 [ 138 (axum::http::header::CONTENT_TYPE, "application/vnd.ipld.car"), ··· 143 ], 144 car_bytes, 145 ) 146 + .into_response()) 147 } 148 149 #[derive(Serialize)] ··· 155 pub block_count: i32, 156 } 157 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()?; 163 let backup_storage = match state.backup_storage.as_ref() { 164 Some(storage) => storage, 165 None => { 166 + return Ok(ApiError::BackupsDisabled.into_response()); 167 } 168 }; 169 170 + let user = match state.backup_repo.get_user_for_backup(&auth_user.did).await { 171 Ok(Some(u)) => u, 172 Ok(None) => { 173 + return Ok(ApiError::AccountNotFound.into_response()); 174 } 175 Err(e) => { 176 error!("DB error fetching user: {:?}", e); 177 + return Ok(ApiError::InternalError(None).into_response()); 178 } 179 }; 180 181 if user.deactivated_at.is_some() { 182 + return Ok(ApiError::AccountDeactivated.into_response()); 183 } 184 185 let repo_rev = match &user.repo_rev { 186 Some(rev) => rev.clone(), 187 None => { 188 + return Ok(ApiError::RepoNotReady.into_response()); 189 } 190 }; 191 192 let head_cid = match Cid::from_str(&user.repo_root_cid) { 193 Ok(c) => c, 194 Err(_) => { 195 + return Ok( 196 + ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(), 197 + ); 198 } 199 }; 200 ··· 209 Ok(bytes) => bytes, 210 Err(e) => { 211 error!("Failed to generate CAR: {:?}", e); 212 + return Ok( 213 + ApiError::InternalError(Some("Failed to generate backup".into())).into_response(), 214 + ); 215 } 216 }; 217 ··· 225 Ok(key) => key, 226 Err(e) => { 227 error!("Failed to upload backup: {:?}", e); 228 + return Ok( 229 + ApiError::InternalError(Some("Failed to store backup".into())).into_response(), 230 + ); 231 } 232 }; 233 ··· 253 "Failed to rollback orphaned backup from S3" 254 ); 255 } 256 + return Ok( 257 + ApiError::InternalError(Some("Failed to record backup".into())).into_response(), 258 + ); 259 } 260 }; 261 ··· 278 warn!(did = %user.did, error = %e, "Failed to cleanup old backups after manual backup"); 279 } 280 281 + Ok(( 282 StatusCode::OK, 283 Json(CreateBackupOutput { 284 id: backup_id.to_string(), ··· 287 block_count, 288 }), 289 ) 290 + .into_response()) 291 } 292 293 async fn cleanup_old_backups( ··· 327 328 pub async fn delete_backup( 329 State(state): State<AppState>, 330 + auth: RequiredAuth, 331 Query(query): Query<DeleteBackupQuery>, 332 + ) -> Result<Response, crate::api::error::ApiError> { 333 + let user = auth.0.require_user()?.require_active()?; 334 let backup_id = match uuid::Uuid::parse_str(&query.id) { 335 Ok(id) => id, 336 Err(_) => { 337 + return Ok(ApiError::InvalidRequest("Invalid backup ID".into()).into_response()); 338 } 339 }; 340 341 let backup = match state 342 .backup_repo 343 + .get_backup_for_deletion(backup_id, &user.did) 344 .await 345 { 346 Ok(Some(b)) => b, 347 Ok(None) => { 348 + return Ok(ApiError::BackupNotFound.into_response()); 349 } 350 Err(e) => { 351 error!("DB error fetching backup: {:?}", e); 352 + return Ok(ApiError::InternalError(None).into_response()); 353 } 354 }; 355 356 if backup.deactivated_at.is_some() { 357 + return Ok(ApiError::AccountDeactivated.into_response()); 358 } 359 360 if let Some(backup_storage) = state.backup_storage.as_ref() ··· 369 370 if let Err(e) = state.backup_repo.delete_backup(backup.id).await { 371 error!("DB error deleting backup: {:?}", e); 372 + return Ok(ApiError::InternalError(Some("Failed to delete backup".into())).into_response()); 373 } 374 375 + info!(did = %user.did, backup_id = %backup_id, "Deleted backup"); 376 377 + Ok(EmptyResponse::ok().into_response()) 378 } 379 380 #[derive(Deserialize)] ··· 385 386 pub async fn set_backup_enabled( 387 State(state): State<AppState>, 388 + auth: RequiredAuth, 389 Json(input): Json<SetBackupEnabledInput>, 390 + ) -> Result<Response, crate::api::error::ApiError> { 391 + let user = auth.0.require_user()?.require_active()?; 392 let deactivated_at = match state 393 .backup_repo 394 + .get_user_deactivated_status(&user.did) 395 .await 396 { 397 Ok(Some(status)) => status, 398 Ok(None) => { 399 + return Ok(ApiError::AccountNotFound.into_response()); 400 } 401 Err(e) => { 402 error!("DB error fetching user: {:?}", e); 403 + return Ok(ApiError::InternalError(None).into_response()); 404 } 405 }; 406 407 if deactivated_at.is_some() { 408 + return Ok(ApiError::AccountDeactivated.into_response()); 409 } 410 411 if let Err(e) = state 412 .backup_repo 413 + .update_backup_enabled(&user.did, input.enabled) 414 .await 415 { 416 error!("DB error updating backup_enabled: {:?}", e); 417 + return Ok( 418 + ApiError::InternalError(Some("Failed to update setting".into())).into_response(), 419 + ); 420 } 421 422 + info!(did = %user.did, enabled = input.enabled, "Updated backup_enabled setting"); 423 424 + Ok(EnabledResponse::response(input.enabled).into_response()) 425 } 426 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 { 433 Ok(Some(id)) => id, 434 Ok(None) => { 435 + return Ok(ApiError::AccountNotFound.into_response()); 436 } 437 Err(e) => { 438 error!("DB error fetching user: {:?}", e); 439 + return Ok(ApiError::InternalError(None).into_response()); 440 } 441 }; 442 ··· 444 Ok(rows) => rows, 445 Err(e) => { 446 error!("DB error fetching blobs: {:?}", e); 447 + return Ok(ApiError::InternalError(None).into_response()); 448 } 449 }; 450 451 if blobs.is_empty() { 452 + return Ok(( 453 StatusCode::OK, 454 [ 455 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 460 ], 461 Vec::<u8>::new(), 462 ) 463 + .into_response()); 464 } 465 466 let mut zip_buffer = std::io::Cursor::new(Vec::new()); ··· 538 539 if let Err(e) = zip.finish() { 540 error!("Failed to finish zip: {:?}", e); 541 + return Ok( 542 + ApiError::InternalError(Some("Failed to create zip file".into())).into_response(), 543 + ); 544 } 545 } 546 547 let zip_bytes = zip_buffer.into_inner(); 548 549 + info!(did = %user.did, blob_count = blobs.len(), size_bytes = zip_bytes.len(), "Exported blobs"); 550 551 + Ok(( 552 StatusCode::OK, 553 [ 554 (axum::http::header::CONTENT_TYPE, "application/zip"), ··· 559 ], 560 zip_bytes, 561 ) 562 + .into_response()) 563 } 564 565 fn mime_to_extension(mime_type: &str) -> &'static str {
+122 -98
crates/tranquil-pds/src/api/delegation.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::create_signed_commit; 3 - use crate::auth::BearerAuth; 4 use crate::delegation::{DelegationActionType, SCOPE_PRESETS, scopes}; 5 use crate::state::{AppState, RateLimitKind}; 6 use crate::types::{Did, Handle, Nsid, Rkey}; ··· 33 pub controllers: Vec<ControllerInfo>, 34 } 35 36 - pub async fn list_controllers(State(state): State<AppState>, auth: BearerAuth) -> Response { 37 let controllers = match state 38 .delegation_repo 39 - .get_delegations_for_account(&auth.0.did) 40 .await 41 { 42 Ok(c) => c, 43 Err(e) => { 44 tracing::error!("Failed to list controllers: {:?}", e); 45 - return ApiError::InternalError(Some("Failed to list controllers".into())) 46 - .into_response(); 47 } 48 }; 49 50 - Json(ListControllersResponse { 51 controllers: controllers 52 .into_iter() 53 .map(|c| ControllerInfo { ··· 59 }) 60 .collect(), 61 }) 62 - .into_response() 63 } 64 65 #[derive(Debug, Deserialize)] ··· 70 71 pub async fn add_controller( 72 State(state): State<AppState>, 73 - auth: BearerAuth, 74 Json(input): Json<AddControllerInput>, 75 - ) -> Response { 76 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 77 - return ApiError::InvalidScopes(e).into_response(); 78 } 79 80 let controller_exists = state ··· 86 .is_some(); 87 88 if !controller_exists { 89 - return ApiError::ControllerNotFound.into_response(); 90 } 91 92 - match state 93 - .delegation_repo 94 - .controls_any_accounts(&auth.0.did) 95 - .await 96 - { 97 Ok(true) => { 98 - return ApiError::InvalidDelegation( 99 "Cannot add controllers to an account that controls other accounts".into(), 100 ) 101 - .into_response(); 102 } 103 Err(e) => { 104 tracing::error!("Failed to check delegation status: {:?}", e); 105 - return ApiError::InternalError(Some("Failed to verify delegation status".into())) 106 - .into_response(); 107 } 108 Ok(false) => {} 109 } ··· 114 .await 115 { 116 Ok(true) => { 117 - return ApiError::InvalidDelegation( 118 "Cannot add a controlled account as a controller".into(), 119 ) 120 - .into_response(); 121 } 122 Err(e) => { 123 tracing::error!("Failed to check controller status: {:?}", e); 124 - return ApiError::InternalError(Some("Failed to verify controller status".into())) 125 - .into_response(); 126 } 127 Ok(false) => {} 128 } ··· 130 match state 131 .delegation_repo 132 .create_delegation( 133 - &auth.0.did, 134 &input.controller_did, 135 &input.granted_scopes, 136 - &auth.0.did, 137 ) 138 .await 139 { ··· 141 let _ = state 142 .delegation_repo 143 .log_delegation_action( 144 - &auth.0.did, 145 - &auth.0.did, 146 Some(&input.controller_did), 147 DelegationActionType::GrantCreated, 148 Some(serde_json::json!({ ··· 153 ) 154 .await; 155 156 - ( 157 StatusCode::OK, 158 Json(serde_json::json!({ 159 "success": true 160 })), 161 ) 162 - .into_response() 163 } 164 Err(e) => { 165 tracing::error!("Failed to add controller: {:?}", e); 166 - ApiError::InternalError(Some("Failed to add controller".into())).into_response() 167 } 168 } 169 } ··· 175 176 pub async fn remove_controller( 177 State(state): State<AppState>, 178 - auth: BearerAuth, 179 Json(input): Json<RemoveControllerInput>, 180 - ) -> Response { 181 match state 182 .delegation_repo 183 - .revoke_delegation(&auth.0.did, &input.controller_did, &auth.0.did) 184 .await 185 { 186 Ok(true) => { 187 let revoked_app_passwords = state 188 .session_repo 189 - .delete_app_passwords_by_controller(&auth.0.did, &input.controller_did) 190 .await 191 .unwrap_or(0) as usize; 192 193 let revoked_oauth_tokens = state 194 .oauth_repo 195 - .revoke_tokens_for_controller(&auth.0.did, &input.controller_did) 196 .await 197 .unwrap_or(0); 198 199 let _ = state 200 .delegation_repo 201 .log_delegation_action( 202 - &auth.0.did, 203 - &auth.0.did, 204 Some(&input.controller_did), 205 DelegationActionType::GrantRevoked, 206 Some(serde_json::json!({ ··· 212 ) 213 .await; 214 215 - ( 216 StatusCode::OK, 217 Json(serde_json::json!({ 218 "success": true 219 })), 220 ) 221 - .into_response() 222 } 223 - Ok(false) => ApiError::DelegationNotFound.into_response(), 224 Err(e) => { 225 tracing::error!("Failed to remove controller: {:?}", e); 226 - ApiError::InternalError(Some("Failed to remove controller".into())).into_response() 227 } 228 } 229 } ··· 236 237 pub async fn update_controller_scopes( 238 State(state): State<AppState>, 239 - auth: BearerAuth, 240 Json(input): Json<UpdateControllerScopesInput>, 241 - ) -> Response { 242 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 243 - return ApiError::InvalidScopes(e).into_response(); 244 } 245 246 match state 247 .delegation_repo 248 - .update_delegation_scopes(&auth.0.did, &input.controller_did, &input.granted_scopes) 249 .await 250 { 251 Ok(true) => { 252 let _ = state 253 .delegation_repo 254 .log_delegation_action( 255 - &auth.0.did, 256 - &auth.0.did, 257 Some(&input.controller_did), 258 DelegationActionType::ScopesModified, 259 Some(serde_json::json!({ ··· 264 ) 265 .await; 266 267 - ( 268 StatusCode::OK, 269 Json(serde_json::json!({ 270 "success": true 271 })), 272 ) 273 - .into_response() 274 } 275 - Ok(false) => ApiError::DelegationNotFound.into_response(), 276 Err(e) => { 277 tracing::error!("Failed to update controller scopes: {:?}", e); 278 - ApiError::InternalError(Some("Failed to update controller scopes".into())) 279 - .into_response() 280 } 281 } 282 } ··· 295 pub accounts: Vec<DelegatedAccountInfo>, 296 } 297 298 - pub async fn list_controlled_accounts(State(state): State<AppState>, auth: BearerAuth) -> Response { 299 let accounts = match state 300 .delegation_repo 301 - .get_accounts_controlled_by(&auth.0.did) 302 .await 303 { 304 Ok(a) => a, 305 Err(e) => { 306 tracing::error!("Failed to list controlled accounts: {:?}", e); 307 - return ApiError::InternalError(Some("Failed to list controlled accounts".into())) 308 - .into_response(); 309 } 310 }; 311 312 - Json(ListControlledAccountsResponse { 313 accounts: accounts 314 .into_iter() 315 .map(|a| DelegatedAccountInfo { ··· 320 }) 321 .collect(), 322 }) 323 - .into_response() 324 } 325 326 #[derive(Debug, Deserialize)] ··· 355 356 pub async fn get_audit_log( 357 State(state): State<AppState>, 358 - auth: BearerAuth, 359 Query(params): Query<AuditLogParams>, 360 - ) -> Response { 361 let limit = params.limit.clamp(1, 100); 362 let offset = params.offset.max(0); 363 364 let entries = match state 365 .delegation_repo 366 - .get_audit_log_for_account(&auth.0.did, limit, offset) 367 .await 368 { 369 Ok(e) => e, 370 Err(e) => { 371 tracing::error!("Failed to get audit log: {:?}", e); 372 - return ApiError::InternalError(Some("Failed to get audit log".into())).into_response(); 373 } 374 }; 375 376 let total = state 377 .delegation_repo 378 - .count_audit_log_entries(&auth.0.did) 379 .await 380 .unwrap_or_default(); 381 382 - Json(GetAuditLogResponse { 383 entries: entries 384 .into_iter() 385 .map(|e| AuditLogEntry { ··· 394 .collect(), 395 total, 396 }) 397 - .into_response() 398 } 399 400 #[derive(Debug, Serialize)] ··· 444 pub async fn create_delegated_account( 445 State(state): State<AppState>, 446 headers: HeaderMap, 447 - auth: BearerAuth, 448 Json(input): Json<CreateDelegatedAccountInput>, 449 - ) -> Response { 450 let client_ip = extract_client_ip(&headers); 451 if !state 452 .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 453 .await 454 { 455 warn!(ip = %client_ip, "Delegated account creation rate limit exceeded"); 456 - return ApiError::RateLimitExceeded(Some( 457 "Too many account creation attempts. Please try again later.".into(), 458 )) 459 - .into_response(); 460 } 461 462 if let Err(e) = scopes::validate_delegation_scopes(&input.controller_scopes) { 463 - return ApiError::InvalidScopes(e).into_response(); 464 } 465 466 - match state.delegation_repo.has_any_controllers(&auth.0.did).await { 467 Ok(true) => { 468 - return ApiError::InvalidDelegation( 469 "Cannot create delegated accounts from a controlled account".into(), 470 ) 471 - .into_response(); 472 } 473 Err(e) => { 474 tracing::error!("Failed to check controller status: {:?}", e); 475 - return ApiError::InternalError(Some("Failed to verify controller status".into())) 476 - .into_response(); 477 } 478 Ok(false) => {} 479 } ··· 494 match crate::api::validation::validate_short_handle(handle_to_validate) { 495 Ok(h) => format!("{}.{}", h, hostname_for_handles), 496 Err(e) => { 497 - return ApiError::InvalidRequest(e.to_string()).into_response(); 498 } 499 } 500 } else { ··· 509 if let Some(ref email) = email 510 && !crate::api::validation::is_valid_email(email) 511 { 512 - return ApiError::InvalidEmail.into_response(); 513 } 514 515 if let Some(ref code) = input.invite_code { ··· 520 .unwrap_or(false); 521 522 if !valid { 523 - return ApiError::InvalidInviteCode.into_response(); 524 } 525 } else { 526 let invite_required = std::env::var("INVITE_CODE_REQUIRED") 527 .map(|v| v == "true" || v == "1") 528 .unwrap_or(false); 529 if invite_required { 530 - return ApiError::InviteCodeRequired.into_response(); 531 } 532 } 533 ··· 542 Ok(k) => k, 543 Err(e) => { 544 error!("Error creating signing key: {:?}", e); 545 - return ApiError::InternalError(None).into_response(); 546 } 547 }; 548 ··· 558 Ok(r) => r, 559 Err(e) => { 560 error!("Error creating PLC genesis operation: {:?}", e); 561 - return ApiError::InternalError(Some("Failed to create PLC operation".into())) 562 - .into_response(); 563 } 564 }; 565 ··· 569 .await 570 { 571 error!("Failed to submit PLC genesis operation: {:?}", e); 572 - return ApiError::UpstreamErrorMsg(format!( 573 "Failed to register DID with PLC directory: {}", 574 e 575 )) 576 - .into_response(); 577 } 578 579 let did = Did::new_unchecked(&genesis_result.did); 580 let handle = Handle::new_unchecked(&handle); 581 - info!(did = %did, handle = %handle, controller = %&auth.0.did, "Created DID for delegated account"); 582 583 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 584 Ok(bytes) => bytes, 585 Err(e) => { 586 error!("Error encrypting signing key: {:?}", e); 587 - return ApiError::InternalError(None).into_response(); 588 } 589 }; 590 ··· 593 Ok(c) => c, 594 Err(e) => { 595 error!("Error persisting MST: {:?}", e); 596 - return ApiError::InternalError(None).into_response(); 597 } 598 }; 599 let rev = Tid::now(LimitedU32::MIN); ··· 602 Ok(result) => result, 603 Err(e) => { 604 error!("Error creating genesis commit: {:?}", e); 605 - return ApiError::InternalError(None).into_response(); 606 } 607 }; 608 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 609 Ok(c) => c, 610 Err(e) => { 611 error!("Error saving genesis commit: {:?}", e); 612 - return ApiError::InternalError(None).into_response(); 613 } 614 }; 615 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; ··· 618 handle: handle.clone(), 619 email: email.clone(), 620 did: did.clone(), 621 - controller_did: auth.0.did.clone(), 622 controller_scopes: input.controller_scopes.clone(), 623 encrypted_key_bytes, 624 encryption_version: crate::config::ENCRYPTION_VERSION, ··· 635 { 636 Ok(id) => id, 637 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { 638 - return ApiError::HandleNotAvailable(None).into_response(); 639 } 640 Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { 641 - return ApiError::EmailTaken.into_response(); 642 } 643 Err(e) => { 644 error!("Error creating delegated account: {:?}", e); 645 - return ApiError::InternalError(None).into_response(); 646 } 647 }; 648 ··· 678 .delegation_repo 679 .log_delegation_action( 680 &did, 681 - &auth.0.did, 682 - Some(&auth.0.did), 683 DelegationActionType::GrantCreated, 684 Some(json!({ 685 "account_created": true, ··· 690 ) 691 .await; 692 693 - info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created"); 694 695 - Json(CreateDelegatedAccountResponse { did, handle }).into_response() 696 }
··· 1 use crate::api::error::ApiError; 2 use crate::api::repo::record::utils::create_signed_commit; 3 + use crate::auth::RequiredAuth; 4 use crate::delegation::{DelegationActionType, SCOPE_PRESETS, scopes}; 5 use crate::state::{AppState, RateLimitKind}; 6 use crate::types::{Did, Handle, Nsid, Rkey}; ··· 33 pub controllers: Vec<ControllerInfo>, 34 } 35 36 + pub async fn list_controllers( 37 + State(state): State<AppState>, 38 + auth: RequiredAuth, 39 + ) -> Result<Response, ApiError> { 40 + let user = auth.0.require_user()?.require_active()?; 41 let controllers = match state 42 .delegation_repo 43 + .get_delegations_for_account(&user.did) 44 .await 45 { 46 Ok(c) => c, 47 Err(e) => { 48 tracing::error!("Failed to list controllers: {:?}", e); 49 + return Ok( 50 + ApiError::InternalError(Some("Failed to list controllers".into())).into_response(), 51 + ); 52 } 53 }; 54 55 + Ok(Json(ListControllersResponse { 56 controllers: controllers 57 .into_iter() 58 .map(|c| ControllerInfo { ··· 64 }) 65 .collect(), 66 }) 67 + .into_response()) 68 } 69 70 #[derive(Debug, Deserialize)] ··· 75 76 pub async fn add_controller( 77 State(state): State<AppState>, 78 + auth: RequiredAuth, 79 Json(input): Json<AddControllerInput>, 80 + ) -> Result<Response, ApiError> { 81 + let user = auth.0.require_user()?.require_active()?; 82 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 83 + return Ok(ApiError::InvalidScopes(e).into_response()); 84 } 85 86 let controller_exists = state ··· 92 .is_some(); 93 94 if !controller_exists { 95 + return Ok(ApiError::ControllerNotFound.into_response()); 96 } 97 98 + match state.delegation_repo.controls_any_accounts(&user.did).await { 99 Ok(true) => { 100 + return Ok(ApiError::InvalidDelegation( 101 "Cannot add controllers to an account that controls other accounts".into(), 102 ) 103 + .into_response()); 104 } 105 Err(e) => { 106 tracing::error!("Failed to check delegation status: {:?}", e); 107 + return Ok( 108 + ApiError::InternalError(Some("Failed to verify delegation status".into())) 109 + .into_response(), 110 + ); 111 } 112 Ok(false) => {} 113 } ··· 118 .await 119 { 120 Ok(true) => { 121 + return Ok(ApiError::InvalidDelegation( 122 "Cannot add a controlled account as a controller".into(), 123 ) 124 + .into_response()); 125 } 126 Err(e) => { 127 tracing::error!("Failed to check controller status: {:?}", e); 128 + return Ok( 129 + ApiError::InternalError(Some("Failed to verify controller status".into())) 130 + .into_response(), 131 + ); 132 } 133 Ok(false) => {} 134 } ··· 136 match state 137 .delegation_repo 138 .create_delegation( 139 + &user.did, 140 &input.controller_did, 141 &input.granted_scopes, 142 + &user.did, 143 ) 144 .await 145 { ··· 147 let _ = state 148 .delegation_repo 149 .log_delegation_action( 150 + &user.did, 151 + &user.did, 152 Some(&input.controller_did), 153 DelegationActionType::GrantCreated, 154 Some(serde_json::json!({ ··· 159 ) 160 .await; 161 162 + Ok(( 163 StatusCode::OK, 164 Json(serde_json::json!({ 165 "success": true 166 })), 167 ) 168 + .into_response()) 169 } 170 Err(e) => { 171 tracing::error!("Failed to add controller: {:?}", e); 172 + Ok(ApiError::InternalError(Some("Failed to add controller".into())).into_response()) 173 } 174 } 175 } ··· 181 182 pub async fn remove_controller( 183 State(state): State<AppState>, 184 + auth: RequiredAuth, 185 Json(input): Json<RemoveControllerInput>, 186 + ) -> Result<Response, ApiError> { 187 + let user = auth.0.require_user()?.require_active()?; 188 match state 189 .delegation_repo 190 + .revoke_delegation(&user.did, &input.controller_did, &user.did) 191 .await 192 { 193 Ok(true) => { 194 let revoked_app_passwords = state 195 .session_repo 196 + .delete_app_passwords_by_controller(&user.did, &input.controller_did) 197 .await 198 .unwrap_or(0) as usize; 199 200 let revoked_oauth_tokens = state 201 .oauth_repo 202 + .revoke_tokens_for_controller(&user.did, &input.controller_did) 203 .await 204 .unwrap_or(0); 205 206 let _ = state 207 .delegation_repo 208 .log_delegation_action( 209 + &user.did, 210 + &user.did, 211 Some(&input.controller_did), 212 DelegationActionType::GrantRevoked, 213 Some(serde_json::json!({ ··· 219 ) 220 .await; 221 222 + Ok(( 223 StatusCode::OK, 224 Json(serde_json::json!({ 225 "success": true 226 })), 227 ) 228 + .into_response()) 229 } 230 + Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), 231 Err(e) => { 232 tracing::error!("Failed to remove controller: {:?}", e); 233 + Ok(ApiError::InternalError(Some("Failed to remove controller".into())).into_response()) 234 } 235 } 236 } ··· 243 244 pub async fn update_controller_scopes( 245 State(state): State<AppState>, 246 + auth: RequiredAuth, 247 Json(input): Json<UpdateControllerScopesInput>, 248 + ) -> Result<Response, ApiError> { 249 + let user = auth.0.require_user()?.require_active()?; 250 if let Err(e) = scopes::validate_delegation_scopes(&input.granted_scopes) { 251 + return Ok(ApiError::InvalidScopes(e).into_response()); 252 } 253 254 match state 255 .delegation_repo 256 + .update_delegation_scopes(&user.did, &input.controller_did, &input.granted_scopes) 257 .await 258 { 259 Ok(true) => { 260 let _ = state 261 .delegation_repo 262 .log_delegation_action( 263 + &user.did, 264 + &user.did, 265 Some(&input.controller_did), 266 DelegationActionType::ScopesModified, 267 Some(serde_json::json!({ ··· 272 ) 273 .await; 274 275 + Ok(( 276 StatusCode::OK, 277 Json(serde_json::json!({ 278 "success": true 279 })), 280 ) 281 + .into_response()) 282 } 283 + Ok(false) => Ok(ApiError::DelegationNotFound.into_response()), 284 Err(e) => { 285 tracing::error!("Failed to update controller scopes: {:?}", e); 286 + Ok( 287 + ApiError::InternalError(Some("Failed to update controller scopes".into())) 288 + .into_response(), 289 + ) 290 } 291 } 292 } ··· 305 pub accounts: Vec<DelegatedAccountInfo>, 306 } 307 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()?; 313 let accounts = match state 314 .delegation_repo 315 + .get_accounts_controlled_by(&user.did) 316 .await 317 { 318 Ok(a) => a, 319 Err(e) => { 320 tracing::error!("Failed to list controlled accounts: {:?}", e); 321 + return Ok( 322 + ApiError::InternalError(Some("Failed to list controlled accounts".into())) 323 + .into_response(), 324 + ); 325 } 326 }; 327 328 + Ok(Json(ListControlledAccountsResponse { 329 accounts: accounts 330 .into_iter() 331 .map(|a| DelegatedAccountInfo { ··· 336 }) 337 .collect(), 338 }) 339 + .into_response()) 340 } 341 342 #[derive(Debug, Deserialize)] ··· 371 372 pub async fn get_audit_log( 373 State(state): State<AppState>, 374 + auth: RequiredAuth, 375 Query(params): Query<AuditLogParams>, 376 + ) -> Result<Response, ApiError> { 377 + let user = auth.0.require_user()?.require_active()?; 378 let limit = params.limit.clamp(1, 100); 379 let offset = params.offset.max(0); 380 381 let entries = match state 382 .delegation_repo 383 + .get_audit_log_for_account(&user.did, limit, offset) 384 .await 385 { 386 Ok(e) => e, 387 Err(e) => { 388 tracing::error!("Failed to get audit log: {:?}", e); 389 + return Ok( 390 + ApiError::InternalError(Some("Failed to get audit log".into())).into_response(), 391 + ); 392 } 393 }; 394 395 let total = state 396 .delegation_repo 397 + .count_audit_log_entries(&user.did) 398 .await 399 .unwrap_or_default(); 400 401 + Ok(Json(GetAuditLogResponse { 402 entries: entries 403 .into_iter() 404 .map(|e| AuditLogEntry { ··· 413 .collect(), 414 total, 415 }) 416 + .into_response()) 417 } 418 419 #[derive(Debug, Serialize)] ··· 463 pub async fn create_delegated_account( 464 State(state): State<AppState>, 465 headers: HeaderMap, 466 + auth: RequiredAuth, 467 Json(input): Json<CreateDelegatedAccountInput>, 468 + ) -> Result<Response, ApiError> { 469 + let user = auth.0.require_user()?.require_active()?; 470 let client_ip = extract_client_ip(&headers); 471 if !state 472 .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 473 .await 474 { 475 warn!(ip = %client_ip, "Delegated account creation rate limit exceeded"); 476 + return Ok(ApiError::RateLimitExceeded(Some( 477 "Too many account creation attempts. Please try again later.".into(), 478 )) 479 + .into_response()); 480 } 481 482 if let Err(e) = scopes::validate_delegation_scopes(&input.controller_scopes) { 483 + return Ok(ApiError::InvalidScopes(e).into_response()); 484 } 485 486 + match state.delegation_repo.has_any_controllers(&user.did).await { 487 Ok(true) => { 488 + return Ok(ApiError::InvalidDelegation( 489 "Cannot create delegated accounts from a controlled account".into(), 490 ) 491 + .into_response()); 492 } 493 Err(e) => { 494 tracing::error!("Failed to check controller status: {:?}", e); 495 + return Ok( 496 + ApiError::InternalError(Some("Failed to verify controller status".into())) 497 + .into_response(), 498 + ); 499 } 500 Ok(false) => {} 501 } ··· 516 match crate::api::validation::validate_short_handle(handle_to_validate) { 517 Ok(h) => format!("{}.{}", h, hostname_for_handles), 518 Err(e) => { 519 + return Ok(ApiError::InvalidRequest(e.to_string()).into_response()); 520 } 521 } 522 } else { ··· 531 if let Some(ref email) = email 532 && !crate::api::validation::is_valid_email(email) 533 { 534 + return Ok(ApiError::InvalidEmail.into_response()); 535 } 536 537 if let Some(ref code) = input.invite_code { ··· 542 .unwrap_or(false); 543 544 if !valid { 545 + return Ok(ApiError::InvalidInviteCode.into_response()); 546 } 547 } else { 548 let invite_required = std::env::var("INVITE_CODE_REQUIRED") 549 .map(|v| v == "true" || v == "1") 550 .unwrap_or(false); 551 if invite_required { 552 + return Ok(ApiError::InviteCodeRequired.into_response()); 553 } 554 } 555 ··· 564 Ok(k) => k, 565 Err(e) => { 566 error!("Error creating signing key: {:?}", e); 567 + return Ok(ApiError::InternalError(None).into_response()); 568 } 569 }; 570 ··· 580 Ok(r) => r, 581 Err(e) => { 582 error!("Error creating PLC genesis operation: {:?}", e); 583 + return Ok( 584 + ApiError::InternalError(Some("Failed to create PLC operation".into())) 585 + .into_response(), 586 + ); 587 } 588 }; 589 ··· 593 .await 594 { 595 error!("Failed to submit PLC genesis operation: {:?}", e); 596 + return Ok(ApiError::UpstreamErrorMsg(format!( 597 "Failed to register DID with PLC directory: {}", 598 e 599 )) 600 + .into_response()); 601 } 602 603 let did = Did::new_unchecked(&genesis_result.did); 604 let handle = Handle::new_unchecked(&handle); 605 + info!(did = %did, handle = %handle, controller = %&user.did, "Created DID for delegated account"); 606 607 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 608 Ok(bytes) => bytes, 609 Err(e) => { 610 error!("Error encrypting signing key: {:?}", e); 611 + return Ok(ApiError::InternalError(None).into_response()); 612 } 613 }; 614 ··· 617 Ok(c) => c, 618 Err(e) => { 619 error!("Error persisting MST: {:?}", e); 620 + return Ok(ApiError::InternalError(None).into_response()); 621 } 622 }; 623 let rev = Tid::now(LimitedU32::MIN); ··· 626 Ok(result) => result, 627 Err(e) => { 628 error!("Error creating genesis commit: {:?}", e); 629 + return Ok(ApiError::InternalError(None).into_response()); 630 } 631 }; 632 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 633 Ok(c) => c, 634 Err(e) => { 635 error!("Error saving genesis commit: {:?}", e); 636 + return Ok(ApiError::InternalError(None).into_response()); 637 } 638 }; 639 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; ··· 642 handle: handle.clone(), 643 email: email.clone(), 644 did: did.clone(), 645 + controller_did: user.did.clone(), 646 controller_scopes: input.controller_scopes.clone(), 647 encrypted_key_bytes, 648 encryption_version: crate::config::ENCRYPTION_VERSION, ··· 659 { 660 Ok(id) => id, 661 Err(tranquil_db_traits::CreateAccountError::HandleTaken) => { 662 + return Ok(ApiError::HandleNotAvailable(None).into_response()); 663 } 664 Err(tranquil_db_traits::CreateAccountError::EmailTaken) => { 665 + return Ok(ApiError::EmailTaken.into_response()); 666 } 667 Err(e) => { 668 error!("Error creating delegated account: {:?}", e); 669 + return Ok(ApiError::InternalError(None).into_response()); 670 } 671 }; 672 ··· 702 .delegation_repo 703 .log_delegation_action( 704 &did, 705 + &user.did, 706 + Some(&user.did), 707 DelegationActionType::GrantCreated, 708 Some(json!({ 709 "account_created": true, ··· 714 ) 715 .await; 716 717 + info!(did = %did, handle = %handle, controller = %&user.did, "Delegated account created"); 718 719 + Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response()) 720 }
+87 -99
crates/tranquil-pds/src/api/identity/did.rs
··· 1 use crate::api::{ApiError, DidResponse, EmptyResponse}; 2 - use crate::auth::BearerAuthAllowDeactivated; 3 use crate::plc::signing_key_to_did_key; 4 use crate::state::AppState; 5 use crate::types::Handle; ··· 518 519 pub async fn get_recommended_did_credentials( 520 State(state): State<AppState>, 521 - auth: BearerAuthAllowDeactivated, 522 - ) -> Response { 523 - let auth_user = auth.0; 524 - let handle = match state.user_repo.get_handle_by_did(&auth_user.did).await { 525 - Ok(Some(h)) => h, 526 - Ok(None) => return ApiError::InternalError(None).into_response(), 527 - Err(_) => return ApiError::InternalError(None).into_response(), 528 - }; 529 - let key_bytes = match auth_user.key_bytes { 530 - Some(kb) => kb, 531 - None => { 532 - return ApiError::AuthenticationFailed(Some( 533 - "OAuth tokens cannot get DID credentials".into(), 534 - )) 535 - .into_response(); 536 - } 537 - }; 538 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 539 let pds_endpoint = format!("https://{}", hostname); 540 - let signing_key = match k256::ecdsa::SigningKey::from_slice(&key_bytes) { 541 - Ok(k) => k, 542 - Err(_) => return ApiError::InternalError(None).into_response(), 543 - }; 544 let did_key = signing_key_to_did_key(&signing_key); 545 let rotation_keys = if auth_user.did.starts_with("did:web:") { 546 vec![] ··· 556 }; 557 vec![server_rotation_key] 558 }; 559 - ( 560 StatusCode::OK, 561 Json(GetRecommendedDidCredentialsOutput { 562 rotation_keys, ··· 570 }, 571 }), 572 ) 573 - .into_response() 574 } 575 576 #[derive(Deserialize)] ··· 580 581 pub async fn update_handle( 582 State(state): State<AppState>, 583 - auth: BearerAuthAllowDeactivated, 584 Json(input): Json<UpdateHandleInput>, 585 - ) -> Response { 586 - let auth_user = auth.0; 587 if let Err(e) = crate::auth::scope_check::check_identity_scope( 588 auth_user.is_oauth, 589 auth_user.scope.as_deref(), 590 crate::oauth::scopes::IdentityAttr::Handle, 591 ) { 592 - return e; 593 } 594 - let did = auth_user.did; 595 if !state 596 .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 597 .await 598 { 599 - return ApiError::RateLimitExceeded(Some( 600 "Too many handle updates. Try again later.".into(), 601 - )) 602 - .into_response(); 603 } 604 if !state 605 .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 606 .await 607 { 608 - return ApiError::RateLimitExceeded(Some("Daily handle update limit exceeded.".into())) 609 - .into_response(); 610 } 611 - let user_row = match state.user_repo.get_id_and_handle_by_did(&did).await { 612 - Ok(Some(row)) => row, 613 - Ok(None) => return ApiError::InternalError(None).into_response(), 614 - Err(_) => return ApiError::InternalError(None).into_response(), 615 - }; 616 let user_id = user_row.id; 617 let current_handle = user_row.handle; 618 let new_handle = input.handle.trim().to_ascii_lowercase(); 619 if new_handle.is_empty() { 620 - return ApiError::InvalidRequest("handle is required".into()).into_response(); 621 } 622 if !new_handle 623 .chars() 624 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 625 { 626 - return ApiError::InvalidHandle(Some("Handle contains invalid characters".into())) 627 - .into_response(); 628 } 629 if new_handle.split('.').any(|segment| segment.is_empty()) { 630 - return ApiError::InvalidHandle(Some("Handle contains empty segment".into())) 631 - .into_response(); 632 } 633 if new_handle 634 .split('.') 635 .any(|segment| segment.starts_with('-') || segment.ends_with('-')) 636 { 637 - return ApiError::InvalidHandle(Some( 638 "Handle segment cannot start or end with hyphen".into(), 639 - )) 640 - .into_response(); 641 } 642 if crate::moderation::has_explicit_slur(&new_handle) { 643 - return ApiError::InvalidHandle(Some("Inappropriate language in handle".into())) 644 - .into_response(); 645 } 646 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 647 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 667 { 668 warn!("Failed to sequence identity event for handle update: {}", e); 669 } 670 - return EmptyResponse::ok().into_response(); 671 } 672 if short_part.contains('.') { 673 - return ApiError::InvalidHandle(Some( 674 "Nested subdomains are not allowed. Use a simple handle without dots.".into(), 675 - )) 676 - .into_response(); 677 } 678 if short_part.len() < 3 { 679 - return ApiError::InvalidHandle(Some("Handle too short".into())).into_response(); 680 } 681 if short_part.len() > 18 { 682 - return ApiError::InvalidHandle(Some("Handle too long".into())).into_response(); 683 } 684 full_handle 685 } else { ··· 691 { 692 warn!("Failed to sequence identity event for handle update: {}", e); 693 } 694 - return EmptyResponse::ok().into_response(); 695 } 696 match crate::handle::verify_handle_ownership(&new_handle, &did).await { 697 Ok(()) => {} 698 Err(crate::handle::HandleResolutionError::NotFound) => { 699 - return ApiError::HandleNotAvailable(None).into_response(); 700 } 701 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { 702 - return ApiError::HandleNotAvailable(Some(format!( 703 "Handle points to different DID. Expected {}, got {}", 704 expected, actual 705 - ))) 706 - .into_response(); 707 } 708 Err(e) => { 709 warn!("Handle verification failed: {}", e); 710 - return ApiError::HandleNotAvailable(Some(format!( 711 "Handle verification failed: {}", 712 e 713 - ))) 714 - .into_response(); 715 } 716 } 717 new_handle.clone() 718 }; 719 - let handle_typed: Handle = match handle.parse() { 720 - Ok(h) => h, 721 - Err(_) => { 722 - return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response(); 723 - } 724 - }; 725 - let handle_exists = match state 726 .user_repo 727 .check_handle_exists(&handle_typed, user_id) 728 .await 729 - { 730 - Ok(exists) => exists, 731 - Err(_) => return ApiError::InternalError(None).into_response(), 732 - }; 733 if handle_exists { 734 - return ApiError::HandleTaken.into_response(); 735 } 736 - let result = state.user_repo.update_handle(user_id, &handle_typed).await; 737 - match result { 738 - Ok(_) => { 739 - if !current_handle.is_empty() { 740 - let _ = state 741 - .cache 742 - .delete(&format!("handle:{}", current_handle)) 743 - .await; 744 - } 745 - let _ = state.cache.delete(&format!("handle:{}", handle)).await; 746 - if let Err(e) = 747 - crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)) 748 - .await 749 - { 750 - warn!("Failed to sequence identity event for handle update: {}", e); 751 - } 752 - if let Err(e) = update_plc_handle(&state, &did, &handle_typed).await { 753 - warn!("Failed to update PLC handle: {}", e); 754 - } 755 - EmptyResponse::ok().into_response() 756 - } 757 - Err(e) => { 758 error!("DB error updating handle: {:?}", e); 759 - ApiError::InternalError(None).into_response() 760 - } 761 } 762 } 763 764 pub async fn update_plc_handle(
··· 1 use crate::api::{ApiError, DidResponse, EmptyResponse}; 2 + use crate::auth::RequiredAuth; 3 use crate::plc::signing_key_to_did_key; 4 use crate::state::AppState; 5 use crate::types::Handle; ··· 518 519 pub async fn get_recommended_did_credentials( 520 State(state): State<AppState>, 521 + auth: 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 + 535 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 536 let pds_endpoint = format!("https://{}", hostname); 537 + let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes) 538 + .map_err(|_| ApiError::InternalError(None))?; 539 let did_key = signing_key_to_did_key(&signing_key); 540 let rotation_keys = if auth_user.did.starts_with("did:web:") { 541 vec![] ··· 551 }; 552 vec![server_rotation_key] 553 }; 554 + Ok(( 555 StatusCode::OK, 556 Json(GetRecommendedDidCredentialsOutput { 557 rotation_keys, ··· 565 }, 566 }), 567 ) 568 + .into_response()) 569 } 570 571 #[derive(Deserialize)] ··· 575 576 pub async fn update_handle( 577 State(state): State<AppState>, 578 + auth: RequiredAuth, 579 Json(input): Json<UpdateHandleInput>, 580 + ) -> Result<Response, ApiError> { 581 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 582 if let Err(e) = crate::auth::scope_check::check_identity_scope( 583 auth_user.is_oauth, 584 auth_user.scope.as_deref(), 585 crate::oauth::scopes::IdentityAttr::Handle, 586 ) { 587 + return Ok(e); 588 } 589 + let did = auth_user.did.clone(); 590 if !state 591 .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 592 .await 593 { 594 + return Err(ApiError::RateLimitExceeded(Some( 595 "Too many handle updates. Try again later.".into(), 596 + ))); 597 } 598 if !state 599 .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 600 .await 601 { 602 + return Err(ApiError::RateLimitExceeded(Some( 603 + "Daily handle update limit exceeded.".into(), 604 + ))); 605 } 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))?; 612 let user_id = user_row.id; 613 let current_handle = user_row.handle; 614 let new_handle = input.handle.trim().to_ascii_lowercase(); 615 if new_handle.is_empty() { 616 + return Err(ApiError::InvalidRequest("handle is required".into())); 617 } 618 if !new_handle 619 .chars() 620 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 621 { 622 + return Err(ApiError::InvalidHandle(Some( 623 + "Handle contains invalid characters".into(), 624 + ))); 625 } 626 if new_handle.split('.').any(|segment| segment.is_empty()) { 627 + return Err(ApiError::InvalidHandle(Some( 628 + "Handle contains empty segment".into(), 629 + ))); 630 } 631 if new_handle 632 .split('.') 633 .any(|segment| segment.starts_with('-') || segment.ends_with('-')) 634 { 635 + return Err(ApiError::InvalidHandle(Some( 636 "Handle segment cannot start or end with hyphen".into(), 637 + ))); 638 } 639 if crate::moderation::has_explicit_slur(&new_handle) { 640 + return Err(ApiError::InvalidHandle(Some( 641 + "Inappropriate language in handle".into(), 642 + ))); 643 } 644 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 645 let hostname_for_handles = hostname.split(':').next().unwrap_or(&hostname); ··· 665 { 666 warn!("Failed to sequence identity event for handle update: {}", e); 667 } 668 + return Ok(EmptyResponse::ok().into_response()); 669 } 670 if short_part.contains('.') { 671 + return Err(ApiError::InvalidHandle(Some( 672 "Nested subdomains are not allowed. Use a simple handle without dots.".into(), 673 + ))); 674 } 675 if short_part.len() < 3 { 676 + return Err(ApiError::InvalidHandle(Some("Handle too short".into()))); 677 } 678 if short_part.len() > 18 { 679 + return Err(ApiError::InvalidHandle(Some("Handle too long".into()))); 680 } 681 full_handle 682 } else { ··· 688 { 689 warn!("Failed to sequence identity event for handle update: {}", e); 690 } 691 + return Ok(EmptyResponse::ok().into_response()); 692 } 693 match crate::handle::verify_handle_ownership(&new_handle, &did).await { 694 Ok(()) => {} 695 Err(crate::handle::HandleResolutionError::NotFound) => { 696 + return Err(ApiError::HandleNotAvailable(None)); 697 } 698 Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => { 699 + return Err(ApiError::HandleNotAvailable(Some(format!( 700 "Handle points to different DID. Expected {}, got {}", 701 expected, actual 702 + )))); 703 } 704 Err(e) => { 705 warn!("Handle verification failed: {}", e); 706 + return Err(ApiError::HandleNotAvailable(Some(format!( 707 "Handle verification failed: {}", 708 e 709 + )))); 710 } 711 } 712 new_handle.clone() 713 }; 714 + let handle_typed: Handle = handle 715 + .parse() 716 + .map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?; 717 + let handle_exists = state 718 .user_repo 719 .check_handle_exists(&handle_typed, user_id) 720 .await 721 + .map_err(|_| ApiError::InternalError(None))?; 722 if handle_exists { 723 + return Err(ApiError::HandleTaken); 724 } 725 + state 726 + .user_repo 727 + .update_handle(user_id, &handle_typed) 728 + .await 729 + .map_err(|e| { 730 error!("DB error updating handle: {:?}", e); 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); 748 } 749 + Ok(EmptyResponse::ok().into_response()) 750 } 751 752 pub async fn update_plc_handle(
+21 -18
crates/tranquil-pds/src/api/identity/plc/request.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuthAllowDeactivated; 4 use crate::state::AppState; 5 use axum::{ 6 extract::State, ··· 15 16 pub async fn request_plc_operation_signature( 17 State(state): State<AppState>, 18 - auth: BearerAuthAllowDeactivated, 19 - ) -> Response { 20 - let auth_user = auth.0; 21 if let Err(e) = crate::auth::scope_check::check_identity_scope( 22 auth_user.is_oauth, 23 auth_user.scope.as_deref(), 24 crate::oauth::scopes::IdentityAttr::Wildcard, 25 ) { 26 - return e; 27 } 28 - let user_id = match state.user_repo.get_id_by_did(&auth_user.did).await { 29 - Ok(Some(id)) => id, 30 - Ok(None) => return ApiError::AccountNotFound.into_response(), 31 - Err(e) => { 32 error!("DB error: {:?}", e); 33 - return ApiError::InternalError(None).into_response(); 34 - } 35 - }; 36 let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await; 37 let plc_token = generate_plc_token(); 38 let expires_at = Utc::now() + Duration::minutes(10); 39 - if let Err(e) = state 40 .infra_repo 41 .insert_plc_token(user_id, &plc_token, expires_at) 42 .await 43 - { 44 - error!("Failed to create PLC token: {:?}", e); 45 - return ApiError::InternalError(None).into_response(); 46 - } 47 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 48 if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation( 49 state.user_repo.as_ref(), ··· 60 "PLC operation signature requested for user {}", 61 auth_user.did 62 ); 63 - EmptyResponse::ok().into_response() 64 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::RequiredAuth; 4 use crate::state::AppState; 5 use axum::{ 6 extract::State, ··· 15 16 pub async fn request_plc_operation_signature( 17 State(state): State<AppState>, 18 + auth: RequiredAuth, 19 + ) -> Result<Response, ApiError> { 20 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 21 if let Err(e) = crate::auth::scope_check::check_identity_scope( 22 auth_user.is_oauth, 23 auth_user.scope.as_deref(), 24 crate::oauth::scopes::IdentityAttr::Wildcard, 25 ) { 26 + return Ok(e); 27 } 28 + let user_id = state 29 + .user_repo 30 + .get_id_by_did(&auth_user.did) 31 + .await 32 + .map_err(|e| { 33 error!("DB error: {:?}", e); 34 + ApiError::InternalError(None) 35 + })? 36 + .ok_or(ApiError::AccountNotFound)?; 37 + 38 let _ = state.infra_repo.delete_plc_tokens_for_user(user_id).await; 39 let plc_token = generate_plc_token(); 40 let expires_at = Utc::now() + Duration::minutes(10); 41 + state 42 .infra_repo 43 .insert_plc_token(user_id, &plc_token, expires_at) 44 .await 45 + .map_err(|e| { 46 + error!("Failed to create PLC token: {:?}", e); 47 + ApiError::InternalError(None) 48 + })?; 49 + 50 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 51 if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation( 52 state.user_repo.as_ref(), ··· 63 "PLC operation signature requested for user {}", 64 auth_user.did 65 ); 66 + Ok(EmptyResponse::ok().into_response()) 67 }
+68 -82
crates/tranquil-pds/src/api/identity/plc/sign.rs
··· 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuthAllowDeactivated; 3 use crate::circuit_breaker::with_circuit_breaker; 4 use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation}; 5 use crate::state::AppState; ··· 40 41 pub async fn sign_plc_operation( 42 State(state): State<AppState>, 43 - auth: BearerAuthAllowDeactivated, 44 Json(input): Json<SignPlcOperationInput>, 45 - ) -> Response { 46 - let auth_user = auth.0; 47 if let Err(e) = crate::auth::scope_check::check_identity_scope( 48 auth_user.is_oauth, 49 auth_user.scope.as_deref(), 50 crate::oauth::scopes::IdentityAttr::Wildcard, 51 ) { 52 - return e; 53 } 54 let did = &auth_user.did; 55 if did.starts_with("did:web:") { 56 - return ApiError::InvalidRequest( 57 "PLC operations are only valid for did:plc identities".into(), 58 - ) 59 - .into_response(); 60 } 61 - let token = match &input.token { 62 - Some(t) => t, 63 - None => { 64 - return ApiError::InvalidRequest( 65 - "Email confirmation token required to sign PLC operations".into(), 66 - ) 67 - .into_response(); 68 - } 69 - }; 70 - let user_id = match state.user_repo.get_id_by_did(did).await { 71 - Ok(Some(id)) => id, 72 - Ok(None) => return ApiError::AccountNotFound.into_response(), 73 - Err(e) => { 74 error!("DB error: {:?}", e); 75 - return ApiError::InternalError(None).into_response(); 76 - } 77 - }; 78 - let token_expiry = match state.infra_repo.get_plc_token_expiry(user_id, token).await { 79 - Ok(Some(expiry)) => expiry, 80 - Ok(None) => { 81 - return ApiError::InvalidToken(Some("Invalid or expired token".into())).into_response(); 82 - } 83 - Err(e) => { 84 error!("DB error: {:?}", e); 85 - return ApiError::InternalError(None).into_response(); 86 - } 87 - }; 88 if Utc::now() > token_expiry { 89 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 90 - return ApiError::ExpiredToken(Some("Token has expired".into())).into_response(); 91 } 92 - let key_row = match state.user_repo.get_user_key_by_id(user_id).await { 93 - Ok(Some(row)) => row, 94 - Ok(None) => { 95 - return ApiError::InternalError(Some("User signing key not found".into())) 96 - .into_response(); 97 - } 98 - Err(e) => { 99 error!("DB error: {:?}", e); 100 - return ApiError::InternalError(None).into_response(); 101 - } 102 - }; 103 - let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 104 - { 105 - Ok(k) => k, 106 - Err(e) => { 107 error!("Failed to decrypt user key: {}", e); 108 - return ApiError::InternalError(None).into_response(); 109 - } 110 - }; 111 - let signing_key = match SigningKey::from_slice(&key_bytes) { 112 - Ok(k) => k, 113 - Err(e) => { 114 - error!("Failed to create signing key: {:?}", e); 115 - return ApiError::InternalError(None).into_response(); 116 - } 117 - }; 118 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 119 let did_clone = did.clone(); 120 - let last_op = match with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 121 plc_client.get_last_op(&did_clone).await 122 }) 123 .await 124 - { 125 - Ok(op) => op, 126 - Err(e) => return ApiError::from(e).into_response(), 127 - }; 128 if last_op.is_tombstone() { 129 - return ApiError::from(PlcError::Tombstoned).into_response(); 130 } 131 let services = input.services.map(|s| { 132 s.into_iter() ··· 141 }) 142 .collect() 143 }); 144 - let unsigned_op = match create_update_op( 145 &last_op, 146 input.rotation_keys, 147 input.verification_methods, 148 input.also_known_as, 149 services, 150 - ) { 151 - Ok(op) => op, 152 - Err(PlcError::Tombstoned) => { 153 - return ApiError::InvalidRequest("Cannot update tombstoned DID".into()).into_response(); 154 - } 155 - Err(e) => { 156 error!("Failed to create PLC operation: {:?}", e); 157 - return ApiError::InternalError(None).into_response(); 158 } 159 - }; 160 - let signed_op = match sign_operation(&unsigned_op, &signing_key) { 161 - Ok(op) => op, 162 - Err(e) => { 163 - error!("Failed to sign PLC operation: {:?}", e); 164 - return ApiError::InternalError(None).into_response(); 165 - } 166 - }; 167 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 168 info!("Signed PLC operation for user {}", did); 169 - ( 170 StatusCode::OK, 171 Json(SignPlcOperationOutput { 172 operation: signed_op, 173 }), 174 ) 175 - .into_response() 176 }
··· 1 use crate::api::ApiError; 2 + use crate::auth::RequiredAuth; 3 use crate::circuit_breaker::with_circuit_breaker; 4 use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation}; 5 use crate::state::AppState; ··· 40 41 pub async fn sign_plc_operation( 42 State(state): State<AppState>, 43 + auth: RequiredAuth, 44 Json(input): Json<SignPlcOperationInput>, 45 + ) -> Result<Response, ApiError> { 46 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 47 if let Err(e) = crate::auth::scope_check::check_identity_scope( 48 auth_user.is_oauth, 49 auth_user.scope.as_deref(), 50 crate::oauth::scopes::IdentityAttr::Wildcard, 51 ) { 52 + return Ok(e); 53 } 54 let did = &auth_user.did; 55 if did.starts_with("did:web:") { 56 + return Err(ApiError::InvalidRequest( 57 "PLC operations are only valid for did:plc identities".into(), 58 + )); 59 } 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| { 69 error!("DB error: {:?}", 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| { 79 error!("DB error: {:?}", e); 80 + ApiError::InternalError(None) 81 + })? 82 + .ok_or_else(|| ApiError::InvalidToken(Some("Invalid or expired token".into())))?; 83 + 84 if Utc::now() > token_expiry { 85 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 86 + return Err(ApiError::ExpiredToken(Some("Token has expired".into()))); 87 } 88 + let key_row = state 89 + .user_repo 90 + .get_user_key_by_id(user_id) 91 + .await 92 + .map_err(|e| { 93 error!("DB error: {:?}", 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| { 100 error!("Failed to decrypt user key: {}", e); 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 + 109 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 110 let did_clone = did.clone(); 111 + let last_op = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 112 plc_client.get_last_op(&did_clone).await 113 }) 114 .await 115 + .map_err(ApiError::from)?; 116 + 117 if last_op.is_tombstone() { 118 + return Err(ApiError::from(PlcError::Tombstoned)); 119 } 120 let services = input.services.map(|s| { 121 s.into_iter() ··· 130 }) 131 .collect() 132 }); 133 + let unsigned_op = create_update_op( 134 &last_op, 135 input.rotation_keys, 136 input.verification_methods, 137 input.also_known_as, 138 services, 139 + ) 140 + .map_err(|e| match e { 141 + PlcError::Tombstoned => ApiError::InvalidRequest("Cannot update tombstoned DID".into()), 142 + _ => { 143 error!("Failed to create PLC operation: {:?}", e); 144 + ApiError::InternalError(None) 145 } 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 + 153 let _ = state.infra_repo.delete_plc_token(user_id, token).await; 154 info!("Signed PLC operation for user {}", did); 155 + Ok(( 156 StatusCode::OK, 157 Json(SignPlcOperationOutput { 158 operation: signed_op, 159 }), 160 ) 161 + .into_response()) 162 }
+56 -58
crates/tranquil-pds/src/api/identity/plc/submit.rs
··· 1 use crate::api::{ApiError, EmptyResponse}; 2 - use crate::auth::BearerAuthAllowDeactivated; 3 use crate::circuit_breaker::with_circuit_breaker; 4 use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; 5 use crate::state::AppState; ··· 20 21 pub async fn submit_plc_operation( 22 State(state): State<AppState>, 23 - auth: BearerAuthAllowDeactivated, 24 Json(input): Json<SubmitPlcOperationInput>, 25 - ) -> Response { 26 - let auth_user = auth.0; 27 if let Err(e) = crate::auth::scope_check::check_identity_scope( 28 auth_user.is_oauth, 29 auth_user.scope.as_deref(), 30 crate::oauth::scopes::IdentityAttr::Wildcard, 31 ) { 32 - return e; 33 } 34 let did = &auth_user.did; 35 if did.starts_with("did:web:") { 36 - return ApiError::InvalidRequest( 37 "PLC operations are only valid for did:plc identities".into(), 38 - ) 39 - .into_response(); 40 } 41 - if let Err(e) = validate_plc_operation(&input.operation) { 42 - return ApiError::InvalidRequest(format!("Invalid operation: {}", e)).into_response(); 43 - } 44 let op = &input.operation; 45 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 46 let public_url = format!("https://{}", hostname); 47 - let user = match state.user_repo.get_id_and_handle_by_did(did).await { 48 - Ok(Some(u)) => u, 49 - Ok(None) => return ApiError::AccountNotFound.into_response(), 50 - Err(e) => { 51 error!("DB error: {:?}", e); 52 - return ApiError::InternalError(None).into_response(); 53 - } 54 - }; 55 - let key_row = match state.user_repo.get_user_key_by_id(user.id).await { 56 - Ok(Some(row)) => row, 57 - Ok(None) => { 58 - return ApiError::InternalError(Some("User signing key not found".into())) 59 - .into_response(); 60 - } 61 - Err(e) => { 62 error!("DB error: {:?}", e); 63 - return ApiError::InternalError(None).into_response(); 64 - } 65 - }; 66 - let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 67 - { 68 - Ok(k) => k, 69 - Err(e) => { 70 error!("Failed to decrypt user key: {}", e); 71 - return ApiError::InternalError(None).into_response(); 72 - } 73 - }; 74 - let signing_key = match SigningKey::from_slice(&key_bytes) { 75 - Ok(k) => k, 76 - Err(e) => { 77 - error!("Failed to create signing key: {:?}", e); 78 - return ApiError::InternalError(None).into_response(); 79 - } 80 - }; 81 let user_did_key = signing_key_to_did_key(&signing_key); 82 let server_rotation_key = 83 std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); ··· 86 .iter() 87 .any(|k| k.as_str() == Some(&server_rotation_key)); 88 if !has_server_key { 89 - return ApiError::InvalidRequest( 90 "Rotation keys do not include server's rotation key".into(), 91 - ) 92 - .into_response(); 93 } 94 } 95 if let Some(services) = op.get("services").and_then(|v| v.as_object()) ··· 98 let service_type = pds.get("type").and_then(|v| v.as_str()); 99 let endpoint = pds.get("endpoint").and_then(|v| v.as_str()); 100 if service_type != Some("AtprotoPersonalDataServer") { 101 - return ApiError::InvalidRequest("Incorrect type on atproto_pds service".into()) 102 - .into_response(); 103 } 104 if endpoint != Some(&public_url) { 105 - return ApiError::InvalidRequest("Incorrect endpoint on atproto_pds service".into()) 106 - .into_response(); 107 } 108 } 109 if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object()) 110 && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 111 && atproto_key != user_did_key 112 { 113 - return ApiError::InvalidRequest("Incorrect signing key in verificationMethods".into()) 114 - .into_response(); 115 } 116 if let Some(also_known_as) = (!user.handle.is_empty()) 117 .then(|| op.get("alsoKnownAs").and_then(|v| v.as_array())) ··· 120 let expected_handle = format!("at://{}", user.handle); 121 let first_aka = also_known_as.first().and_then(|v| v.as_str()); 122 if first_aka != Some(&expected_handle) { 123 - return ApiError::InvalidRequest("Incorrect handle in alsoKnownAs".into()) 124 - .into_response(); 125 } 126 } 127 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 128 let operation_clone = input.operation.clone(); 129 let did_clone = did.clone(); 130 - if let Err(e) = with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 131 plc_client 132 .send_operation(&did_clone, &operation_clone) 133 .await 134 }) 135 .await 136 - { 137 - return ApiError::from(e).into_response(); 138 - } 139 match state 140 .repo_repo 141 .insert_identity_event(did, Some(&user.handle)) ··· 157 warn!(did = %did, "Failed to refresh DID cache after PLC update"); 158 } 159 info!(did = %did, "PLC operation submitted successfully"); 160 - EmptyResponse::ok().into_response() 161 }
··· 1 use crate::api::{ApiError, EmptyResponse}; 2 + use crate::auth::RequiredAuth; 3 use crate::circuit_breaker::with_circuit_breaker; 4 use crate::plc::{PlcClient, signing_key_to_did_key, validate_plc_operation}; 5 use crate::state::AppState; ··· 20 21 pub async fn submit_plc_operation( 22 State(state): State<AppState>, 23 + auth: RequiredAuth, 24 Json(input): Json<SubmitPlcOperationInput>, 25 + ) -> Result<Response, ApiError> { 26 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 27 if let Err(e) = crate::auth::scope_check::check_identity_scope( 28 auth_user.is_oauth, 29 auth_user.scope.as_deref(), 30 crate::oauth::scopes::IdentityAttr::Wildcard, 31 ) { 32 + return Ok(e); 33 } 34 let did = &auth_user.did; 35 if did.starts_with("did:web:") { 36 + return Err(ApiError::InvalidRequest( 37 "PLC operations are only valid for did:plc identities".into(), 38 + )); 39 } 40 + validate_plc_operation(&input.operation) 41 + .map_err(|e| ApiError::InvalidRequest(format!("Invalid operation: {}", e)))?; 42 + 43 let op = &input.operation; 44 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 45 let public_url = format!("https://{}", hostname); 46 + let user = state 47 + .user_repo 48 + .get_id_and_handle_by_did(did) 49 + .await 50 + .map_err(|e| { 51 error!("DB error: {:?}", 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| { 61 error!("DB error: {:?}", 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| { 68 error!("Failed to decrypt user key: {}", e); 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 + 77 let user_did_key = signing_key_to_did_key(&signing_key); 78 let server_rotation_key = 79 std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); ··· 82 .iter() 83 .any(|k| k.as_str() == Some(&server_rotation_key)); 84 if !has_server_key { 85 + return Err(ApiError::InvalidRequest( 86 "Rotation keys do not include server's rotation key".into(), 87 + )); 88 } 89 } 90 if let Some(services) = op.get("services").and_then(|v| v.as_object()) ··· 93 let service_type = pds.get("type").and_then(|v| v.as_str()); 94 let endpoint = pds.get("endpoint").and_then(|v| v.as_str()); 95 if service_type != Some("AtprotoPersonalDataServer") { 96 + return Err(ApiError::InvalidRequest( 97 + "Incorrect type on atproto_pds service".into(), 98 + )); 99 } 100 if endpoint != Some(&public_url) { 101 + return Err(ApiError::InvalidRequest( 102 + "Incorrect endpoint on atproto_pds service".into(), 103 + )); 104 } 105 } 106 if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object()) 107 && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 108 && atproto_key != user_did_key 109 { 110 + return Err(ApiError::InvalidRequest( 111 + "Incorrect signing key in verificationMethods".into(), 112 + )); 113 } 114 if let Some(also_known_as) = (!user.handle.is_empty()) 115 .then(|| op.get("alsoKnownAs").and_then(|v| v.as_array())) ··· 118 let expected_handle = format!("at://{}", user.handle); 119 let first_aka = also_known_as.first().and_then(|v| v.as_str()); 120 if first_aka != Some(&expected_handle) { 121 + return Err(ApiError::InvalidRequest( 122 + "Incorrect handle in alsoKnownAs".into(), 123 + )); 124 } 125 } 126 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone())); 127 let operation_clone = input.operation.clone(); 128 let did_clone = did.clone(); 129 + with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 130 plc_client 131 .send_operation(&did_clone, &operation_clone) 132 .await 133 }) 134 .await 135 + .map_err(ApiError::from)?; 136 + 137 match state 138 .repo_repo 139 .insert_identity_event(did, Some(&user.handle)) ··· 155 warn!(did = %did, "Failed to refresh DID cache after PLC update"); 156 } 157 info!(did = %did, "PLC operation submitted successfully"); 158 + Ok(EmptyResponse::ok().into_response()) 159 }
+8 -5
crates/tranquil-pds/src/api/moderation/mod.rs
··· 1 use crate::api::ApiError; 2 use crate::api::proxy_client::{is_ssrf_safe, proxy_client}; 3 - use crate::auth::extractor::BearerAuthAllowTakendown; 4 use crate::state::AppState; 5 use axum::{ 6 Json, ··· 42 43 pub async fn create_report( 44 State(state): State<AppState>, 45 - auth: BearerAuthAllowTakendown, 46 Json(input): Json<CreateReportInput>, 47 ) -> Response { 48 - let auth_user = auth.0; 49 let did = &auth_user.did; 50 51 if let Some((service_url, service_did)) = get_report_service_config() { 52 - return proxy_to_report_service(&state, &auth_user, &service_url, &service_did, &input) 53 .await; 54 } 55 56 - create_report_locally(&state, did, auth_user.is_takendown(), input).await 57 } 58 59 async fn proxy_to_report_service(
··· 1 use crate::api::ApiError; 2 use crate::api::proxy_client::{is_ssrf_safe, proxy_client}; 3 + use crate::auth::RequiredAuth; 4 use crate::state::AppState; 5 use axum::{ 6 Json, ··· 42 43 pub async fn create_report( 44 State(state): State<AppState>, 45 + auth: RequiredAuth, 46 Json(input): Json<CreateReportInput>, 47 ) -> Response { 48 + let auth_user = match auth.0.require_user() { 49 + Ok(u) => u, 50 + Err(e) => return e.into_response(), 51 + }; 52 let did = &auth_user.did; 53 54 if let Some((service_url, service_did)) = get_report_service_config() { 55 + return proxy_to_report_service(&state, auth_user, &service_url, &service_did, &input) 56 .await; 57 } 58 59 + create_report_locally(&state, did, auth_user.status.is_takendown(), input).await 60 } 61 62 async fn proxy_to_report_service(
+74 -85
crates/tranquil-pds/src/api/notification_prefs.rs
··· 1 use crate::api::error::ApiError; 2 - use crate::auth::BearerAuth; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 23 pub signal_verified: bool, 24 } 25 26 - pub async fn get_notification_prefs(State(state): State<AppState>, auth: BearerAuth) -> Response { 27 - let user = auth.0; 28 - let prefs = match state.user_repo.get_notification_prefs(&user.did).await { 29 - Ok(Some(p)) => p, 30 - Ok(None) => return ApiError::AccountNotFound.into_response(), 31 - Err(e) => { 32 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 33 - } 34 - }; 35 - Json(NotificationPrefsResponse { 36 preferred_channel: prefs.preferred_channel, 37 email: prefs.email, 38 discord_id: prefs.discord_id, ··· 42 signal_number: prefs.signal_number, 43 signal_verified: prefs.signal_verified, 44 }) 45 - .into_response() 46 } 47 48 #[derive(Serialize)] ··· 62 pub notifications: Vec<NotificationHistoryEntry>, 63 } 64 65 - pub async fn get_notification_history(State(state): State<AppState>, auth: BearerAuth) -> Response { 66 - let user = auth.0; 67 68 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&user.did).await { 69 - Ok(Some(id)) => id, 70 - Ok(None) => return ApiError::AccountNotFound.into_response(), 71 - Err(e) => { 72 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 73 - } 74 - }; 75 76 - let rows = match state.infra_repo.get_notification_history(user_id, 50).await { 77 - Ok(r) => r, 78 - Err(e) => { 79 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 80 - } 81 - }; 82 83 let sensitive_types = [ 84 "email_verification", ··· 111 }) 112 .collect(); 113 114 - Json(GetNotificationHistoryResponse { notifications }).into_response() 115 } 116 117 #[derive(Deserialize)] ··· 184 185 pub async fn update_notification_prefs( 186 State(state): State<AppState>, 187 - auth: BearerAuth, 188 Json(input): Json<UpdateNotificationPrefsInput>, 189 - ) -> Response { 190 - let user = auth.0; 191 192 - let user_row = match state.user_repo.get_id_handle_email_by_did(&user.did).await { 193 - Ok(Some(row)) => row, 194 - Ok(None) => return ApiError::AccountNotFound.into_response(), 195 - Err(e) => { 196 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 197 - } 198 - }; 199 200 let user_id = user_row.id; 201 let handle = user_row.handle; ··· 206 if let Some(ref channel) = input.preferred_channel { 207 let valid_channels = ["email", "discord", "telegram", "signal"]; 208 if !valid_channels.contains(&channel.as_str()) { 209 - return ApiError::InvalidRequest( 210 "Invalid channel. Must be one of: email, discord, telegram, signal".into(), 211 - ) 212 - .into_response(); 213 } 214 - if let Err(e) = state 215 .user_repo 216 .update_preferred_comms_channel(&user.did, channel) 217 .await 218 - { 219 - return ApiError::InternalError(Some(format!("Database error: {}", e))).into_response(); 220 - } 221 info!(did = %user.did, channel = %channel, "Updated preferred notification channel"); 222 } 223 224 if let Some(ref new_email) = input.email { 225 let email_clean = new_email.trim().to_lowercase(); 226 if email_clean.is_empty() { 227 - return ApiError::InvalidRequest("Email cannot be empty".into()).into_response(); 228 } 229 230 if !crate::api::validation::is_valid_email(&email_clean) { 231 - return ApiError::InvalidEmail.into_response(); 232 } 233 234 - if current_email.as_ref().map(|e| e.to_lowercase()) == Some(email_clean.clone()) { 235 - info!(did = %user.did, "Email unchanged, skipping"); 236 - } else { 237 - if let Err(e) = request_channel_verification( 238 &state, 239 user_id, 240 &user.did, ··· 243 Some(&handle), 244 ) 245 .await 246 - { 247 - return ApiError::InternalError(Some(e)).into_response(); 248 - } 249 verification_required.push("email".to_string()); 250 info!(did = %user.did, "Requested email verification"); 251 } ··· 253 254 if let Some(ref discord_id) = input.discord_id { 255 if discord_id.is_empty() { 256 - if let Err(e) = state.user_repo.clear_discord(user_id).await { 257 - return ApiError::InternalError(Some(format!("Database error: {}", e))) 258 - .into_response(); 259 - } 260 info!(did = %user.did, "Cleared Discord ID"); 261 } else { 262 - if let Err(e) = request_channel_verification( 263 - &state, user_id, &user.did, "discord", discord_id, None, 264 - ) 265 - .await 266 - { 267 - return ApiError::InternalError(Some(e)).into_response(); 268 - } 269 verification_required.push("discord".to_string()); 270 info!(did = %user.did, "Requested Discord verification"); 271 } ··· 274 if let Some(ref telegram) = input.telegram_username { 275 let telegram_clean = telegram.trim_start_matches('@'); 276 if telegram_clean.is_empty() { 277 - if let Err(e) = state.user_repo.clear_telegram(user_id).await { 278 - return ApiError::InternalError(Some(format!("Database error: {}", e))) 279 - .into_response(); 280 - } 281 info!(did = %user.did, "Cleared Telegram username"); 282 } else { 283 - if let Err(e) = request_channel_verification( 284 &state, 285 user_id, 286 &user.did, ··· 289 None, 290 ) 291 .await 292 - { 293 - return ApiError::InternalError(Some(e)).into_response(); 294 - } 295 verification_required.push("telegram".to_string()); 296 info!(did = %user.did, "Requested Telegram verification"); 297 } ··· 299 300 if let Some(ref signal) = input.signal_number { 301 if signal.is_empty() { 302 - if let Err(e) = state.user_repo.clear_signal(user_id).await { 303 - return ApiError::InternalError(Some(format!("Database error: {}", e))) 304 - .into_response(); 305 - } 306 info!(did = %user.did, "Cleared Signal number"); 307 } else { 308 - if let Err(e) = 309 - request_channel_verification(&state, user_id, &user.did, "signal", signal, None) 310 - .await 311 - { 312 - return ApiError::InternalError(Some(e)).into_response(); 313 - } 314 verification_required.push("signal".to_string()); 315 info!(did = %user.did, "Requested Signal verification"); 316 } 317 } 318 319 - Json(UpdateNotificationPrefsResponse { 320 success: true, 321 verification_required, 322 }) 323 - .into_response() 324 }
··· 1 use crate::api::error::ApiError; 2 + use crate::auth::RequiredAuth; 3 use crate::state::AppState; 4 use axum::{ 5 Json, ··· 23 pub signal_verified: bool, 24 } 25 26 + pub async fn get_notification_prefs( 27 + State(state): State<AppState>, 28 + auth: 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 { 38 preferred_channel: prefs.preferred_channel, 39 email: prefs.email, 40 discord_id: prefs.discord_id, ··· 44 signal_number: prefs.signal_number, 45 signal_verified: prefs.signal_verified, 46 }) 47 + .into_response()) 48 } 49 50 #[derive(Serialize)] ··· 64 pub notifications: Vec<NotificationHistoryEntry>, 65 } 66 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()?; 72 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)?; 79 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))))?; 85 86 let sensitive_types = [ 87 "email_verification", ··· 114 }) 115 .collect(); 116 117 + Ok(Json(GetNotificationHistoryResponse { notifications }).into_response()) 118 } 119 120 #[derive(Deserialize)] ··· 187 188 pub async fn update_notification_prefs( 189 State(state): State<AppState>, 190 + auth: RequiredAuth, 191 Json(input): Json<UpdateNotificationPrefsInput>, 192 + ) -> Result<Response, ApiError> { 193 + let user = auth.0.require_user()?.require_active()?; 194 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)?; 201 202 let user_id = user_row.id; 203 let handle = user_row.handle; ··· 208 if let Some(ref channel) = input.preferred_channel { 209 let valid_channels = ["email", "discord", "telegram", "signal"]; 210 if !valid_channels.contains(&channel.as_str()) { 211 + return Err(ApiError::InvalidRequest( 212 "Invalid channel. Must be one of: email, discord, telegram, signal".into(), 213 + )); 214 } 215 + state 216 .user_repo 217 .update_preferred_comms_channel(&user.did, channel) 218 .await 219 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 220 info!(did = %user.did, channel = %channel, "Updated preferred notification channel"); 221 } 222 223 if let Some(ref new_email) = input.email { 224 let email_clean = new_email.trim().to_lowercase(); 225 if email_clean.is_empty() { 226 + return Err(ApiError::InvalidRequest("Email cannot be empty".into())); 227 } 228 229 if !crate::api::validation::is_valid_email(&email_clean) { 230 + return Err(ApiError::InvalidEmail); 231 } 232 233 + if current_email.as_ref().map(|e| e.to_lowercase()) != Some(email_clean.clone()) { 234 + request_channel_verification( 235 &state, 236 user_id, 237 &user.did, ··· 240 Some(&handle), 241 ) 242 .await 243 + .map_err(|e| ApiError::InternalError(Some(e)))?; 244 verification_required.push("email".to_string()); 245 info!(did = %user.did, "Requested email verification"); 246 } ··· 248 249 if let Some(ref discord_id) = input.discord_id { 250 if discord_id.is_empty() { 251 + state 252 + .user_repo 253 + .clear_discord(user_id) 254 + .await 255 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 256 info!(did = %user.did, "Cleared Discord ID"); 257 } else { 258 + request_channel_verification(&state, user_id, &user.did, "discord", discord_id, None) 259 + .await 260 + .map_err(|e| ApiError::InternalError(Some(e)))?; 261 verification_required.push("discord".to_string()); 262 info!(did = %user.did, "Requested Discord verification"); 263 } ··· 266 if let Some(ref telegram) = input.telegram_username { 267 let telegram_clean = telegram.trim_start_matches('@'); 268 if telegram_clean.is_empty() { 269 + state 270 + .user_repo 271 + .clear_telegram(user_id) 272 + .await 273 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 274 info!(did = %user.did, "Cleared Telegram username"); 275 } else { 276 + request_channel_verification( 277 &state, 278 user_id, 279 &user.did, ··· 282 None, 283 ) 284 .await 285 + .map_err(|e| ApiError::InternalError(Some(e)))?; 286 verification_required.push("telegram".to_string()); 287 info!(did = %user.did, "Requested Telegram verification"); 288 } ··· 290 291 if let Some(ref signal) = input.signal_number { 292 if signal.is_empty() { 293 + state 294 + .user_repo 295 + .clear_signal(user_id) 296 + .await 297 + .map_err(|e| ApiError::InternalError(Some(format!("Database error: {}", e))))?; 298 info!(did = %user.did, "Cleared Signal number"); 299 } else { 300 + request_channel_verification(&state, user_id, &user.did, "signal", signal, None) 301 + .await 302 + .map_err(|e| ApiError::InternalError(Some(e)))?; 303 verification_required.push("signal".to_string()); 304 info!(did = %user.did, "Requested Signal verification"); 305 } 306 } 307 308 + Ok(Json(UpdateNotificationPrefsResponse { 309 success: true, 310 verification_required, 311 }) 312 + .into_response()) 313 }
+129 -130
crates/tranquil-pds/src/api/repo/import.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 use crate::api::repo::record::create_signed_commit; 4 - use crate::auth::BearerAuthAllowDeactivated; 5 use crate::state::AppState; 6 use crate::sync::import::{ImportError, apply_import, parse_car}; 7 use crate::sync::verify::CarVerifier; ··· 23 24 pub async fn import_repo( 25 State(state): State<AppState>, 26 - auth: BearerAuthAllowDeactivated, 27 body: Bytes, 28 - ) -> Response { 29 let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS") 30 .map(|v| v != "false" && v != "0") 31 .unwrap_or(true); 32 if !accepting_imports { 33 - return ApiError::InvalidRequest("Service is not accepting repo imports".into()) 34 - .into_response(); 35 } 36 let max_size: usize = std::env::var("MAX_IMPORT_SIZE") 37 .ok() 38 .and_then(|s| s.parse().ok()) 39 .unwrap_or(DEFAULT_MAX_IMPORT_SIZE); 40 if body.len() > max_size { 41 - return ApiError::PayloadTooLarge(format!( 42 "Import size exceeds limit of {} bytes", 43 max_size 44 - )) 45 - .into_response(); 46 } 47 - let auth_user = auth.0; 48 let did = &auth_user.did; 49 - let user = match state.user_repo.get_by_did(did).await { 50 - Ok(Some(row)) => row, 51 - Ok(None) => { 52 - return ApiError::AccountNotFound.into_response(); 53 - } 54 - Err(e) => { 55 error!("DB error fetching user: {:?}", e); 56 - return ApiError::InternalError(None).into_response(); 57 - } 58 - }; 59 if user.takedown_ref.is_some() { 60 - return ApiError::AccountTakedown.into_response(); 61 } 62 let user_id = user.id; 63 let (root, blocks) = match parse_car(&body).await { 64 Ok((r, b)) => (r, b), 65 Err(ImportError::InvalidRootCount) => { 66 - return ApiError::InvalidRequest("Expected exactly one root in CAR file".into()) 67 - .into_response(); 68 } 69 Err(ImportError::CarParse(msg)) => { 70 - return ApiError::InvalidRequest(format!("Failed to parse CAR file: {}", msg)) 71 - .into_response(); 72 } 73 Err(e) => { 74 error!("CAR parsing error: {:?}", e); 75 - return ApiError::InvalidRequest(format!("Invalid CAR file: {}", e)).into_response(); 76 } 77 }; 78 info!( ··· 82 root 83 ); 84 let Some(root_block) = blocks.get(&root) else { 85 - return ApiError::InvalidRequest("Root block not found in CAR file".into()).into_response(); 86 }; 87 let commit_did = match jacquard_repo::commit::Commit::from_cbor(root_block) { 88 Ok(commit) => commit.did().to_string(), 89 Err(e) => { 90 - return ApiError::InvalidRequest(format!("Invalid commit: {}", e)).into_response(); 91 } 92 }; 93 if commit_did != *did { 94 - return ApiError::InvalidRepo(format!( 95 "CAR file is for DID {} but you are authenticated as {}", 96 commit_did, did 97 - )) 98 - .into_response(); 99 } 100 let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION") 101 .map(|v| v == "true" || v == "1") ··· 117 commit_did, 118 expected_did, 119 }) => { 120 - return ApiError::InvalidRepo(format!( 121 "CAR file is for DID {} but you are authenticated as {}", 122 commit_did, expected_did 123 - )) 124 - .into_response(); 125 } 126 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 127 - return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 128 - .into_response(); 129 } 130 Err(e) => { 131 error!("CAR structure verification error: {:?}", e); 132 - return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) 133 - .into_response(); 134 } 135 } 136 } else { ··· 147 commit_did, 148 expected_did, 149 }) => { 150 - return ApiError::InvalidRepo(format!( 151 "CAR file is for DID {} but you are authenticated as {}", 152 commit_did, expected_did 153 - )) 154 - .into_response(); 155 } 156 Err(crate::sync::verify::VerifyError::InvalidSignature) => { 157 - return ApiError::InvalidRequest( 158 "CAR file commit signature verification failed".into(), 159 - ) 160 - .into_response(); 161 } 162 Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => { 163 warn!("DID resolution failed during import verification: {}", msg); 164 - return ApiError::InvalidRequest(format!("Failed to verify DID: {}", msg)) 165 - .into_response(); 166 } 167 Err(crate::sync::verify::VerifyError::NoSigningKey) => { 168 - return ApiError::InvalidRequest( 169 "DID document does not contain a signing key".into(), 170 - ) 171 - .into_response(); 172 } 173 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 174 - return ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 175 - .into_response(); 176 } 177 Err(e) => { 178 error!("CAR verification error: {:?}", e); 179 - return ApiError::InvalidRequest(format!("CAR verification failed: {}", e)) 180 - .into_response(); 181 } 182 } 183 } ··· 227 } 228 } 229 } 230 - let key_row = match state.user_repo.get_user_with_key_by_did(did).await { 231 - Ok(Some(row)) => row, 232 - Ok(None) => { 233 error!("No signing key found for user {}", did); 234 - return ApiError::InternalError(Some("Signing key not found".into())) 235 - .into_response(); 236 - } 237 - Err(e) => { 238 - error!("DB error fetching signing key: {:?}", e); 239 - return ApiError::InternalError(None).into_response(); 240 - } 241 - }; 242 let key_bytes = 243 - match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) { 244 - Ok(k) => k, 245 - Err(e) => { 246 error!("Failed to decrypt signing key: {}", e); 247 - return ApiError::InternalError(None).into_response(); 248 - } 249 - }; 250 - let signing_key = match SigningKey::from_slice(&key_bytes) { 251 - Ok(k) => k, 252 - Err(e) => { 253 - error!("Invalid signing key: {:?}", e); 254 - return ApiError::InternalError(None).into_response(); 255 - } 256 - }; 257 let new_rev = Tid::now(LimitedU32::MIN); 258 let new_rev_str = new_rev.to_string(); 259 - let (commit_bytes, _sig) = match create_signed_commit( 260 did, 261 import_result.data_cid, 262 &new_rev_str, 263 None, 264 &signing_key, 265 - ) { 266 - Ok(result) => result, 267 - Err(e) => { 268 - error!("Failed to create new commit: {}", e); 269 - return ApiError::InternalError(None).into_response(); 270 - } 271 - }; 272 - let new_root_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 273 - Ok(cid) => cid, 274 - Err(e) => { 275 error!("Failed to store new commit block: {:?}", e); 276 - return ApiError::InternalError(None).into_response(); 277 - } 278 - }; 279 let new_root_cid_link = CidLink::new_unchecked(new_root_cid.to_string()); 280 - if let Err(e) = state 281 .repo_repo 282 .update_repo_root(user_id, &new_root_cid_link, &new_rev_str) 283 .await 284 - { 285 - error!("Failed to update repo root: {:?}", e); 286 - return ApiError::InternalError(None).into_response(); 287 - } 288 let mut all_block_cids: Vec<Vec<u8>> = blocks.keys().map(|c| c.to_bytes()).collect(); 289 all_block_cids.push(new_root_cid.to_bytes()); 290 - if let Err(e) = state 291 .repo_repo 292 .insert_user_blocks(user_id, &all_block_cids, &new_rev_str) 293 .await 294 - { 295 - error!("Failed to insert user_blocks: {:?}", e); 296 - return ApiError::InternalError(None).into_response(); 297 - } 298 let new_root_str = new_root_cid.to_string(); 299 info!( 300 "Created new commit for imported repo: cid={}, rev={}", ··· 324 ); 325 } 326 } 327 - EmptyResponse::ok().into_response() 328 } 329 - Err(ImportError::SizeLimitExceeded) => { 330 - ApiError::PayloadTooLarge(format!("Import exceeds block limit of {}", max_blocks)) 331 - .into_response() 332 - } 333 - Err(ImportError::RepoNotFound) => { 334 - ApiError::RepoNotFound(Some("Repository not initialized for this account".into())) 335 - .into_response() 336 - } 337 - Err(ImportError::InvalidCbor(msg)) => { 338 - ApiError::InvalidRequest(format!("Invalid CBOR data: {}", msg)).into_response() 339 - } 340 - Err(ImportError::InvalidCommit(msg)) => { 341 - ApiError::InvalidRequest(format!("Invalid commit structure: {}", msg)).into_response() 342 - } 343 - Err(ImportError::BlockNotFound(cid)) => { 344 - ApiError::InvalidRequest(format!("Referenced block not found in CAR: {}", cid)) 345 - .into_response() 346 - } 347 - Err(ImportError::ConcurrentModification) => ApiError::InvalidSwap(Some( 348 "Repository is being modified by another operation, please retry".into(), 349 - )) 350 - .into_response(), 351 - Err(ImportError::VerificationFailed(ve)) => { 352 - ApiError::InvalidRequest(format!("CAR verification failed: {}", ve)).into_response() 353 - } 354 - Err(ImportError::DidMismatch { car_did, auth_did }) => ApiError::InvalidRequest(format!( 355 - "CAR is for {} but authenticated as {}", 356 - car_did, auth_did 357 - )) 358 - .into_response(), 359 Err(e) => { 360 error!("Import error: {:?}", e); 361 - ApiError::InternalError(None).into_response() 362 } 363 } 364 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 use crate::api::repo::record::create_signed_commit; 4 + use crate::auth::RequiredAuth; 5 use crate::state::AppState; 6 use crate::sync::import::{ImportError, apply_import, parse_car}; 7 use crate::sync::verify::CarVerifier; ··· 23 24 pub async fn import_repo( 25 State(state): State<AppState>, 26 + auth: RequiredAuth, 27 body: Bytes, 28 + ) -> Result<Response, ApiError> { 29 let accepting_imports = std::env::var("ACCEPTING_REPO_IMPORTS") 30 .map(|v| v != "false" && v != "0") 31 .unwrap_or(true); 32 if !accepting_imports { 33 + return Err(ApiError::InvalidRequest( 34 + "Service is not accepting repo imports".into(), 35 + )); 36 } 37 let max_size: usize = std::env::var("MAX_IMPORT_SIZE") 38 .ok() 39 .and_then(|s| s.parse().ok()) 40 .unwrap_or(DEFAULT_MAX_IMPORT_SIZE); 41 if body.len() > max_size { 42 + return Err(ApiError::PayloadTooLarge(format!( 43 "Import size exceeds limit of {} bytes", 44 max_size 45 + ))); 46 } 47 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 48 let did = &auth_user.did; 49 + let user = state 50 + .user_repo 51 + .get_by_did(did) 52 + .await 53 + .map_err(|e| { 54 error!("DB error fetching user: {:?}", e); 55 + ApiError::InternalError(None) 56 + })? 57 + .ok_or(ApiError::AccountNotFound)?; 58 if user.takedown_ref.is_some() { 59 + return Err(ApiError::AccountTakedown); 60 } 61 let user_id = user.id; 62 let (root, blocks) = match parse_car(&body).await { 63 Ok((r, b)) => (r, b), 64 Err(ImportError::InvalidRootCount) => { 65 + return Err(ApiError::InvalidRequest( 66 + "Expected exactly one root in CAR file".into(), 67 + )); 68 } 69 Err(ImportError::CarParse(msg)) => { 70 + return Err(ApiError::InvalidRequest(format!( 71 + "Failed to parse CAR file: {}", 72 + msg 73 + ))); 74 } 75 Err(e) => { 76 error!("CAR parsing error: {:?}", e); 77 + return Err(ApiError::InvalidRequest(format!("Invalid CAR file: {}", e))); 78 } 79 }; 80 info!( ··· 84 root 85 ); 86 let Some(root_block) = blocks.get(&root) else { 87 + return Err(ApiError::InvalidRequest( 88 + "Root block not found in CAR file".into(), 89 + )); 90 }; 91 let commit_did = match jacquard_repo::commit::Commit::from_cbor(root_block) { 92 Ok(commit) => commit.did().to_string(), 93 Err(e) => { 94 + return Err(ApiError::InvalidRequest(format!("Invalid commit: {}", e))); 95 } 96 }; 97 if commit_did != *did { 98 + return Err(ApiError::InvalidRepo(format!( 99 "CAR file is for DID {} but you are authenticated as {}", 100 commit_did, did 101 + ))); 102 } 103 let skip_verification = std::env::var("SKIP_IMPORT_VERIFICATION") 104 .map(|v| v == "true" || v == "1") ··· 120 commit_did, 121 expected_did, 122 }) => { 123 + return Err(ApiError::InvalidRepo(format!( 124 "CAR file is for DID {} but you are authenticated as {}", 125 commit_did, expected_did 126 + ))); 127 } 128 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 129 + return Err(ApiError::InvalidRequest(format!( 130 + "MST validation failed: {}", 131 + msg 132 + ))); 133 } 134 Err(e) => { 135 error!("CAR structure verification error: {:?}", e); 136 + return Err(ApiError::InvalidRequest(format!( 137 + "CAR verification failed: {}", 138 + e 139 + ))); 140 } 141 } 142 } else { ··· 153 commit_did, 154 expected_did, 155 }) => { 156 + return Err(ApiError::InvalidRepo(format!( 157 "CAR file is for DID {} but you are authenticated as {}", 158 commit_did, expected_did 159 + ))); 160 } 161 Err(crate::sync::verify::VerifyError::InvalidSignature) => { 162 + return Err(ApiError::InvalidRequest( 163 "CAR file commit signature verification failed".into(), 164 + )); 165 } 166 Err(crate::sync::verify::VerifyError::DidResolutionFailed(msg)) => { 167 warn!("DID resolution failed during import verification: {}", msg); 168 + return Err(ApiError::InvalidRequest(format!( 169 + "Failed to verify DID: {}", 170 + msg 171 + ))); 172 } 173 Err(crate::sync::verify::VerifyError::NoSigningKey) => { 174 + return Err(ApiError::InvalidRequest( 175 "DID document does not contain a signing key".into(), 176 + )); 177 } 178 Err(crate::sync::verify::VerifyError::MstValidationFailed(msg)) => { 179 + return Err(ApiError::InvalidRequest(format!( 180 + "MST validation failed: {}", 181 + msg 182 + ))); 183 } 184 Err(e) => { 185 error!("CAR verification error: {:?}", e); 186 + return Err(ApiError::InvalidRequest(format!( 187 + "CAR verification failed: {}", 188 + e 189 + ))); 190 } 191 } 192 } ··· 236 } 237 } 238 } 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(|| { 248 error!("No signing key found for user {}", did); 249 + ApiError::InternalError(Some("Signing key not found".into())) 250 + })?; 251 let key_bytes = 252 + crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) 253 + .map_err(|e| { 254 error!("Failed to decrypt signing key: {}", e); 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 + })?; 261 let new_rev = Tid::now(LimitedU32::MIN); 262 let new_rev_str = new_rev.to_string(); 263 + let (commit_bytes, _sig) = create_signed_commit( 264 did, 265 import_result.data_cid, 266 &new_rev_str, 267 None, 268 &signing_key, 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| { 276 error!("Failed to store new commit block: {:?}", e); 277 + ApiError::InternalError(None) 278 + })?; 279 let new_root_cid_link = CidLink::new_unchecked(new_root_cid.to_string()); 280 + state 281 .repo_repo 282 .update_repo_root(user_id, &new_root_cid_link, &new_rev_str) 283 .await 284 + .map_err(|e| { 285 + error!("Failed to update repo root: {:?}", e); 286 + ApiError::InternalError(None) 287 + })?; 288 let mut all_block_cids: Vec<Vec<u8>> = blocks.keys().map(|c| c.to_bytes()).collect(); 289 all_block_cids.push(new_root_cid.to_bytes()); 290 + state 291 .repo_repo 292 .insert_user_blocks(user_id, &all_block_cids, &new_rev_str) 293 .await 294 + .map_err(|e| { 295 + error!("Failed to insert user_blocks: {:?}", e); 296 + ApiError::InternalError(None) 297 + })?; 298 let new_root_str = new_root_cid.to_string(); 299 info!( 300 "Created new commit for imported repo: cid={}, rev={}", ··· 324 ); 325 } 326 } 327 + Ok(EmptyResponse::ok().into_response()) 328 } 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( 349 "Repository is being modified by another operation, please retry".into(), 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 + )), 358 Err(e) => { 359 error!("Import error: {:?}", e); 360 + Err(ApiError::InternalError(None)) 361 } 362 } 363 }
+61 -62
crates/tranquil-pds/src/api/repo/record/batch.rs
··· 1 use super::validation::validate_record_with_status; 2 use crate::api::error::ApiError; 3 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; 4 - use crate::auth::BearerAuth; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 262 263 pub async fn apply_writes( 264 State(state): State<AppState>, 265 - auth: BearerAuth, 266 Json(input): Json<ApplyWritesInput>, 267 - ) -> Response { 268 info!( 269 "apply_writes called: repo={}, writes={}", 270 input.repo, 271 input.writes.len() 272 ); 273 - let auth_user = auth.0; 274 let did = auth_user.did.clone(); 275 let is_oauth = auth_user.is_oauth; 276 - let scope = auth_user.scope; 277 let controller_did = auth_user.controller_did.clone(); 278 if input.repo.as_str() != did { 279 - return ApiError::InvalidRepo("Repo does not match authenticated user".into()) 280 - .into_response(); 281 } 282 if state 283 .user_repo ··· 285 .await 286 .unwrap_or(false) 287 { 288 - return ApiError::AccountMigrated.into_response(); 289 } 290 let is_verified = state 291 .user_repo ··· 298 .await 299 .unwrap_or(false); 300 if !is_verified && !is_delegated { 301 - return ApiError::AccountNotVerified.into_response(); 302 } 303 if input.writes.is_empty() { 304 - return ApiError::InvalidRequest("writes array is empty".into()).into_response(); 305 } 306 if input.writes.len() > MAX_BATCH_WRITES { 307 - return ApiError::InvalidRequest(format!("Too many writes (max {})", MAX_BATCH_WRITES)) 308 - .into_response(); 309 } 310 311 let has_custom_scope = scope ··· 374 }) 375 .next() 376 { 377 - return err; 378 } 379 } 380 381 - let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&did).await { 382 - Ok(Some(id)) => id, 383 - _ => return ApiError::InternalError(Some("User not found".into())).into_response(), 384 - }; 385 - let root_cid_str = match state.repo_repo.get_repo_root_cid_by_user_id(user_id).await { 386 - Ok(Some(cid_str)) => cid_str, 387 - _ => return ApiError::InternalError(Some("Repo root not found".into())).into_response(), 388 - }; 389 - let current_root_cid = match Cid::from_str(&root_cid_str) { 390 - Ok(c) => c, 391 - Err(_) => { 392 - return ApiError::InternalError(Some("Invalid repo root CID".into())).into_response(); 393 - } 394 - }; 395 if let Some(swap_commit) = &input.swap_commit 396 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 397 { 398 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 399 } 400 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 401 - let commit_bytes = match tracking_store.get(&current_root_cid).await { 402 - Ok(Some(b)) => b, 403 - _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(), 404 - }; 405 - let commit = match Commit::from_cbor(&commit_bytes) { 406 - Ok(c) => c, 407 - _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(), 408 - }; 409 let original_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 410 let initial_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 411 let WriteAccumulator { ··· 424 .await 425 { 426 Ok(acc) => acc, 427 - Err(response) => return response, 428 }; 429 - let new_mst_root = match mst.persist().await { 430 - Ok(c) => c, 431 - Err(_) => { 432 - return ApiError::InternalError(Some("Failed to persist MST".into())).into_response(); 433 - } 434 - }; 435 let (new_mst_blocks, old_mst_blocks) = { 436 let mut new_blocks = std::collections::BTreeMap::new(); 437 let mut old_blocks = std::collections::BTreeMap::new(); 438 for key in &modified_keys { 439 - if mst.blocks_for_path(key, &mut new_blocks).await.is_err() { 440 - return ApiError::InternalError(Some( 441 - "Failed to get new MST blocks for path".into(), 442 - )) 443 - .into_response(); 444 - } 445 - if original_mst 446 .blocks_for_path(key, &mut old_blocks) 447 .await 448 - .is_err() 449 - { 450 - return ApiError::InternalError(Some( 451 - "Failed to get old MST blocks for path".into(), 452 - )) 453 - .into_response(); 454 - } 455 } 456 (new_blocks, old_blocks) 457 }; ··· 503 { 504 Ok(res) => res, 505 Err(e) if e.contains("ConcurrentModification") => { 506 - return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response(); 507 } 508 Err(e) => { 509 error!("Commit failed: {}", e); 510 - return ApiError::InternalError(Some("Failed to commit changes".into())) 511 - .into_response(); 512 } 513 }; 514 ··· 557 .await; 558 } 559 560 - ( 561 StatusCode::OK, 562 Json(ApplyWritesOutput { 563 commit: CommitInfo { ··· 567 results, 568 }), 569 ) 570 - .into_response() 571 }
··· 1 use super::validation::validate_record_with_status; 2 use crate::api::error::ApiError; 3 use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log, extract_blob_cids}; 4 + use crate::auth::RequiredAuth; 5 use crate::delegation::DelegationActionType; 6 use crate::repo::tracking::TrackingBlockStore; 7 use crate::state::AppState; ··· 262 263 pub async fn apply_writes( 264 State(state): State<AppState>, 265 + auth: RequiredAuth, 266 Json(input): Json<ApplyWritesInput>, 267 + ) -> Result<Response, ApiError> { 268 info!( 269 "apply_writes called: repo={}, writes={}", 270 input.repo, 271 input.writes.len() 272 ); 273 + let auth_user = auth.0.require_user()?.require_active()?; 274 let did = auth_user.did.clone(); 275 let is_oauth = auth_user.is_oauth; 276 + let scope = auth_user.scope.clone(); 277 let controller_did = auth_user.controller_did.clone(); 278 if input.repo.as_str() != did { 279 + return Err(ApiError::InvalidRepo( 280 + "Repo does not match authenticated user".into(), 281 + )); 282 } 283 if state 284 .user_repo ··· 286 .await 287 .unwrap_or(false) 288 { 289 + return Err(ApiError::AccountMigrated); 290 } 291 let is_verified = state 292 .user_repo ··· 299 .await 300 .unwrap_or(false); 301 if !is_verified && !is_delegated { 302 + return Err(ApiError::AccountNotVerified); 303 } 304 if input.writes.is_empty() { 305 + return Err(ApiError::InvalidRequest("writes array is empty".into())); 306 } 307 if input.writes.len() > MAX_BATCH_WRITES { 308 + return Err(ApiError::InvalidRequest(format!( 309 + "Too many writes (max {})", 310 + MAX_BATCH_WRITES 311 + ))); 312 } 313 314 let has_custom_scope = scope ··· 377 }) 378 .next() 379 { 380 + return Ok(err); 381 } 382 } 383 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())))?; 400 if let Some(swap_commit) = &input.swap_commit 401 && Cid::from_str(swap_commit).ok() != Some(current_root_cid) 402 { 403 + return Err(ApiError::InvalidSwap(Some("Repo has been modified".into()))); 404 } 405 let tracking_store = TrackingBlockStore::new(state.block_store.clone()); 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())))?; 414 let original_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 415 let initial_mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None); 416 let WriteAccumulator { ··· 429 .await 430 { 431 Ok(acc) => acc, 432 + Err(response) => return Ok(response), 433 }; 434 + let new_mst_root = mst 435 + .persist() 436 + .await 437 + .map_err(|_| ApiError::InternalError(Some("Failed to persist MST".into())))?; 438 let (new_mst_blocks, old_mst_blocks) = { 439 let mut new_blocks = std::collections::BTreeMap::new(); 440 let mut old_blocks = std::collections::BTreeMap::new(); 441 for key in &modified_keys { 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 448 .blocks_for_path(key, &mut old_blocks) 449 .await 450 + .map_err(|_| { 451 + ApiError::InternalError(Some("Failed to get old MST blocks for path".into())) 452 + })?; 453 } 454 (new_blocks, old_blocks) 455 }; ··· 501 { 502 Ok(res) => res, 503 Err(e) if e.contains("ConcurrentModification") => { 504 + return Err(ApiError::InvalidSwap(Some("Repo has been modified".into()))); 505 } 506 Err(e) => { 507 error!("Commit failed: {}", e); 508 + return Err(ApiError::InternalError(Some( 509 + "Failed to commit changes".into(), 510 + ))); 511 } 512 }; 513 ··· 556 .await; 557 } 558 559 + Ok(( 560 StatusCode::OK, 561 Json(ApplyWritesOutput { 562 commit: CommitInfo { ··· 566 results, 567 }), 568 ) 569 + .into_response()) 570 }
+124 -117
crates/tranquil-pds/src/api/server/app_password.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::{BearerAuth, generate_app_password}; 4 use crate::delegation::{DelegationActionType, intersect_scopes}; 5 use crate::state::{AppState, RateLimitKind}; 6 use axum::{ ··· 33 34 pub async fn list_app_passwords( 35 State(state): State<AppState>, 36 - BearerAuth(auth_user): BearerAuth, 37 - ) -> Response { 38 - let user = match state.user_repo.get_by_did(&auth_user.did).await { 39 - Ok(Some(u)) => u, 40 - Ok(None) => return ApiError::AccountNotFound.into_response(), 41 - Err(e) => { 42 error!("DB error getting user: {:?}", e); 43 - return ApiError::InternalError(None).into_response(); 44 - } 45 - }; 46 47 - match state.session_repo.list_app_passwords(user.id).await { 48 - Ok(rows) => { 49 - let passwords: Vec<AppPassword> = rows 50 - .iter() 51 - .map(|row| AppPassword { 52 - name: row.name.clone(), 53 - created_at: row.created_at.to_rfc3339(), 54 - privileged: row.privileged, 55 - scopes: row.scopes.clone(), 56 - created_by_controller: row 57 - .created_by_controller_did 58 - .as_ref() 59 - .map(|d| d.to_string()), 60 - }) 61 - .collect(); 62 - Json(ListAppPasswordsOutput { passwords }).into_response() 63 - } 64 - Err(e) => { 65 error!("DB error listing app passwords: {:?}", e); 66 - ApiError::InternalError(None).into_response() 67 - } 68 - } 69 } 70 71 #[derive(Deserialize)] ··· 89 pub async fn create_app_password( 90 State(state): State<AppState>, 91 headers: HeaderMap, 92 - BearerAuth(auth_user): BearerAuth, 93 Json(input): Json<CreateAppPasswordInput>, 94 - ) -> Response { 95 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 96 if !state 97 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 98 .await 99 { 100 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 101 - return ApiError::RateLimitExceeded(None).into_response(); 102 } 103 104 - let user = match state.user_repo.get_by_did(&auth_user.did).await { 105 - Ok(Some(u)) => u, 106 - Ok(None) => return ApiError::AccountNotFound.into_response(), 107 - Err(e) => { 108 error!("DB error getting user: {:?}", e); 109 - return ApiError::InternalError(None).into_response(); 110 - } 111 - }; 112 113 let name = input.name.trim(); 114 if name.is_empty() { 115 - return ApiError::InvalidRequest("name is required".into()).into_response(); 116 } 117 118 - match state 119 .session_repo 120 .get_app_password_by_name(user.id, name) 121 .await 122 - { 123 - Ok(Some(_)) => return ApiError::DuplicateAppPassword.into_response(), 124 - Err(e) => { 125 error!("DB error checking app password: {:?}", e); 126 - return ApiError::InternalError(None).into_response(); 127 - } 128 - Ok(None) => {} 129 } 130 131 let (final_scopes, controller_did) = if let Some(ref controller) = auth_user.controller_did { ··· 141 let intersected = intersect_scopes(requested, &granted_scopes); 142 143 if intersected.is_empty() && !granted_scopes.is_empty() { 144 - return ApiError::InsufficientScope(None).into_response(); 145 } 146 147 let scope_result = if intersected.is_empty() { ··· 157 let password = generate_app_password(); 158 159 let password_clone = password.clone(); 160 - let password_hash = match tokio::task::spawn_blocking(move || { 161 - bcrypt::hash(&password_clone, bcrypt::DEFAULT_COST) 162 - }) 163 - .await 164 - { 165 - Ok(Ok(h)) => h, 166 - Ok(Err(e)) => { 167 - error!("Failed to hash password: {:?}", e); 168 - return ApiError::InternalError(None).into_response(); 169 - } 170 - Err(e) => { 171 - error!("Failed to spawn blocking task: {:?}", e); 172 - return ApiError::InternalError(None).into_response(); 173 - } 174 - }; 175 176 let privileged = input.privileged.unwrap_or(false); 177 let created_at = chrono::Utc::now(); ··· 185 created_by_controller_did: controller_did.clone(), 186 }; 187 188 - match state.session_repo.create_app_password(&create_data).await { 189 - Ok(_) => { 190 - if let Some(ref controller) = controller_did { 191 - let _ = state 192 - .delegation_repo 193 - .log_delegation_action( 194 - &auth_user.did, 195 - controller, 196 - Some(controller), 197 - DelegationActionType::AccountAction, 198 - Some(json!({ 199 - "action": "create_app_password", 200 - "name": name, 201 - "scopes": final_scopes 202 - })), 203 - None, 204 - None, 205 - ) 206 - .await; 207 - } 208 - Json(CreateAppPasswordOutput { 209 - name: name.to_string(), 210 - password, 211 - created_at: created_at.to_rfc3339(), 212 - privileged, 213 - scopes: final_scopes, 214 - }) 215 - .into_response() 216 - } 217 - Err(e) => { 218 error!("DB error creating app password: {:?}", e); 219 - ApiError::InternalError(None).into_response() 220 - } 221 } 222 } 223 224 #[derive(Deserialize)] ··· 228 229 pub async fn revoke_app_password( 230 State(state): State<AppState>, 231 - BearerAuth(auth_user): BearerAuth, 232 Json(input): Json<RevokeAppPasswordInput>, 233 - ) -> Response { 234 - let user = match state.user_repo.get_by_did(&auth_user.did).await { 235 - Ok(Some(u)) => u, 236 - Ok(None) => return ApiError::AccountNotFound.into_response(), 237 - Err(e) => { 238 error!("DB error getting user: {:?}", e); 239 - return ApiError::InternalError(None).into_response(); 240 - } 241 - }; 242 243 let name = input.name.trim(); 244 if name.is_empty() { 245 - return ApiError::InvalidRequest("name is required".into()).into_response(); 246 } 247 248 let sessions_to_invalidate = state ··· 251 .await 252 .unwrap_or_default(); 253 254 - if let Err(e) = state 255 .session_repo 256 .delete_sessions_by_app_password(&auth_user.did, name) 257 .await 258 - { 259 - error!("DB error revoking sessions for app password: {:?}", e); 260 - return ApiError::InternalError(None).into_response(); 261 - } 262 263 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 264 let cache_key = format!("auth:session:{}:{}", &auth_user.did, jti); ··· 269 })) 270 .await; 271 272 - if let Err(e) = state.session_repo.delete_app_password(user.id, name).await { 273 - error!("DB error revoking app password: {:?}", e); 274 - return ApiError::InternalError(None).into_response(); 275 - } 276 277 - EmptyResponse::ok().into_response() 278 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::{RequiredAuth, generate_app_password}; 4 use crate::delegation::{DelegationActionType, intersect_scopes}; 5 use crate::state::{AppState, RateLimitKind}; 6 use axum::{ ··· 33 34 pub async fn list_app_passwords( 35 State(state): State<AppState>, 36 + auth: 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| { 44 error!("DB error getting user: {:?}", e); 45 + ApiError::InternalError(None) 46 + })? 47 + .ok_or(ApiError::AccountNotFound)?; 48 49 + let rows = state 50 + .session_repo 51 + .list_app_passwords(user.id) 52 + .await 53 + .map_err(|e| { 54 error!("DB error listing app passwords: {:?}", e); 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()) 71 } 72 73 #[derive(Deserialize)] ··· 91 pub async fn create_app_password( 92 State(state): State<AppState>, 93 headers: HeaderMap, 94 + auth: RequiredAuth, 95 Json(input): Json<CreateAppPasswordInput>, 96 + ) -> Result<Response, ApiError> { 97 + let auth_user = auth.0.require_user()?.require_active()?; 98 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 99 if !state 100 .check_rate_limit(RateLimitKind::AppPassword, &client_ip) 101 .await 102 { 103 warn!(ip = %client_ip, "App password creation rate limit exceeded"); 104 + return Err(ApiError::RateLimitExceeded(None)); 105 } 106 107 + let user = state 108 + .user_repo 109 + .get_by_did(&auth_user.did) 110 + .await 111 + .map_err(|e| { 112 error!("DB error getting user: {:?}", e); 113 + ApiError::InternalError(None) 114 + })? 115 + .ok_or(ApiError::AccountNotFound)?; 116 117 let name = input.name.trim(); 118 if name.is_empty() { 119 + return Err(ApiError::InvalidRequest("name is required".into())); 120 } 121 122 + if state 123 .session_repo 124 .get_app_password_by_name(user.id, name) 125 .await 126 + .map_err(|e| { 127 error!("DB error checking app password: {:?}", e); 128 + ApiError::InternalError(None) 129 + })? 130 + .is_some() 131 + { 132 + return Err(ApiError::DuplicateAppPassword); 133 } 134 135 let (final_scopes, controller_did) = if let Some(ref controller) = auth_user.controller_did { ··· 145 let intersected = intersect_scopes(requested, &granted_scopes); 146 147 if intersected.is_empty() && !granted_scopes.is_empty() { 148 + return Err(ApiError::InsufficientScope(None)); 149 } 150 151 let scope_result = if intersected.is_empty() { ··· 161 let password = generate_app_password(); 162 163 let password_clone = password.clone(); 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 176 let privileged = input.privileged.unwrap_or(false); 177 let created_at = chrono::Utc::now(); ··· 185 created_by_controller_did: controller_did.clone(), 186 }; 187 188 + state 189 + .session_repo 190 + .create_app_password(&create_data) 191 + .await 192 + .map_err(|e| { 193 error!("DB error creating app password: {:?}", e); 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; 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()) 223 } 224 225 #[derive(Deserialize)] ··· 229 230 pub async fn revoke_app_password( 231 State(state): State<AppState>, 232 + auth: RequiredAuth, 233 Json(input): Json<RevokeAppPasswordInput>, 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| { 241 error!("DB error getting user: {:?}", e); 242 + ApiError::InternalError(None) 243 + })? 244 + .ok_or(ApiError::AccountNotFound)?; 245 246 let name = input.name.trim(); 247 if name.is_empty() { 248 + return Err(ApiError::InvalidRequest("name is required".into())); 249 } 250 251 let sessions_to_invalidate = state ··· 254 .await 255 .unwrap_or_default(); 256 257 + state 258 .session_repo 259 .delete_sessions_by_app_password(&auth_user.did, name) 260 .await 261 + .map_err(|e| { 262 + error!("DB error revoking sessions for app password: {:?}", e); 263 + ApiError::InternalError(None) 264 + })?; 265 266 futures::future::join_all(sessions_to_invalidate.iter().map(|jti| { 267 let cache_key = format!("auth:session:{}:{}", &auth_user.did, jti); ··· 272 })) 273 .await; 274 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 + })?; 283 284 + Ok(EmptyResponse::ok().into_response()) 285 }
+93 -85
crates/tranquil-pds/src/api/server/email.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 3 - use crate::auth::BearerAuth; 4 use crate::state::{AppState, RateLimitKind}; 5 use axum::{ 6 Json, ··· 45 pub async fn request_email_update( 46 State(state): State<AppState>, 47 headers: axum::http::HeaderMap, 48 - auth: BearerAuth, 49 input: Option<Json<RequestEmailUpdateInput>>, 50 - ) -> Response { 51 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 52 if !state 53 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 54 .await 55 { 56 warn!(ip = %client_ip, "Email update rate limit exceeded"); 57 - return ApiError::RateLimitExceeded(None).into_response(); 58 } 59 60 if let Err(e) = crate::auth::scope_check::check_account_scope( 61 - auth.0.is_oauth, 62 - auth.0.scope.as_deref(), 63 crate::oauth::scopes::AccountAttr::Email, 64 crate::oauth::scopes::AccountAction::Manage, 65 ) { 66 - return e; 67 } 68 69 - let user = match state.user_repo.get_email_info_by_did(&auth.0.did).await { 70 - Ok(Some(row)) => row, 71 - Ok(None) => { 72 - return ApiError::AccountNotFound.into_response(); 73 - } 74 - Err(e) => { 75 error!("DB error: {:?}", e); 76 - return ApiError::InternalError(None).into_response(); 77 - } 78 - }; 79 80 let Some(current_email) = user.email else { 81 - return ApiError::InvalidRequest("account does not have an email address".into()) 82 - .into_response(); 83 }; 84 85 let token_required = user.email_verified; 86 87 if token_required { 88 let code = crate::auth::verification_token::generate_channel_update_token( 89 - &auth.0.did, 90 "email_update", 91 &current_email.to_lowercase(), 92 ); ··· 103 authorized: false, 104 }; 105 if let Ok(json) = serde_json::to_string(&pending) { 106 - let cache_key = email_update_cache_key(&auth.0.did); 107 if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await { 108 warn!("Failed to cache pending email update: {:?}", e); 109 } ··· 127 } 128 129 info!("Email update requested for user {}", user.id); 130 - TokenRequiredResponse::response(token_required).into_response() 131 } 132 133 #[derive(Deserialize)] ··· 140 pub async fn confirm_email( 141 State(state): State<AppState>, 142 headers: axum::http::HeaderMap, 143 - auth: BearerAuth, 144 Json(input): Json<ConfirmEmailInput>, 145 - ) -> Response { 146 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 147 if !state 148 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 149 .await 150 { 151 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 152 - return ApiError::RateLimitExceeded(None).into_response(); 153 } 154 155 if let Err(e) = crate::auth::scope_check::check_account_scope( 156 - auth.0.is_oauth, 157 - auth.0.scope.as_deref(), 158 crate::oauth::scopes::AccountAttr::Email, 159 crate::oauth::scopes::AccountAction::Manage, 160 ) { 161 - return e; 162 } 163 164 - let did = &auth.0.did; 165 - let user = match state.user_repo.get_email_info_by_did(did).await { 166 - Ok(Some(row)) => row, 167 - Ok(None) => { 168 - return ApiError::AccountNotFound.into_response(); 169 - } 170 - Err(e) => { 171 error!("DB error: {:?}", e); 172 - return ApiError::InternalError(None).into_response(); 173 - } 174 - }; 175 176 let Some(ref email) = user.email else { 177 - return ApiError::InvalidEmail.into_response(); 178 }; 179 let current_email = email.to_lowercase(); 180 181 let provided_email = input.email.trim().to_lowercase(); 182 if provided_email != current_email { 183 - return ApiError::InvalidEmail.into_response(); 184 } 185 186 if user.email_verified { 187 - return EmptyResponse::ok().into_response(); 188 } 189 190 let confirmation_code = ··· 199 match verified { 200 Ok(token_data) => { 201 if token_data.did != did.as_str() { 202 - return ApiError::InvalidToken(None).into_response(); 203 } 204 } 205 Err(crate::auth::verification_token::VerifyError::Expired) => { 206 - return ApiError::ExpiredToken(None).into_response(); 207 } 208 Err(_) => { 209 - return ApiError::InvalidToken(None).into_response(); 210 } 211 } 212 213 - if let Err(e) = state.user_repo.set_email_verified(user.id, true).await { 214 - error!("DB error confirming email: {:?}", e); 215 - return ApiError::InternalError(None).into_response(); 216 - } 217 218 info!("Email confirmed for user {}", user.id); 219 - EmptyResponse::ok().into_response() 220 } 221 222 #[derive(Deserialize)] ··· 230 231 pub async fn update_email( 232 State(state): State<AppState>, 233 - auth: BearerAuth, 234 Json(input): Json<UpdateEmailInput>, 235 - ) -> Response { 236 - let auth_user = auth.0; 237 238 if let Err(e) = crate::auth::scope_check::check_account_scope( 239 auth_user.is_oauth, ··· 241 crate::oauth::scopes::AccountAttr::Email, 242 crate::oauth::scopes::AccountAction::Manage, 243 ) { 244 - return e; 245 } 246 247 let did = &auth_user.did; 248 - let user = match state.user_repo.get_email_info_by_did(did).await { 249 - Ok(Some(row)) => row, 250 - Ok(None) => { 251 - return ApiError::AccountNotFound.into_response(); 252 - } 253 - Err(e) => { 254 error!("DB error: {:?}", e); 255 - return ApiError::InternalError(None).into_response(); 256 - } 257 - }; 258 259 let user_id = user.id; 260 let current_email = user.email.clone(); ··· 262 let new_email = input.email.trim().to_lowercase(); 263 264 if !crate::api::validation::is_valid_email(&new_email) { 265 - return ApiError::InvalidRequest( 266 "This email address is not supported, please use a different email.".into(), 267 - ) 268 - .into_response(); 269 } 270 271 if let Some(ref current) = current_email 272 && new_email == current.to_lowercase() 273 { 274 - return EmptyResponse::ok().into_response(); 275 } 276 277 if email_verified { ··· 290 291 if !authorized_via_link { 292 let Some(ref t) = input.token else { 293 - return ApiError::TokenRequired.into_response(); 294 }; 295 let confirmation_token = 296 crate::auth::verification_token::normalize_token_input(t.trim()); ··· 309 match verified { 310 Ok(token_data) => { 311 if token_data.did != did.as_str() { 312 - return ApiError::InvalidToken(None).into_response(); 313 } 314 } 315 Err(crate::auth::verification_token::VerifyError::Expired) => { 316 - return ApiError::ExpiredToken(None).into_response(); 317 } 318 Err(_) => { 319 - return ApiError::InvalidToken(None).into_response(); 320 } 321 } 322 } 323 } 324 325 - if let Err(e) = state.user_repo.update_email(user_id, &new_email).await { 326 - error!("DB error updating email: {:?}", e); 327 - return ApiError::InternalError(None).into_response(); 328 - } 329 330 let verification_token = 331 crate::auth::verification_token::generate_signup_token(did, "email", &new_email); ··· 358 } 359 360 info!("Email updated for user {}", user_id); 361 - EmptyResponse::ok().into_response() 362 } 363 364 #[derive(Deserialize)] ··· 497 pub async fn check_email_update_status( 498 State(state): State<AppState>, 499 headers: axum::http::HeaderMap, 500 - auth: BearerAuth, 501 - ) -> Response { 502 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 503 if !state 504 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 505 .await 506 { 507 - return ApiError::RateLimitExceeded(None).into_response(); 508 } 509 510 if let Err(e) = crate::auth::scope_check::check_account_scope( 511 - auth.0.is_oauth, 512 - auth.0.scope.as_deref(), 513 crate::oauth::scopes::AccountAttr::Email, 514 crate::oauth::scopes::AccountAction::Read, 515 ) { 516 - return e; 517 } 518 519 - let cache_key = email_update_cache_key(&auth.0.did); 520 let pending_json = match state.cache.get(&cache_key).await { 521 Some(json) => json, 522 None => { 523 - return Json(json!({ "pending": false, "authorized": false })).into_response(); 524 } 525 }; 526 527 let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) { 528 Ok(p) => p, 529 Err(_) => { 530 - return Json(json!({ "pending": false, "authorized": false })).into_response(); 531 } 532 }; 533 534 - Json(json!({ 535 "pending": true, 536 "authorized": pending.authorized, 537 "newEmail": pending.new_email, 538 })) 539 - .into_response() 540 } 541 542 #[derive(Deserialize)]
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, TokenRequiredResponse, VerifiedResponse}; 3 + use crate::auth::RequiredAuth; 4 use crate::state::{AppState, RateLimitKind}; 5 use axum::{ 6 Json, ··· 45 pub async fn request_email_update( 46 State(state): State<AppState>, 47 headers: axum::http::HeaderMap, 48 + auth: RequiredAuth, 49 input: Option<Json<RequestEmailUpdateInput>>, 50 + ) -> Result<Response, ApiError> { 51 + let auth_user = auth.0.require_user()?.require_active()?; 52 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 53 if !state 54 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 55 .await 56 { 57 warn!(ip = %client_ip, "Email update rate limit exceeded"); 58 + return Err(ApiError::RateLimitExceeded(None)); 59 } 60 61 if let Err(e) = crate::auth::scope_check::check_account_scope( 62 + auth_user.is_oauth, 63 + auth_user.scope.as_deref(), 64 crate::oauth::scopes::AccountAttr::Email, 65 crate::oauth::scopes::AccountAction::Manage, 66 ) { 67 + return Ok(e); 68 } 69 70 + let user = state 71 + .user_repo 72 + .get_email_info_by_did(&auth_user.did) 73 + .await 74 + .map_err(|e| { 75 error!("DB error: {:?}", e); 76 + ApiError::InternalError(None) 77 + })? 78 + .ok_or(ApiError::AccountNotFound)?; 79 80 let Some(current_email) = user.email else { 81 + return Err(ApiError::InvalidRequest( 82 + "account does not have an email address".into(), 83 + )); 84 }; 85 86 let token_required = user.email_verified; 87 88 if token_required { 89 let code = crate::auth::verification_token::generate_channel_update_token( 90 + &auth_user.did, 91 "email_update", 92 &current_email.to_lowercase(), 93 ); ··· 104 authorized: false, 105 }; 106 if let Ok(json) = serde_json::to_string(&pending) { 107 + let cache_key = email_update_cache_key(&auth_user.did); 108 if let Err(e) = state.cache.set(&cache_key, &json, EMAIL_UPDATE_TTL).await { 109 warn!("Failed to cache pending email update: {:?}", e); 110 } ··· 128 } 129 130 info!("Email update requested for user {}", user.id); 131 + Ok(TokenRequiredResponse::response(token_required).into_response()) 132 } 133 134 #[derive(Deserialize)] ··· 141 pub async fn confirm_email( 142 State(state): State<AppState>, 143 headers: axum::http::HeaderMap, 144 + auth: RequiredAuth, 145 Json(input): Json<ConfirmEmailInput>, 146 + ) -> Result<Response, ApiError> { 147 + let auth_user = auth.0.require_user()?.require_active()?; 148 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 149 if !state 150 .check_rate_limit(RateLimitKind::EmailUpdate, &client_ip) 151 .await 152 { 153 warn!(ip = %client_ip, "Confirm email rate limit exceeded"); 154 + return Err(ApiError::RateLimitExceeded(None)); 155 } 156 157 if let Err(e) = crate::auth::scope_check::check_account_scope( 158 + auth_user.is_oauth, 159 + auth_user.scope.as_deref(), 160 crate::oauth::scopes::AccountAttr::Email, 161 crate::oauth::scopes::AccountAction::Manage, 162 ) { 163 + return Ok(e); 164 } 165 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| { 172 error!("DB error: {:?}", e); 173 + ApiError::InternalError(None) 174 + })? 175 + .ok_or(ApiError::AccountNotFound)?; 176 177 let Some(ref email) = user.email else { 178 + return Err(ApiError::InvalidEmail); 179 }; 180 let current_email = email.to_lowercase(); 181 182 let provided_email = input.email.trim().to_lowercase(); 183 if provided_email != current_email { 184 + return Err(ApiError::InvalidEmail); 185 } 186 187 if user.email_verified { 188 + return Ok(EmptyResponse::ok().into_response()); 189 } 190 191 let confirmation_code = ··· 200 match verified { 201 Ok(token_data) => { 202 if token_data.did != did.as_str() { 203 + return Err(ApiError::InvalidToken(None)); 204 } 205 } 206 Err(crate::auth::verification_token::VerifyError::Expired) => { 207 + return Err(ApiError::ExpiredToken(None)); 208 } 209 Err(_) => { 210 + return Err(ApiError::InvalidToken(None)); 211 } 212 } 213 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 + })?; 222 223 info!("Email confirmed for user {}", user.id); 224 + Ok(EmptyResponse::ok().into_response()) 225 } 226 227 #[derive(Deserialize)] ··· 235 236 pub async fn update_email( 237 State(state): State<AppState>, 238 + auth: RequiredAuth, 239 Json(input): Json<UpdateEmailInput>, 240 + ) -> Result<Response, ApiError> { 241 + let auth_user = auth.0.require_user()?.require_active()?; 242 243 if let Err(e) = crate::auth::scope_check::check_account_scope( 244 auth_user.is_oauth, ··· 246 crate::oauth::scopes::AccountAttr::Email, 247 crate::oauth::scopes::AccountAction::Manage, 248 ) { 249 + return Ok(e); 250 } 251 252 let did = &auth_user.did; 253 + let user = state 254 + .user_repo 255 + .get_email_info_by_did(did) 256 + .await 257 + .map_err(|e| { 258 error!("DB error: {:?}", e); 259 + ApiError::InternalError(None) 260 + })? 261 + .ok_or(ApiError::AccountNotFound)?; 262 263 let user_id = user.id; 264 let current_email = user.email.clone(); ··· 266 let new_email = input.email.trim().to_lowercase(); 267 268 if !crate::api::validation::is_valid_email(&new_email) { 269 + return Err(ApiError::InvalidRequest( 270 "This email address is not supported, please use a different email.".into(), 271 + )); 272 } 273 274 if let Some(ref current) = current_email 275 && new_email == current.to_lowercase() 276 { 277 + return Ok(EmptyResponse::ok().into_response()); 278 } 279 280 if email_verified { ··· 293 294 if !authorized_via_link { 295 let Some(ref t) = input.token else { 296 + return Err(ApiError::TokenRequired); 297 }; 298 let confirmation_token = 299 crate::auth::verification_token::normalize_token_input(t.trim()); ··· 312 match verified { 313 Ok(token_data) => { 314 if token_data.did != did.as_str() { 315 + return Err(ApiError::InvalidToken(None)); 316 } 317 } 318 Err(crate::auth::verification_token::VerifyError::Expired) => { 319 + return Err(ApiError::ExpiredToken(None)); 320 } 321 Err(_) => { 322 + return Err(ApiError::InvalidToken(None)); 323 } 324 } 325 } 326 } 327 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 + })?; 336 337 let verification_token = 338 crate::auth::verification_token::generate_signup_token(did, "email", &new_email); ··· 365 } 366 367 info!("Email updated for user {}", user_id); 368 + Ok(EmptyResponse::ok().into_response()) 369 } 370 371 #[derive(Deserialize)] ··· 504 pub async fn check_email_update_status( 505 State(state): State<AppState>, 506 headers: axum::http::HeaderMap, 507 + auth: RequiredAuth, 508 + ) -> Result<Response, ApiError> { 509 + let auth_user = auth.0.require_user()?.require_active()?; 510 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 511 if !state 512 .check_rate_limit(RateLimitKind::VerificationCheck, &client_ip) 513 .await 514 { 515 + return Err(ApiError::RateLimitExceeded(None)); 516 } 517 518 if let Err(e) = crate::auth::scope_check::check_account_scope( 519 + auth_user.is_oauth, 520 + auth_user.scope.as_deref(), 521 crate::oauth::scopes::AccountAttr::Email, 522 crate::oauth::scopes::AccountAction::Read, 523 ) { 524 + return Ok(e); 525 } 526 527 + let cache_key = email_update_cache_key(&auth_user.did); 528 let pending_json = match state.cache.get(&cache_key).await { 529 Some(json) => json, 530 None => { 531 + return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); 532 } 533 }; 534 535 let pending: PendingEmailUpdate = match serde_json::from_str(&pending_json) { 536 Ok(p) => p, 537 Err(_) => { 538 + return Ok(Json(json!({ "pending": false, "authorized": false })).into_response()); 539 } 540 }; 541 542 + Ok(Json(json!({ 543 "pending": true, 544 "authorized": pending.authorized, 545 "newEmail": pending.new_email, 546 })) 547 + .into_response()) 548 } 549 550 #[derive(Deserialize)]
+46 -45
crates/tranquil-pds/src/api/server/invite.rs
··· 1 use crate::api::ApiError; 2 - use crate::auth::BearerAuth; 3 - use crate::auth::extractor::BearerAuthAdmin; 4 use crate::state::AppState; 5 use crate::types::Did; 6 use axum::{ ··· 44 45 pub async fn create_invite_code( 46 State(state): State<AppState>, 47 - BearerAuthAdmin(auth_user): BearerAuthAdmin, 48 Json(input): Json<CreateInviteCodeInput>, 49 - ) -> Response { 50 if input.use_count < 1 { 51 - return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 52 } 53 54 let for_account: Did = match &input.for_account { 55 - Some(acct) => match acct.parse() { 56 - Ok(d) => d, 57 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 58 - }, 59 None => auth_user.did.clone(), 60 }; 61 let code = gen_invite_code(); ··· 65 .create_invite_code(&code, input.use_count, Some(&for_account)) 66 .await 67 { 68 - Ok(true) => Json(CreateInviteCodeOutput { code }).into_response(), 69 Ok(false) => { 70 error!("No admin user found to create invite code"); 71 - ApiError::InternalError(None).into_response() 72 } 73 Err(e) => { 74 error!("DB error creating invite code: {:?}", e); 75 - ApiError::InternalError(None).into_response() 76 } 77 } 78 } ··· 98 99 pub async fn create_invite_codes( 100 State(state): State<AppState>, 101 - BearerAuthAdmin(auth_user): BearerAuthAdmin, 102 Json(input): Json<CreateInviteCodesInput>, 103 - ) -> Response { 104 if input.use_count < 1 { 105 - return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 106 } 107 108 let code_count = input.code_count.unwrap_or(1).max(1); 109 let for_accounts: Vec<Did> = match &input.for_accounts { 110 - Some(accounts) if !accounts.is_empty() => { 111 - let parsed: Result<Vec<Did>, _> = accounts.iter().map(|a| a.parse()).collect(); 112 - match parsed { 113 - Ok(dids) => dids, 114 - Err(_) => return ApiError::InvalidDid("Invalid DID format".into()).into_response(), 115 - } 116 - } 117 _ => vec![auth_user.did.clone()], 118 }; 119 120 - let admin_user_id = match state.user_repo.get_any_admin_user_id().await { 121 - Ok(Some(id)) => id, 122 - Ok(None) => { 123 error!("No admin user found to create invite codes"); 124 - return ApiError::InternalError(None).into_response(); 125 - } 126 - Err(e) => { 127 - error!("DB error looking up admin user: {:?}", e); 128 - return ApiError::InternalError(None).into_response(); 129 - } 130 - }; 131 132 let result = futures::future::try_join_all(for_accounts.into_iter().map(|account| { 133 let infra_repo = state.infra_repo.clone(); ··· 146 .await; 147 148 match result { 149 - Ok(result_codes) => Json(CreateInviteCodesOutput { 150 codes: result_codes, 151 }) 152 - .into_response(), 153 Err(e) => { 154 error!("DB error creating invite codes: {:?}", e); 155 - ApiError::InternalError(None).into_response() 156 } 157 } 158 } ··· 192 193 pub async fn get_account_invite_codes( 194 State(state): State<AppState>, 195 - BearerAuth(auth_user): BearerAuth, 196 axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>, 197 - ) -> Response { 198 let include_used = params.include_used.unwrap_or(true); 199 200 - let codes_info = match state 201 .infra_repo 202 .get_invite_codes_for_account(&auth_user.did) 203 .await 204 - { 205 - Ok(info) => info, 206 - Err(e) => { 207 error!("DB error fetching invite codes: {:?}", e); 208 - return ApiError::InternalError(None).into_response(); 209 - } 210 - }; 211 212 let filtered_codes: Vec<_> = codes_info 213 .into_iter() ··· 254 .await; 255 256 let codes: Vec<InviteCode> = codes.into_iter().flatten().collect(); 257 - Json(GetAccountInviteCodesOutput { codes }).into_response() 258 }
··· 1 use crate::api::ApiError; 2 + use crate::auth::RequiredAuth; 3 use crate::state::AppState; 4 use crate::types::Did; 5 use axum::{ ··· 43 44 pub async fn create_invite_code( 45 State(state): State<AppState>, 46 + auth: RequiredAuth, 47 Json(input): Json<CreateInviteCodeInput>, 48 + ) -> Result<Response, ApiError> { 49 + let auth_user = auth.0.require_user()?.require_active()?.require_admin()?; 50 if input.use_count < 1 { 51 + return Err(ApiError::InvalidRequest( 52 + "useCount must be at least 1".into(), 53 + )); 54 } 55 56 let for_account: Did = match &input.for_account { 57 + Some(acct) => acct 58 + .parse() 59 + .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?, 60 None => auth_user.did.clone(), 61 }; 62 let code = gen_invite_code(); ··· 66 .create_invite_code(&code, input.use_count, Some(&for_account)) 67 .await 68 { 69 + Ok(true) => Ok(Json(CreateInviteCodeOutput { code }).into_response()), 70 Ok(false) => { 71 error!("No admin user found to create invite code"); 72 + Err(ApiError::InternalError(None)) 73 } 74 Err(e) => { 75 error!("DB error creating invite code: {:?}", e); 76 + Err(ApiError::InternalError(None)) 77 } 78 } 79 } ··· 99 100 pub async fn create_invite_codes( 101 State(state): State<AppState>, 102 + auth: RequiredAuth, 103 Json(input): Json<CreateInviteCodesInput>, 104 + ) -> Result<Response, ApiError> { 105 + let auth_user = auth.0.require_user()?.require_active()?.require_admin()?; 106 if input.use_count < 1 { 107 + return Err(ApiError::InvalidRequest( 108 + "useCount must be at least 1".into(), 109 + )); 110 } 111 112 let code_count = input.code_count.unwrap_or(1).max(1); 113 let for_accounts: Vec<Did> = match &input.for_accounts { 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()))?, 119 _ => vec![auth_user.did.clone()], 120 }; 121 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(|| { 131 error!("No admin user found to create invite codes"); 132 + ApiError::InternalError(None) 133 + })?; 134 135 let result = futures::future::try_join_all(for_accounts.into_iter().map(|account| { 136 let infra_repo = state.infra_repo.clone(); ··· 149 .await; 150 151 match result { 152 + Ok(result_codes) => Ok(Json(CreateInviteCodesOutput { 153 codes: result_codes, 154 }) 155 + .into_response()), 156 Err(e) => { 157 error!("DB error creating invite codes: {:?}", e); 158 + Err(ApiError::InternalError(None)) 159 } 160 } 161 } ··· 195 196 pub async fn get_account_invite_codes( 197 State(state): State<AppState>, 198 + auth: RequiredAuth, 199 axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>, 200 + ) -> Result<Response, ApiError> { 201 + let auth_user = auth.0.require_user()?.require_active()?; 202 let include_used = params.include_used.unwrap_or(true); 203 204 + let codes_info = state 205 .infra_repo 206 .get_invite_codes_for_account(&auth_user.did) 207 .await 208 + .map_err(|e| { 209 error!("DB error fetching invite codes: {:?}", e); 210 + ApiError::InternalError(None) 211 + })?; 212 213 let filtered_codes: Vec<_> = codes_info 214 .into_iter() ··· 255 .await; 256 257 let codes: Vec<InviteCode> = codes.into_iter().flatten().collect(); 258 + Ok(Json(GetAccountInviteCodesOutput { codes }).into_response()) 259 }
+114 -146
crates/tranquil-pds/src/api/server/passkeys.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuth; 4 use crate::auth::webauthn::WebAuthnConfig; 5 use crate::state::AppState; 6 use axum::{ ··· 34 35 pub async fn start_passkey_registration( 36 State(state): State<AppState>, 37 - auth: BearerAuth, 38 Json(input): Json<StartRegistrationInput>, 39 - ) -> Response { 40 - let webauthn = match get_webauthn() { 41 - Ok(w) => w, 42 - Err(e) => return e.into_response(), 43 - }; 44 45 - let handle = match state.user_repo.get_handle_by_did(&auth.0.did).await { 46 - Ok(Some(h)) => h, 47 - Ok(None) => { 48 - return ApiError::AccountNotFound.into_response(); 49 - } 50 - Err(e) => { 51 error!("DB error fetching user: {:?}", e); 52 - return ApiError::InternalError(None).into_response(); 53 - } 54 - }; 55 56 - let existing_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { 57 - Ok(passkeys) => passkeys, 58 - Err(e) => { 59 error!("DB error fetching existing passkeys: {:?}", e); 60 - return ApiError::InternalError(None).into_response(); 61 - } 62 - }; 63 64 let exclude_credentials: Vec<CredentialID> = existing_passkeys 65 .iter() ··· 68 69 let display_name = input.friendly_name.as_deref().unwrap_or(&handle); 70 71 - let (ccr, reg_state) = match webauthn.start_registration( 72 - &auth.0.did, 73 - &handle, 74 - display_name, 75 - exclude_credentials, 76 - ) { 77 - Ok(result) => result, 78 - Err(e) => { 79 error!("Failed to start passkey registration: {}", e); 80 - return ApiError::InternalError(Some("Failed to start registration".into())) 81 - .into_response(); 82 - } 83 - }; 84 85 - let state_json = match serde_json::to_string(&reg_state) { 86 - Ok(s) => s, 87 - Err(e) => { 88 - error!("Failed to serialize registration state: {:?}", e); 89 - return ApiError::InternalError(None).into_response(); 90 - } 91 - }; 92 93 - if let Err(e) = state 94 .user_repo 95 - .save_webauthn_challenge(&auth.0.did, "registration", &state_json) 96 .await 97 - { 98 - error!("Failed to save registration state: {:?}", e); 99 - return ApiError::InternalError(None).into_response(); 100 - } 101 102 let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 103 104 - info!(did = %auth.0.did, "Passkey registration started"); 105 106 - Json(StartRegistrationResponse { options }).into_response() 107 } 108 109 #[derive(Deserialize)] ··· 122 123 pub async fn finish_passkey_registration( 124 State(state): State<AppState>, 125 - auth: BearerAuth, 126 Json(input): Json<FinishRegistrationInput>, 127 - ) -> Response { 128 - let webauthn = match get_webauthn() { 129 - Ok(w) => w, 130 - Err(e) => return e.into_response(), 131 - }; 132 133 - let reg_state_json = match state 134 .user_repo 135 - .load_webauthn_challenge(&auth.0.did, "registration") 136 .await 137 - { 138 - Ok(Some(json)) => json, 139 - Ok(None) => { 140 - return ApiError::NoRegistrationInProgress.into_response(); 141 - } 142 - Err(e) => { 143 error!("DB error loading registration state: {:?}", e); 144 - return ApiError::InternalError(None).into_response(); 145 - } 146 - }; 147 148 - let reg_state: SecurityKeyRegistration = match serde_json::from_str(&reg_state_json) { 149 - Ok(s) => s, 150 - Err(e) => { 151 error!("Failed to deserialize registration state: {:?}", e); 152 - return ApiError::InternalError(None).into_response(); 153 - } 154 - }; 155 156 - let credential: RegisterPublicKeyCredential = match serde_json::from_value(input.credential) { 157 - Ok(c) => c, 158 - Err(e) => { 159 warn!("Failed to parse credential: {:?}", e); 160 - return ApiError::InvalidCredential.into_response(); 161 - } 162 - }; 163 164 - let passkey = match webauthn.finish_registration(&credential, &reg_state) { 165 - Ok(pk) => pk, 166 - Err(e) => { 167 warn!("Failed to finish passkey registration: {}", e); 168 - return ApiError::RegistrationFailed.into_response(); 169 - } 170 - }; 171 172 - let public_key = match serde_json::to_vec(&passkey) { 173 - Ok(pk) => pk, 174 - Err(e) => { 175 - error!("Failed to serialize passkey: {:?}", e); 176 - return ApiError::InternalError(None).into_response(); 177 - } 178 - }; 179 180 - let passkey_id = match state 181 .user_repo 182 .save_passkey( 183 - &auth.0.did, 184 passkey.cred_id(), 185 &public_key, 186 input.friendly_name.as_deref(), 187 ) 188 .await 189 - { 190 - Ok(id) => id, 191 - Err(e) => { 192 error!("Failed to save passkey: {:?}", e); 193 - return ApiError::InternalError(None).into_response(); 194 - } 195 - }; 196 197 if let Err(e) = state 198 .user_repo 199 - .delete_webauthn_challenge(&auth.0.did, "registration") 200 .await 201 { 202 warn!("Failed to delete registration state: {:?}", e); ··· 207 passkey.cred_id(), 208 ); 209 210 - info!(did = %auth.0.did, passkey_id = %passkey_id, "Passkey registered"); 211 212 - Json(FinishRegistrationResponse { 213 id: passkey_id.to_string(), 214 credential_id: credential_id_base64, 215 }) 216 - .into_response() 217 } 218 219 #[derive(Serialize)] ··· 232 pub passkeys: Vec<PasskeyInfo>, 233 } 234 235 - pub async fn list_passkeys(State(state): State<AppState>, auth: BearerAuth) -> Response { 236 - let passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { 237 - Ok(pks) => pks, 238 - Err(e) => { 239 error!("DB error fetching passkeys: {:?}", e); 240 - return ApiError::InternalError(None).into_response(); 241 - } 242 - }; 243 244 let passkey_infos: Vec<PasskeyInfo> = passkeys 245 .into_iter() ··· 252 }) 253 .collect(); 254 255 - Json(ListPasskeysResponse { 256 passkeys: passkey_infos, 257 }) 258 - .into_response() 259 } 260 261 #[derive(Deserialize)] ··· 266 267 pub async fn delete_passkey( 268 State(state): State<AppState>, 269 - auth: BearerAuth, 270 Json(input): Json<DeletePasskeyInput>, 271 - ) -> Response { 272 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 273 .await 274 { 275 - return crate::api::server::reauth::legacy_mfa_required_response( 276 &*state.user_repo, 277 &*state.session_repo, 278 - &auth.0.did, 279 ) 280 - .await; 281 } 282 283 - if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await { 284 - return crate::api::server::reauth::reauth_required_response( 285 &*state.user_repo, 286 &*state.session_repo, 287 - &auth.0.did, 288 ) 289 - .await; 290 } 291 292 - let id: uuid::Uuid = match input.id.parse() { 293 - Ok(id) => id, 294 - Err(_) => { 295 - return ApiError::InvalidId.into_response(); 296 - } 297 - }; 298 299 - match state.user_repo.delete_passkey(id, &auth.0.did).await { 300 Ok(true) => { 301 - info!(did = %auth.0.did, passkey_id = %id, "Passkey deleted"); 302 - EmptyResponse::ok().into_response() 303 } 304 - Ok(false) => ApiError::PasskeyNotFound.into_response(), 305 Err(e) => { 306 error!("DB error deleting passkey: {:?}", e); 307 - ApiError::InternalError(None).into_response() 308 } 309 } 310 } ··· 318 319 pub async fn update_passkey( 320 State(state): State<AppState>, 321 - auth: BearerAuth, 322 Json(input): Json<UpdatePasskeyInput>, 323 - ) -> Response { 324 - let id: uuid::Uuid = match input.id.parse() { 325 - Ok(id) => id, 326 - Err(_) => { 327 - return ApiError::InvalidId.into_response(); 328 - } 329 - }; 330 331 match state 332 .user_repo 333 - .update_passkey_name(id, &auth.0.did, &input.friendly_name) 334 .await 335 { 336 Ok(true) => { 337 - info!(did = %auth.0.did, passkey_id = %id, "Passkey renamed"); 338 - EmptyResponse::ok().into_response() 339 } 340 - Ok(false) => ApiError::PasskeyNotFound.into_response(), 341 Err(e) => { 342 error!("DB error updating passkey: {:?}", e); 343 - ApiError::InternalError(None).into_response() 344 } 345 } 346 }
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::RequiredAuth; 4 use crate::auth::webauthn::WebAuthnConfig; 5 use crate::state::AppState; 6 use axum::{ ··· 34 35 pub async fn start_passkey_registration( 36 State(state): State<AppState>, 37 + auth: RequiredAuth, 38 Json(input): Json<StartRegistrationInput>, 39 + ) -> Result<Response, ApiError> { 40 + let auth_user = auth.0.require_user()?.require_active()?; 41 + let webauthn = get_webauthn()?; 42 43 + let handle = state 44 + .user_repo 45 + .get_handle_by_did(&auth_user.did) 46 + .await 47 + .map_err(|e| { 48 error!("DB error fetching user: {:?}", e); 49 + ApiError::InternalError(None) 50 + })? 51 + .ok_or(ApiError::AccountNotFound)?; 52 53 + let existing_passkeys = state 54 + .user_repo 55 + .get_passkeys_for_user(&auth_user.did) 56 + .await 57 + .map_err(|e| { 58 error!("DB error fetching existing passkeys: {:?}", e); 59 + ApiError::InternalError(None) 60 + })?; 61 62 let exclude_credentials: Vec<CredentialID> = existing_passkeys 63 .iter() ··· 66 67 let display_name = input.friendly_name.as_deref().unwrap_or(&handle); 68 69 + let (ccr, reg_state) = webauthn 70 + .start_registration(&auth_user.did, &handle, display_name, exclude_credentials) 71 + .map_err(|e| { 72 error!("Failed to start passkey registration: {}", e); 73 + ApiError::InternalError(Some("Failed to start registration".into())) 74 + })?; 75 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 + })?; 80 81 + state 82 .user_repo 83 + .save_webauthn_challenge(&auth_user.did, "registration", &state_json) 84 .await 85 + .map_err(|e| { 86 + error!("Failed to save registration state: {:?}", e); 87 + ApiError::InternalError(None) 88 + })?; 89 90 let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 91 92 + info!(did = %auth_user.did, "Passkey registration started"); 93 94 + Ok(Json(StartRegistrationResponse { options }).into_response()) 95 } 96 97 #[derive(Deserialize)] ··· 110 111 pub async fn finish_passkey_registration( 112 State(state): State<AppState>, 113 + auth: RequiredAuth, 114 Json(input): Json<FinishRegistrationInput>, 115 + ) -> Result<Response, ApiError> { 116 + let auth_user = auth.0.require_user()?.require_active()?; 117 + let webauthn = get_webauthn()?; 118 119 + let reg_state_json = state 120 .user_repo 121 + .load_webauthn_challenge(&auth_user.did, "registration") 122 .await 123 + .map_err(|e| { 124 error!("DB error loading registration state: {:?}", e); 125 + ApiError::InternalError(None) 126 + })? 127 + .ok_or(ApiError::NoRegistrationInProgress)?; 128 129 + let reg_state: SecurityKeyRegistration = 130 + serde_json::from_str(&reg_state_json).map_err(|e| { 131 error!("Failed to deserialize registration state: {:?}", e); 132 + ApiError::InternalError(None) 133 + })?; 134 135 + let credential: RegisterPublicKeyCredential = serde_json::from_value(input.credential) 136 + .map_err(|e| { 137 warn!("Failed to parse credential: {:?}", e); 138 + ApiError::InvalidCredential 139 + })?; 140 141 + let passkey = webauthn 142 + .finish_registration(&credential, &reg_state) 143 + .map_err(|e| { 144 warn!("Failed to finish passkey registration: {}", e); 145 + ApiError::RegistrationFailed 146 + })?; 147 148 + let public_key = serde_json::to_vec(&passkey).map_err(|e| { 149 + error!("Failed to serialize passkey: {:?}", e); 150 + ApiError::InternalError(None) 151 + })?; 152 153 + let passkey_id = state 154 .user_repo 155 .save_passkey( 156 + &auth_user.did, 157 passkey.cred_id(), 158 &public_key, 159 input.friendly_name.as_deref(), 160 ) 161 .await 162 + .map_err(|e| { 163 error!("Failed to save passkey: {:?}", e); 164 + ApiError::InternalError(None) 165 + })?; 166 167 if let Err(e) = state 168 .user_repo 169 + .delete_webauthn_challenge(&auth_user.did, "registration") 170 .await 171 { 172 warn!("Failed to delete registration state: {:?}", e); ··· 177 passkey.cred_id(), 178 ); 179 180 + info!(did = %auth_user.did, passkey_id = %passkey_id, "Passkey registered"); 181 182 + Ok(Json(FinishRegistrationResponse { 183 id: passkey_id.to_string(), 184 credential_id: credential_id_base64, 185 }) 186 + .into_response()) 187 } 188 189 #[derive(Serialize)] ··· 202 pub passkeys: Vec<PasskeyInfo>, 203 } 204 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| { 215 error!("DB error fetching passkeys: {:?}", e); 216 + ApiError::InternalError(None) 217 + })?; 218 219 let passkey_infos: Vec<PasskeyInfo> = passkeys 220 .into_iter() ··· 227 }) 228 .collect(); 229 230 + Ok(Json(ListPasskeysResponse { 231 passkeys: passkey_infos, 232 }) 233 + .into_response()) 234 } 235 236 #[derive(Deserialize)] ··· 241 242 pub async fn delete_passkey( 243 State(state): State<AppState>, 244 + auth: RequiredAuth, 245 Json(input): Json<DeletePasskeyInput>, 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) 249 .await 250 { 251 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 252 &*state.user_repo, 253 &*state.session_repo, 254 + &auth_user.did, 255 ) 256 + .await); 257 } 258 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( 262 &*state.user_repo, 263 &*state.session_repo, 264 + &auth_user.did, 265 ) 266 + .await); 267 } 268 269 + let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?; 270 271 + match state.user_repo.delete_passkey(id, &auth_user.did).await { 272 Ok(true) => { 273 + info!(did = %auth_user.did, passkey_id = %id, "Passkey deleted"); 274 + Ok(EmptyResponse::ok().into_response()) 275 } 276 + Ok(false) => Err(ApiError::PasskeyNotFound), 277 Err(e) => { 278 error!("DB error deleting passkey: {:?}", e); 279 + Err(ApiError::InternalError(None)) 280 } 281 } 282 } ··· 290 291 pub async fn update_passkey( 292 State(state): State<AppState>, 293 + auth: RequiredAuth, 294 Json(input): Json<UpdatePasskeyInput>, 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)?; 298 299 match state 300 .user_repo 301 + .update_passkey_name(id, &auth_user.did, &input.friendly_name) 302 .await 303 { 304 Ok(true) => { 305 + info!(did = %auth_user.did, passkey_id = %id, "Passkey renamed"); 306 + Ok(EmptyResponse::ok().into_response()) 307 } 308 + Ok(false) => Err(ApiError::PasskeyNotFound), 309 Err(e) => { 310 error!("DB error updating passkey: {:?}", e); 311 + Err(ApiError::InternalError(None)) 312 } 313 } 314 }
+131 -124
crates/tranquil-pds/src/api/server/password.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; 3 - use crate::auth::BearerAuth; 4 use crate::state::{AppState, RateLimitKind}; 5 use crate::types::PlainPassword; 6 use crate::validation::validate_password; ··· 227 228 pub async fn change_password( 229 State(state): State<AppState>, 230 - auth: BearerAuth, 231 Json(input): Json<ChangePasswordInput>, 232 - ) -> Response { 233 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 234 .await 235 { 236 - return crate::api::server::reauth::legacy_mfa_required_response( 237 &*state.user_repo, 238 &*state.session_repo, 239 - &auth.0.did, 240 ) 241 - .await; 242 } 243 244 let current_password = &input.current_password; 245 let new_password = &input.new_password; 246 if current_password.is_empty() { 247 - return ApiError::InvalidRequest("currentPassword is required".into()).into_response(); 248 } 249 if new_password.is_empty() { 250 - return ApiError::InvalidRequest("newPassword is required".into()).into_response(); 251 } 252 if let Err(e) = validate_password(new_password) { 253 - return ApiError::InvalidRequest(e.to_string()).into_response(); 254 } 255 - let user = match state 256 .user_repo 257 - .get_id_and_password_hash_by_did(&auth.0.did) 258 .await 259 - { 260 - Ok(Some(u)) => u, 261 - Ok(None) => { 262 - return ApiError::AccountNotFound.into_response(); 263 - } 264 - Err(e) => { 265 error!("DB error in change_password: {:?}", e); 266 - return ApiError::InternalError(None).into_response(); 267 - } 268 - }; 269 let (user_id, password_hash) = (user.id, user.password_hash); 270 - let valid = match verify(current_password, &password_hash) { 271 - Ok(v) => v, 272 - Err(e) => { 273 - error!("Password verification error: {:?}", e); 274 - return ApiError::InternalError(None).into_response(); 275 - } 276 - }; 277 if !valid { 278 - return ApiError::InvalidPassword("Current password is incorrect".into()).into_response(); 279 } 280 let new_password_clone = new_password.to_string(); 281 - let new_hash = 282 - match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await { 283 - Ok(Ok(h)) => h, 284 - Ok(Err(e)) => { 285 - error!("Failed to hash password: {:?}", e); 286 - return ApiError::InternalError(None).into_response(); 287 - } 288 - Err(e) => { 289 - error!("Failed to spawn blocking task: {:?}", e); 290 - return ApiError::InternalError(None).into_response(); 291 - } 292 - }; 293 - if let Err(e) = state 294 .user_repo 295 .update_password_hash(user_id, &new_hash) 296 .await 297 - { 298 - error!("DB error updating password: {:?}", e); 299 - return ApiError::InternalError(None).into_response(); 300 - } 301 - info!(did = %&auth.0.did, "Password changed successfully"); 302 - EmptyResponse::ok().into_response() 303 } 304 305 - pub async fn get_password_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 306 - match state.user_repo.has_password_by_did(&auth.0.did).await { 307 - Ok(Some(has)) => HasPasswordResponse::response(has).into_response(), 308 - Ok(None) => ApiError::AccountNotFound.into_response(), 309 Err(e) => { 310 error!("DB error: {:?}", e); 311 - ApiError::InternalError(None).into_response() 312 } 313 } 314 } 315 316 - pub async fn remove_password(State(state): State<AppState>, auth: BearerAuth) -> Response { 317 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 318 .await 319 { 320 - return crate::api::server::reauth::legacy_mfa_required_response( 321 &*state.user_repo, 322 &*state.session_repo, 323 - &auth.0.did, 324 ) 325 - .await; 326 } 327 328 if crate::api::server::reauth::check_reauth_required_cached( 329 &*state.session_repo, 330 &state.cache, 331 - &auth.0.did, 332 ) 333 .await 334 { 335 - return crate::api::server::reauth::reauth_required_response( 336 &*state.user_repo, 337 &*state.session_repo, 338 - &auth.0.did, 339 ) 340 - .await; 341 } 342 343 let has_passkeys = state 344 .user_repo 345 - .has_passkeys(&auth.0.did) 346 .await 347 .unwrap_or(false); 348 if !has_passkeys { 349 - return ApiError::InvalidRequest( 350 "You must have at least one passkey registered before removing your password".into(), 351 - ) 352 - .into_response(); 353 } 354 355 - let user = match state.user_repo.get_password_info_by_did(&auth.0.did).await { 356 - Ok(Some(u)) => u, 357 - Ok(None) => { 358 - return ApiError::AccountNotFound.into_response(); 359 - } 360 - Err(e) => { 361 error!("DB error: {:?}", e); 362 - return ApiError::InternalError(None).into_response(); 363 - } 364 - }; 365 366 if user.password_hash.is_none() { 367 - return ApiError::InvalidRequest("Account already has no password".into()).into_response(); 368 } 369 370 - if let Err(e) = state.user_repo.remove_user_password(user.id).await { 371 - error!("DB error removing password: {:?}", e); 372 - return ApiError::InternalError(None).into_response(); 373 - } 374 375 - info!(did = %&auth.0.did, "Password removed - account is now passkey-only"); 376 - SuccessResponse::ok().into_response() 377 } 378 379 #[derive(Deserialize)] ··· 384 385 pub async fn set_password( 386 State(state): State<AppState>, 387 - auth: BearerAuth, 388 Json(input): Json<SetPasswordInput>, 389 - ) -> Response { 390 let has_password = state 391 .user_repo 392 - .has_password_by_did(&auth.0.did) 393 .await 394 .ok() 395 .flatten() 396 .unwrap_or(false); 397 let has_passkeys = state 398 .user_repo 399 - .has_passkeys(&auth.0.did) 400 .await 401 .unwrap_or(false); 402 let has_totp = state 403 .user_repo 404 - .has_totp_enabled(&auth.0.did) 405 .await 406 .unwrap_or(false); 407 ··· 411 && crate::api::server::reauth::check_reauth_required_cached( 412 &*state.session_repo, 413 &state.cache, 414 - &auth.0.did, 415 ) 416 .await 417 { 418 - return crate::api::server::reauth::reauth_required_response( 419 &*state.user_repo, 420 &*state.session_repo, 421 - &auth.0.did, 422 ) 423 - .await; 424 } 425 426 let new_password = &input.new_password; 427 if new_password.is_empty() { 428 - return ApiError::InvalidRequest("newPassword is required".into()).into_response(); 429 } 430 if let Err(e) = validate_password(new_password) { 431 - return ApiError::InvalidRequest(e.to_string()).into_response(); 432 } 433 434 - let user = match state.user_repo.get_password_info_by_did(&auth.0.did).await { 435 - Ok(Some(u)) => u, 436 - Ok(None) => { 437 - return ApiError::AccountNotFound.into_response(); 438 - } 439 - Err(e) => { 440 error!("DB error: {:?}", e); 441 - return ApiError::InternalError(None).into_response(); 442 - } 443 - }; 444 445 if user.password_hash.is_some() { 446 - return ApiError::InvalidRequest( 447 "Account already has a password. Use changePassword instead.".into(), 448 - ) 449 - .into_response(); 450 } 451 452 let new_password_clone = new_password.to_string(); 453 - let new_hash = 454 - match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await { 455 - Ok(Ok(h)) => h, 456 - Ok(Err(e)) => { 457 - error!("Failed to hash password: {:?}", e); 458 - return ApiError::InternalError(None).into_response(); 459 - } 460 - Err(e) => { 461 - error!("Failed to spawn blocking task: {:?}", e); 462 - return ApiError::InternalError(None).into_response(); 463 - } 464 - }; 465 466 - if let Err(e) = state 467 .user_repo 468 .set_new_user_password(user.id, &new_hash) 469 .await 470 - { 471 - error!("DB error setting password: {:?}", e); 472 - return ApiError::InternalError(None).into_response(); 473 - } 474 475 - info!(did = %&auth.0.did, "Password set for passkey-only account"); 476 - SuccessResponse::ok().into_response() 477 }
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, HasPasswordResponse, SuccessResponse}; 3 + use crate::auth::RequiredAuth; 4 use crate::state::{AppState, RateLimitKind}; 5 use crate::types::PlainPassword; 6 use crate::validation::validate_password; ··· 227 228 pub async fn change_password( 229 State(state): State<AppState>, 230 + auth: RequiredAuth, 231 Json(input): Json<ChangePasswordInput>, 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) 235 .await 236 { 237 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 238 &*state.user_repo, 239 &*state.session_repo, 240 + &auth_user.did, 241 ) 242 + .await); 243 } 244 245 let current_password = &input.current_password; 246 let new_password = &input.new_password; 247 if current_password.is_empty() { 248 + return Err(ApiError::InvalidRequest( 249 + "currentPassword is required".into(), 250 + )); 251 } 252 if new_password.is_empty() { 253 + return Err(ApiError::InvalidRequest("newPassword is required".into())); 254 } 255 if let Err(e) = validate_password(new_password) { 256 + return Err(ApiError::InvalidRequest(e.to_string())); 257 } 258 + let user = state 259 .user_repo 260 + .get_id_and_password_hash_by_did(&auth_user.did) 261 .await 262 + .map_err(|e| { 263 error!("DB error in change_password: {:?}", e); 264 + ApiError::InternalError(None) 265 + })? 266 + .ok_or(ApiError::AccountNotFound)?; 267 + 268 let (user_id, password_hash) = (user.id, user.password_hash); 269 + let valid = verify(current_password, &password_hash).map_err(|e| { 270 + error!("Password verification error: {:?}", e); 271 + ApiError::InternalError(None) 272 + })?; 273 if !valid { 274 + return Err(ApiError::InvalidPassword( 275 + "Current password is incorrect".into(), 276 + )); 277 } 278 let new_password_clone = new_password.to_string(); 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 291 .user_repo 292 .update_password_hash(user_id, &new_hash) 293 .await 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()) 301 } 302 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), 311 Err(e) => { 312 error!("DB error: {:?}", e); 313 + Err(ApiError::InternalError(None)) 314 } 315 } 316 } 317 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) 324 .await 325 { 326 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 327 &*state.user_repo, 328 &*state.session_repo, 329 + &auth_user.did, 330 ) 331 + .await); 332 } 333 334 if crate::api::server::reauth::check_reauth_required_cached( 335 &*state.session_repo, 336 &state.cache, 337 + &auth_user.did, 338 ) 339 .await 340 { 341 + return Ok(crate::api::server::reauth::reauth_required_response( 342 &*state.user_repo, 343 &*state.session_repo, 344 + &auth_user.did, 345 ) 346 + .await); 347 } 348 349 let has_passkeys = state 350 .user_repo 351 + .has_passkeys(&auth_user.did) 352 .await 353 .unwrap_or(false); 354 if !has_passkeys { 355 + return Err(ApiError::InvalidRequest( 356 "You must have at least one passkey registered before removing your password".into(), 357 + )); 358 } 359 360 + let user = state 361 + .user_repo 362 + .get_password_info_by_did(&auth_user.did) 363 + .await 364 + .map_err(|e| { 365 error!("DB error: {:?}", e); 366 + ApiError::InternalError(None) 367 + })? 368 + .ok_or(ApiError::AccountNotFound)?; 369 370 if user.password_hash.is_none() { 371 + return Err(ApiError::InvalidRequest( 372 + "Account already has no password".into(), 373 + )); 374 } 375 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 + })?; 384 385 + info!(did = %&auth_user.did, "Password removed - account is now passkey-only"); 386 + Ok(SuccessResponse::ok().into_response()) 387 } 388 389 #[derive(Deserialize)] ··· 394 395 pub async fn set_password( 396 State(state): State<AppState>, 397 + auth: RequiredAuth, 398 Json(input): Json<SetPasswordInput>, 399 + ) -> Result<Response, ApiError> { 400 + let auth_user = auth.0.require_user()?.require_active()?; 401 let has_password = state 402 .user_repo 403 + .has_password_by_did(&auth_user.did) 404 .await 405 .ok() 406 .flatten() 407 .unwrap_or(false); 408 let has_passkeys = state 409 .user_repo 410 + .has_passkeys(&auth_user.did) 411 .await 412 .unwrap_or(false); 413 let has_totp = state 414 .user_repo 415 + .has_totp_enabled(&auth_user.did) 416 .await 417 .unwrap_or(false); 418 ··· 422 && crate::api::server::reauth::check_reauth_required_cached( 423 &*state.session_repo, 424 &state.cache, 425 + &auth_user.did, 426 ) 427 .await 428 { 429 + return Ok(crate::api::server::reauth::reauth_required_response( 430 &*state.user_repo, 431 &*state.session_repo, 432 + &auth_user.did, 433 ) 434 + .await); 435 } 436 437 let new_password = &input.new_password; 438 if new_password.is_empty() { 439 + return Err(ApiError::InvalidRequest("newPassword is required".into())); 440 } 441 if let Err(e) = validate_password(new_password) { 442 + return Err(ApiError::InvalidRequest(e.to_string())); 443 } 444 445 + let user = state 446 + .user_repo 447 + .get_password_info_by_did(&auth_user.did) 448 + .await 449 + .map_err(|e| { 450 error!("DB error: {:?}", e); 451 + ApiError::InternalError(None) 452 + })? 453 + .ok_or(ApiError::AccountNotFound)?; 454 455 if user.password_hash.is_some() { 456 + return Err(ApiError::InvalidRequest( 457 "Account already has a password. Use changePassword instead.".into(), 458 + )); 459 } 460 461 let new_password_clone = new_password.to_string(); 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 + })?; 472 473 + state 474 .user_repo 475 .set_new_user_password(user.id, &new_hash) 476 .await 477 + .map_err(|e| { 478 + error!("DB error setting password: {:?}", e); 479 + ApiError::InternalError(None) 480 + })?; 481 482 + info!(did = %&auth_user.did, "Password set for passkey-only account"); 483 + Ok(SuccessResponse::ok().into_response()) 484 }
+137 -147
crates/tranquil-pds/src/api/server/reauth.rs
··· 10 use tracing::{error, info, warn}; 11 use tranquil_db_traits::{SessionRepository, UserRepository}; 12 13 - use crate::auth::BearerAuth; 14 use crate::state::{AppState, RateLimitKind}; 15 use crate::types::PlainPassword; 16 ··· 24 pub available_methods: Vec<String>, 25 } 26 27 - pub async fn get_reauth_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 28 - let last_reauth_at = match state.session_repo.get_last_reauth_at(&auth.0.did).await { 29 - Ok(t) => t, 30 - Err(e) => { 31 error!("DB error: {:?}", e); 32 - return ApiError::InternalError(None).into_response(); 33 - } 34 - }; 35 36 let reauth_required = is_reauth_required(last_reauth_at); 37 let available_methods = 38 - get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth.0.did).await; 39 40 - Json(ReauthStatusResponse { 41 last_reauth_at, 42 reauth_required, 43 available_methods, 44 }) 45 - .into_response() 46 } 47 48 #[derive(Deserialize)] ··· 59 60 pub async fn reauth_password( 61 State(state): State<AppState>, 62 - auth: BearerAuth, 63 Json(input): Json<PasswordReauthInput>, 64 - ) -> Response { 65 - let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await { 66 - Ok(Some(hash)) => hash, 67 - Ok(None) => { 68 - return ApiError::AccountNotFound.into_response(); 69 - } 70 - Err(e) => { 71 error!("DB error: {:?}", e); 72 - return ApiError::InternalError(None).into_response(); 73 - } 74 - }; 75 76 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 77 78 if !password_valid { 79 let app_password_hashes = state 80 .session_repo 81 - .get_app_password_hashes_by_did(&auth.0.did) 82 .await 83 .unwrap_or_default(); 84 ··· 87 }); 88 89 if !app_password_valid { 90 - warn!(did = %&auth.0.did, "Re-auth failed: invalid password"); 91 - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 92 } 93 } 94 95 - match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await { 96 - Ok(reauthed_at) => { 97 - info!(did = %&auth.0.did, "Re-auth successful via password"); 98 - Json(ReauthResponse { reauthed_at }).into_response() 99 - } 100 - Err(e) => { 101 error!("DB error updating reauth: {:?}", e); 102 - ApiError::InternalError(None).into_response() 103 - } 104 - } 105 } 106 107 #[derive(Deserialize)] ··· 112 113 pub async fn reauth_totp( 114 State(state): State<AppState>, 115 - auth: BearerAuth, 116 Json(input): Json<TotpReauthInput>, 117 - ) -> Response { 118 if !state 119 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 120 .await 121 { 122 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 123 - return ApiError::RateLimitExceeded(Some( 124 "Too many verification attempts. Please try again in a few minutes.".into(), 125 - )) 126 - .into_response(); 127 } 128 129 - let valid = 130 - crate::api::server::totp::verify_totp_or_backup_for_user(&state, &auth.0.did, &input.code) 131 - .await; 132 133 if !valid { 134 - warn!(did = %&auth.0.did, "Re-auth failed: invalid TOTP code"); 135 - return ApiError::InvalidCode(Some("Invalid TOTP or backup code".into())).into_response(); 136 } 137 138 - match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await { 139 - Ok(reauthed_at) => { 140 - info!(did = %&auth.0.did, "Re-auth successful via TOTP"); 141 - Json(ReauthResponse { reauthed_at }).into_response() 142 - } 143 - Err(e) => { 144 error!("DB error updating reauth: {:?}", e); 145 - ApiError::InternalError(None).into_response() 146 - } 147 - } 148 } 149 150 #[derive(Serialize)] ··· 153 pub options: serde_json::Value, 154 } 155 156 - pub async fn reauth_passkey_start(State(state): State<AppState>, auth: BearerAuth) -> Response { 157 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 158 159 - let stored_passkeys = match state.user_repo.get_passkeys_for_user(&auth.0.did).await { 160 - Ok(pks) => pks, 161 - Err(e) => { 162 error!("Failed to get passkeys: {:?}", e); 163 - return ApiError::InternalError(None).into_response(); 164 - } 165 - }; 166 167 if stored_passkeys.is_empty() { 168 - return ApiError::NoPasskeys.into_response(); 169 } 170 171 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys ··· 174 .collect(); 175 176 if passkeys.is_empty() { 177 - return ApiError::InternalError(Some("Failed to load passkeys".into())).into_response(); 178 } 179 180 - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 181 - Ok(w) => w, 182 - Err(e) => { 183 - error!("Failed to create WebAuthn config: {:?}", e); 184 - return ApiError::InternalError(None).into_response(); 185 - } 186 - }; 187 188 - let (rcr, auth_state) = match webauthn.start_authentication(passkeys) { 189 - Ok(result) => result, 190 - Err(e) => { 191 - error!("Failed to start passkey authentication: {:?}", e); 192 - return ApiError::InternalError(None).into_response(); 193 - } 194 - }; 195 196 - let state_json = match serde_json::to_string(&auth_state) { 197 - Ok(s) => s, 198 - Err(e) => { 199 - error!("Failed to serialize authentication state: {:?}", e); 200 - return ApiError::InternalError(None).into_response(); 201 - } 202 - }; 203 204 - if let Err(e) = state 205 .user_repo 206 - .save_webauthn_challenge(&auth.0.did, "authentication", &state_json) 207 .await 208 - { 209 - error!("Failed to save authentication state: {:?}", e); 210 - return ApiError::InternalError(None).into_response(); 211 - } 212 213 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 214 - Json(PasskeyReauthStartResponse { options }).into_response() 215 } 216 217 #[derive(Deserialize)] ··· 222 223 pub async fn reauth_passkey_finish( 224 State(state): State<AppState>, 225 - auth: BearerAuth, 226 Json(input): Json<PasskeyReauthFinishInput>, 227 - ) -> Response { 228 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 229 230 - let auth_state_json = match state 231 .user_repo 232 - .load_webauthn_challenge(&auth.0.did, "authentication") 233 .await 234 - { 235 - Ok(Some(json)) => json, 236 - Ok(None) => { 237 - return ApiError::NoChallengeInProgress.into_response(); 238 - } 239 - Err(e) => { 240 error!("Failed to load authentication state: {:?}", e); 241 - return ApiError::InternalError(None).into_response(); 242 - } 243 - }; 244 245 let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = 246 - match serde_json::from_str(&auth_state_json) { 247 - Ok(s) => s, 248 - Err(e) => { 249 - error!("Failed to deserialize authentication state: {:?}", e); 250 - return ApiError::InternalError(None).into_response(); 251 - } 252 - }; 253 254 let credential: webauthn_rs::prelude::PublicKeyCredential = 255 - match serde_json::from_value(input.credential) { 256 - Ok(c) => c, 257 - Err(e) => { 258 - warn!("Failed to parse credential: {:?}", e); 259 - return ApiError::InvalidCredential.into_response(); 260 - } 261 - }; 262 263 - let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 264 - Ok(w) => w, 265 - Err(e) => { 266 - error!("Failed to create WebAuthn config: {:?}", e); 267 - return ApiError::InternalError(None).into_response(); 268 - } 269 - }; 270 271 - let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { 272 - Ok(r) => r, 273 - Err(e) => { 274 - warn!(did = %&auth.0.did, "Passkey re-auth failed: {:?}", e); 275 - return ApiError::AuthenticationFailed(Some("Passkey authentication failed".into())) 276 - .into_response(); 277 - } 278 - }; 279 280 let cred_id_bytes = auth_result.cred_id().as_ref(); 281 match state ··· 284 .await 285 { 286 Ok(false) => { 287 - warn!(did = %&auth.0.did, "Passkey counter anomaly detected - possible cloned key"); 288 let _ = state 289 .user_repo 290 - .delete_webauthn_challenge(&auth.0.did, "authentication") 291 .await; 292 - return ApiError::PasskeyCounterAnomaly.into_response(); 293 } 294 Err(e) => { 295 error!("Failed to update passkey counter: {:?}", e); ··· 299 300 let _ = state 301 .user_repo 302 - .delete_webauthn_challenge(&auth.0.did, "authentication") 303 .await; 304 305 - match update_last_reauth_cached(&*state.session_repo, &state.cache, &auth.0.did).await { 306 - Ok(reauthed_at) => { 307 - info!(did = %&auth.0.did, "Re-auth successful via passkey"); 308 - Json(ReauthResponse { reauthed_at }).into_response() 309 - } 310 - Err(e) => { 311 error!("DB error updating reauth: {:?}", e); 312 - ApiError::InternalError(None).into_response() 313 - } 314 - } 315 } 316 317 pub async fn update_last_reauth_cached(
··· 10 use tracing::{error, info, warn}; 11 use tranquil_db_traits::{SessionRepository, UserRepository}; 12 13 + use crate::auth::RequiredAuth; 14 use crate::state::{AppState, RateLimitKind}; 15 use crate::types::PlainPassword; 16 ··· 24 pub available_methods: Vec<String>, 25 } 26 27 + pub async fn get_reauth_status( 28 + State(state): State<AppState>, 29 + auth: 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| { 37 error!("DB error: {:?}", e); 38 + ApiError::InternalError(None) 39 + })?; 40 41 let reauth_required = is_reauth_required(last_reauth_at); 42 let available_methods = 43 + get_available_reauth_methods(&*state.user_repo, &*state.session_repo, &auth_user.did).await; 44 45 + Ok(Json(ReauthStatusResponse { 46 last_reauth_at, 47 reauth_required, 48 available_methods, 49 }) 50 + .into_response()) 51 } 52 53 #[derive(Deserialize)] ··· 64 65 pub async fn reauth_password( 66 State(state): State<AppState>, 67 + auth: RequiredAuth, 68 Json(input): Json<PasswordReauthInput>, 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| { 76 error!("DB error: {:?}", e); 77 + ApiError::InternalError(None) 78 + })? 79 + .ok_or(ApiError::AccountNotFound)?; 80 81 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 82 83 if !password_valid { 84 let app_password_hashes = state 85 .session_repo 86 + .get_app_password_hashes_by_did(&auth_user.did) 87 .await 88 .unwrap_or_default(); 89 ··· 92 }); 93 94 if !app_password_valid { 95 + warn!(did = %&auth_user.did, "Re-auth failed: invalid password"); 96 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 97 } 98 } 99 100 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth_user.did) 101 + .await 102 + .map_err(|e| { 103 error!("DB error updating reauth: {:?}", e); 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()) 109 } 110 111 #[derive(Deserialize)] ··· 116 117 pub async fn reauth_totp( 118 State(state): State<AppState>, 119 + auth: RequiredAuth, 120 Json(input): Json<TotpReauthInput>, 121 + ) -> Result<Response, ApiError> { 122 + let auth_user = auth.0.require_user()?.require_active()?; 123 if !state 124 + .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 125 .await 126 { 127 + warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 128 + return Err(ApiError::RateLimitExceeded(Some( 129 "Too many verification attempts. Please try again in a few minutes.".into(), 130 + ))); 131 } 132 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; 139 140 if !valid { 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 + ))); 145 } 146 147 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth_user.did) 148 + .await 149 + .map_err(|e| { 150 error!("DB error updating reauth: {:?}", e); 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()) 156 } 157 158 #[derive(Serialize)] ··· 161 pub options: serde_json::Value, 162 } 163 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()?; 169 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 170 171 + let stored_passkeys = state 172 + .user_repo 173 + .get_passkeys_for_user(&auth_user.did) 174 + .await 175 + .map_err(|e| { 176 error!("Failed to get passkeys: {:?}", e); 177 + ApiError::InternalError(None) 178 + })?; 179 180 if stored_passkeys.is_empty() { 181 + return Err(ApiError::NoPasskeys); 182 } 183 184 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys ··· 187 .collect(); 188 189 if passkeys.is_empty() { 190 + return Err(ApiError::InternalError(Some( 191 + "Failed to load passkeys".into(), 192 + ))); 193 } 194 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 + })?; 199 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 + })?; 204 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 + })?; 209 210 + state 211 .user_repo 212 + .save_webauthn_challenge(&auth_user.did, "authentication", &state_json) 213 .await 214 + .map_err(|e| { 215 + error!("Failed to save authentication state: {:?}", e); 216 + ApiError::InternalError(None) 217 + })?; 218 219 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 220 + Ok(Json(PasskeyReauthStartResponse { options }).into_response()) 221 } 222 223 #[derive(Deserialize)] ··· 228 229 pub async fn reauth_passkey_finish( 230 State(state): State<AppState>, 231 + auth: RequiredAuth, 232 Json(input): Json<PasskeyReauthFinishInput>, 233 + ) -> Result<Response, ApiError> { 234 + let auth_user = auth.0.require_user()?.require_active()?; 235 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 236 237 + let auth_state_json = state 238 .user_repo 239 + .load_webauthn_challenge(&auth_user.did, "authentication") 240 .await 241 + .map_err(|e| { 242 error!("Failed to load authentication state: {:?}", e); 243 + ApiError::InternalError(None) 244 + })? 245 + .ok_or(ApiError::NoChallengeInProgress)?; 246 247 let auth_state: webauthn_rs::prelude::SecurityKeyAuthentication = 248 + serde_json::from_str(&auth_state_json).map_err(|e| { 249 + error!("Failed to deserialize authentication state: {:?}", e); 250 + ApiError::InternalError(None) 251 + })?; 252 253 let credential: webauthn_rs::prelude::PublicKeyCredential = 254 + serde_json::from_value(input.credential).map_err(|e| { 255 + warn!("Failed to parse credential: {:?}", e); 256 + ApiError::InvalidCredential 257 + })?; 258 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 + })?; 263 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 + })?; 270 271 let cred_id_bytes = auth_result.cred_id().as_ref(); 272 match state ··· 275 .await 276 { 277 Ok(false) => { 278 + warn!(did = %&auth_user.did, "Passkey counter anomaly detected - possible cloned key"); 279 let _ = state 280 .user_repo 281 + .delete_webauthn_challenge(&auth_user.did, "authentication") 282 .await; 283 + return Err(ApiError::PasskeyCounterAnomaly); 284 } 285 Err(e) => { 286 error!("Failed to update passkey counter: {:?}", e); ··· 290 291 let _ = state 292 .user_repo 293 + .delete_webauthn_challenge(&auth_user.did, "authentication") 294 .await; 295 296 + let reauthed_at = update_last_reauth_cached(&*state.session_repo, &state.cache, &auth_user.did) 297 + .await 298 + .map_err(|e| { 299 error!("DB error updating reauth: {:?}", e); 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()) 305 } 306 307 pub async fn update_last_reauth_cached(
+174 -169
crates/tranquil-pds/src/api/server/session.rs
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, SuccessResponse}; 3 - use crate::auth::{BearerAuth, BearerAuthAllowDeactivated}; 4 use crate::state::{AppState, RateLimitKind}; 5 use crate::types::{AccountState, Did, Handle, PlainPassword}; 6 use axum::{ ··· 279 280 pub async fn get_session( 281 State(state): State<AppState>, 282 - BearerAuthAllowDeactivated(auth_user): BearerAuthAllowDeactivated, 283 - ) -> Response { 284 let permissions = auth_user.permissions(); 285 let can_read_email = permissions.allows_email_read(); 286 ··· 337 if let Some(doc) = did_doc { 338 response["didDoc"] = doc; 339 } 340 - Json(response).into_response() 341 } 342 - Ok(None) => ApiError::AuthenticationFailed(None).into_response(), 343 Err(e) => { 344 error!("Database error in get_session: {:?}", e); 345 - ApiError::InternalError(None).into_response() 346 } 347 } 348 } ··· 350 pub async fn delete_session( 351 State(state): State<AppState>, 352 headers: axum::http::HeaderMap, 353 - _auth: BearerAuth, 354 - ) -> Response { 355 - let extracted = match crate::auth::extract_auth_token_from_header( 356 headers.get("Authorization").and_then(|h| h.to_str().ok()), 357 - ) { 358 - Some(t) => t, 359 - None => return ApiError::AuthenticationRequired.into_response(), 360 - }; 361 - let jti = match crate::auth::get_jti_from_token(&extracted.token) { 362 - Ok(jti) => jti, 363 - Err(_) => return ApiError::AuthenticationFailed(None).into_response(), 364 - }; 365 let did = crate::auth::get_did_from_token(&extracted.token).ok(); 366 match state.session_repo.delete_session_by_access_jti(&jti).await { 367 Ok(rows) if rows > 0 => { ··· 369 let session_cache_key = format!("auth:session:{}:{}", did, jti); 370 let _ = state.cache.delete(&session_cache_key).await; 371 } 372 - EmptyResponse::ok().into_response() 373 } 374 - Ok(_) => ApiError::AuthenticationFailed(None).into_response(), 375 - Err(_) => ApiError::AuthenticationFailed(None).into_response(), 376 } 377 } 378 ··· 796 pub async fn list_sessions( 797 State(state): State<AppState>, 798 headers: HeaderMap, 799 - auth: BearerAuth, 800 - ) -> Response { 801 let current_jti = headers 802 .get("authorization") 803 .and_then(|v| v.to_str().ok()) 804 .and_then(|v| v.strip_prefix("Bearer ")) 805 .and_then(|token| crate::auth::get_jti_from_token(token).ok()); 806 807 - let jwt_rows = match state.session_repo.list_sessions_by_did(&auth.0.did).await { 808 - Ok(rows) => rows, 809 - Err(e) => { 810 error!("DB error fetching JWT sessions: {:?}", e); 811 - return ApiError::InternalError(None).into_response(); 812 - } 813 - }; 814 815 - let oauth_rows = match state.oauth_repo.list_sessions_by_did(&auth.0.did).await { 816 - Ok(rows) => rows, 817 - Err(e) => { 818 error!("DB error fetching OAuth sessions: {:?}", e); 819 - return ApiError::InternalError(None).into_response(); 820 - } 821 - }; 822 823 let jwt_sessions = jwt_rows.into_iter().map(|row| SessionInfo { 824 id: format!("jwt:{}", row.id), ··· 829 is_current: current_jti.as_ref() == Some(&row.access_jti), 830 }); 831 832 - let is_oauth = auth.0.is_oauth; 833 let oauth_sessions = oauth_rows.into_iter().map(|row| { 834 let client_name = extract_client_name(&row.client_id); 835 let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str()); ··· 846 let mut sessions: Vec<SessionInfo> = jwt_sessions.chain(oauth_sessions).collect(); 847 sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); 848 849 - (StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response() 850 } 851 852 fn extract_client_name(client_id: &str) -> String { ··· 867 868 pub async fn revoke_session( 869 State(state): State<AppState>, 870 - auth: BearerAuth, 871 Json(input): Json<RevokeSessionInput>, 872 - ) -> Response { 873 if let Some(jwt_id) = input.session_id.strip_prefix("jwt:") { 874 - let Ok(session_id) = jwt_id.parse::<i32>() else { 875 - return ApiError::InvalidRequest("Invalid session ID".into()).into_response(); 876 - }; 877 - let access_jti = match state 878 .session_repo 879 - .get_session_access_jti_by_id(session_id, &auth.0.did) 880 .await 881 - { 882 - Ok(Some(jti)) => jti, 883 - Ok(None) => { 884 - return ApiError::SessionNotFound.into_response(); 885 - } 886 - Err(e) => { 887 error!("DB error in revoke_session: {:?}", e); 888 - return ApiError::InternalError(None).into_response(); 889 - } 890 - }; 891 - if let Err(e) = state.session_repo.delete_session_by_id(session_id).await { 892 - error!("DB error deleting session: {:?}", e); 893 - return ApiError::InternalError(None).into_response(); 894 - } 895 - let cache_key = format!("auth:session:{}:{}", &auth.0.did, access_jti); 896 if let Err(e) = state.cache.delete(&cache_key).await { 897 warn!("Failed to invalidate session cache: {:?}", e); 898 } 899 - info!(did = %&auth.0.did, session_id = %session_id, "JWT session revoked"); 900 } else if let Some(oauth_id) = input.session_id.strip_prefix("oauth:") { 901 - let Ok(session_id) = oauth_id.parse::<i32>() else { 902 - return ApiError::InvalidRequest("Invalid session ID".into()).into_response(); 903 - }; 904 - match state 905 .oauth_repo 906 - .delete_session_by_id(session_id, &auth.0.did) 907 .await 908 - { 909 - Ok(0) => { 910 - return ApiError::SessionNotFound.into_response(); 911 - } 912 - Err(e) => { 913 error!("DB error deleting OAuth session: {:?}", e); 914 - return ApiError::InternalError(None).into_response(); 915 - } 916 - _ => {} 917 } 918 - info!(did = %&auth.0.did, session_id = %session_id, "OAuth session revoked"); 919 } else { 920 - return ApiError::InvalidRequest("Invalid session ID format".into()).into_response(); 921 } 922 - EmptyResponse::ok().into_response() 923 } 924 925 pub async fn revoke_all_sessions( 926 State(state): State<AppState>, 927 headers: HeaderMap, 928 - auth: BearerAuth, 929 - ) -> Response { 930 - let current_jti = crate::auth::extract_auth_token_from_header( 931 headers.get("authorization").and_then(|v| v.to_str().ok()), 932 ) 933 - .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()); 934 935 - let Some(ref jti) = current_jti else { 936 - return ApiError::InvalidToken(None).into_response(); 937 - }; 938 - 939 - if auth.0.is_oauth { 940 - if let Err(e) = state.session_repo.delete_sessions_by_did(&auth.0.did).await { 941 - error!("DB error revoking JWT sessions: {:?}", e); 942 - return ApiError::InternalError(None).into_response(); 943 - } 944 let jti_typed = TokenId::from(jti.clone()); 945 - if let Err(e) = state 946 .oauth_repo 947 - .delete_sessions_by_did_except(&auth.0.did, &jti_typed) 948 .await 949 - { 950 - error!("DB error revoking OAuth sessions: {:?}", e); 951 - return ApiError::InternalError(None).into_response(); 952 - } 953 } else { 954 - if let Err(e) = state 955 .session_repo 956 - .delete_sessions_by_did_except_jti(&auth.0.did, jti) 957 .await 958 - { 959 - error!("DB error revoking JWT sessions: {:?}", e); 960 - return ApiError::InternalError(None).into_response(); 961 - } 962 - if let Err(e) = state.oauth_repo.delete_sessions_by_did(&auth.0.did).await { 963 - error!("DB error revoking OAuth sessions: {:?}", e); 964 - return ApiError::InternalError(None).into_response(); 965 - } 966 } 967 968 - info!(did = %&auth.0.did, "All other sessions revoked"); 969 - SuccessResponse::ok().into_response() 970 } 971 972 #[derive(Serialize)] ··· 978 979 pub async fn get_legacy_login_preference( 980 State(state): State<AppState>, 981 - auth: BearerAuth, 982 - ) -> Response { 983 - match state.user_repo.get_legacy_login_pref(&auth.0.did).await { 984 - Ok(Some(pref)) => Json(LegacyLoginPreferenceOutput { 985 - allow_legacy_login: pref.allow_legacy_login, 986 - has_mfa: pref.has_mfa, 987 - }) 988 - .into_response(), 989 - Ok(None) => ApiError::AccountNotFound.into_response(), 990 - Err(e) => { 991 error!("DB error: {:?}", e); 992 - ApiError::InternalError(None).into_response() 993 - } 994 - } 995 } 996 997 #[derive(Deserialize)] ··· 1002 1003 pub async fn update_legacy_login_preference( 1004 State(state): State<AppState>, 1005 - auth: BearerAuth, 1006 Json(input): Json<UpdateLegacyLoginInput>, 1007 - ) -> Response { 1008 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 1009 .await 1010 { 1011 - return crate::api::server::reauth::legacy_mfa_required_response( 1012 &*state.user_repo, 1013 &*state.session_repo, 1014 - &auth.0.did, 1015 ) 1016 - .await; 1017 } 1018 1019 - if crate::api::server::reauth::check_reauth_required(&*state.session_repo, &auth.0.did).await { 1020 - return crate::api::server::reauth::reauth_required_response( 1021 &*state.user_repo, 1022 &*state.session_repo, 1023 - &auth.0.did, 1024 ) 1025 - .await; 1026 } 1027 1028 - match state 1029 .user_repo 1030 - .update_legacy_login(&auth.0.did, input.allow_legacy_login) 1031 .await 1032 - { 1033 - Ok(true) => { 1034 - info!( 1035 - did = %&auth.0.did, 1036 - allow_legacy_login = input.allow_legacy_login, 1037 - "Legacy login preference updated" 1038 - ); 1039 - Json(json!({ 1040 - "allowLegacyLogin": input.allow_legacy_login 1041 - })) 1042 - .into_response() 1043 - } 1044 - Ok(false) => ApiError::AccountNotFound.into_response(), 1045 - Err(e) => { 1046 error!("DB error: {:?}", e); 1047 - ApiError::InternalError(None).into_response() 1048 - } 1049 } 1050 } 1051 1052 use crate::comms::VALID_LOCALES; ··· 1059 1060 pub async fn update_locale( 1061 State(state): State<AppState>, 1062 - auth: BearerAuth, 1063 Json(input): Json<UpdateLocaleInput>, 1064 - ) -> Response { 1065 if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) { 1066 - return ApiError::InvalidRequest(format!( 1067 "Invalid locale. Valid options: {}", 1068 VALID_LOCALES.join(", ") 1069 - )) 1070 - .into_response(); 1071 } 1072 1073 - match state 1074 .user_repo 1075 - .update_locale(&auth.0.did, &input.preferred_locale) 1076 .await 1077 - { 1078 - Ok(true) => { 1079 - info!( 1080 - did = %&auth.0.did, 1081 - locale = %input.preferred_locale, 1082 - "User locale preference updated" 1083 - ); 1084 - Json(json!({ 1085 - "preferredLocale": input.preferred_locale 1086 - })) 1087 - .into_response() 1088 - } 1089 - Ok(false) => ApiError::AccountNotFound.into_response(), 1090 - Err(e) => { 1091 error!("DB error updating locale: {:?}", e); 1092 - ApiError::InternalError(None).into_response() 1093 - } 1094 } 1095 }
··· 1 use crate::api::error::ApiError; 2 use crate::api::{EmptyResponse, SuccessResponse}; 3 + use crate::auth::RequiredAuth; 4 use crate::state::{AppState, RateLimitKind}; 5 use crate::types::{AccountState, Did, Handle, PlainPassword}; 6 use axum::{ ··· 279 280 pub async fn get_session( 281 State(state): State<AppState>, 282 + auth: RequiredAuth, 283 + ) -> Result<Response, ApiError> { 284 + let auth_user = auth.0.require_user()?.require_not_takendown()?; 285 let permissions = auth_user.permissions(); 286 let can_read_email = permissions.allows_email_read(); 287 ··· 338 if let Some(doc) = did_doc { 339 response["didDoc"] = doc; 340 } 341 + Ok(Json(response).into_response()) 342 } 343 + Ok(None) => Err(ApiError::AuthenticationFailed(None)), 344 Err(e) => { 345 error!("Database error in get_session: {:?}", e); 346 + Err(ApiError::InternalError(None)) 347 } 348 } 349 } ··· 351 pub async fn delete_session( 352 State(state): State<AppState>, 353 headers: axum::http::HeaderMap, 354 + auth: RequiredAuth, 355 + ) -> Result<Response, ApiError> { 356 + auth.0.require_user()?.require_active()?; 357 + let extracted = crate::auth::extract_auth_token_from_header( 358 headers.get("Authorization").and_then(|h| h.to_str().ok()), 359 + ) 360 + .ok_or(ApiError::AuthenticationRequired)?; 361 + let jti = crate::auth::get_jti_from_token(&extracted.token) 362 + .map_err(|_| ApiError::AuthenticationFailed(None))?; 363 let did = crate::auth::get_did_from_token(&extracted.token).ok(); 364 match state.session_repo.delete_session_by_access_jti(&jti).await { 365 Ok(rows) if rows > 0 => { ··· 367 let session_cache_key = format!("auth:session:{}:{}", did, jti); 368 let _ = state.cache.delete(&session_cache_key).await; 369 } 370 + Ok(EmptyResponse::ok().into_response()) 371 } 372 + Ok(_) => Err(ApiError::AuthenticationFailed(None)), 373 + Err(_) => Err(ApiError::AuthenticationFailed(None)), 374 } 375 } 376 ··· 794 pub async fn list_sessions( 795 State(state): State<AppState>, 796 headers: HeaderMap, 797 + auth: RequiredAuth, 798 + ) -> Result<Response, ApiError> { 799 + let auth_user = auth.0.require_user()?.require_active()?; 800 let current_jti = headers 801 .get("authorization") 802 .and_then(|v| v.to_str().ok()) 803 .and_then(|v| v.strip_prefix("Bearer ")) 804 .and_then(|token| crate::auth::get_jti_from_token(token).ok()); 805 806 + let jwt_rows = state 807 + .session_repo 808 + .list_sessions_by_did(&auth_user.did) 809 + .await 810 + .map_err(|e| { 811 error!("DB error fetching JWT sessions: {:?}", e); 812 + ApiError::InternalError(None) 813 + })?; 814 815 + let oauth_rows = state 816 + .oauth_repo 817 + .list_sessions_by_did(&auth_user.did) 818 + .await 819 + .map_err(|e| { 820 error!("DB error fetching OAuth sessions: {:?}", e); 821 + ApiError::InternalError(None) 822 + })?; 823 824 let jwt_sessions = jwt_rows.into_iter().map(|row| SessionInfo { 825 id: format!("jwt:{}", row.id), ··· 830 is_current: current_jti.as_ref() == Some(&row.access_jti), 831 }); 832 833 + let is_oauth = auth_user.is_oauth; 834 let oauth_sessions = oauth_rows.into_iter().map(|row| { 835 let client_name = extract_client_name(&row.client_id); 836 let is_current_oauth = is_oauth && current_jti.as_deref() == Some(row.token_id.as_str()); ··· 847 let mut sessions: Vec<SessionInfo> = jwt_sessions.chain(oauth_sessions).collect(); 848 sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); 849 850 + Ok((StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response()) 851 } 852 853 fn extract_client_name(client_id: &str) -> String { ··· 868 869 pub async fn revoke_session( 870 State(state): State<AppState>, 871 + auth: RequiredAuth, 872 Json(input): Json<RevokeSessionInput>, 873 + ) -> Result<Response, ApiError> { 874 + let auth_user = auth.0.require_user()?.require_active()?; 875 if let Some(jwt_id) = input.session_id.strip_prefix("jwt:") { 876 + let session_id: i32 = jwt_id 877 + .parse() 878 + .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; 879 + let access_jti = state 880 .session_repo 881 + .get_session_access_jti_by_id(session_id, &auth_user.did) 882 .await 883 + .map_err(|e| { 884 error!("DB error in revoke_session: {:?}", e); 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); 897 if let Err(e) = state.cache.delete(&cache_key).await { 898 warn!("Failed to invalidate session cache: {:?}", e); 899 } 900 + info!(did = %&auth_user.did, session_id = %session_id, "JWT session revoked"); 901 } else if let Some(oauth_id) = input.session_id.strip_prefix("oauth:") { 902 + let session_id: i32 = oauth_id 903 + .parse() 904 + .map_err(|_| ApiError::InvalidRequest("Invalid session ID".into()))?; 905 + let deleted = state 906 .oauth_repo 907 + .delete_session_by_id(session_id, &auth_user.did) 908 .await 909 + .map_err(|e| { 910 error!("DB error deleting OAuth session: {:?}", e); 911 + ApiError::InternalError(None) 912 + })?; 913 + if deleted == 0 { 914 + return Err(ApiError::SessionNotFound); 915 } 916 + info!(did = %&auth_user.did, session_id = %session_id, "OAuth session revoked"); 917 } else { 918 + return Err(ApiError::InvalidRequest("Invalid session ID format".into())); 919 } 920 + Ok(EmptyResponse::ok().into_response()) 921 } 922 923 pub async fn revoke_all_sessions( 924 State(state): State<AppState>, 925 headers: HeaderMap, 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( 930 headers.get("authorization").and_then(|v| v.to_str().ok()), 931 ) 932 + .and_then(|extracted| crate::auth::get_jti_from_token(&extracted.token).ok()) 933 + .ok_or(ApiError::InvalidToken(None))?; 934 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 let jti_typed = TokenId::from(jti.clone()); 945 + state 946 .oauth_repo 947 + .delete_sessions_by_did_except(&auth_user.did, &jti_typed) 948 .await 949 + .map_err(|e| { 950 + error!("DB error revoking OAuth sessions: {:?}", e); 951 + ApiError::InternalError(None) 952 + })?; 953 } else { 954 + state 955 .session_repo 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) 965 .await 966 + .map_err(|e| { 967 + error!("DB error revoking OAuth sessions: {:?}", e); 968 + ApiError::InternalError(None) 969 + })?; 970 } 971 972 + info!(did = %&auth_user.did, "All other sessions revoked"); 973 + Ok(SuccessResponse::ok().into_response()) 974 } 975 976 #[derive(Serialize)] ··· 982 983 pub async fn get_legacy_login_preference( 984 State(state): State<AppState>, 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| { 993 error!("DB error: {:?}", e); 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()) 1002 } 1003 1004 #[derive(Deserialize)] ··· 1009 1010 pub async fn update_legacy_login_preference( 1011 State(state): State<AppState>, 1012 + auth: RequiredAuth, 1013 Json(input): Json<UpdateLegacyLoginInput>, 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) 1017 .await 1018 { 1019 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 1020 &*state.user_repo, 1021 &*state.session_repo, 1022 + &auth_user.did, 1023 ) 1024 + .await); 1025 } 1026 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( 1030 &*state.user_repo, 1031 &*state.session_repo, 1032 + &auth_user.did, 1033 ) 1034 + .await); 1035 } 1036 1037 + let updated = state 1038 .user_repo 1039 + .update_legacy_login(&auth_user.did, input.allow_legacy_login) 1040 .await 1041 + .map_err(|e| { 1042 error!("DB error: {:?}", e); 1043 + ApiError::InternalError(None) 1044 + })?; 1045 + if !updated { 1046 + return Err(ApiError::AccountNotFound); 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()) 1057 } 1058 1059 use crate::comms::VALID_LOCALES; ··· 1066 1067 pub async fn update_locale( 1068 State(state): State<AppState>, 1069 + auth: RequiredAuth, 1070 Json(input): Json<UpdateLocaleInput>, 1071 + ) -> Result<Response, ApiError> { 1072 + let auth_user = auth.0.require_user()?.require_active()?; 1073 if !VALID_LOCALES.contains(&input.preferred_locale.as_str()) { 1074 + return Err(ApiError::InvalidRequest(format!( 1075 "Invalid locale. Valid options: {}", 1076 VALID_LOCALES.join(", ") 1077 + ))); 1078 } 1079 1080 + let updated = state 1081 .user_repo 1082 + .update_locale(&auth_user.did, &input.preferred_locale) 1083 .await 1084 + .map_err(|e| { 1085 error!("DB error updating locale: {:?}", e); 1086 + ApiError::InternalError(None) 1087 + })?; 1088 + if !updated { 1089 + return Err(ApiError::AccountNotFound); 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()) 1100 }
+166 -160
crates/tranquil-pds/src/api/server/totp.rs
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 - use crate::auth::BearerAuth; 4 use crate::auth::{ 5 decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, 6 generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, ··· 26 pub qr_base64: String, 27 } 28 29 - pub async fn create_totp_secret(State(state): State<AppState>, auth: BearerAuth) -> Response { 30 - match state.user_repo.get_totp_record(&auth.0.did).await { 31 - Ok(Some(record)) if record.verified => return ApiError::TotpAlreadyEnabled.into_response(), 32 Ok(_) => {} 33 Err(e) => { 34 error!("DB error checking TOTP: {:?}", e); 35 - return ApiError::InternalError(None).into_response(); 36 } 37 } 38 39 let secret = generate_totp_secret(); 40 41 - let handle = match state.user_repo.get_handle_by_did(&auth.0.did).await { 42 - Ok(Some(h)) => h, 43 - Ok(None) => return ApiError::AccountNotFound.into_response(), 44 - Err(e) => { 45 error!("DB error fetching handle: {:?}", e); 46 - return ApiError::InternalError(None).into_response(); 47 - } 48 - }; 49 50 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 51 let uri = generate_totp_uri(&secret, &handle, &hostname); 52 53 - let qr_code = match generate_qr_png_base64(&secret, &handle, &hostname) { 54 - Ok(qr) => qr, 55 - Err(e) => { 56 - error!("Failed to generate QR code: {:?}", e); 57 - return ApiError::InternalError(Some("Failed to generate QR code".into())) 58 - .into_response(); 59 - } 60 - }; 61 62 - let encrypted_secret = match encrypt_totp_secret(&secret) { 63 - Ok(enc) => enc, 64 - Err(e) => { 65 - error!("Failed to encrypt TOTP secret: {:?}", e); 66 - return ApiError::InternalError(None).into_response(); 67 - } 68 - }; 69 70 - if let Err(e) = state 71 .user_repo 72 - .upsert_totp_secret(&auth.0.did, &encrypted_secret, ENCRYPTION_VERSION) 73 .await 74 - { 75 - error!("Failed to store TOTP secret: {:?}", e); 76 - return ApiError::InternalError(None).into_response(); 77 - } 78 79 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 80 81 - info!(did = %&auth.0.did, "TOTP secret created (pending verification)"); 82 83 - Json(CreateTotpSecretResponse { 84 secret: secret_base32, 85 uri, 86 qr_base64: qr_code, 87 }) 88 - .into_response() 89 } 90 91 #[derive(Deserialize)] ··· 101 102 pub async fn enable_totp( 103 State(state): State<AppState>, 104 - auth: BearerAuth, 105 Json(input): Json<EnableTotpInput>, 106 - ) -> Response { 107 if !state 108 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 109 .await 110 { 111 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 112 - return ApiError::RateLimitExceeded(None).into_response(); 113 } 114 115 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 116 Ok(Some(row)) => row, 117 - Ok(None) => return ApiError::TotpNotEnabled.into_response(), 118 Err(e) => { 119 error!("DB error fetching TOTP: {:?}", e); 120 - return ApiError::InternalError(None).into_response(); 121 } 122 }; 123 124 if totp_record.verified { 125 - return ApiError::TotpAlreadyEnabled.into_response(); 126 } 127 128 - let secret = match decrypt_totp_secret( 129 &totp_record.secret_encrypted, 130 totp_record.encryption_version, 131 - ) { 132 - Ok(s) => s, 133 - Err(e) => { 134 - error!("Failed to decrypt TOTP secret: {:?}", e); 135 - return ApiError::InternalError(None).into_response(); 136 - } 137 - }; 138 139 let code = input.code.trim(); 140 if !verify_totp_code(&secret, code) { 141 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 142 } 143 144 let backup_codes = generate_backup_codes(); 145 - let backup_hashes: Result<Vec<_>, _> = 146 - backup_codes.iter().map(|c| hash_backup_code(c)).collect(); 147 - let backup_hashes = match backup_hashes { 148 - Ok(hashes) => hashes, 149 - Err(e) => { 150 error!("Failed to hash backup code: {:?}", e); 151 - return ApiError::InternalError(None).into_response(); 152 - } 153 - }; 154 155 - if let Err(e) = state 156 .user_repo 157 - .enable_totp_with_backup_codes(&auth.0.did, &backup_hashes) 158 .await 159 - { 160 - error!("Failed to enable TOTP: {:?}", e); 161 - return ApiError::InternalError(None).into_response(); 162 - } 163 164 - info!(did = %&auth.0.did, "TOTP enabled with {} backup codes", backup_codes.len()); 165 166 - Json(EnableTotpResponse { backup_codes }).into_response() 167 } 168 169 #[derive(Deserialize)] ··· 174 175 pub async fn disable_totp( 176 State(state): State<AppState>, 177 - auth: BearerAuth, 178 Json(input): Json<DisableTotpInput>, 179 - ) -> Response { 180 - if !crate::api::server::reauth::check_legacy_session_mfa(&*state.session_repo, &auth.0.did) 181 .await 182 { 183 - return crate::api::server::reauth::legacy_mfa_required_response( 184 &*state.user_repo, 185 &*state.session_repo, 186 - &auth.0.did, 187 ) 188 - .await; 189 } 190 191 if !state 192 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 193 .await 194 { 195 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 196 - return ApiError::RateLimitExceeded(None).into_response(); 197 } 198 199 - let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await { 200 - Ok(Some(hash)) => hash, 201 - Ok(None) => return ApiError::AccountNotFound.into_response(), 202 - Err(e) => { 203 error!("DB error fetching user: {:?}", e); 204 - return ApiError::InternalError(None).into_response(); 205 - } 206 - }; 207 208 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 209 if !password_valid { 210 - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 211 } 212 213 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 214 Ok(Some(row)) if row.verified => row, 215 - Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 216 Err(e) => { 217 error!("DB error fetching TOTP: {:?}", e); 218 - return ApiError::InternalError(None).into_response(); 219 } 220 }; 221 222 let code = input.code.trim(); 223 let code_valid = if is_backup_code_format(code) { 224 - verify_backup_code_for_user(&state, &auth.0.did, code).await 225 } else { 226 - let secret = match decrypt_totp_secret( 227 &totp_record.secret_encrypted, 228 totp_record.encryption_version, 229 - ) { 230 - Ok(s) => s, 231 - Err(e) => { 232 - error!("Failed to decrypt TOTP secret: {:?}", e); 233 - return ApiError::InternalError(None).into_response(); 234 - } 235 - }; 236 verify_totp_code(&secret, code) 237 }; 238 239 if !code_valid { 240 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 241 } 242 243 - if let Err(e) = state 244 .user_repo 245 - .delete_totp_and_backup_codes(&auth.0.did) 246 .await 247 - { 248 - error!("Failed to delete TOTP: {:?}", e); 249 - return ApiError::InternalError(None).into_response(); 250 - } 251 252 - info!(did = %&auth.0.did, "TOTP disabled"); 253 254 - EmptyResponse::ok().into_response() 255 } 256 257 #[derive(Serialize)] ··· 262 pub backup_codes_remaining: i64, 263 } 264 265 - pub async fn get_totp_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 266 - let enabled = match state.user_repo.get_totp_record(&auth.0.did).await { 267 Ok(Some(row)) => row.verified, 268 Ok(None) => false, 269 Err(e) => { 270 error!("DB error fetching TOTP status: {:?}", e); 271 - return ApiError::InternalError(None).into_response(); 272 } 273 }; 274 275 - let backup_count = match state.user_repo.count_unused_backup_codes(&auth.0.did).await { 276 - Ok(count) => count, 277 - Err(e) => { 278 error!("DB error counting backup codes: {:?}", e); 279 - return ApiError::InternalError(None).into_response(); 280 - } 281 - }; 282 283 - Json(GetTotpStatusResponse { 284 enabled, 285 has_backup_codes: backup_count > 0, 286 backup_codes_remaining: backup_count, 287 }) 288 - .into_response() 289 } 290 291 #[derive(Deserialize)] ··· 302 303 pub async fn regenerate_backup_codes( 304 State(state): State<AppState>, 305 - auth: BearerAuth, 306 Json(input): Json<RegenerateBackupCodesInput>, 307 - ) -> Response { 308 if !state 309 - .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 310 .await 311 { 312 - warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 313 - return ApiError::RateLimitExceeded(None).into_response(); 314 } 315 316 - let password_hash = match state.user_repo.get_password_hash_by_did(&auth.0.did).await { 317 - Ok(Some(hash)) => hash, 318 - Ok(None) => return ApiError::AccountNotFound.into_response(), 319 - Err(e) => { 320 error!("DB error fetching user: {:?}", e); 321 - return ApiError::InternalError(None).into_response(); 322 - } 323 - }; 324 325 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 326 if !password_valid { 327 - return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 328 } 329 330 - let totp_record = match state.user_repo.get_totp_record(&auth.0.did).await { 331 Ok(Some(row)) if row.verified => row, 332 - Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 333 Err(e) => { 334 error!("DB error fetching TOTP: {:?}", e); 335 - return ApiError::InternalError(None).into_response(); 336 } 337 }; 338 339 - let secret = match decrypt_totp_secret( 340 &totp_record.secret_encrypted, 341 totp_record.encryption_version, 342 - ) { 343 - Ok(s) => s, 344 - Err(e) => { 345 - error!("Failed to decrypt TOTP secret: {:?}", e); 346 - return ApiError::InternalError(None).into_response(); 347 - } 348 - }; 349 350 let code = input.code.trim(); 351 if !verify_totp_code(&secret, code) { 352 - return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 353 } 354 355 let backup_codes = generate_backup_codes(); 356 - let backup_hashes: Result<Vec<_>, _> = 357 - backup_codes.iter().map(|c| hash_backup_code(c)).collect(); 358 - let backup_hashes = match backup_hashes { 359 - Ok(hashes) => hashes, 360 - Err(e) => { 361 error!("Failed to hash backup code: {:?}", e); 362 - return ApiError::InternalError(None).into_response(); 363 - } 364 - }; 365 366 - if let Err(e) = state 367 .user_repo 368 - .replace_backup_codes(&auth.0.did, &backup_hashes) 369 .await 370 - { 371 - error!("Failed to regenerate backup codes: {:?}", e); 372 - return ApiError::InternalError(None).into_response(); 373 - } 374 375 - info!(did = %&auth.0.did, "Backup codes regenerated"); 376 377 - Json(RegenerateBackupCodesResponse { backup_codes }).into_response() 378 } 379 380 async fn verify_backup_code_for_user(
··· 1 use crate::api::EmptyResponse; 2 use crate::api::error::ApiError; 3 + use crate::auth::RequiredAuth; 4 use crate::auth::{ 5 decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, 6 generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, ··· 26 pub qr_base64: String, 27 } 28 29 + pub async fn create_totp_secret( 30 + State(state): State<AppState>, 31 + auth: 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), 36 Ok(_) => {} 37 Err(e) => { 38 error!("DB error checking TOTP: {:?}", e); 39 + return Err(ApiError::InternalError(None)); 40 } 41 } 42 43 let secret = generate_totp_secret(); 44 45 + let handle = state 46 + .user_repo 47 + .get_handle_by_did(&auth_user.did) 48 + .await 49 + .map_err(|e| { 50 error!("DB error fetching handle: {:?}", e); 51 + ApiError::InternalError(None) 52 + })? 53 + .ok_or(ApiError::AccountNotFound)?; 54 55 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 56 let uri = generate_totp_uri(&secret, &handle, &hostname); 57 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 + })?; 62 63 + let encrypted_secret = encrypt_totp_secret(&secret).map_err(|e| { 64 + error!("Failed to encrypt TOTP secret: {:?}", e); 65 + ApiError::InternalError(None) 66 + })?; 67 68 + state 69 .user_repo 70 + .upsert_totp_secret(&auth_user.did, &encrypted_secret, ENCRYPTION_VERSION) 71 .await 72 + .map_err(|e| { 73 + error!("Failed to store TOTP secret: {:?}", e); 74 + ApiError::InternalError(None) 75 + })?; 76 77 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 78 79 + info!(did = %&auth_user.did, "TOTP secret created (pending verification)"); 80 81 + Ok(Json(CreateTotpSecretResponse { 82 secret: secret_base32, 83 uri, 84 qr_base64: qr_code, 85 }) 86 + .into_response()) 87 } 88 89 #[derive(Deserialize)] ··· 99 100 pub async fn enable_totp( 101 State(state): State<AppState>, 102 + auth: RequiredAuth, 103 Json(input): Json<EnableTotpInput>, 104 + ) -> Result<Response, ApiError> { 105 + let auth_user = auth.0.require_user()?.require_active()?; 106 if !state 107 + .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 108 .await 109 { 110 + warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 111 + return Err(ApiError::RateLimitExceeded(None)); 112 } 113 114 + let totp_record = match state.user_repo.get_totp_record(&auth_user.did).await { 115 Ok(Some(row)) => row, 116 + Ok(None) => return Err(ApiError::TotpNotEnabled), 117 Err(e) => { 118 error!("DB error fetching TOTP: {:?}", e); 119 + return Err(ApiError::InternalError(None)); 120 } 121 }; 122 123 if totp_record.verified { 124 + return Err(ApiError::TotpAlreadyEnabled); 125 } 126 127 + let secret = decrypt_totp_secret( 128 &totp_record.secret_encrypted, 129 totp_record.encryption_version, 130 + ) 131 + .map_err(|e| { 132 + error!("Failed to decrypt TOTP secret: {:?}", e); 133 + ApiError::InternalError(None) 134 + })?; 135 136 let code = input.code.trim(); 137 if !verify_totp_code(&secret, code) { 138 + return Err(ApiError::InvalidCode(Some( 139 + "Invalid verification code".into(), 140 + ))); 141 } 142 143 let backup_codes = generate_backup_codes(); 144 + let backup_hashes: Vec<_> = backup_codes 145 + .iter() 146 + .map(|c| hash_backup_code(c)) 147 + .collect::<Result<Vec<_>, _>>() 148 + .map_err(|e| { 149 error!("Failed to hash backup code: {:?}", e); 150 + ApiError::InternalError(None) 151 + })?; 152 153 + state 154 .user_repo 155 + .enable_totp_with_backup_codes(&auth_user.did, &backup_hashes) 156 .await 157 + .map_err(|e| { 158 + error!("Failed to enable TOTP: {:?}", e); 159 + ApiError::InternalError(None) 160 + })?; 161 162 + info!(did = %&auth_user.did, "TOTP enabled with {} backup codes", backup_codes.len()); 163 164 + Ok(Json(EnableTotpResponse { backup_codes }).into_response()) 165 } 166 167 #[derive(Deserialize)] ··· 172 173 pub async fn disable_totp( 174 State(state): State<AppState>, 175 + auth: RequiredAuth, 176 Json(input): Json<DisableTotpInput>, 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) 180 .await 181 { 182 + return Ok(crate::api::server::reauth::legacy_mfa_required_response( 183 &*state.user_repo, 184 &*state.session_repo, 185 + &auth_user.did, 186 ) 187 + .await); 188 } 189 190 if !state 191 + .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 192 .await 193 { 194 + warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 195 + return Err(ApiError::RateLimitExceeded(None)); 196 } 197 198 + let password_hash = state 199 + .user_repo 200 + .get_password_hash_by_did(&auth_user.did) 201 + .await 202 + .map_err(|e| { 203 error!("DB error fetching user: {:?}", e); 204 + ApiError::InternalError(None) 205 + })? 206 + .ok_or(ApiError::AccountNotFound)?; 207 208 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 209 if !password_valid { 210 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 211 } 212 213 + let totp_record = match state.user_repo.get_totp_record(&auth_user.did).await { 214 Ok(Some(row)) if row.verified => row, 215 + Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 216 Err(e) => { 217 error!("DB error fetching TOTP: {:?}", e); 218 + return Err(ApiError::InternalError(None)); 219 } 220 }; 221 222 let code = input.code.trim(); 223 let code_valid = if is_backup_code_format(code) { 224 + verify_backup_code_for_user(&state, &auth_user.did, code).await 225 } else { 226 + let secret = decrypt_totp_secret( 227 &totp_record.secret_encrypted, 228 totp_record.encryption_version, 229 + ) 230 + .map_err(|e| { 231 + error!("Failed to decrypt TOTP secret: {:?}", e); 232 + ApiError::InternalError(None) 233 + })?; 234 verify_totp_code(&secret, code) 235 }; 236 237 if !code_valid { 238 + return Err(ApiError::InvalidCode(Some( 239 + "Invalid verification code".into(), 240 + ))); 241 } 242 243 + state 244 .user_repo 245 + .delete_totp_and_backup_codes(&auth_user.did) 246 .await 247 + .map_err(|e| { 248 + error!("Failed to delete TOTP: {:?}", e); 249 + ApiError::InternalError(None) 250 + })?; 251 252 + info!(did = %&auth_user.did, "TOTP disabled"); 253 254 + Ok(EmptyResponse::ok().into_response()) 255 } 256 257 #[derive(Serialize)] ··· 262 pub backup_codes_remaining: i64, 263 } 264 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 { 271 Ok(Some(row)) => row.verified, 272 Ok(None) => false, 273 Err(e) => { 274 error!("DB error fetching TOTP status: {:?}", e); 275 + return Err(ApiError::InternalError(None)); 276 } 277 }; 278 279 + let backup_count = state 280 + .user_repo 281 + .count_unused_backup_codes(&auth_user.did) 282 + .await 283 + .map_err(|e| { 284 error!("DB error counting backup codes: {:?}", e); 285 + ApiError::InternalError(None) 286 + })?; 287 288 + Ok(Json(GetTotpStatusResponse { 289 enabled, 290 has_backup_codes: backup_count > 0, 291 backup_codes_remaining: backup_count, 292 }) 293 + .into_response()) 294 } 295 296 #[derive(Deserialize)] ··· 307 308 pub async fn regenerate_backup_codes( 309 State(state): State<AppState>, 310 + auth: RequiredAuth, 311 Json(input): Json<RegenerateBackupCodesInput>, 312 + ) -> Result<Response, ApiError> { 313 + let auth_user = auth.0.require_user()?.require_active()?; 314 if !state 315 + .check_rate_limit(RateLimitKind::TotpVerify, &auth_user.did) 316 .await 317 { 318 + warn!(did = %&auth_user.did, "TOTP verification rate limit exceeded"); 319 + return Err(ApiError::RateLimitExceeded(None)); 320 } 321 322 + let password_hash = state 323 + .user_repo 324 + .get_password_hash_by_did(&auth_user.did) 325 + .await 326 + .map_err(|e| { 327 error!("DB error fetching user: {:?}", e); 328 + ApiError::InternalError(None) 329 + })? 330 + .ok_or(ApiError::AccountNotFound)?; 331 332 let password_valid = bcrypt::verify(&input.password, &password_hash).unwrap_or(false); 333 if !password_valid { 334 + return Err(ApiError::InvalidPassword("Password is incorrect".into())); 335 } 336 337 + let totp_record = match state.user_repo.get_totp_record(&auth_user.did).await { 338 Ok(Some(row)) if row.verified => row, 339 + Ok(Some(_)) | Ok(None) => return Err(ApiError::TotpNotEnabled), 340 Err(e) => { 341 error!("DB error fetching TOTP: {:?}", e); 342 + return Err(ApiError::InternalError(None)); 343 } 344 }; 345 346 + let secret = decrypt_totp_secret( 347 &totp_record.secret_encrypted, 348 totp_record.encryption_version, 349 + ) 350 + .map_err(|e| { 351 + error!("Failed to decrypt TOTP secret: {:?}", e); 352 + ApiError::InternalError(None) 353 + })?; 354 355 let code = input.code.trim(); 356 if !verify_totp_code(&secret, code) { 357 + return Err(ApiError::InvalidCode(Some( 358 + "Invalid verification code".into(), 359 + ))); 360 } 361 362 let backup_codes = generate_backup_codes(); 363 + let backup_hashes: Vec<_> = backup_codes 364 + .iter() 365 + .map(|c| hash_backup_code(c)) 366 + .collect::<Result<Vec<_>, _>>() 367 + .map_err(|e| { 368 error!("Failed to hash backup code: {:?}", e); 369 + ApiError::InternalError(None) 370 + })?; 371 372 + state 373 .user_repo 374 + .replace_backup_codes(&auth_user.did, &backup_hashes) 375 .await 376 + .map_err(|e| { 377 + error!("Failed to regenerate backup codes: {:?}", e); 378 + ApiError::InternalError(None) 379 + })?; 380 381 + info!(did = %&auth_user.did, "Backup codes regenerated"); 382 383 + Ok(Json(RegenerateBackupCodesResponse { backup_codes }).into_response()) 384 } 385 386 async fn verify_backup_code_for_user(
+60 -55
crates/tranquil-pds/src/api/server/trusted_devices.rs
··· 11 use tranquil_db_traits::OAuthRepository; 12 use tranquil_types::DeviceId; 13 14 - use crate::auth::BearerAuth; 15 use crate::state::AppState; 16 17 const TRUST_DURATION_DAYS: i64 = 30; ··· 71 pub devices: Vec<TrustedDevice>, 72 } 73 74 - pub async fn list_trusted_devices(State(state): State<AppState>, auth: BearerAuth) -> Response { 75 - match state.oauth_repo.list_trusted_devices(&auth.0.did).await { 76 - Ok(rows) => { 77 - let devices = rows 78 - .into_iter() 79 - .map(|row| { 80 - let trust_state = 81 - DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until); 82 - TrustedDevice { 83 - id: row.id, 84 - user_agent: row.user_agent, 85 - friendly_name: row.friendly_name, 86 - trusted_at: row.trusted_at, 87 - trusted_until: row.trusted_until, 88 - last_seen_at: row.last_seen_at, 89 - trust_state, 90 - } 91 - }) 92 - .collect(); 93 - Json(ListTrustedDevicesResponse { devices }).into_response() 94 - } 95 - Err(e) => { 96 error!("DB error: {:?}", e); 97 - ApiError::InternalError(None).into_response() 98 - } 99 - } 100 } 101 102 #[derive(Deserialize)] ··· 107 108 pub async fn revoke_trusted_device( 109 State(state): State<AppState>, 110 - auth: BearerAuth, 111 Json(input): Json<RevokeTrustedDeviceInput>, 112 - ) -> Response { 113 let device_id = DeviceId::from(input.device_id.clone()); 114 match state 115 .oauth_repo 116 - .device_belongs_to_user(&device_id, &auth.0.did) 117 .await 118 { 119 Ok(true) => {} 120 Ok(false) => { 121 - return ApiError::DeviceNotFound.into_response(); 122 } 123 Err(e) => { 124 error!("DB error: {:?}", e); 125 - return ApiError::InternalError(None).into_response(); 126 } 127 } 128 129 - match state.oauth_repo.revoke_device_trust(&device_id).await { 130 - Ok(()) => { 131 - info!(did = %&auth.0.did, device_id = %input.device_id, "Trusted device revoked"); 132 - SuccessResponse::ok().into_response() 133 - } 134 - Err(e) => { 135 error!("DB error: {:?}", e); 136 - ApiError::InternalError(None).into_response() 137 - } 138 - } 139 } 140 141 #[derive(Deserialize)] ··· 147 148 pub async fn update_trusted_device( 149 State(state): State<AppState>, 150 - auth: BearerAuth, 151 Json(input): Json<UpdateTrustedDeviceInput>, 152 - ) -> Response { 153 let device_id = DeviceId::from(input.device_id.clone()); 154 match state 155 .oauth_repo 156 - .device_belongs_to_user(&device_id, &auth.0.did) 157 .await 158 { 159 Ok(true) => {} 160 Ok(false) => { 161 - return ApiError::DeviceNotFound.into_response(); 162 } 163 Err(e) => { 164 error!("DB error: {:?}", e); 165 - return ApiError::InternalError(None).into_response(); 166 } 167 } 168 169 - match state 170 .oauth_repo 171 .update_device_friendly_name(&device_id, input.friendly_name.as_deref()) 172 .await 173 - { 174 - Ok(()) => { 175 - info!(did = %auth.0.did, device_id = %input.device_id, "Trusted device updated"); 176 - SuccessResponse::ok().into_response() 177 - } 178 - Err(e) => { 179 error!("DB error: {:?}", e); 180 - ApiError::InternalError(None).into_response() 181 - } 182 - } 183 } 184 185 pub async fn get_device_trust_state(
··· 11 use tranquil_db_traits::OAuthRepository; 12 use tranquil_types::DeviceId; 13 14 + use crate::auth::RequiredAuth; 15 use crate::state::AppState; 16 17 const TRUST_DURATION_DAYS: i64 = 30; ··· 71 pub devices: Vec<TrustedDevice>, 72 } 73 74 + pub async fn list_trusted_devices( 75 + State(state): State<AppState>, 76 + auth: 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| { 84 error!("DB error: {:?}", e); 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()) 105 } 106 107 #[derive(Deserialize)] ··· 112 113 pub async fn revoke_trusted_device( 114 State(state): State<AppState>, 115 + auth: RequiredAuth, 116 Json(input): Json<RevokeTrustedDeviceInput>, 117 + ) -> Result<Response, ApiError> { 118 + let auth_user = auth.0.require_user()?.require_active()?; 119 let device_id = DeviceId::from(input.device_id.clone()); 120 match state 121 .oauth_repo 122 + .device_belongs_to_user(&device_id, &auth_user.did) 123 .await 124 { 125 Ok(true) => {} 126 Ok(false) => { 127 + return Err(ApiError::DeviceNotFound); 128 } 129 Err(e) => { 130 error!("DB error: {:?}", e); 131 + return Err(ApiError::InternalError(None)); 132 } 133 } 134 135 + state 136 + .oauth_repo 137 + .revoke_device_trust(&device_id) 138 + .await 139 + .map_err(|e| { 140 error!("DB error: {:?}", e); 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()) 146 } 147 148 #[derive(Deserialize)] ··· 154 155 pub async fn update_trusted_device( 156 State(state): State<AppState>, 157 + auth: RequiredAuth, 158 Json(input): Json<UpdateTrustedDeviceInput>, 159 + ) -> Result<Response, ApiError> { 160 + let auth_user = auth.0.require_user()?.require_active()?; 161 let device_id = DeviceId::from(input.device_id.clone()); 162 match state 163 .oauth_repo 164 + .device_belongs_to_user(&device_id, &auth_user.did) 165 .await 166 { 167 Ok(true) => {} 168 Ok(false) => { 169 + return Err(ApiError::DeviceNotFound); 170 } 171 Err(e) => { 172 error!("DB error: {:?}", e); 173 + return Err(ApiError::InternalError(None)); 174 } 175 } 176 177 + state 178 .oauth_repo 179 .update_device_friendly_name(&device_id, input.friendly_name.as_deref()) 180 .await 181 + .map_err(|e| { 182 error!("DB error: {:?}", e); 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()) 188 } 189 190 pub async fn get_device_trust_state(
+4 -2
crates/tranquil-pds/src/sso/endpoints.rs
··· 644 645 pub async fn get_linked_accounts( 646 State(state): State<AppState>, 647 - crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth, 648 ) -> Result<Json<LinkedAccountsResponse>, ApiError> { 649 let identities = state 650 .sso_repo 651 .get_external_identities_by_did(&auth.did) ··· 679 680 pub async fn unlink_account( 681 State(state): State<AppState>, 682 - crate::auth::extractor::BearerAuth(auth): crate::auth::extractor::BearerAuth, 683 Json(input): Json<UnlinkAccountRequest>, 684 ) -> Result<Json<UnlinkAccountResponse>, ApiError> { 685 if !state 686 .check_rate_limit(RateLimitKind::SsoUnlink, auth.did.as_str()) 687 .await
··· 644 645 pub async fn get_linked_accounts( 646 State(state): State<AppState>, 647 + auth: crate::auth::RequiredAuth, 648 ) -> Result<Json<LinkedAccountsResponse>, ApiError> { 649 + let auth = auth.0.require_user()?.require_active()?; 650 let identities = state 651 .sso_repo 652 .get_external_identities_by_did(&auth.did) ··· 680 681 pub async fn unlink_account( 682 State(state): State<AppState>, 683 + auth: crate::auth::RequiredAuth, 684 Json(input): Json<UnlinkAccountRequest>, 685 ) -> Result<Json<UnlinkAccountResponse>, ApiError> { 686 + let auth = auth.0.require_user()?.require_active()?; 687 if !state 688 .check_rate_limit(RateLimitKind::SsoUnlink, auth.did.as_str()) 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