From 42976295df342841e6923d76fc30ae35099b1c90 Mon Sep 17 00:00:00 2001 From: Deep Date: Mon, 22 Sep 2025 02:29:47 +0530 Subject: [PATCH] Add folder support --- src/handlers/ciphers.rs | 2 + src/handlers/folders.rs | 124 ++++++++++++++++++++++++++++++++++++++++ src/handlers/mod.rs | 1 + src/handlers/sync.rs | 8 ++- src/models/folder.rs | 31 +++++++++- src/models/sync.rs | 8 +-- src/router.rs | 8 ++- 7 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 src/handlers/folders.rs diff --git a/src/handlers/ciphers.rs b/src/handlers/ciphers.rs index 6df9ba1..931b9e9 100644 --- a/src/handlers/ciphers.rs +++ b/src/handlers/ciphers.rs @@ -147,6 +147,8 @@ pub async fn update_cipher( cipher.favorite, cipher.folder_id, cipher.updated_at, + id, + claims.sub, ).map_err(|_|AppError::Database)? .run() .await?; diff --git a/src/handlers/folders.rs b/src/handlers/folders.rs new file mode 100644 index 0000000..d7e82c8 --- /dev/null +++ b/src/handlers/folders.rs @@ -0,0 +1,124 @@ +use axum::{extract::State, Json}; +use chrono::Utc; +use std::sync::Arc; +use uuid::Uuid; +use worker::{query, Env}; + +use crate::auth::Claims; +use crate::db; +use crate::error::AppError; +use crate::models::folder::{CreateFolderRequest, Folder, FolderResponse}; +use axum::extract::Path; + +#[worker::send] +pub async fn create_folder( + claims: Claims, + State(env): State>, + Json(payload): Json, +) -> Result, AppError> { + let db = db::get_db(&env)?; + let now = Utc::now(); + let now = now.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); + + let folder = Folder { + id: Uuid::new_v4().to_string(), + user_id: claims.sub.clone(), + name: payload.name, + created_at: now.clone(), + updated_at: now.clone(), + }; + + query!( + &db, + "INSERT INTO folders (id, user_id, name, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)", + folder.id, + folder.user_id, + folder.name, + folder.created_at, + folder.updated_at + ) + .map_err(|_| AppError::Database)? + .run() + .await?; + + let response = FolderResponse { + id: folder.id, + name: folder.name, + revision_date: folder.updated_at, + object: "folder".to_string(), + }; + + Ok(Json(response)) +} + +#[worker::send] +pub async fn delete_folder( + claims: Claims, + State(env): State>, + Path(id): Path, +) -> Result, AppError> { + let db = db::get_db(&env)?; + + query!( + &db, + "DELETE FROM folders WHERE id = ?1 AND user_id = ?2", + id, + claims.sub + ) + .map_err(|_| AppError::Database)? + .run() + .await?; + + Ok(Json(())) +} +#[worker::send] +pub async fn update_folder( + claims: Claims, + State(env): State>, + Path(id): Path, + Json(payload): Json, +) -> Result, AppError> { + let db = db::get_db(&env)?; + let now = Utc::now(); + let now = now.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); + + let existing_folder: Folder = query!( + &db, + "SELECT * FROM folders WHERE id = ?1 AND user_id = ?2", + id, + claims.sub + ) + .map_err(|_| AppError::Database)? + .first(None) + .await? + .ok_or(AppError::NotFound("Folder not found".to_string()))?; + + let folder = Folder { + id: id.clone(), + user_id: existing_folder.user_id, + name: payload.name, + created_at: existing_folder.created_at, + updated_at: now.clone(), + }; + + query!( + &db, + "UPDATE folders SET name = ?1, updated_at = ?2 WHERE id = ?3 AND user_id = ?4", + folder.name, + folder.updated_at, + folder.id, + folder.user_id + ) + .map_err(|_| AppError::Database)? + .run() + .await?; + + let response = FolderResponse { + id: folder.id, + name: folder.name, + revision_date: folder.updated_at, + object: "folder".to_string(), + }; + + Ok(Json(response)) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 35e7ec4..9d5fbb4 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -3,3 +3,4 @@ pub mod ciphers; pub mod config; pub mod identity; pub mod sync; +pub mod folders; diff --git a/src/handlers/sync.rs b/src/handlers/sync.rs index 98e2c3c..becf9e3 100644 --- a/src/handlers/sync.rs +++ b/src/handlers/sync.rs @@ -9,7 +9,7 @@ use crate::{ error::AppError, models::{ cipher::{Cipher, CipherDBModel}, - folder::Folder, + folder::{Folder, FolderResponse}, sync::{Profile, SyncResponse}, user::User, }, @@ -32,13 +32,15 @@ pub async fn get_sync_data( .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; // Fetch folders - let folders: Vec = db + let folders_db: Vec = db .prepare("SELECT * FROM folders WHERE user_id = ?1") .bind(&[user_id.clone().into()])? .all() .await? .results()?; + let folders: Vec = folders_db.into_iter().map(|f| f.into()).collect(); + // Fetch ciphers let ciphers: Vec = db .prepare("SELECT * FROM ciphers WHERE user_id = ?1") @@ -91,4 +93,4 @@ pub async fn get_sync_data( }; Ok(Json(response)) -} +} \ No newline at end of file diff --git a/src/models/folder.rs b/src/models/folder.rs index eaf64ba..4d66673 100644 --- a/src/models/folder.rs +++ b/src/models/folder.rs @@ -1,7 +1,6 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Folder { pub id: String, pub user_id: String, @@ -10,3 +9,33 @@ pub struct Folder { pub created_at: String, pub updated_at: String, } + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FolderResponse { + pub id: String, + pub name: String, + pub revision_date: String, + #[serde(default = "default_object")] + pub object: String, +} + +fn default_object() -> String { + "folder".to_string() +} + +impl From for FolderResponse { + fn from(folder: Folder) -> Self { + FolderResponse { + id: folder.id, + name: folder.name, + revision_date: folder.updated_at, + object: "folder".to_string(), + } + } +} + +#[derive(Debug, Deserialize)] +pub struct CreateFolderRequest { + pub name: String, +} diff --git a/src/models/sync.rs b/src/models/sync.rs index b10e53b..a4155f0 100644 --- a/src/models/sync.rs +++ b/src/models/sync.rs @@ -1,4 +1,4 @@ -use super::{cipher::Cipher, folder::Folder}; +use super::{cipher::Cipher, folder::FolderResponse}; use serde::Serialize; use serde_json::Value; @@ -33,10 +33,10 @@ pub struct Profile { #[derive(Debug, Serialize)] pub struct SyncResponse { - #[serde(rename = "Profile")] + #[serde(rename = "profile")] pub profile: Profile, - #[serde(rename = "Folders")] - pub folders: Vec, + #[serde(rename = "folders")] + pub folders: Vec, #[serde(rename = "ciphers")] pub ciphers: Vec, #[serde(rename = "Domains")] diff --git a/src/router.rs b/src/router.rs index fab218a..a21dd66 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,11 +1,11 @@ use axum::{ - routing::{get, post, put}, + routing::{get, post, put, delete}, Router, }; use std::sync::Arc; use worker::Env; -use crate::handlers::{accounts, ciphers, config, identity, sync}; +use crate::handlers::{accounts, ciphers, config, identity, sync, folders}; pub fn api_router(env: Env) -> Router { let app_state = Arc::new(env); @@ -28,6 +28,10 @@ pub fn api_router(env: Env) -> Router { .route("/api/ciphers/create", post(ciphers::create_cipher)) .route("/api/ciphers/{id}", put(ciphers::update_cipher)) .route("/api/ciphers/{id}/delete", put(ciphers::delete_cipher)) + // Folders CRUD + .route("/api/folders", post(folders::create_folder)) + .route("/api/folders/{id}", put(folders::update_folder)) + .route("/api/folders/{id}", delete(folders::delete_folder)) .route("/api/config", get(config::config)) .with_state(app_state) }