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
wip
baileytownsend.dev
2 weeks ago
9543e5d5
0ca2a483
+86
-12
2 changed files
expand all
collapse all
unified
split
html_templates
admin
accounts.hbs
src
admin
routes.rs
+12
html_templates/admin/accounts.hbs
···
36
36
<th>Handle</th>
37
37
<th>DID</th>
38
38
<th>Email</th>
39
39
+
<th>Status</th>
39
40
</tr>
40
41
</thead>
41
42
<tbody>
···
44
45
<td><a href="/admin/accounts/{{this.did}}">{{this.handle}}</a></td>
45
46
<td class="did-cell">{{this.did}}</td>
46
47
<td>{{this.email}}</td>
48
48
+
<td>
49
49
+
{{#if (or this.is_taken_down (eq this.status "takedown"))}}
50
50
+
<span class="badge badge-danger">Taken Down</span>
51
51
+
{{/if}}
52
52
+
{{#if (or this.deactivatedAt (eq this.status "deactivated"))}}
53
53
+
<span class="badge badge-warning">Deactivated</span>
54
54
+
{{/if}}
55
55
+
{{#if (and (not this.is_taken_down) (not (eq this.status "takedown")) (not this.deactivatedAt) (not (eq this.status "deactivated")))}}
56
56
+
<span class="badge badge-success">Active</span>
57
57
+
{{/if}}
58
58
+
</td>
47
59
</tr>
48
60
{{/each}}
49
61
</tbody>
+74
-12
src/admin/routes.rs
···
1
1
+
use super::middleware::{AdminPermissions, AdminSession};
2
2
+
use super::pds_proxy;
3
3
+
use super::session;
1
4
use crate::AppState;
2
5
use axum::{
3
6
extract::{Extension, Path, Query, State},
···
9
12
use jacquard_common::types::handle::Handle;
10
13
use jacquard_identity::resolver::IdentityResolver;
11
14
use serde::Deserialize;
12
12
-
13
13
-
use super::middleware::{AdminPermissions, AdminSession};
14
14
-
use super::pds_proxy;
15
15
-
use super::session;
15
15
+
use tracing::log;
16
16
17
17
// ─── Query parameter types ───────────────────────────────────────────────────
18
18
···
293
293
Query(params): Query<AccountsParams>,
294
294
) -> Response {
295
295
if !permissions.can_view_accounts {
296
296
-
return flash_redirect("/admin/", None, Some("Access denied"));
296
296
+
return flash_redirect("/admin/dashboard", None, Some("Access denied"));
297
297
}
298
298
299
299
let pds = pds_url(&state);
300
300
let password = admin_password(&state);
301
301
302
302
-
// Build query params for listRepos with pagination
303
302
let limit = 5;
304
303
// Yeah I know this looks bad, but I'd like to have the limit in one spot instead of two
305
304
let limit_as_string = limit.to_string();
···
332
331
// Extract the next-page cursor from the response
333
332
let next_cursor = repos["cursor"].as_str().map(|s| s.to_string());
334
333
335
335
-
let dids: Vec<String> = repos["repos"]
334
334
+
let repo_infos: std::collections::HashMap<String, (bool, Option<String>)> = repos["repos"]
336
335
.as_array()
337
336
.map(|arr| {
338
337
arr.iter()
339
339
-
.filter_map(|r| r["did"].as_str().map(|s| s.to_string()))
338
338
+
.filter_map(|r| {
339
339
+
let did = r["did"].as_str()?.to_string();
340
340
+
let active = r["active"].as_bool().unwrap_or(true);
341
341
+
let status = r["status"].as_str().map(|s| s.to_string());
342
342
+
Some((did, (active, status)))
343
343
+
})
340
344
.collect()
341
345
})
342
346
.unwrap_or_default();
343
347
344
344
-
let accounts: serde_json::Value = if !dids.is_empty() {
348
348
+
let dids: Vec<String> = repo_infos.keys().cloned().collect();
349
349
+
350
350
+
let accounts_raw: serde_json::Value = if !dids.is_empty() {
345
351
let did_params: Vec<(&str, &str)> = dids.iter().map(|d| ("dids", d.as_str())).collect();
346
352
match pds_proxy::admin_xrpc_get::<serde_json::Value>(
347
353
pds,
···
360
366
} else {
361
367
serde_json::json!([])
362
368
};
369
369
+
log::debug!("Accounts: {:?}", accounts_raw);
370
370
+
371
371
+
let accounts: Vec<serde_json::Value> = if let Some(arr) = accounts_raw.as_array() {
372
372
+
arr.iter()
373
373
+
.map(|account| {
374
374
+
let mut a = account.clone();
375
375
+
let did = a["did"].as_str().unwrap_or_default();
363
376
364
364
-
let account_count = accounts.as_array().map(|a| a.len()).unwrap_or(0);
377
377
+
if let Some((active, status)) = repo_infos.get(did) {
378
378
+
// Map active/status to is_taken_down
379
379
+
380
380
+
let is_taken_down = status.as_deref() == Some("takendown");
381
381
+
a["is_taken_down"] = is_taken_down.into();
382
382
+
383
383
+
// Also ensure we have the status field if it was missing in AccountView
384
384
+
if a["status"].is_null() {
385
385
+
if let Some(s) = status {
386
386
+
a["status"] = s.clone().into();
387
387
+
}
388
388
+
}
389
389
+
}
390
390
+
a
391
391
+
})
392
392
+
.collect()
393
393
+
} else {
394
394
+
Vec::new()
395
395
+
};
396
396
+
397
397
+
let account_count = accounts.len();
365
398
366
399
let mut data = serde_json::json!({
367
400
"accounts": accounts,
···
1178
1211
1179
1212
let query = params.q.unwrap_or_default();
1180
1213
1181
1181
-
let accounts: serde_json::Value = if !query.is_empty() {
1214
1214
+
let accounts_raw: serde_json::Value = if !query.is_empty() {
1182
1215
// Bug 7 fix: use "email" parameter per the lexicon spec
1183
1216
match pds_proxy::admin_xrpc_get::<serde_json::Value>(
1184
1217
pds_url(&state),
···
1198
1231
serde_json::json!([])
1199
1232
};
1200
1233
1201
1201
-
let account_count = accounts.as_array().map(|a| a.len()).unwrap_or(0);
1234
1234
+
let pds = pds_url(&state);
1235
1235
+
let password = admin_password(&state);
1236
1236
+
1237
1237
+
let accounts: Vec<serde_json::Value> = if let Some(arr) = accounts_raw.as_array() {
1238
1238
+
let mut processed = Vec::new();
1239
1239
+
for account in arr {
1240
1240
+
let mut a = account.clone();
1241
1241
+
let did = a["did"].as_str().unwrap_or_default();
1242
1242
+
let status_res = pds_proxy::admin_xrpc_get::<serde_json::Value>(
1243
1243
+
pds,
1244
1244
+
password,
1245
1245
+
"com.atproto.admin.getSubjectStatus",
1246
1246
+
&[("did", did)],
1247
1247
+
)
1248
1248
+
.await;
1249
1249
+
1250
1250
+
let is_taken_down = status_res
1251
1251
+
.ok()
1252
1252
+
.and_then(|s| s["takedown"]["applied"].as_bool())
1253
1253
+
.unwrap_or(false);
1254
1254
+
1255
1255
+
a["is_taken_down"] = is_taken_down.into();
1256
1256
+
processed.push(a);
1257
1257
+
}
1258
1258
+
processed
1259
1259
+
} else {
1260
1260
+
Vec::new()
1261
1261
+
};
1262
1262
+
1263
1263
+
let account_count = accounts.len();
1202
1264
1203
1265
let mut data = serde_json::json!({
1204
1266
"accounts": accounts,