use axum::{ extract::{Path, State}, http::HeaderMap, http::StatusCode, response::{Html, IntoResponse, Json}, }; use serde_json::json; use crate::server::AppState; // === Admin Auth Helper === fn verify_admin_or_401( state: &AppState, headers: &HeaderMap, ) -> Result<(), impl IntoResponse> { let auth_header = headers .get("Authorization") .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")); match auth_header { Some(token) if state.auth.verify_admin_token(token).is_some() => Ok(()), _ => Err(( StatusCode::UNAUTHORIZED, Json(json!({"ok": false, "error": "Invalid admin token"})), )), } } // === Admin Authentication Handlers === pub async fn admin_login_handler( State(state): State, Json(body): Json, ) -> impl IntoResponse { match state.auth.admin_login(&body.username, &body.password) { Some(response) => (StatusCode::OK, Json(response)).into_response(), None => ( StatusCode::UNAUTHORIZED, Json(json!({"error": "Invalid admin credentials"})), ) .into_response(), } } pub async fn admin_verify_handler( State(state): State, headers: HeaderMap, ) -> impl IntoResponse { let auth_header = headers .get("Authorization") .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")); if let Some(token) = auth_header { if let Some(session) = state.auth.verify_admin_token(token) { return ( StatusCode::OK, Json(json!({ "ok": true, "username": session.username, "expires_at": session.expires_at })), ) .into_response(); } } ( StatusCode::UNAUTHORIZED, Json(json!({"ok": false, "error": "Invalid admin token"})), ) .into_response() } // === Admin Page Handlers === pub async fn admin_products_page( State(state): State, headers: HeaderMap, ) -> impl IntoResponse { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } Html(include_str!("../product_manager.html")).into_response() } pub async fn admin_files_page( State(state): State, headers: HeaderMap, ) -> impl IntoResponse { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } Html(include_str!("../file_list.html")).into_response() } pub async fn admin_upload_page( State(state): State, headers: HeaderMap, ) -> impl IntoResponse { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } Html(include_str!("../upload.html")).into_response() } // === Admin-Wrapped Product/File API Handlers === pub async fn admin_list_all_products( State(state): State, headers: HeaderMap, ) -> axum::response::Response { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } crate::download::product_handlers::list_all_products(State(state)) .await .into_response() } pub async fn admin_create_product( State(state): State, headers: HeaderMap, Json(payload): Json, ) -> axum::response::Response { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } crate::download::product_handlers::create_product_handler(State(state), Json(payload)) .await .into_response() } pub async fn admin_get_series_stats( State(state): State, headers: HeaderMap, ) -> axum::response::Response { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } crate::download::product_handlers::get_series_stats(State(state)) .await .into_response() } pub async fn admin_get_product_files( State(state): State, headers: HeaderMap, Path(product_id): Path, ) -> axum::response::Response { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } crate::download::product_handlers::get_product_files(Path(product_id), State(state)) .await .into_response() } pub async fn admin_delete_product( State(state): State, headers: HeaderMap, Path(product_id): Path, ) -> axum::response::Response { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } crate::download::product_handlers::delete_product(Path(product_id), State(state)) .await .into_response() } pub async fn admin_assign_files( State(state): State, headers: HeaderMap, Path(product_id): Path, Json(payload): Json, ) -> axum::response::Response { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } crate::download::product_handlers::assign_files_to_product( Path(product_id), State(state), Json(payload), ) .await .into_response() } pub async fn admin_list_uploaded_files( State(state): State, headers: HeaderMap, Path(user_id): Path, ) -> axum::response::Response { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } crate::download::handlers::list_uploaded_files(Path(user_id)) .await .into_response() } // === Sync Handlers === pub async fn manual_sync_handler( State(state): State, headers: HeaderMap, ) -> impl IntoResponse { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } let syncer = crate::pg_client::SftpGoSync::new(&state.auth_db_path); match syncer { Ok(syncer) => match syncer.full_sync().await { Ok(result) => { if result.status == "success" { ( StatusCode::OK, Json(json!({ "status": "success", "users_synced": result.users_synced, "groups_synced": result.groups_synced, "mappings_synced": result.mappings_synced })), ) .into_response() } else if result.status == "partial_success" { ( StatusCode::OK, Json(json!({ "status": "partial_success", "users_synced": result.users_synced, "users_failed": result.users_failed, "groups_synced": result.groups_synced, "groups_failed": result.groups_failed, "errors": result.errors })), ) .into_response() } else { ( StatusCode::OK, Json(json!({ "status": result.status, "errors": result.errors })), ) .into_response() } } Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "status": "failed", "error": e.to_string() })), ) .into_response(), }, Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "status": "failed", "error": e.to_string() })), ) .into_response(), } } pub async fn sync_status_handler( State(state): State, headers: HeaderMap, ) -> impl IntoResponse { if let Err(resp) = verify_admin_or_401(&state, &headers) { return resp.into_response(); } let auth_db = crate::sync::AuthDb::new(&state.auth_db_path); match auth_db { Ok(db) => match db.open() { Ok(conn) => { match conn.query_row( "SELECT sync_type, sync_time, users_synced, users_failed, groups_synced, groups_failed, mappings_synced, status FROM sync_log ORDER BY sync_time DESC LIMIT 5", [], |row| { Ok(json!({ "sync_type": row.get::<_, String>(0)?, "sync_time": row.get::<_, i64>(1)?, "users_synced": row.get::<_, usize>(2)?, "users_failed": row.get::<_, usize>(3)?, "groups_synced": row.get::<_, usize>(4)?, "groups_failed": row.get::<_, usize>(5)?, "mappings_synced": row.get::<_, usize>(6)?, "status": row.get::<_, String>(7)?, })) }, ) { Ok(entries) => (StatusCode::OK, Json(entries)).into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})), ) .into_response(), } } Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})), ) .into_response(), }, Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})), ) .into_response(), } }