warden-worker/src/handlers/sync.rs
qaz741wsd856 dfa82a0918 feat: add meta and settings endpoints for improved API functionality
- Introduced new meta endpoints: `/api/alive`, `/api/now`, `/api/version`, and `/api/hibp/breach` for health checks and versioning.
- Added settings endpoints: `/api/settings/domains` for retrieving and managing equivalent domains, with stub implementations for future persistence.
- Updated router to include new handlers and ensure proper routing for the added endpoints.
2025-12-28 18:35:19 +00:00

100 lines
3.6 KiB
Rust

use axum::extract::State;
use std::sync::Arc;
use worker::Env;
use crate::{
auth::Claims,
db,
error::AppError,
handlers::{attachments, ciphers, two_factor_enabled},
models::{
folder::{Folder, FolderResponse},
sync::Profile,
user::User,
},
};
use ciphers::RawJson;
use serde_json::{json, Value};
#[worker::send]
pub async fn get_sync_data(
claims: Claims,
State(env): State<Arc<Env>>,
) -> Result<RawJson, AppError> {
let user_id = claims.sub;
let db = db::get_db(&env)?;
// Fetch profile
let user: User = db
.prepare("SELECT * FROM users WHERE id = ?1")
.bind(&[user_id.clone().into()])?
.first(None)
.await?
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
let two_factor_enabled = two_factor_enabled(&db, &user_id).await?;
let has_master_password = !user.master_password_hash.is_empty();
let master_password_unlock = if has_master_password {
// Mirrors vaultwarden's `ciphers::sync` casing (lower camelCase).
// We don't support SSO, so this is always derived from the current user record.
json!({
"kdf": {
"kdfType": user.kdf_type,
"iterations": user.kdf_iterations,
"memory": user.kdf_memory,
"parallelism": user.kdf_parallelism
},
// This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps.
// https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26
"masterKeyEncryptedUserKey": user.key,
"masterKeyWrappedUserKey": user.key,
"salt": user.email
})
} else {
Value::Null
};
// Fetch folders
let folders_db: Vec<Folder> = db
.prepare("SELECT * FROM folders WHERE user_id = ?1")
.bind(&[user_id.clone().into()])?
.all()
.await?
.results()?;
let folders: Vec<FolderResponse> = folders_db.into_iter().map(|f| f.into()).collect();
// Fetch ciphers as raw JSON array string (no parsing in Rust!)
let include_attachments = attachments::attachments_enabled(env.as_ref());
let ciphers_json = ciphers::fetch_cipher_json_array_raw(
&db,
include_attachments,
"WHERE c.user_id = ?1",
&[user_id.clone().into()],
"",
)
.await?;
// Serialize profile and folders (small data, acceptable CPU cost)
let mut profile = Profile::from_user(user, two_factor_enabled)?;
// Match vaultwarden semantics: `_status` is `Invited` when no master password is set.
// We don't implement org invitations, but this helps clients interpret the account state.
profile.status = if has_master_password { 0 } else { 1 };
let profile_json = serde_json::to_string(&profile).map_err(|_| AppError::Internal)?;
let folders_json = serde_json::to_string(&folders).map_err(|_| AppError::Internal)?;
// Build response JSON via string concatenation (ciphers already raw JSON)
let user_decryption_json = serde_json::to_string(&json!({
"masterPasswordUnlock": master_password_unlock
}))
.map_err(|_| AppError::Internal)?;
let response = format!(
r#"{{"profile":{},"folders":{},"collections":[],"policies":[],"ciphers":{},"domains":{{"equivalentDomains":[],"globalEquivalentDomains":[],"object":"domains"}},"sends":[],"userDecryption":{},"object":"sync"}}"#,
profile_json, folders_json, ciphers_json, user_decryption_json
);
Ok(RawJson(response))
}