feat: support for moving selected ciphers to a folder
This commit is contained in:
parent
40f07dcd58
commit
6d2fb7aa3e
3 changed files with 71 additions and 1 deletions
|
|
@ -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
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue