tangled
alpha
login
or
join now
baileytownsend.dev
/
pds-gatekeeper
89
fork
atom
Microservice to bring 2FA to self hosted PDSes
89
fork
atom
overview
issues
1
pulls
3
pipelines
removed search for now since it doesnt look to work
baileytownsend.dev
1 week ago
f5f575a0
5698301c
+56
-114
3 changed files
expand all
collapse all
unified
split
html_templates
admin
accounts.hbs
src
admin
mod.rs
routes.rs
+2
-12
html_templates/admin/accounts.hbs
···
16
17
<h1 class="page-title">Accounts</h1>
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
<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}}"/>
27
<button type="submit" class="btn btn-primary">Lookup</button>
28
</form>
29
···
75
{{/if}}
76
{{else}}
77
<div class="empty-state">
78
-
{{#if search_query}}
79
-
No accounts matching "{{search_query}}"
80
-
{{else}}
81
-
No accounts found
82
-
{{/if}}
83
</div>
84
{{/if}}
85
</main>
···
16
17
<h1 class="page-title">Accounts</h1>
18
0
0
0
0
0
19
<form class="search-form" method="GET" action="/admin/accounts/lookup">
20
+
<input type="text" name="direct_lookup" placeholder="Direct lookup by did or handle"/>
0
21
<button type="submit" class="btn btn-primary">Lookup</button>
22
</form>
23
···
69
{{/if}}
70
{{else}}
71
<div class="empty-state">
72
+
No accounts found
0
0
0
0
73
</div>
74
{{/if}}
75
</main>
-1
src/admin/mod.rs
···
53
"/create-account",
54
get(routes::get_create_account).post(routes::post_create_account),
55
)
56
-
.route("/search", get(routes::search_accounts))
57
.route(
58
"/request-crawl",
59
get(routes::get_request_crawl).post(routes::post_request_crawl),
···
53
"/create-account",
54
get(routes::get_create_account).post(routes::post_create_account),
55
)
0
56
.route(
57
"/request-crawl",
58
get(routes::get_request_crawl).post(routes::post_request_crawl),
+54
-101
src/admin/routes.rs
···
32
33
#[derive(Debug, Deserialize)]
34
pub struct AccountsParams {
35
-
pub q: Option<String>,
36
pub cursor: Option<String>,
37
pub flash_success: Option<String>,
38
pub flash_error: Option<String>,
···
279
render_template(&state, "admin/dashboard.hbs", data)
280
}
281
282
-
/// GET /admin/accounts — Account list (paginated) or Search
283
pub async fn accounts_list(
284
State(state): State<AppState>,
285
Extension(session): Extension<AdminSession>,
···
292
293
let pds = pds_url(&state);
294
let password = admin_password(&state);
295
-
let query = params.q.clone().unwrap_or_default();
296
297
let limit = 5;
298
let limit_as_string = limit.to_string();
···
300
let mut repo_infos: std::collections::HashMap<String, (bool, Option<String>)> =
301
std::collections::HashMap::new();
302
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
-
}
327
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
-
}
344
}
345
}
0
346
347
-
let dids: Vec<String> = repo_infos.keys().cloned().collect();
348
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
-
}
365
}
366
-
} else {
367
-
(serde_json::json!([]), next_cursor)
368
}
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
-
);
377
}
378
}
0
0
0
0
0
0
0
0
379
};
380
-
381
-
println!("{:?}", accounts_raw);
382
383
let accounts: Vec<serde_json::Value> = if let Some(arr) = accounts_raw.as_array() {
384
let mut processed = Vec::new();
···
396
a["status"] = s.clone().into();
397
}
398
}
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
} 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();
417
}
418
processed.push(a);
419
}
···
427
let mut data = serde_json::json!({
428
"accounts": accounts,
429
"account_count": account_count,
430
-
"search_query": query,
431
"pds_hostname": state.app_config.pds_hostname,
432
"active_page": "accounts",
433
});
···
444
// Pagination: "Previous" link
445
if params.cursor.is_some() {
446
data["has_prev"] = true.into();
447
-
data["prev_url"] = "/admin/accounts".to_string().into();
0
448
}
449
450
if let Some(msg) = ¶ms.flash_success {
···
1218
Some(&format!("Failed to create account: {}", e)),
1219
),
1220
}
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
}
1232
1233
/// GET /admin/request-crawl — Request Crawl form (Gap 3)
···
32
33
#[derive(Debug, Deserialize)]
34
pub struct AccountsParams {
0
35
pub cursor: Option<String>,
36
pub flash_success: Option<String>,
37
pub flash_error: Option<String>,
···
278
render_template(&state, "admin/dashboard.hbs", data)
279
}
280
281
+
/// GET /admin/accounts — Account list (paginated)
282
pub async fn accounts_list(
283
State(state): State<AppState>,
284
Extension(session): Extension<AdminSession>,
···
291
292
let pds = pds_url(&state);
293
let password = admin_password(&state);
0
294
295
let limit = 5;
296
let limit_as_string = limit.to_string();
···
298
let mut repo_infos: std::collections::HashMap<String, (bool, Option<String>)> =
299
std::collections::HashMap::new();
300
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
+
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
308
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));
0
324
}
325
}
326
+
}
327
328
+
let dids: Vec<String> = repo_infos.keys().cloned().collect();
329
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)
0
345
}
0
0
346
}
347
+
} else {
348
+
(serde_json::json!([]), next_cursor)
0
0
0
0
0
0
349
}
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
+
}
359
};
0
0
360
361
let accounts: Vec<serde_json::Value> = if let Some(arr) = accounts_raw.as_array() {
362
let mut processed = Vec::new();
···
374
a["status"] = s.clone().into();
375
}
376
}
0
0
0
377
} else {
378
+
// If we don't have repo info for this DID (shouldn't happen with our logic)
379
+
a["is_taken_down"] = false.into();
0
0
0
0
0
0
0
0
0
0
0
0
380
}
381
processed.push(a);
382
}
···
390
let mut data = serde_json::json!({
391
"accounts": accounts,
392
"account_count": account_count,
0
393
"pds_hostname": state.app_config.pds_hostname,
394
"active_page": "accounts",
395
});
···
406
// Pagination: "Previous" link
407
if params.cursor.is_some() {
408
data["has_prev"] = true.into();
409
+
let prev_url = "/admin/accounts".to_string();
410
+
data["prev_url"] = prev_url.into();
411
}
412
413
if let Some(msg) = ¶ms.flash_success {
···
1181
Some(&format!("Failed to create account: {}", e)),
1182
),
1183
}
0
0
0
0
0
0
0
0
0
0
1184
}
1185
1186
/// GET /admin/request-crawl — Request Crawl form (Gap 3)