Merge origin SMB fixes with local Phase 21-22 features
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:
324
markbase-core/src/api/admin.rs
Normal file
324
markbase-core/src/api/admin.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user