@@ -44,10 +44,8 @@ async fn main() -> anyhow::Result<()> {
4444 // REST endpoints
4545 . route ( "/api/cohorts" , get ( list_cohorts) )
4646 // Admin REST endpoints
47- . route (
48- "/api/admin/cohorts" ,
49- get ( admin_list_cohorts) . post ( create_cohort) ,
50- )
47+ . route ( "/api/admin/overview" , get ( get_admin_overview) )
48+ . route ( "/api/admin/cohorts" , post ( create_cohort) )
5149 . route ( "/api/admin/cohorts/:name" , put ( update_cohort) )
5250 . route (
5351 "/api/admin/cohorts/:name/members" ,
@@ -57,16 +55,13 @@ async fn main() -> anyhow::Result<()> {
5755 "/api/admin/cohorts/:name/members/:id" ,
5856 put ( update_member) . delete ( remove_member) ,
5957 )
60- . route ( "/api/admin/config" , get ( get_config) . put ( update_config) )
61- . route ( "/api/admin/users" , get ( list_users) )
62- . route ( "/api/admin/users/details" , get ( list_users_detailed) )
58+ . route ( "/api/admin/config" , put ( update_config) )
6359 . route ( "/api/admin/users/:id/admin" , put ( toggle_admin) )
6460 . route (
6561 "/api/admin/users/:id/display-name" ,
6662 put ( admin_update_display_name) ,
6763 )
6864 . route ( "/api/admin/users/:id" , delete ( delete_user_endpoint) )
69- . route ( "/api/admin/available-dbs" , get ( list_available_dbs) )
7065 // Authenticated user endpoints
7166 . route ( "/api/users/me/display-name" , put ( update_my_display_name) )
7267 // Utility routes
@@ -258,18 +253,106 @@ async fn check_admin(state: &AppState, claims: &AccessClaims) -> Result<(), (Sta
258253 }
259254}
260255
256+ #[ derive( Serialize ) ]
257+ struct AdminOverview {
258+ cohorts : Vec < CohortInfo > ,
259+ config : GlobalConfig ,
260+ available_dbs : Vec < String > ,
261+ users : Vec < UserWithCohorts > ,
262+ }
263+
261264#[ axum:: debug_handler]
262- async fn admin_list_cohorts (
265+ async fn get_admin_overview (
263266 claims : AccessClaims ,
264267 State ( state) : State < AppState > ,
265- ) -> Result < Json < Vec < CohortInfo > > , ( StatusCode , String ) > {
268+ ) -> Result < Json < AdminOverview > , ( StatusCode , String ) > {
266269 check_admin ( & state, & claims) . await ?;
267- state
270+
271+ let cohorts = state
268272 . global_db
269273 . get_all_cohorts ( )
270274 . await
271- . map ( Json )
272- . map_err ( |e| ( StatusCode :: INTERNAL_SERVER_ERROR , e. to_string ( ) ) )
275+ . map_err ( |e| ( StatusCode :: INTERNAL_SERVER_ERROR , e. to_string ( ) ) ) ?;
276+
277+ let active_auction_cohort_id = state
278+ . global_db
279+ . get_config ( "active_auction_cohort_id" )
280+ . await
281+ . unwrap_or ( None )
282+ . and_then ( |v| v. parse ( ) . ok ( ) ) ;
283+ let default_cohort_id = state
284+ . global_db
285+ . get_config ( "default_cohort_id" )
286+ . await
287+ . unwrap_or ( None )
288+ . and_then ( |v| v. parse ( ) . ok ( ) ) ;
289+ let public_auction_enabled = state
290+ . global_db
291+ . get_config ( "public_auction_enabled" )
292+ . await
293+ . unwrap_or ( None )
294+ . is_some_and ( |v| v == "true" ) ;
295+ let config = GlobalConfig {
296+ active_auction_cohort_id,
297+ default_cohort_id,
298+ public_auction_enabled,
299+ } ;
300+
301+ let data_dir = std:: env:: var ( "DATABASE_URL" )
302+ . ok ( )
303+ . and_then ( |url| {
304+ let path = url. trim_start_matches ( "sqlite://" ) ;
305+ Path :: new ( path)
306+ . parent ( )
307+ . map ( |p| p. to_string_lossy ( ) . into_owned ( ) )
308+ } )
309+ . unwrap_or_else ( || "/data" . to_string ( ) ) ;
310+ let used: std:: collections:: HashSet < String > =
311+ cohorts. iter ( ) . map ( |c| c. db_path . clone ( ) ) . collect ( ) ;
312+ let mut available_dbs = Vec :: new ( ) ;
313+ if let Ok ( entries) = std:: fs:: read_dir ( & data_dir) {
314+ for entry in entries. flatten ( ) {
315+ let path = entry. path ( ) ;
316+ if path. extension ( ) . is_some_and ( |ext| ext == "sqlite" ) {
317+ let full_path = path. to_string_lossy ( ) . to_string ( ) ;
318+ if !used. contains ( & full_path) {
319+ if let Some ( stem) = path. file_stem ( ) {
320+ available_dbs. push ( stem. to_string_lossy ( ) . to_string ( ) ) ;
321+ }
322+ }
323+ }
324+ }
325+ }
326+ available_dbs. sort ( ) ;
327+
328+ let users_raw = state
329+ . global_db
330+ . get_all_users ( )
331+ . await
332+ . map_err ( |e| ( StatusCode :: INTERNAL_SERVER_ERROR , e. to_string ( ) ) ) ?;
333+ let mut users = Vec :: with_capacity ( users_raw. len ( ) ) ;
334+ for user in users_raw {
335+ let user_cohort_infos = state
336+ . global_db
337+ . get_user_cohorts ( user. id )
338+ . await
339+ . unwrap_or_default ( ) ;
340+ let cohorts = user_cohort_infos
341+ . into_iter ( )
342+ . map ( |ci| UserCohortDetail {
343+ cohort_name : ci. name ,
344+ cohort_display_name : ci. display_name ,
345+ } )
346+ . collect ( ) ;
347+ users. push ( UserWithCohorts { user, cohorts } ) ;
348+ }
349+
350+ Ok ( Json ( AdminOverview {
351+ cohorts,
352+ config,
353+ available_dbs,
354+ users,
355+ } ) )
273356}
274357
275358#[ derive( Deserialize ) ]
@@ -509,41 +592,6 @@ struct GlobalConfig {
509592 public_auction_enabled : bool ,
510593}
511594
512- #[ axum:: debug_handler]
513- async fn get_config (
514- claims : AccessClaims ,
515- State ( state) : State < AppState > ,
516- ) -> Result < Json < GlobalConfig > , ( StatusCode , String ) > {
517- check_admin ( & state, & claims) . await ?;
518-
519- let active_auction_cohort_id = state
520- . global_db
521- . get_config ( "active_auction_cohort_id" )
522- . await
523- . unwrap_or ( None )
524- . and_then ( |v| v. parse ( ) . ok ( ) ) ;
525-
526- let default_cohort_id = state
527- . global_db
528- . get_config ( "default_cohort_id" )
529- . await
530- . unwrap_or ( None )
531- . and_then ( |v| v. parse ( ) . ok ( ) ) ;
532-
533- let public_auction_enabled = state
534- . global_db
535- . get_config ( "public_auction_enabled" )
536- . await
537- . unwrap_or ( None )
538- . is_some_and ( |v| v == "true" ) ;
539-
540- Ok ( Json ( GlobalConfig {
541- active_auction_cohort_id,
542- default_cohort_id,
543- public_auction_enabled,
544- } ) )
545- }
546-
547595#[ derive( Deserialize ) ]
548596#[ allow( clippy:: option_option) ] // Intentional: distinguishes "not provided" from "set to null"
549597struct UpdateConfigRequest {
@@ -593,26 +641,10 @@ async fn update_config(
593641 Ok ( StatusCode :: OK )
594642}
595643
596- #[ axum:: debug_handler]
597- async fn list_users (
598- claims : AccessClaims ,
599- State ( state) : State < AppState > ,
600- ) -> Result < Json < Vec < backend:: global_db:: GlobalUser > > , ( StatusCode , String ) > {
601- check_admin ( & state, & claims) . await ?;
602-
603- state
604- . global_db
605- . get_all_users ( )
606- . await
607- . map ( Json )
608- . map_err ( |e| ( StatusCode :: INTERNAL_SERVER_ERROR , e. to_string ( ) ) )
609- }
610-
611644#[ derive( Serialize ) ]
612645struct UserCohortDetail {
613646 cohort_name : String ,
614647 cohort_display_name : String ,
615- balance : Option < f64 > ,
616648}
617649
618650#[ derive( Serialize ) ]
@@ -622,51 +654,6 @@ struct UserWithCohorts {
622654 cohorts : Vec < UserCohortDetail > ,
623655}
624656
625- #[ axum:: debug_handler]
626- async fn list_users_detailed (
627- claims : AccessClaims ,
628- State ( state) : State < AppState > ,
629- ) -> Result < Json < Vec < UserWithCohorts > > , ( StatusCode , String ) > {
630- check_admin ( & state, & claims) . await ?;
631-
632- let users = state
633- . global_db
634- . get_all_users ( )
635- . await
636- . map_err ( |e| ( StatusCode :: INTERNAL_SERVER_ERROR , e. to_string ( ) ) ) ?;
637-
638- let mut result = Vec :: with_capacity ( users. len ( ) ) ;
639- for user in users {
640- let user_cohort_infos = state
641- . global_db
642- . get_user_cohorts ( user. id )
643- . await
644- . unwrap_or_default ( ) ;
645-
646- let mut cohorts = Vec :: new ( ) ;
647- for ci in & user_cohort_infos {
648- let balance = if let Some ( cs) = state. cohorts . get ( & ci. name ) {
649- cs. db
650- . get_balance_by_global_user_id ( user. id )
651- . await
652- . ok ( )
653- . flatten ( )
654- } else {
655- None
656- } ;
657- cohorts. push ( UserCohortDetail {
658- cohort_name : ci. name . clone ( ) ,
659- cohort_display_name : ci. display_name . clone ( ) ,
660- balance,
661- } ) ;
662- }
663-
664- result. push ( UserWithCohorts { user, cohorts } ) ;
665- }
666-
667- Ok ( Json ( result) )
668- }
669-
670657#[ derive( Deserialize ) ]
671658struct ToggleAdminRequest {
672659 is_admin : bool ,
@@ -798,52 +785,6 @@ async fn update_member(
798785 Ok ( StatusCode :: OK )
799786}
800787
801- #[ axum:: debug_handler]
802- async fn list_available_dbs (
803- claims : AccessClaims ,
804- State ( state) : State < AppState > ,
805- ) -> Result < Json < Vec < String > > , ( StatusCode , String ) > {
806- check_admin ( & state, & claims) . await ?;
807-
808- let data_dir = std:: env:: var ( "DATABASE_URL" )
809- . ok ( )
810- . and_then ( |url| {
811- let path = url. trim_start_matches ( "sqlite://" ) ;
812- Path :: new ( path)
813- . parent ( )
814- . map ( |p| p. to_string_lossy ( ) . into_owned ( ) )
815- } )
816- . unwrap_or_else ( || "/data" . to_string ( ) ) ;
817-
818- // Collect db_paths already used by cohorts
819- let cohorts = state
820- . global_db
821- . get_all_cohorts ( )
822- . await
823- . map_err ( |e| ( StatusCode :: INTERNAL_SERVER_ERROR , e. to_string ( ) ) ) ?;
824- let used: std:: collections:: HashSet < String > = cohorts. into_iter ( ) . map ( |c| c. db_path ) . collect ( ) ;
825-
826- let mut available = Vec :: new ( ) ;
827- let Ok ( entries) = std:: fs:: read_dir ( & data_dir) else {
828- return Ok ( Json ( available) ) ;
829- } ;
830-
831- for entry in entries. flatten ( ) {
832- let path = entry. path ( ) ;
833- if path. extension ( ) . is_some_and ( |ext| ext == "sqlite" ) {
834- let full_path = path. to_string_lossy ( ) . to_string ( ) ;
835- if !used. contains ( & full_path) {
836- if let Some ( stem) = path. file_stem ( ) {
837- available. push ( stem. to_string_lossy ( ) . to_string ( ) ) ;
838- }
839- }
840- }
841- }
842-
843- available. sort ( ) ;
844- Ok ( Json ( available) )
845- }
846-
847788// --- Utility Endpoints ---
848789
849790#[ axum:: debug_handler]
0 commit comments