From 538ddf70d874e4532d88d417e03a231869f4580d Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Mon, 20 Jul 2026 18:37:24 -0400 Subject: [PATCH] Add version check and cloud delete endpoints --- server/src/desktop.rs | 83 +++++++++++++++++++++++++++++++++++++++++++ server/src/main.rs | 5 +-- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/server/src/desktop.rs b/server/src/desktop.rs index c984d2f..9abc6cd 100644 --- a/server/src/desktop.rs +++ b/server/src/desktop.rs @@ -23,6 +23,34 @@ const TEXT_EXTENSIONS: [&str; 10] = [ ".typ", ".toml", ".bib", ".csl", ".yml", ".yaml", ".json", ".md", ".txt", ".csv", ]; +pub const MIN_DESKTOP_VERSION: &str = "1.0.0"; + +fn parse_version(version: &str) -> (u32, u32, u32) { + let mut parts = version.trim().split('.').map(|part| part.parse::().unwrap_or(0)); + ( + parts.next().unwrap_or(0), + parts.next().unwrap_or(0), + parts.next().unwrap_or(0), + ) +} + +fn version_at_least(actual: &str, required: &str) -> bool { + parse_version(actual) >= parse_version(required) +} + +#[derive(Serialize)] +pub struct ServerVersionInfo { + pub server_version: String, + pub min_desktop_version: String, +} + +pub async fn version_info() -> Json { + Json(ServerVersionInfo { + server_version: env!("CARGO_PKG_VERSION").to_string(), + min_desktop_version: MIN_DESKTOP_VERSION.to_string(), + }) +} + fn is_text_path(path: &str) -> bool { let lower = path.to_lowercase(); TEXT_EXTENSIONS.iter().any(|ext| lower.ends_with(ext)) @@ -111,6 +139,7 @@ pub struct DeviceLoginRequest { pub email: String, pub password: String, pub device_name: Option, + pub client_version: Option, } #[derive(Serialize)] @@ -125,6 +154,18 @@ pub async fn login( State(state): State, Json(payload): Json, ) -> Result, (StatusCode, String)> { + if let Some(client_version) = &payload.client_version { + if !version_at_least(client_version, MIN_DESKTOP_VERSION) { + return Err(( + StatusCode::UPGRADE_REQUIRED, + format!( + "This server requires typst-desktop v{} or newer (you have v{}). Please update the app.", + MIN_DESKTOP_VERSION, client_version + ), + )); + } + } + let user = sqlx::query_as::<_, User>( "SELECT id, username, email, password_hash, is_admin FROM users WHERE email = ?", ) @@ -1010,6 +1051,27 @@ pub async fn create_document( })) } +pub async fn delete_document( + State(state): State, + headers: HeaderMap, + Path(id): Path, +) -> Result { + let user_id = authenticate(&state, &headers).await?; + + let result = sqlx::query("DELETE FROM documents WHERE id = ? AND owner_id = ?") + .bind(&id) + .bind(&user_id) + .execute(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + if result.rows_affected() == 0 { + return Err((StatusCode::NOT_FOUND, "Document not found or unauthorized".to_string())); + } + + Ok(StatusCode::NO_CONTENT) +} + #[derive(Serialize)] pub struct CloudFile { pub id: String, @@ -1092,3 +1154,24 @@ pub async fn pull_account_file( content: BASE64.encode(&data), })) } + +pub async fn delete_account_file( + State(state): State, + headers: HeaderMap, + Path(id): Path, +) -> Result { + let user_id = authenticate(&state, &headers).await?; + + let result = sqlx::query("DELETE FROM files WHERE id = ? AND owner_id = ?") + .bind(&id) + .bind(&user_id) + .execute(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + if result.rows_affected() == 0 { + return Err((StatusCode::NOT_FOUND, "File not found or unauthorized".to_string())); + } + + Ok(StatusCode::NO_CONTENT) +} diff --git a/server/src/main.rs b/server/src/main.rs index c2e44f5..69c365c 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -142,6 +142,7 @@ async fn main() { .route("/packages/{name}", get(packages::list_versions).delete(packages::delete_package)); let desktop_routes = Router::new() + .route("/version", get(desktop::version_info)) .route("/auth/login", post(desktop::login)) .route("/auth/logout", post(desktop::logout)) .route("/auth/me", get(desktop::me)) @@ -150,10 +151,10 @@ async fn main() { .route("/projects/{id}/manifest", get(desktop::get_manifest)) .route("/folders", get(desktop::list_folders)) .route("/documents", get(desktop::list_documents).post(desktop::create_document)) - .route("/documents/{id}", get(desktop::pull_document).put(desktop::push_document)) + .route("/documents/{id}", get(desktop::pull_document).put(desktop::push_document).delete(desktop::delete_document)) .route("/shared", get(desktop::list_shared)) .route("/files", get(desktop::list_account_files)) - .route("/files/{id}", get(desktop::pull_account_file)) + .route("/files/{id}", get(desktop::pull_account_file).delete(desktop::delete_account_file)) .route("/projects/{id}/file", get(desktop::pull_file).put(desktop::push_file).delete(desktop::delete_file)); let v1_routes = Router::new()