feat: support for moving selected ciphers to a folder

This commit is contained in:
qaz741wsd856 2025-12-05 08:21:54 +00:00
parent 40f07dcd58
commit 6d2fb7aa3e
3 changed files with 71 additions and 1 deletions

View file

@ -10,7 +10,7 @@ use worker::{query, D1PreparedStatement, Env};
use crate::auth::Claims;
use crate::db;
use crate::error::AppError;
use crate::models::cipher::{Cipher, CipherData, CipherRequestData, CreateCipherRequest};
use crate::models::cipher::{Cipher, CipherData, CipherRequestData, CreateCipherRequest, MoveCipherData};
use crate::models::user::{PasswordOrOtpData, User};
use axum::extract::Path;
@ -443,6 +443,65 @@ pub async fn create_cipher_simple(
Ok(Json(cipher))
}
/// Move selected ciphers to a folder (POST/PUT /api/ciphers/move)
#[worker::send]
pub async fn move_cipher_selected(
claims: Claims,
State(env): State<Arc<Env>>,
Json(payload): Json<MoveCipherData>,
) -> Result<Json<()>, AppError> {
let db = db::get_db(&env)?;
let user_id = &claims.sub;
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
// Validate folder exists and belongs to user (if folder_id is provided)
if let Some(ref folder_id) = payload.folder_id {
let folder_exists: Option<serde_json::Value> = db
.prepare("SELECT id FROM folders WHERE id = ?1 AND user_id = ?2")
.bind(&[folder_id.clone().into(), user_id.clone().into()])?
.first(None)
.await?;
if folder_exists.is_none() {
return Err(AppError::BadRequest(
"Invalid folder: Folder does not exist or belongs to another user".to_string(),
));
}
}
if payload.ids.is_empty() {
return Ok(Json(()));
}
// Use json_each() to update all matching ciphers in a single query
// This avoids N+1 query problem by updating all ciphers at once
let ids_json = serde_json::to_string(&payload.ids).map_err(|_| AppError::Internal)?;
// Update folder_id for all ciphers that belong to the user and are in the ids list
// Using json_each() allows us to update all matching ciphers in a single query
db.prepare(
"UPDATE ciphers SET folder_id = ?1, updated_at = ?2
WHERE user_id = ?3 AND id IN (SELECT value FROM json_each(?4))",
)
.bind(&[
payload
.folder_id
.clone()
.map(|s| s.into())
.unwrap_or(worker::wasm_bindgen::JsValue::NULL),
now.into(),
user_id.clone().into(),
ids_json.into(),
])?
.run()
.await?;
// Update user's revision date
db::touch_user_updated_at(&db, user_id).await?;
Ok(Json(()))
}
/// Purge the user's vault - delete all ciphers and folders
/// POST /api/ciphers/purge
///

View file

@ -312,3 +312,11 @@ pub struct CreateCipherRequest {
#[serde(alias = "CollectionIds")]
pub collection_ids: Vec<String>,
}
/// Request body for moving ciphers to a folder
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MoveCipherData {
pub folder_id: Option<String>,
pub ids: Vec<String>,
}

View file

@ -67,6 +67,9 @@ pub fn api_router(env: Env) -> Router {
.route("/api/ciphers/{id}/restore", put(ciphers::restore_cipher))
// Cipher bulk restore
.route("/api/ciphers/restore", put(ciphers::restore_ciphers_bulk))
// Move ciphers to folder
.route("/api/ciphers/move", post(ciphers::move_cipher_selected))
.route("/api/ciphers/move", put(ciphers::move_cipher_selected))
// Purge vault - delete all ciphers and folders (requires password verification)
.route("/api/ciphers/purge", post(ciphers::purge_vault))
// Folders CRUD