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::auth::Claims;
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::error::AppError;
|
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 crate::models::user::{PasswordOrOtpData, User};
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
|
|
||||||
|
|
@ -443,6 +443,65 @@ pub async fn create_cipher_simple(
|
||||||
Ok(Json(cipher))
|
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
|
/// Purge the user's vault - delete all ciphers and folders
|
||||||
/// POST /api/ciphers/purge
|
/// POST /api/ciphers/purge
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -312,3 +312,11 @@ pub struct CreateCipherRequest {
|
||||||
#[serde(alias = "CollectionIds")]
|
#[serde(alias = "CollectionIds")]
|
||||||
pub collection_ids: Vec<String>,
|
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))
|
.route("/api/ciphers/{id}/restore", put(ciphers::restore_cipher))
|
||||||
// Cipher bulk restore
|
// Cipher bulk restore
|
||||||
.route("/api/ciphers/restore", put(ciphers::restore_ciphers_bulk))
|
.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)
|
// Purge vault - delete all ciphers and folders (requires password verification)
|
||||||
.route("/api/ciphers/purge", post(ciphers::purge_vault))
|
.route("/api/ciphers/purge", post(ciphers::purge_vault))
|
||||||
// Folders CRUD
|
// Folders CRUD
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue