diff --git a/src/application/services/people_service.rs b/src/application/services/people_service.rs index 7dd260b8..6e3ac8b5 100644 --- a/src/application/services/people_service.rs +++ b/src/application/services/people_service.rs @@ -243,17 +243,6 @@ impl PeopleService { self.repo.rename_person(caller_id, person_id, name).await } - pub async fn set_hidden( - &self, - caller_id: Uuid, - person_id: Uuid, - hidden: bool, - ) -> Result<(), DomainError> { - self.repo - .set_person_hidden(caller_id, person_id, hidden) - .await - } - /// Merge `from` into `into` by reassigning all of `from`'s faces. The /// now-empty `from` person is hidden by `list_people`. pub async fn merge(&self, caller_id: Uuid, into: Uuid, from: Uuid) -> Result<(), DomainError> { diff --git a/src/common/errors.rs b/src/common/errors.rs index f49f1396..7c096f84 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -9,9 +9,6 @@ // Re-export domain errors for compatibility pub use crate::domain::errors::{DomainError, ErrorKind, Result}; -// Re-export AppError from interfaces for backward compatibility -// NOTE: The canonical location of AppError is now crate::interfaces::errors - // Infrastructure error conversions have been moved to: // crate::infrastructure::adapters::error_adapters // diff --git a/src/interfaces/api/handlers/admin_handler.rs b/src/interfaces/api/handlers/admin_handler.rs index 98556dd4..ce184be4 100644 --- a/src/interfaces/api/handlers/admin_handler.rs +++ b/src/interfaces/api/handlers/admin_handler.rs @@ -49,7 +49,6 @@ pub fn admin_routes() -> Router> { "/settings/storage/generate-key", post(generate_encryption_key), ) - .route("/settings/general", get(get_general_settings)) // Dashboard / stats .route("/dashboard", get(get_dashboard_stats)) // User management @@ -62,7 +61,6 @@ pub fn admin_routes() -> Router> { .route("/users/{id}/quota", put(update_user_quota)) .route("/users/{id}/password", put(reset_user_password)) // Registration control - .route("/settings/registration", get(get_registration_setting)) .route("/settings/registration", put(set_registration_setting)) // Audio metadata .route("/audio/metadata/reextract", post(reextract_audio_metadata)) @@ -625,44 +623,6 @@ fn build_backend_from_config( } } -/// GET /api/admin/settings/general — system overview (backward compat) -#[utoipa::path( - get, - path = "/api/admin/settings/general", - responses( - (status = 200, description = "General system settings"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Admin required") - ), - security(("bearerAuth" = [])), - tag = "admin" -)] -pub async fn get_general_settings( - State(state): State>, - headers: HeaderMap, -) -> Result { - admin_guard(&state, &headers).await?; - - let auth = state - .auth_service - .as_ref() - .ok_or_else(|| AppError::internal_error("Auth service not configured"))?; - - let user_count = auth - .auth_application_service - .count_users_efficient() - .await - .unwrap_or(0); - let oidc_configured = auth.auth_application_service.oidc_enabled(); - - Ok(Json(serde_json::json!({ - "server_version": env!("CARGO_PKG_VERSION"), - "auth_enabled": true, - "total_users": user_count, - "oidc_configured": oidc_configured, - }))) -} - // ============================================================================ // Dashboard / Stats // ============================================================================ @@ -1137,36 +1097,6 @@ pub async fn reset_user_password( // Registration Control // ============================================================================ -/// GET /api/admin/settings/registration — check if public registration is enabled -#[utoipa::path( - get, - path = "/api/admin/settings/registration", - responses( - (status = 200, description = "Registration setting"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Admin required") - ), - security(("bearerAuth" = [])), - tag = "admin" -)] -pub async fn get_registration_setting( - State(state): State>, - headers: HeaderMap, -) -> Result { - admin_guard(&state, &headers).await?; - - let svc = state - .admin_settings_service - .as_ref() - .ok_or_else(|| AppError::internal_error("Admin settings service not available"))?; - - let val = svc.get_registration_enabled().await; - - Ok(Json(serde_json::json!({ - "registration_enabled": val, - }))) -} - /// PUT /api/admin/settings/registration — enable/disable public registration #[utoipa::path( put, diff --git a/src/interfaces/api/handlers/dedup_handler.rs b/src/interfaces/api/handlers/dedup_handler.rs index ec4f8576..1d25780a 100644 --- a/src/interfaces/api/handlers/dedup_handler.rs +++ b/src/interfaces/api/handlers/dedup_handler.rs @@ -1,6 +1,6 @@ use axum::{ body::Body, - extract::{Json, Multipart, Path, State}, + extract::{Json, Path, State}, http::{Response, StatusCode, header}, response::IntoResponse, }; @@ -9,7 +9,6 @@ use utoipa::ToSchema; use crate::common::di::AppState; use crate::interfaces::middleware::auth::AuthUser; -use crate::interfaces::upload_ingest; use std::sync::Arc; /// Global application state for dependency injection @@ -57,21 +56,6 @@ pub struct HashBatchResponse { pub owned: Vec, } -/// Response for upload with dedup endpoint -#[derive(Debug, Serialize, ToSchema)] -pub struct DedupUploadResponse { - /// Whether this was a new file or an existing one - pub is_new: bool, - /// The BLAKE3 hash of the content - pub hash: String, - /// The size of the content in bytes - pub size: u64, - /// Bytes saved by deduplication (0 if new file) - pub bytes_saved: u64, - /// Current reference count for this blob - pub ref_count: u32, -} - /// Response for dedup stats endpoint #[derive(Debug, Serialize, ToSchema)] pub struct StatsResponse { @@ -223,101 +207,6 @@ impl DedupHandler { .into_response() } - /// Upload content with automatic deduplication (streaming). - /// - /// Streams the multipart field straight into the CDC chunk store — - /// chunking, BLAKE3 hashing and dedup checks happen while the bytes - /// arrive (no temp file, no re-read; peak RAM is bounded regardless - /// of file size). - /// - /// POST /api/dedup/upload - pub(super) async fn upload_with_dedup_impl( - State(state): State, - _auth_user: AuthUser, - mut multipart: Multipart, - ) -> impl IntoResponse { - let dedup = &state.core.dedup_service; - - // Process multipart form - while let Some(field) = multipart.next_field().await.unwrap_or(None) { - let name = field.name().unwrap_or("").to_string(); - - if name == "file" { - let content_type = field - .content_type() - .unwrap_or("application/octet-stream") - .to_string(); - let filename = field.file_name().unwrap_or("unnamed").to_string(); - - // ── Stream into the CDC chunk store ────────────────── - let source = upload_ingest::multipart_field_stream(field); - let ingested = match upload_ingest::ingest_stream_to_cas( - source, - dedup, - &filename, - &content_type, - usize::MAX, - None, - ) - .await - { - Ok(ingested) => ingested, - Err(e) => { - tracing::warn!("Dedup upload ingest failed: {}", e.message); - return e.into_response(); - } - }; - - if ingested.size == 0 { - upload_ingest::discard_ingested(dedup, &ingested).await; - return Response::builder() - .status(StatusCode::BAD_REQUEST) - .header(header::CONTENT_TYPE, "application/json") - .body(Body::from(r#"{"error": "Empty file not allowed"}"#)) - .unwrap() - .into_response(); - } - - let metadata = dedup.get_blob_metadata(&ingested.hash).await; - - let response = DedupUploadResponse { - is_new: ingested.is_new_blob, - hash: ingested.hash.clone(), - size: ingested.size, - bytes_saved: ingested.bytes_saved, - ref_count: metadata.map(|m| m.ref_count).unwrap_or(1), - }; - - tracing::info!( - "🔗 Dedup upload: hash={}, new={}, saved={}", - ingested.hash, - ingested.is_new_blob, - ingested.bytes_saved - ); - - return Response::builder() - .status(if ingested.is_new_blob { - StatusCode::CREATED - } else { - StatusCode::OK - }) - .header(header::CONTENT_TYPE, "application/json") - .body(Body::from(serde_json::to_string(&response).unwrap())) - .unwrap() - .into_response(); - } - } - - Response::builder() - .status(StatusCode::BAD_REQUEST) - .header(header::CONTENT_TYPE, "application/json") - .body(Body::from( - r#"{"error": "No file field found in multipart form"}"#, - )) - .unwrap() - .into_response() - } - /// Get deduplication statistics /// /// GET /api/dedup/stats @@ -562,27 +451,6 @@ pub async fn check_hashes_batch( DedupHandler::check_hashes_batch_impl(state, auth_user, body).await } -#[utoipa::path( - post, - path = "/api/dedup/upload", - request_body(content_type = "multipart/form-data", description = "Multipart form with a 'file' field"), - responses( - (status = 201, description = "New blob stored", body = DedupUploadResponse), - (status = 200, description = "Blob already existed (dedup hit)", body = DedupUploadResponse), - (status = 400, description = "No file field or empty file"), - (status = 500, description = "Upload failed"), - ), - tag = "dedup", - security(("bearerAuth" = [])) -)] -pub async fn upload_with_dedup( - state: State, - auth_user: AuthUser, - multipart: Multipart, -) -> impl IntoResponse { - DedupHandler::upload_with_dedup_impl(state, auth_user, multipart).await -} - #[utoipa::path( get, path = "/api/dedup/stats", @@ -803,45 +671,6 @@ mod tests { assert!(!tokio::fs::try_exists(&temp_path).await.unwrap_or(true)); } - /// Verify the DedupUploadResponse serializes correctly for new blobs. - #[test] - fn dedup_upload_response_serialization_new_blob() { - let response = DedupUploadResponse { - is_new: true, - hash: "a".repeat(64), - size: 1024, - bytes_saved: 0, - ref_count: 1, - }; - - let json = serde_json::to_string(&response).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); - - assert_eq!(parsed["is_new"], true); - assert_eq!(parsed["size"], 1024); - assert_eq!(parsed["bytes_saved"], 0); - assert_eq!(parsed["ref_count"], 1); - } - - /// Verify the DedupUploadResponse serializes correctly for dedup hits. - #[test] - fn dedup_upload_response_serialization_dedup_hit() { - let response = DedupUploadResponse { - is_new: false, - hash: "b".repeat(64), - size: 2048, - bytes_saved: 2048, - ref_count: 3, - }; - - let json = serde_json::to_string(&response).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); - - assert_eq!(parsed["is_new"], false); - assert_eq!(parsed["bytes_saved"], 2048); - assert_eq!(parsed["ref_count"], 3); - } - #[test] fn valid_blob_hash_accepts_64_hex_only() { assert!(is_valid_blob_hash(&"abcdef0123456789".repeat(4))); // 64 hex chars diff --git a/src/interfaces/api/handlers/folder_handler.rs b/src/interfaces/api/handlers/folder_handler.rs index b3fc7e51..7dc304df 100644 --- a/src/interfaces/api/handlers/folder_handler.rs +++ b/src/interfaces/api/handlers/folder_handler.rs @@ -17,7 +17,6 @@ use crate::application::dtos::folder_dto::{ ListResourcesOptions, MoveFolderDto, RenameFolderDto, }; use crate::application::dtos::grant_dto::{ResourceContentDto, ResourceTypeDto}; -use crate::application::dtos::pagination::PaginationRequestDto; use crate::application::ports::folder_ports::FolderUseCase; use crate::application::ports::trash_ports::TrashUseCase; use crate::application::services::folder_service::FolderService; @@ -107,16 +106,6 @@ impl FolderHandler { Self::list_folders_scoped(service, None, &auth_user).await } - /// Lists root folders with pagination. - /// Scoped to the authenticated user — only returns folders owned by this user. - pub(super) async fn list_root_folders_paginated_impl( - State(service): State, - auth_user: AuthUser, - _pagination: Query, - ) -> axum::response::Response { - Self::list_folders_scoped(service, None, &auth_user).await - } - /// Internal helper: lists folders scoped to the authenticated user. /// Uses `list_folders_for_owner` — the DB query filters by `user_id`, /// so no data from other users ever leaves the database. @@ -378,24 +367,6 @@ pub async fn list_root_folders( FolderHandler::list_root_folders_impl(state, auth_user).await } -#[utoipa::path( - get, - path = "/api/folders/paginated", - params(PaginationRequestDto), - responses( - (status = 200, description = "Paginated list of root folders"), - ), - security(("bearerAuth" = [])), - tag = "folders" -)] -pub async fn list_root_folders_paginated( - state: State, - auth_user: AuthUser, - pagination: Query, -) -> axum::response::Response { - FolderHandler::list_root_folders_paginated_impl(state, auth_user, pagination).await -} - #[utoipa::path( put, path = "/api/folders/{id}/rename", diff --git a/src/interfaces/api/handlers/people_handler.rs b/src/interfaces/api/handlers/people_handler.rs index cb217313..e1e391fd 100644 --- a/src/interfaces/api/handlers/people_handler.rs +++ b/src/interfaces/api/handlers/people_handler.rs @@ -88,30 +88,6 @@ pub async fn rename_person( } } -#[derive(Deserialize)] -pub struct HideBody { - pub hidden: bool, -} - -/// POST /api/people/{id}/hide — hide/unhide a person from the grid. -pub async fn hide_person( - State(state): State>, - auth_user: AuthUser, - Path(id): Path, - Json(body): Json, -) -> Response { - let Some(svc) = state.people_service.as_ref() else { - return disabled(); - }; - let Ok(person_id) = Uuid::parse_str(&id) else { - return bad_id(); - }; - match svc.set_hidden(auth_user.id, person_id, body.hidden).await { - Ok(()) => StatusCode::NO_CONTENT.into_response(), - Err(e) => AppError::from(e).into_response(), - } -} - #[derive(Deserialize)] pub struct MergeBody { pub into: String, diff --git a/src/interfaces/api/mod.rs b/src/interfaces/api/mod.rs index 7afbc523..06d87a8c 100644 --- a/src/interfaces/api/mod.rs +++ b/src/interfaces/api/mod.rs @@ -56,9 +56,7 @@ use crate::interfaces::api::handlers::contacts_handler::{ AddMemberRequest, AddressBookResponse, CreateAddressBookRequest, CreateContactRequest, GroupNameRequest, UpdateAddressBookRequest, UpdateContactRequest, }; -use crate::interfaces::api::handlers::dedup_handler::{ - DedupUploadResponse, HashCheckResponse, StatsResponse, -}; +use crate::interfaces::api::handlers::dedup_handler::{HashCheckResponse, StatsResponse}; use crate::interfaces::api::handlers::file_handler::MoveFilePayload; #[derive(OpenApi)] @@ -98,7 +96,6 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; handlers::folder_handler::create_folder, handlers::folder_handler::get_folder, handlers::folder_handler::list_root_folders, - handlers::folder_handler::list_root_folders_paginated, handlers::folder_handler::list_folder_resources, handlers::folder_handler::rename_folder, handlers::folder_handler::move_folder, @@ -122,7 +119,6 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; handlers::chunked_upload_handler::cancel_upload, // Dedup handlers — all free functions for the same utoipa reason as chunked uploads. handlers::dedup_handler::check_hash, - handlers::dedup_handler::upload_with_dedup, handlers::dedup_handler::get_stats, handlers::dedup_handler::get_blob, handlers::dedup_handler::recalculate_stats, @@ -217,9 +213,7 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; handlers::admin_handler::update_user_active, handlers::admin_handler::update_user_quota, handlers::admin_handler::reset_user_password, - handlers::admin_handler::get_registration_setting, handlers::admin_handler::set_registration_setting, - handlers::admin_handler::get_general_settings, handlers::admin_handler::get_oidc_settings, handlers::admin_handler::save_oidc_settings, handlers::admin_handler::get_storage_settings, @@ -328,7 +322,6 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; UploadStatusResponseDto, // Dedup schemas HashCheckResponse, - DedupUploadResponse, StatsResponse, // Contacts / address-book schemas AddressBookResponse, diff --git a/src/interfaces/api/routes.rs b/src/interfaces/api/routes.rs index 3acc1cda..17b53ad7 100644 --- a/src/interfaces/api/routes.rs +++ b/src/interfaces/api/routes.rs @@ -62,11 +62,9 @@ use crate::interfaces::api::handlers::file_handler::{ create_file_by_hash, delete_file, download_file, get_file_metadata, get_thumbnail, list_files_query, move_file_simple, rename_file, upload_file_with_thumbnails, upload_thumbnail, }; -#[allow(deprecated)] use crate::interfaces::api::handlers::folder_handler::{ create_folder, delete_folder_with_trash, download_folder_zip, get_folder, - list_folder_resources, list_root_folders, list_root_folders_paginated, move_folder, - rename_folder, + list_folder_resources, list_root_folders, move_folder, rename_folder, }; use crate::interfaces::api::handlers::i18n_handler::{ get_locales, get_translations_by_locale, translate, @@ -160,9 +158,6 @@ pub fn create_public_api_routes(app_state: &Arc) -> Router) -> Router> { // Extract services from the pre-built AppState let folder_service = app_state.applications.folder_service_concrete.clone(); @@ -196,7 +191,6 @@ pub fn create_api_routes(app_state: &Arc) -> Router> { let folders_basic_router = Router::new() .route("/", post(create_folder)) .route("/", get(list_root_folders)) - .route("/paginated", get(list_root_folders_paginated)) .route("/{id}", get(get_folder)) .route("/{id}/resources", get(list_folder_resources)) .route("/{id}/rename", put(rename_folder)) @@ -383,12 +377,11 @@ pub fn create_api_routes(app_state: &Arc) -> Router> { // All handlers are free functions — see dedup_handler.rs for why // #[utoipa::path] cannot be applied to DedupHandler impl methods directly. use super::handlers::dedup_handler::{ - check_hash, check_hashes_batch, get_blob, get_stats, recalculate_stats, upload_with_dedup, + check_hash, check_hashes_batch, get_blob, get_stats, recalculate_stats, }; let dedup_router = Router::new() .route("/check/{hash}", get(check_hash)) .route("/check-batch", post(check_hashes_batch)) - .route("/upload", post(upload_with_dedup)) .route("/stats", get(get_stats)) .route("/blob/{hash}", get(get_blob)) // NOTE: remove_reference is intentionally NOT exposed as a public @@ -447,7 +440,6 @@ pub fn create_api_routes(app_state: &Arc) -> Router> { .route("/faces/{file_id}", get(people_handler::faces_for_file)) .route("/{id}", patch(people_handler::rename_person)) .route("/{id}/photos", get(people_handler::person_photos)) - .route("/{id}/hide", post(people_handler::hide_person)) .with_state(app_state.clone()); router = router.nest("/people", people_router);