Add version check and cloud delete endpoints
This commit is contained in:
@@ -23,6 +23,34 @@ const TEXT_EXTENSIONS: [&str; 10] = [
|
|||||||
".typ", ".toml", ".bib", ".csl", ".yml", ".yaml", ".json", ".md", ".txt", ".csv",
|
".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::<u32>().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<ServerVersionInfo> {
|
||||||
|
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 {
|
fn is_text_path(path: &str) -> bool {
|
||||||
let lower = path.to_lowercase();
|
let lower = path.to_lowercase();
|
||||||
TEXT_EXTENSIONS.iter().any(|ext| lower.ends_with(ext))
|
TEXT_EXTENSIONS.iter().any(|ext| lower.ends_with(ext))
|
||||||
@@ -111,6 +139,7 @@ pub struct DeviceLoginRequest {
|
|||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub device_name: Option<String>,
|
pub device_name: Option<String>,
|
||||||
|
pub client_version: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -125,6 +154,18 @@ pub async fn login(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<DeviceLoginRequest>,
|
Json(payload): Json<DeviceLoginRequest>,
|
||||||
) -> Result<Json<DeviceLoginResponse>, (StatusCode, String)> {
|
) -> Result<Json<DeviceLoginResponse>, (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>(
|
let user = sqlx::query_as::<_, User>(
|
||||||
"SELECT id, username, email, password_hash, is_admin FROM users WHERE email = ?",
|
"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<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<StatusCode, (StatusCode, String)> {
|
||||||
|
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)]
|
#[derive(Serialize)]
|
||||||
pub struct CloudFile {
|
pub struct CloudFile {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -1092,3 +1154,24 @@ pub async fn pull_account_file(
|
|||||||
content: BASE64.encode(&data),
|
content: BASE64.encode(&data),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_account_file(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<StatusCode, (StatusCode, String)> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
+3
-2
@@ -142,6 +142,7 @@ async fn main() {
|
|||||||
.route("/packages/{name}", get(packages::list_versions).delete(packages::delete_package));
|
.route("/packages/{name}", get(packages::list_versions).delete(packages::delete_package));
|
||||||
|
|
||||||
let desktop_routes = Router::new()
|
let desktop_routes = Router::new()
|
||||||
|
.route("/version", get(desktop::version_info))
|
||||||
.route("/auth/login", post(desktop::login))
|
.route("/auth/login", post(desktop::login))
|
||||||
.route("/auth/logout", post(desktop::logout))
|
.route("/auth/logout", post(desktop::logout))
|
||||||
.route("/auth/me", get(desktop::me))
|
.route("/auth/me", get(desktop::me))
|
||||||
@@ -150,10 +151,10 @@ async fn main() {
|
|||||||
.route("/projects/{id}/manifest", get(desktop::get_manifest))
|
.route("/projects/{id}/manifest", get(desktop::get_manifest))
|
||||||
.route("/folders", get(desktop::list_folders))
|
.route("/folders", get(desktop::list_folders))
|
||||||
.route("/documents", get(desktop::list_documents).post(desktop::create_document))
|
.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("/shared", get(desktop::list_shared))
|
||||||
.route("/files", get(desktop::list_account_files))
|
.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));
|
.route("/projects/{id}/file", get(desktop::pull_file).put(desktop::push_file).delete(desktop::delete_file));
|
||||||
|
|
||||||
let v1_routes = Router::new()
|
let v1_routes = Router::new()
|
||||||
|
|||||||
Reference in New Issue
Block a user