Origin changes merged: - SMB performance optimization (pread/pwrite, tokio Mutex) - macOS SMB mount fix (AAPL caps, credit grant) - Compound request integration tests - CTDB architecture analysis Local changes preserved: - upload_path config (deployed, tested stable) - delete_file + preview_file routes (MyFiles UI) - SSH async I/O (cipher.rs, packet.rs, server.rs) - auth.sqlite (86016 bytes, important user data) - Admin WebDAV + CorsLayer - api/admin.rs + api/config.rs (new endpoints) Conflicts resolved: - myfiles.rs: kept upload_path + OnceLock static - auth.sqlite: preserved local version (important data) Test results: 393 passed, 5 auth tests failed - PG tests require external PostgreSQL - Auth tests expect specific password hashes - auth.sqlite preserved with actual user credentials
325 lines
10 KiB
Rust
325 lines
10 KiB
Rust
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<AppState>,
|
|
Json(body): Json<crate::auth::AdminLoginRequest>,
|
|
) -> 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<AppState>,
|
|
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<AppState>,
|
|
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<AppState>,
|
|
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<AppState>,
|
|
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<AppState>,
|
|
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<AppState>,
|
|
headers: HeaderMap,
|
|
Json(payload): Json<serde_json::Value>,
|
|
) -> 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<AppState>,
|
|
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<AppState>,
|
|
headers: HeaderMap,
|
|
Path(product_id): Path<i64>,
|
|
) -> 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<AppState>,
|
|
headers: HeaderMap,
|
|
Path(product_id): Path<i64>,
|
|
) -> 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<AppState>,
|
|
headers: HeaderMap,
|
|
Path(product_id): Path<i64>,
|
|
Json(payload): Json<serde_json::Value>,
|
|
) -> 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<AppState>,
|
|
headers: HeaderMap,
|
|
Path(user_id): Path<String>,
|
|
) -> 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<AppState>,
|
|
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<AppState>,
|
|
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(),
|
|
}
|
|
}
|