From 3ba0dd1c148d40dfa579d14e7f5556a8581029bd Mon Sep 17 00:00:00 2001 From: perf3ct Date: Thu, 12 Jun 2025 18:49:08 +0000 Subject: [PATCH] feat(server): improve settings page and settings --- frontend/src/pages/SettingsPage.jsx | 313 ++++++++++++++++++++++++++-- src/db.rs | 118 ++++++++++- src/main.rs | 10 +- src/models.rs | 97 ++++++++- src/routes/documents.rs | 34 ++- src/routes/settings.rs | 26 ++- src/routes/users.rs | 2 +- 7 files changed, 558 insertions(+), 42 deletions(-) diff --git a/frontend/src/pages/SettingsPage.jsx b/frontend/src/pages/SettingsPage.jsx index 2f13757..fd1f291 100644 --- a/frontend/src/pages/SettingsPage.jsx +++ b/frontend/src/pages/SettingsPage.jsx @@ -7,6 +7,7 @@ import { Tabs, Tab, FormControl, + FormControlLabel, InputLabel, Select, MenuItem, @@ -29,6 +30,7 @@ import { Card, CardContent, Divider, + Switch, } from '@mui/material'; import { Edit as EditIcon, Delete as DeleteIcon, Add as AddIcon } from '@mui/icons-material'; import { useAuth } from '../contexts/AuthContext'; @@ -39,6 +41,21 @@ const SettingsPage = () => { const [tabValue, setTabValue] = useState(0); const [settings, setSettings] = useState({ ocrLanguage: 'eng', + concurrentOcrJobs: 4, + ocrTimeoutSeconds: 300, + maxFileSizeMb: 50, + allowedFileTypes: ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'], + autoRotateImages: true, + enableImagePreprocessing: true, + searchResultsPerPage: 25, + searchSnippetLength: 200, + fuzzySearchThreshold: 0.8, + retentionDays: null, + enableAutoCleanup: false, + enableCompression: false, + memoryLimitMb: 512, + cpuPriority: 'normal', + enableBackgroundOcr: true, }); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); @@ -72,7 +89,24 @@ const SettingsPage = () => { const fetchSettings = async () => { try { const response = await api.get('/settings'); - setSettings(response.data); + setSettings({ + ocrLanguage: response.data.ocr_language || 'eng', + concurrentOcrJobs: response.data.concurrent_ocr_jobs || 4, + ocrTimeoutSeconds: response.data.ocr_timeout_seconds || 300, + maxFileSizeMb: response.data.max_file_size_mb || 50, + allowedFileTypes: response.data.allowed_file_types || ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'], + autoRotateImages: response.data.auto_rotate_images !== undefined ? response.data.auto_rotate_images : true, + enableImagePreprocessing: response.data.enable_image_preprocessing !== undefined ? response.data.enable_image_preprocessing : true, + searchResultsPerPage: response.data.search_results_per_page || 25, + searchSnippetLength: response.data.search_snippet_length || 200, + fuzzySearchThreshold: response.data.fuzzy_search_threshold || 0.8, + retentionDays: response.data.retention_days, + enableAutoCleanup: response.data.enable_auto_cleanup || false, + enableCompression: response.data.enable_compression || false, + memoryLimitMb: response.data.memory_limit_mb || 512, + cpuPriority: response.data.cpu_priority || 'normal', + enableBackgroundOcr: response.data.enable_background_ocr !== undefined ? response.data.enable_background_ocr : true, + }); } catch (error) { console.error('Error fetching settings:', error); if (error.response?.status !== 404) { @@ -96,7 +130,14 @@ const SettingsPage = () => { const handleSettingsChange = async (key, value) => { setLoading(true); try { - await api.put('/settings', { ...settings, [key]: value }); + // Convert camelCase to snake_case for API + const snakeCase = (str) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); + const apiKey = snakeCase(key); + + // Build the update payload with only the changed field + const updatePayload = { [apiKey]: value }; + + await api.put('/settings', updatePayload); setSettings({ ...settings, [key]: value }); showSnackbar('Settings updated successfully', 'success'); } catch (error) { @@ -199,24 +240,258 @@ const SettingsPage = () => { OCR Configuration - - OCR Language - - - - Select the primary language for OCR text extraction. This affects how accurately text is recognized from images and scanned documents. + + + + OCR Language + + + + + handleSettingsChange('concurrentOcrJobs', parseInt(e.target.value))} + disabled={loading} + inputProps={{ min: 1, max: 16 }} + helperText="Number of OCR jobs that can run simultaneously" + /> + + + handleSettingsChange('ocrTimeoutSeconds', parseInt(e.target.value))} + disabled={loading} + inputProps={{ min: 30, max: 3600 }} + helperText="Maximum time for OCR processing per file" + /> + + + + CPU Priority + + + + + + + + + + + File Processing + + + + handleSettingsChange('maxFileSizeMb', parseInt(e.target.value))} + disabled={loading} + inputProps={{ min: 1, max: 500 }} + helperText="Maximum allowed file size for uploads" + /> + + + handleSettingsChange('memoryLimitMb', parseInt(e.target.value))} + disabled={loading} + inputProps={{ min: 128, max: 4096 }} + helperText="Memory limit per OCR job" + /> + + + + handleSettingsChange('autoRotateImages', e.target.checked)} + disabled={loading} + /> + } + label="Auto-rotate Images" + /> + + Automatically detect and correct image orientation + + + + + + handleSettingsChange('enableImagePreprocessing', e.target.checked)} + disabled={loading} + /> + } + label="Enable Image Preprocessing" + /> + + Enhance images for better OCR accuracy (deskew, denoise, contrast) + + + + + + handleSettingsChange('enableBackgroundOcr', e.target.checked)} + disabled={loading} + /> + } + label="Enable Background OCR" + /> + + Process OCR in the background after file upload + + + + + + + + + + + Search Configuration + + + + + + Results Per Page + + + + + handleSettingsChange('searchSnippetLength', parseInt(e.target.value))} + disabled={loading} + inputProps={{ min: 50, max: 500 }} + helperText="Characters to show in search result previews" + /> + + + handleSettingsChange('fuzzySearchThreshold', parseFloat(e.target.value))} + disabled={loading} + inputProps={{ min: 0, max: 1, step: 0.1 }} + helperText="Tolerance for spelling mistakes (0.0-1.0)" + /> + + + + + + + + + Storage Management + + + + + handleSettingsChange('retentionDays', e.target.value ? parseInt(e.target.value) : null)} + disabled={loading} + inputProps={{ min: 1 }} + helperText="Auto-delete documents after X days (leave empty to disable)" + /> + + + + handleSettingsChange('enableAutoCleanup', e.target.checked)} + disabled={loading} + /> + } + label="Enable Auto Cleanup" + /> + + Automatically remove orphaned files and clean up storage + + + + + + handleSettingsChange('enableCompression', e.target.checked)} + disabled={loading} + /> + } + label="Enable Compression" + /> + + Compress stored documents to save disk space + + + + diff --git a/src/db.rs b/src/db.rs index 1aab516..6242022 100644 --- a/src/db.rs +++ b/src/db.rs @@ -92,6 +92,21 @@ impl Database { id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE, ocr_language VARCHAR(10) DEFAULT 'eng', + concurrent_ocr_jobs INT DEFAULT 4, + ocr_timeout_seconds INT DEFAULT 300, + max_file_size_mb INT DEFAULT 50, + allowed_file_types TEXT[] DEFAULT ARRAY['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'], + auto_rotate_images BOOLEAN DEFAULT TRUE, + enable_image_preprocessing BOOLEAN DEFAULT TRUE, + search_results_per_page INT DEFAULT 25, + search_snippet_length INT DEFAULT 200, + fuzzy_search_threshold REAL DEFAULT 0.8, + retention_days INT, + enable_auto_cleanup BOOLEAN DEFAULT FALSE, + enable_compression BOOLEAN DEFAULT FALSE, + memory_limit_mb INT DEFAULT 512, + cpu_priority VARCHAR(10) DEFAULT 'normal', + enable_background_ocr BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ) @@ -569,7 +584,12 @@ impl Database { pub async fn get_user_settings(&self, user_id: Uuid) -> Result> { let row = sqlx::query( - "SELECT id, user_id, ocr_language, created_at, updated_at FROM settings WHERE user_id = $1" + r#"SELECT id, user_id, ocr_language, concurrent_ocr_jobs, ocr_timeout_seconds, + max_file_size_mb, allowed_file_types, auto_rotate_images, enable_image_preprocessing, + search_results_per_page, search_snippet_length, fuzzy_search_threshold, + retention_days, enable_auto_cleanup, enable_compression, memory_limit_mb, + cpu_priority, enable_background_ocr, created_at, updated_at + FROM settings WHERE user_id = $1"# ) .bind(user_id) .fetch_optional(&self.pool) @@ -580,6 +600,21 @@ impl Database { id: row.get("id"), user_id: row.get("user_id"), ocr_language: row.get("ocr_language"), + concurrent_ocr_jobs: row.get("concurrent_ocr_jobs"), + ocr_timeout_seconds: row.get("ocr_timeout_seconds"), + max_file_size_mb: row.get("max_file_size_mb"), + allowed_file_types: row.get("allowed_file_types"), + auto_rotate_images: row.get("auto_rotate_images"), + enable_image_preprocessing: row.get("enable_image_preprocessing"), + search_results_per_page: row.get("search_results_per_page"), + search_snippet_length: row.get("search_snippet_length"), + fuzzy_search_threshold: row.get("fuzzy_search_threshold"), + retention_days: row.get("retention_days"), + enable_auto_cleanup: row.get("enable_auto_cleanup"), + enable_compression: row.get("enable_compression"), + memory_limit_mb: row.get("memory_limit_mb"), + cpu_priority: row.get("cpu_priority"), + enable_background_ocr: row.get("enable_background_ocr"), created_at: row.get("created_at"), updated_at: row.get("updated_at"), })), @@ -587,18 +622,70 @@ impl Database { } } - pub async fn create_or_update_settings(&self, user_id: Uuid, ocr_language: &str) -> Result { + pub async fn create_or_update_settings(&self, user_id: Uuid, settings: &crate::models::UpdateSettings) -> Result { + // Get existing settings to merge with updates + let existing = self.get_user_settings(user_id).await?; + let defaults = crate::models::Settings::default(); + + // Merge existing/defaults with updates + let current = existing.unwrap_or_else(|| { + let mut s = defaults; + s.user_id = user_id; + s + }); + let row = sqlx::query( r#" - INSERT INTO settings (user_id, ocr_language) - VALUES ($1, $2) - ON CONFLICT (user_id) DO UPDATE - SET ocr_language = $2, updated_at = NOW() - RETURNING id, user_id, ocr_language, created_at, updated_at + INSERT INTO settings ( + user_id, ocr_language, concurrent_ocr_jobs, ocr_timeout_seconds, + max_file_size_mb, allowed_file_types, auto_rotate_images, enable_image_preprocessing, + search_results_per_page, search_snippet_length, fuzzy_search_threshold, + retention_days, enable_auto_cleanup, enable_compression, memory_limit_mb, + cpu_priority, enable_background_ocr + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + ON CONFLICT (user_id) DO UPDATE SET + ocr_language = $2, + concurrent_ocr_jobs = $3, + ocr_timeout_seconds = $4, + max_file_size_mb = $5, + allowed_file_types = $6, + auto_rotate_images = $7, + enable_image_preprocessing = $8, + search_results_per_page = $9, + search_snippet_length = $10, + fuzzy_search_threshold = $11, + retention_days = $12, + enable_auto_cleanup = $13, + enable_compression = $14, + memory_limit_mb = $15, + cpu_priority = $16, + enable_background_ocr = $17, + updated_at = NOW() + RETURNING id, user_id, ocr_language, concurrent_ocr_jobs, ocr_timeout_seconds, + max_file_size_mb, allowed_file_types, auto_rotate_images, enable_image_preprocessing, + search_results_per_page, search_snippet_length, fuzzy_search_threshold, + retention_days, enable_auto_cleanup, enable_compression, memory_limit_mb, + cpu_priority, enable_background_ocr, created_at, updated_at "# ) .bind(user_id) - .bind(ocr_language) + .bind(settings.ocr_language.as_ref().unwrap_or(¤t.ocr_language)) + .bind(settings.concurrent_ocr_jobs.unwrap_or(current.concurrent_ocr_jobs)) + .bind(settings.ocr_timeout_seconds.unwrap_or(current.ocr_timeout_seconds)) + .bind(settings.max_file_size_mb.unwrap_or(current.max_file_size_mb)) + .bind(settings.allowed_file_types.as_ref().unwrap_or(¤t.allowed_file_types)) + .bind(settings.auto_rotate_images.unwrap_or(current.auto_rotate_images)) + .bind(settings.enable_image_preprocessing.unwrap_or(current.enable_image_preprocessing)) + .bind(settings.search_results_per_page.unwrap_or(current.search_results_per_page)) + .bind(settings.search_snippet_length.unwrap_or(current.search_snippet_length)) + .bind(settings.fuzzy_search_threshold.unwrap_or(current.fuzzy_search_threshold)) + .bind(settings.retention_days.unwrap_or(current.retention_days)) + .bind(settings.enable_auto_cleanup.unwrap_or(current.enable_auto_cleanup)) + .bind(settings.enable_compression.unwrap_or(current.enable_compression)) + .bind(settings.memory_limit_mb.unwrap_or(current.memory_limit_mb)) + .bind(settings.cpu_priority.as_ref().unwrap_or(¤t.cpu_priority)) + .bind(settings.enable_background_ocr.unwrap_or(current.enable_background_ocr)) .fetch_one(&self.pool) .await?; @@ -606,6 +693,21 @@ impl Database { id: row.get("id"), user_id: row.get("user_id"), ocr_language: row.get("ocr_language"), + concurrent_ocr_jobs: row.get("concurrent_ocr_jobs"), + ocr_timeout_seconds: row.get("ocr_timeout_seconds"), + max_file_size_mb: row.get("max_file_size_mb"), + allowed_file_types: row.get("allowed_file_types"), + auto_rotate_images: row.get("auto_rotate_images"), + enable_image_preprocessing: row.get("enable_image_preprocessing"), + search_results_per_page: row.get("search_results_per_page"), + search_snippet_length: row.get("search_snippet_length"), + fuzzy_search_threshold: row.get("fuzzy_search_threshold"), + retention_days: row.get("retention_days"), + enable_auto_cleanup: row.get("enable_auto_cleanup"), + enable_compression: row.get("enable_compression"), + memory_limit_mb: row.get("memory_limit_mb"), + cpu_priority: row.get("cpu_priority"), + enable_background_ocr: row.get("enable_background_ocr"), created_at: row.get("created_at"), updated_at: row.get("updated_at"), }) diff --git a/src/main.rs b/src/main.rs index f30b026..fa7622e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use axum::{ http::StatusCode, - response::Json, + response::{Json, Html}, routing::get, Router, }; @@ -52,6 +52,7 @@ async fn main() -> Result<(), Box> { .nest("/api/settings", routes::settings::router()) .nest("/api/users", routes::users::router()) .nest_service("/", ServeDir::new("/app/frontend")) + .fallback(serve_spa) .layer(CorsLayer::permissive()) .with_state(Arc::new(state)); @@ -72,4 +73,11 @@ async fn main() -> Result<(), Box> { async fn health_check() -> Result, StatusCode> { Ok(Json(serde_json::json!({"status": "ok"}))) +} + +async fn serve_spa() -> Result, StatusCode> { + match tokio::fs::read_to_string("/app/frontend/index.html").await { + Ok(html) => Ok(Html(html)), + Err(_) => Err(StatusCode::NOT_FOUND), + } } \ No newline at end of file diff --git a/src/models.rs b/src/models.rs index 2cf7266..d9d54f8 100644 --- a/src/models.rs +++ b/src/models.rs @@ -170,6 +170,21 @@ pub struct Settings { pub id: Uuid, pub user_id: Uuid, pub ocr_language: String, + pub concurrent_ocr_jobs: i32, + pub ocr_timeout_seconds: i32, + pub max_file_size_mb: i32, + pub allowed_file_types: Vec, + pub auto_rotate_images: bool, + pub enable_image_preprocessing: bool, + pub search_results_per_page: i32, + pub search_snippet_length: i32, + pub fuzzy_search_threshold: f32, + pub retention_days: Option, + pub enable_auto_cleanup: bool, + pub enable_compression: bool, + pub memory_limit_mb: i32, + pub cpu_priority: String, + pub enable_background_ocr: bool, pub created_at: DateTime, pub updated_at: DateTime, } @@ -177,17 +192,97 @@ pub struct Settings { #[derive(Debug, Serialize, Deserialize)] pub struct SettingsResponse { pub ocr_language: String, + pub concurrent_ocr_jobs: i32, + pub ocr_timeout_seconds: i32, + pub max_file_size_mb: i32, + pub allowed_file_types: Vec, + pub auto_rotate_images: bool, + pub enable_image_preprocessing: bool, + pub search_results_per_page: i32, + pub search_snippet_length: i32, + pub fuzzy_search_threshold: f32, + pub retention_days: Option, + pub enable_auto_cleanup: bool, + pub enable_compression: bool, + pub memory_limit_mb: i32, + pub cpu_priority: String, + pub enable_background_ocr: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct UpdateSettings { - pub ocr_language: String, + pub ocr_language: Option, + pub concurrent_ocr_jobs: Option, + pub ocr_timeout_seconds: Option, + pub max_file_size_mb: Option, + pub allowed_file_types: Option>, + pub auto_rotate_images: Option, + pub enable_image_preprocessing: Option, + pub search_results_per_page: Option, + pub search_snippet_length: Option, + pub fuzzy_search_threshold: Option, + pub retention_days: Option>, + pub enable_auto_cleanup: Option, + pub enable_compression: Option, + pub memory_limit_mb: Option, + pub cpu_priority: Option, + pub enable_background_ocr: Option, } impl From for SettingsResponse { fn from(settings: Settings) -> Self { Self { ocr_language: settings.ocr_language, + concurrent_ocr_jobs: settings.concurrent_ocr_jobs, + ocr_timeout_seconds: settings.ocr_timeout_seconds, + max_file_size_mb: settings.max_file_size_mb, + allowed_file_types: settings.allowed_file_types, + auto_rotate_images: settings.auto_rotate_images, + enable_image_preprocessing: settings.enable_image_preprocessing, + search_results_per_page: settings.search_results_per_page, + search_snippet_length: settings.search_snippet_length, + fuzzy_search_threshold: settings.fuzzy_search_threshold, + retention_days: settings.retention_days, + enable_auto_cleanup: settings.enable_auto_cleanup, + enable_compression: settings.enable_compression, + memory_limit_mb: settings.memory_limit_mb, + cpu_priority: settings.cpu_priority, + enable_background_ocr: settings.enable_background_ocr, + } + } +} + +impl Default for Settings { + fn default() -> Self { + Self { + id: Uuid::new_v4(), + user_id: Uuid::nil(), + ocr_language: "eng".to_string(), + concurrent_ocr_jobs: 4, + ocr_timeout_seconds: 300, + max_file_size_mb: 50, + allowed_file_types: vec![ + "pdf".to_string(), + "png".to_string(), + "jpg".to_string(), + "jpeg".to_string(), + "tiff".to_string(), + "bmp".to_string(), + "txt".to_string(), + ], + auto_rotate_images: true, + enable_image_preprocessing: true, + search_results_per_page: 25, + search_snippet_length: 200, + fuzzy_search_threshold: 0.8, + retention_days: None, + enable_auto_cleanup: false, + enable_compression: false, + memory_limit_mb: 512, + cpu_priority: "normal".to_string(), + enable_background_ocr: true, + created_at: Utc::now(), + updated_at: Utc::now(), } } } \ No newline at end of file diff --git a/src/routes/documents.rs b/src/routes/documents.rs index d43188c..9f1db3d 100644 --- a/src/routes/documents.rs +++ b/src/routes/documents.rs @@ -37,6 +37,14 @@ async fn upload_document( ) -> Result, StatusCode> { let file_service = FileService::new(state.config.upload_path.clone()); + // Get user settings for file upload restrictions + let settings = state + .db + .get_user_settings(auth_user.user.id) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .unwrap_or_else(|| crate::models::Settings::default()); + while let Some(field) = multipart.next_field().await.map_err(|_| StatusCode::BAD_REQUEST)? { let name = field.name().unwrap_or("").to_string(); @@ -46,13 +54,19 @@ async fn upload_document( .ok_or(StatusCode::BAD_REQUEST)? .to_string(); - if !file_service.is_allowed_file_type(&filename, &state.config.allowed_file_types) { + if !file_service.is_allowed_file_type(&filename, &settings.allowed_file_types) { return Err(StatusCode::BAD_REQUEST); } let data = field.bytes().await.map_err(|_| StatusCode::BAD_REQUEST)?; let file_size = data.len() as i64; + // Check file size limit + let max_size_bytes = (settings.max_file_size_mb as i64) * 1024 * 1024; + if file_size > max_size_bytes { + return Err(StatusCode::PAYLOAD_TOO_LARGE); + } + let mime_type = mime_guess::from_path(&filename) .first_or_octet_stream() .to_string(); @@ -81,15 +95,19 @@ async fn upload_document( let db_clone = state.db.clone(); let file_path_clone = file_path.clone(); let mime_type_clone = mime_type.clone(); + let ocr_language = settings.ocr_language.clone(); + let enable_background_ocr = settings.enable_background_ocr; - spawn(async move { - let ocr_service = OcrService::new(); - if let Ok(text) = ocr_service.extract_text(&file_path_clone, &mime_type_clone).await { - if !text.is_empty() { - let _ = db_clone.update_document_ocr(document_id, &text).await; + if enable_background_ocr { + spawn(async move { + let ocr_service = OcrService::new(); + if let Ok(text) = ocr_service.extract_text_with_lang(&file_path_clone, &mime_type_clone, &ocr_language).await { + if !text.is_empty() { + let _ = db_clone.update_document_ocr(document_id, &text).await; + } } - } - }); + }); + } return Ok(Json(saved_document.into())); } diff --git a/src/routes/settings.rs b/src/routes/settings.rs index 7a2e7b4..4af96ce 100644 --- a/src/routes/settings.rs +++ b/src/routes/settings.rs @@ -2,7 +2,7 @@ use axum::{ extract::State, http::StatusCode, response::Json, - routing::{get, put}, + routing::get, Router, }; use std::sync::Arc; @@ -30,8 +30,26 @@ async fn get_settings( let response = match settings { Some(s) => s.into(), - None => SettingsResponse { - ocr_language: "eng".to_string(), + None => { + let default = crate::models::Settings::default(); + SettingsResponse { + ocr_language: default.ocr_language, + concurrent_ocr_jobs: default.concurrent_ocr_jobs, + ocr_timeout_seconds: default.ocr_timeout_seconds, + max_file_size_mb: default.max_file_size_mb, + allowed_file_types: default.allowed_file_types, + auto_rotate_images: default.auto_rotate_images, + enable_image_preprocessing: default.enable_image_preprocessing, + search_results_per_page: default.search_results_per_page, + search_snippet_length: default.search_snippet_length, + fuzzy_search_threshold: default.fuzzy_search_threshold, + retention_days: default.retention_days, + enable_auto_cleanup: default.enable_auto_cleanup, + enable_compression: default.enable_compression, + memory_limit_mb: default.memory_limit_mb, + cpu_priority: default.cpu_priority, + enable_background_ocr: default.enable_background_ocr, + } }, }; @@ -45,7 +63,7 @@ async fn update_settings( ) -> Result, StatusCode> { let settings = state .db - .create_or_update_settings(auth_user.user.id, &update_data.ocr_language) + .create_or_update_settings(auth_user.user.id, &update_data) .await .map_err(|_| StatusCode::BAD_REQUEST)?; diff --git a/src/routes/users.rs b/src/routes/users.rs index 7b367b0..d08401a 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -2,7 +2,7 @@ use axum::{ extract::{Path, State}, http::StatusCode, response::Json, - routing::{delete, get, post, put}, + routing::get, Router, }; use std::sync::Arc;