feat: add folder management routes and update API version

This commit is contained in:
qaz741wsd856 2025-12-10 18:08:15 +00:00
parent 89137fb150
commit 5e04837b5d
5 changed files with 78 additions and 5 deletions

View file

@ -838,5 +838,7 @@ pub(crate) async fn fetch_cipher_json_array_raw(
.await .await
.map_err(db::map_d1_json_error)?; .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()))
} }

View file

@ -40,8 +40,8 @@ pub async fn config(
// We should make sure that we keep this updated when we support the new server features // We should make sure that we keep this updated when we support the new server features
// Version history: // Version history:
// - Individual cipher key encryption: 2024.2.0 // - Individual cipher key encryption: 2024.2.0
"version": "2024.7.0", "version": "2025.6.0",
"gitHash": "25cf6119-dirty", "gitHash": "5d84f176",
"server": { "server": {
"name": "Vaultwarden", "name": "Vaultwarden",
"url": "https://github.com/dani-garcia/vaultwarden" "url": "https://github.com/dani-garcia/vaultwarden"

View file

@ -1,5 +1,7 @@
use axum::{extract::State, Json}; use axum::extract::{Path, State};
use axum::Json;
use chrono::Utc; use chrono::Utc;
use serde_json::{json, Value};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
use worker::{query, Env}; use worker::{query, Env};
@ -8,7 +10,56 @@ use crate::auth::Claims;
use crate::db::{self, touch_user_updated_at}; use crate::db::{self, touch_user_updated_at};
use crate::error::AppError; use crate::error::AppError;
use crate::models::folder::{CreateFolderRequest, Folder, FolderResponse}; use crate::models::folder::{CreateFolderRequest, Folder, FolderResponse};
use axum::extract::Path;
#[worker::send]
pub async fn list_folders(
claims: Claims,
State(env): State<Arc<Env>>,
) -> Result<Json<Value>, AppError> {
let db = db::get_db(&env)?;
let folders_db: Vec<Folder> = db
.prepare("SELECT * FROM folders WHERE user_id = ?1")
.bind(&[claims.sub.clone().into()])?
.all()
.await?
.results()
.map_err(|_| AppError::Database)?;
let folders: Vec<FolderResponse> = 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<Arc<Env>>,
Path(id): Path<String>,
) -> Result<Json<FolderResponse>, 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] #[worker::send]
pub async fn create_folder( pub async fn create_folder(

View file

@ -89,6 +89,21 @@ where
deserializer.deserialize_any(BoolOrIntVisitor) deserializer.deserialize_any(BoolOrIntVisitor)
} }
// Custom deserialization function for cipher types
fn deserialize_cipher_type<'de, D>(deserializer: D) -> Result<i32, D::Error>
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. // The struct that is stored in the database and used in handlers.
// For serialization to JSON for the client, we implement a custom `Serialize`. // For serialization to JSON for the client, we implement a custom `Serialize`.
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@ -296,6 +311,7 @@ pub struct CipherRequestData {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub organization_id: Option<String>, pub organization_id: Option<String>,
#[serde(rename = "type")] #[serde(rename = "type")]
#[serde(deserialize_with = "deserialize_cipher_type")]
pub r#type: i32, pub r#type: i32,
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]

View file

@ -16,6 +16,7 @@ pub fn api_router(env: Env) -> Router {
Router::new() Router::new()
// Identity/Auth routes // Identity/Auth routes
.route("/identity/accounts/prelogin", post(accounts::prelogin)) .route("/identity/accounts/prelogin", post(accounts::prelogin))
.route("/identity/accounts/register", post(accounts::register))
.route( .route(
"/identity/accounts/register/finish", "/identity/accounts/register/finish",
post(accounts::register), post(accounts::register),
@ -123,9 +124,12 @@ pub fn api_router(env: Env) -> Router {
// 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
.route("/api/folders", get(folders::list_folders))
.route("/api/folders", post(folders::create_folder)) .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}", put(folders::update_folder))
.route("/api/folders/{id}", delete(folders::delete_folder)) .route("/api/folders/{id}", delete(folders::delete_folder))
.route("/api/folders/{id}/delete", post(folders::delete_folder))
.route("/api/config", get(config::config)) .route("/api/config", get(config::config))
// Emergency access (stub - returns empty lists, feature not supported) // Emergency access (stub - returns empty lists, feature not supported)
.route( .route(