···38}
3940#[derive(Debug, Deserialize)]
000000041pub struct InviteCodesParams {
42 pub cursor: Option<String>,
43 pub flash_success: Option<String>,
···278 render_template(&state, "admin/dashboard.hbs", data)
279}
280281-/// GET /admin/accounts — Account list
282pub async fn accounts_list(
283 State(state): State<AppState>,
284 Extension(session): Extension<AdminSession>,
285 Extension(permissions): Extension<AdminPermissions>,
286- Query(flash): Query<FlashParams>,
287) -> Response {
288 if !permissions.can_view_accounts {
289 return flash_redirect("/admin/", None, Some("Access denied"));
···292 let pds = pds_url(&state);
293 let password = admin_password(&state);
294295- // Get all repos first
00000000000296 let repos = match pds_proxy::public_xrpc_get::<serde_json::Value>(
297 pds,
298 "com.atproto.sync.listRepos",
299- &[("limit", "1000")],
300 )
301 .await
302 {
···311 }
312 };
313000314 let dids: Vec<String> = repos["repos"]
315 .as_array()
316 .map(|arr| {
···349 "active_page": "accounts",
350 });
351352- if let Some(msg) = flash.flash_success {
0000000000000000353 data["flash_success"] = msg.into();
354 }
355- if let Some(msg) = flash.flash_error {
356 data["flash_error"] = msg.into();
357 }
358
···38}
3940#[derive(Debug, Deserialize)]
41+pub struct AccountsParams {
42+ pub cursor: Option<String>,
43+ pub flash_success: Option<String>,
44+ pub flash_error: Option<String>,
45+}
46+47+#[derive(Debug, Deserialize)]
48pub struct InviteCodesParams {
49 pub cursor: Option<String>,
50 pub flash_success: Option<String>,
···285 render_template(&state, "admin/dashboard.hbs", data)
286}
287288+/// GET /admin/accounts — Account list (paginated, 100 per page)
289pub async fn accounts_list(
290 State(state): State<AppState>,
291 Extension(session): Extension<AdminSession>,
292 Extension(permissions): Extension<AdminPermissions>,
293+ Query(params): Query<AccountsParams>,
294) -> Response {
295 if !permissions.can_view_accounts {
296 return flash_redirect("/admin/", None, Some("Access denied"));
···299 let pds = pds_url(&state);
300 let password = admin_password(&state);
301302+ // Build query params for listRepos with pagination
303+ let limit = 5;
304+ // Yeah I know this looks bad, but I'd like to have the limit in one spot instead of two
305+ let limit_as_string = limit.to_string();
306+ let limit_as_str = limit_as_string.as_str();
307+ let mut query_params: Vec<(&str, &str)> = vec![("limit", limit_as_str)];
308+ let cursor_val;
309+ if let Some(ref c) = params.cursor {
310+ cursor_val = c.clone();
311+ query_params.push(("cursor", &cursor_val));
312+ }
313+314 let repos = match pds_proxy::public_xrpc_get::<serde_json::Value>(
315 pds,
316 "com.atproto.sync.listRepos",
317+ &query_params,
318 )
319 .await
320 {
···329 }
330 };
331332+ // Extract the next-page cursor from the response
333+ let next_cursor = repos["cursor"].as_str().map(|s| s.to_string());
334+335 let dids: Vec<String> = repos["repos"]
336 .as_array()
337 .map(|arr| {
···370 "active_page": "accounts",
371 });
372373+ // Pagination: "Next" link
374+ if let Some(ref cursor) = next_cursor {
375+ // If the count returned is not the same as the limit, then we are at the end of the list
376+ if dids.len() == limit {
377+ data["has_next"] = true.into();
378+ let next_url = format!("/admin/accounts?cursor={}", urlencoding::encode(cursor));
379+ data["next_url"] = next_url.into();
380+ }
381+ }
382+383+ // Pagination: "Previous" link (visible when not on page 1)
384+ if params.cursor.is_some() {
385+ data["has_prev"] = true.into();
386+ data["prev_url"] = "/admin/accounts".to_string().into();
387+ }
388+389+ if let Some(msg) = params.flash_success {
390 data["flash_success"] = msg.into();
391 }
392+ if let Some(msg) = params.flash_error {
393 data["flash_error"] = msg.into();
394 }
395