Microservice to bring 2FA to self hosted PDSes

removed search for now since it doesnt look to work

+56 -114
+2 -12
html_templates/admin/accounts.hbs
··· 16 16 17 17 <h1 class="page-title">Accounts</h1> 18 18 19 - <form class="search-form" method="GET" action="/admin/search"> 20 - <input type="text" name="q" placeholder="Search by email..." value="{{search_query}}"/> 21 - <button type="submit" class="btn btn-primary">Search</button> 22 - </form> 23 - 24 19 <form class="search-form" method="GET" action="/admin/accounts/lookup"> 25 - <input type="text" name="direct_lookup" placeholder="Direct lookup by did or handle" 26 - value="{{search_query}}"/> 20 + <input type="text" name="direct_lookup" placeholder="Direct lookup by did or handle"/> 27 21 <button type="submit" class="btn btn-primary">Lookup</button> 28 22 </form> 29 23 ··· 75 69 {{/if}} 76 70 {{else}} 77 71 <div class="empty-state"> 78 - {{#if search_query}} 79 - No accounts matching "{{search_query}}" 80 - {{else}} 81 - No accounts found 82 - {{/if}} 72 + No accounts found 83 73 </div> 84 74 {{/if}} 85 75 </main>
-1
src/admin/mod.rs
··· 53 53 "/create-account", 54 54 get(routes::get_create_account).post(routes::post_create_account), 55 55 ) 56 - .route("/search", get(routes::search_accounts)) 57 56 .route( 58 57 "/request-crawl", 59 58 get(routes::get_request_crawl).post(routes::post_request_crawl),
+54 -101
src/admin/routes.rs
··· 32 32 33 33 #[derive(Debug, Deserialize)] 34 34 pub struct AccountsParams { 35 - pub q: Option<String>, 36 35 pub cursor: Option<String>, 37 36 pub flash_success: Option<String>, 38 37 pub flash_error: Option<String>, ··· 279 278 render_template(&state, "admin/dashboard.hbs", data) 280 279 } 281 280 282 - /// GET /admin/accounts — Account list (paginated) or Search 281 + /// GET /admin/accounts — Account list (paginated) 283 282 pub async fn accounts_list( 284 283 State(state): State<AppState>, 285 284 Extension(session): Extension<AdminSession>, ··· 292 291 293 292 let pds = pds_url(&state); 294 293 let password = admin_password(&state); 295 - let query = params.q.clone().unwrap_or_default(); 296 294 297 295 let limit = 5; 298 296 let limit_as_string = limit.to_string(); ··· 300 298 let mut repo_infos: std::collections::HashMap<String, (bool, Option<String>)> = 301 299 std::collections::HashMap::new(); 302 300 303 - let (accounts_raw, next_cursor) = if !query.is_empty() { 304 - // Search by email 305 - match pds_proxy::admin_xrpc_get::<serde_json::Value>( 306 - pds, 307 - password, 308 - "com.atproto.admin.searchAccounts", 309 - &[("email", &query)], 310 - ) 311 - .await 312 - { 313 - Ok(res) => (res["accounts"].clone(), None), 314 - Err(e) => { 315 - tracing::error!("Search failed: {}", e); 316 - (serde_json::json!([]), None) 317 - } 318 - } 319 - } else { 320 - // Regular list 321 - let mut query_params: Vec<(&str, &str)> = vec![("limit", &limit_as_string)]; 322 - let cursor_val; 323 - if let Some(ref c) = params.cursor { 324 - cursor_val = c.clone(); 325 - query_params.push(("cursor", &cursor_val)); 326 - } 301 + // Regular list 302 + let mut query_params: Vec<(&str, &str)> = vec![("limit", &limit_as_string)]; 303 + let cursor_val; 304 + if let Some(ref c) = params.cursor { 305 + cursor_val = c.clone(); 306 + query_params.push(("cursor", &cursor_val)); 307 + } 327 308 328 - match pds_proxy::public_xrpc_get::<serde_json::Value>( 329 - pds, 330 - "com.atproto.sync.listRepos", 331 - &query_params, 332 - ) 333 - .await 334 - { 335 - Ok(repos) => { 336 - let next_cursor = repos["cursor"].as_str().map(|s| s.to_string()); 337 - if let Some(arr) = repos["repos"].as_array() { 338 - for r in arr { 339 - if let Some(did) = r["did"].as_str() { 340 - let active = r["active"].as_bool().unwrap_or(true); 341 - let status = r["status"].as_str().map(|s| s.to_string()); 342 - repo_infos.insert(did.to_string(), (active, status)); 343 - } 309 + let (accounts_raw, next_cursor) = match pds_proxy::public_xrpc_get::<serde_json::Value>( 310 + pds, 311 + "com.atproto.sync.listRepos", 312 + &query_params, 313 + ) 314 + .await 315 + { 316 + Ok(repos) => { 317 + let next_cursor = repos["cursor"].as_str().map(|s| s.to_string()); 318 + if let Some(arr) = repos["repos"].as_array() { 319 + for r in arr { 320 + if let Some(did) = r["did"].as_str() { 321 + let active = r["active"].as_bool().unwrap_or(true); 322 + let status = r["status"].as_str().map(|s| s.to_string()); 323 + repo_infos.insert(did.to_string(), (active, status)); 344 324 } 345 325 } 326 + } 346 327 347 - let dids: Vec<String> = repo_infos.keys().cloned().collect(); 328 + let dids: Vec<String> = repo_infos.keys().cloned().collect(); 348 329 349 - if !dids.is_empty() { 350 - let did_params: Vec<(&str, &str)> = 351 - dids.iter().map(|d| ("dids", d.as_str())).collect(); 352 - match pds_proxy::admin_xrpc_get::<serde_json::Value>( 353 - pds, 354 - password, 355 - "com.atproto.admin.getAccountInfos", 356 - &did_params, 357 - ) 358 - .await 359 - { 360 - Ok(res) => (res["infos"].clone(), next_cursor), 361 - Err(e) => { 362 - tracing::error!("Failed to get account infos: {}", e); 363 - (serde_json::json!([]), next_cursor) 364 - } 330 + if !dids.is_empty() { 331 + let did_params: Vec<(&str, &str)> = 332 + dids.iter().map(|d| ("dids", d.as_str())).collect(); 333 + match pds_proxy::admin_xrpc_get::<serde_json::Value>( 334 + pds, 335 + password, 336 + "com.atproto.admin.getAccountInfos", 337 + &did_params, 338 + ) 339 + .await 340 + { 341 + Ok(res) => (res["infos"].clone(), next_cursor), 342 + Err(e) => { 343 + tracing::error!("Failed to get account infos: {}", e); 344 + (serde_json::json!([]), next_cursor) 365 345 } 366 - } else { 367 - (serde_json::json!([]), next_cursor) 368 346 } 369 - } 370 - Err(e) => { 371 - tracing::error!("Failed to list repos: {}", e); 372 - return flash_redirect( 373 - "/admin/dashboard", 374 - None, 375 - Some(&format!("Failed to list accounts: {}", e)), 376 - ); 347 + } else { 348 + (serde_json::json!([]), next_cursor) 377 349 } 378 350 } 351 + Err(e) => { 352 + tracing::error!("Failed to list repos: {}", e); 353 + return flash_redirect( 354 + "/admin/dashboard", 355 + None, 356 + Some(&format!("Failed to list accounts: {}", e)), 357 + ); 358 + } 379 359 }; 380 - 381 - println!("{:?}", accounts_raw); 382 360 383 361 let accounts: Vec<serde_json::Value> = if let Some(arr) = accounts_raw.as_array() { 384 362 let mut processed = Vec::new(); ··· 396 374 a["status"] = s.clone().into(); 397 375 } 398 376 } 399 - } else if query.is_empty() { 400 - // If it's a list but we don't have repo info for this DID (shouldn't happen with our logic) 401 - a["is_taken_down"] = false.into(); 402 377 } else { 403 - let status_res = pds_proxy::admin_xrpc_get::<serde_json::Value>( 404 - pds, 405 - password, 406 - "com.atproto.admin.getSubjectStatus", 407 - &[("did", did)], 408 - ) 409 - .await; 410 - 411 - let is_taken_down = status_res 412 - .ok() 413 - .and_then(|s| s["takedown"]["applied"].as_bool()) 414 - .unwrap_or(false); 415 - 416 - a["is_taken_down"] = is_taken_down.into(); 378 + // If we don't have repo info for this DID (shouldn't happen with our logic) 379 + a["is_taken_down"] = false.into(); 417 380 } 418 381 processed.push(a); 419 382 } ··· 427 390 let mut data = serde_json::json!({ 428 391 "accounts": accounts, 429 392 "account_count": account_count, 430 - "search_query": query, 431 393 "pds_hostname": state.app_config.pds_hostname, 432 394 "active_page": "accounts", 433 395 }); ··· 444 406 // Pagination: "Previous" link 445 407 if params.cursor.is_some() { 446 408 data["has_prev"] = true.into(); 447 - data["prev_url"] = "/admin/accounts".to_string().into(); 409 + let prev_url = "/admin/accounts".to_string(); 410 + data["prev_url"] = prev_url.into(); 448 411 } 449 412 450 413 if let Some(msg) = &params.flash_success { ··· 1218 1181 Some(&format!("Failed to create account: {}", e)), 1219 1182 ), 1220 1183 } 1221 - } 1222 - 1223 - /// GET /admin/search — Search accounts 1224 - pub async fn search_accounts( 1225 - state: State<AppState>, 1226 - session: Extension<AdminSession>, 1227 - permissions: Extension<AdminPermissions>, 1228 - params: Query<AccountsParams>, 1229 - ) -> Response { 1230 - accounts_list(state, session, permissions, params).await 1231 1184 } 1232 1185 1233 1186 /// GET /admin/request-crawl — Request Crawl form (Gap 3)