From 5e04837b5d4367ed1ed5ebacdf8377d269b0870a Mon Sep 17 00:00:00 2001 From: qaz741wsd856 Date: Wed, 10 Dec 2025 18:08:15 +0000 Subject: [PATCH] feat: add folder management routes and update API version --- src/handlers/ciphers.rs | 4 ++- src/handlers/config.rs | 4 +-- src/handlers/folders.rs | 55 +++++++++++++++++++++++++++++++++++++++-- src/models/cipher.rs | 16 ++++++++++++ src/router.rs | 4 +++ 5 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/handlers/ciphers.rs b/src/handlers/ciphers.rs index 7148f2d..e80c80e 100644 --- a/src/handlers/ciphers.rs +++ b/src/handlers/ciphers.rs @@ -838,5 +838,7 @@ pub(crate) async fn fetch_cipher_json_array_raw( .await .map_err(db::map_d1_json_error)?; - Ok(row.map(|r| r.ciphers_json).unwrap_or_else(|| "[]".to_string())) + Ok(row + .map(|r| r.ciphers_json) + .unwrap_or_else(|| "[]".to_string())) } diff --git a/src/handlers/config.rs b/src/handlers/config.rs index 643bdd0..4c3105e 100644 --- a/src/handlers/config.rs +++ b/src/handlers/config.rs @@ -40,8 +40,8 @@ pub async fn config( // We should make sure that we keep this updated when we support the new server features // Version history: // - Individual cipher key encryption: 2024.2.0 - "version": "2024.7.0", - "gitHash": "25cf6119-dirty", + "version": "2025.6.0", + "gitHash": "5d84f176", "server": { "name": "Vaultwarden", "url": "https://github.com/dani-garcia/vaultwarden" diff --git a/src/handlers/folders.rs b/src/handlers/folders.rs index 15dc035..a441a8f 100644 --- a/src/handlers/folders.rs +++ b/src/handlers/folders.rs @@ -1,5 +1,7 @@ -use axum::{extract::State, Json}; +use axum::extract::{Path, State}; +use axum::Json; use chrono::Utc; +use serde_json::{json, Value}; use std::sync::Arc; use uuid::Uuid; use worker::{query, Env}; @@ -8,7 +10,56 @@ use crate::auth::Claims; use crate::db::{self, touch_user_updated_at}; use crate::error::AppError; use crate::models::folder::{CreateFolderRequest, Folder, FolderResponse}; -use axum::extract::Path; + +#[worker::send] +pub async fn list_folders( + claims: Claims, + State(env): State>, +) -> Result, AppError> { + let db = db::get_db(&env)?; + + let folders_db: Vec = db + .prepare("SELECT * FROM folders WHERE user_id = ?1") + .bind(&[claims.sub.clone().into()])? + .all() + .await? + .results() + .map_err(|_| AppError::Database)?; + + let folders: Vec = folders_db.into_iter().map(|f| f.into()).collect(); + + Ok(Json(json!({ + "data": folders, + "object": "list", + "continuationToken": null, + }))) +} + +#[worker::send] +pub async fn get_folder( + claims: Claims, + State(env): State>, + Path(id): Path, +) -> Result, AppError> { + let db = db::get_db(&env)?; + + let 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_else(|| { + AppError::BadRequest( + "Invalid folder: Folder does not exist or belongs to another user".to_string(), + ) + })?; + + Ok(Json(folder.into())) +} #[worker::send] pub async fn create_folder( diff --git a/src/models/cipher.rs b/src/models/cipher.rs index 796f674..66eb550 100644 --- a/src/models/cipher.rs +++ b/src/models/cipher.rs @@ -89,6 +89,21 @@ where deserializer.deserialize_any(BoolOrIntVisitor) } +// Custom deserialization function for cipher types +fn deserialize_cipher_type<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value = i32::deserialize(deserializer)?; + match value { + 1..=5 => Ok(value), // Valid cipher types: Login, SecureNote, Card, Identity, SshKey + _ => Err(de::Error::invalid_value( + de::Unexpected::Signed(value as i64), + &"a valid cipher type (1=Login, 2=SecureNote, 3=Card, 4=Identity, 5=SshKey)", + )), + } +} + // The struct that is stored in the database and used in handlers. // For serialization to JSON for the client, we implement a custom `Serialize`. #[derive(Debug, Deserialize, Clone)] @@ -296,6 +311,7 @@ pub struct CipherRequestData { #[serde(skip_serializing_if = "Option::is_none")] pub organization_id: Option, #[serde(rename = "type")] + #[serde(deserialize_with = "deserialize_cipher_type")] pub r#type: i32, pub name: String, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/router.rs b/src/router.rs index 964d864..cac8d9f 100644 --- a/src/router.rs +++ b/src/router.rs @@ -16,6 +16,7 @@ pub fn api_router(env: Env) -> Router { Router::new() // Identity/Auth routes .route("/identity/accounts/prelogin", post(accounts::prelogin)) + .route("/identity/accounts/register", post(accounts::register)) .route( "/identity/accounts/register/finish", post(accounts::register), @@ -123,9 +124,12 @@ pub fn api_router(env: Env) -> Router { // Purge vault - delete all ciphers and folders (requires password verification) .route("/api/ciphers/purge", post(ciphers::purge_vault)) // Folders CRUD + .route("/api/folders", get(folders::list_folders)) .route("/api/folders", post(folders::create_folder)) + .route("/api/folders/{id}", get(folders::get_folder)) .route("/api/folders/{id}", put(folders::update_folder)) .route("/api/folders/{id}", delete(folders::delete_folder)) + .route("/api/folders/{id}/delete", post(folders::delete_folder)) .route("/api/config", get(config::config)) // Emergency access (stub - returns empty lists, feature not supported) .route(