Merge origin SMB fixes with local Phase 21-22 features
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

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
This commit is contained in:
Warren
2026-06-30 07:25:04 +08:00
parent deac3b9b6e
commit 4fa8fd8c1f
17 changed files with 1246 additions and 716 deletions

View File

@@ -0,0 +1,324 @@
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(),
}
}