diff --git a/src/handlers/meta.rs b/src/handlers/meta.rs new file mode 100644 index 0000000..f1a4f9b --- /dev/null +++ b/src/handlers/meta.rs @@ -0,0 +1,58 @@ +use axum::{extract::State, Json}; +use chrono::{SecondsFormat, Utc}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::sync::Arc; +use worker::Env; + +use crate::{db, error::AppError}; + +/// GET /api/now +/// +/// Mirrors vaultwarden's `/api/now`: returns current UTC timestamp as an RFC3339 string. +#[worker::send] +pub async fn now() -> Json { + Json(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)) +} + +/// GET /api/alive +/// +/// Simple healthcheck. Vaultwarden uses this to also verify DB connectivity. +#[worker::send] +pub async fn alive( + State(env): State>, +) -> Result, AppError> { + // Verify D1 binding is present + basic query works. + let db = db::get_db(&env)?; + db.prepare("SELECT 1 as ok") + .first::(Some("ok")) + .await + .map_err(|_| AppError::Database)?; + Ok(now().await) +} + +/// GET /api/version +/// +/// Returns a Bitwarden-server-like version string. Clients sometimes call this endpoint. +#[worker::send] +pub async fn version() -> Json<&'static str> { + // Keep this in sync with `src/handlers/config.rs`'s `version`. + Json("2025.12.0") +} + +#[derive(Debug, Deserialize)] +pub struct HibpBreachQuery { + #[allow(dead_code)] // stub endpoint doesn't use it yet + pub username: String, +} + +/// GET /api/hibp/breach?username=... +/// +/// Vaultwarden can proxy HaveIBeenPwned if configured. This minimal server doesn't. +/// We return an empty array to indicate "no breach data" without surfacing an error in clients. +#[worker::send] +pub async fn hibp_breach(_query: axum::extract::Query) -> Json { + Json(json!([])) +} + + diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index aa35a21..0f66f59 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -7,7 +7,9 @@ pub mod emergency_access; pub mod folders; pub mod identity; pub mod import; +pub mod meta; pub mod purge; +pub mod settings; pub mod sync; pub mod twofactor; pub mod webauth; diff --git a/src/handlers/settings.rs b/src/handlers/settings.rs new file mode 100644 index 0000000..a7cc4e7 --- /dev/null +++ b/src/handlers/settings.rs @@ -0,0 +1,61 @@ +use axum::Json; +use serde::Deserialize; +use serde_json::{json, Value}; + +use crate::{auth::Claims, error::AppError}; + +/// GET /api/settings/domains +/// +/// Equivalent domains (eq_domains) are used by clients to treat some domains as interchangeable +/// for URI matching (e.g. `google.com` vs `youtube.com` in predefined "global" groups). +/// +/// Vaultwarden persists per-user: +/// - `equivalentDomains`: custom groups set by the user +/// - `excludedGlobalEquivalentDomains`: which predefined groups are disabled +/// +/// This minimal server currently does not persist these settings. We return empty data to +/// prevent 404s. In the future, we can implement storage by adding two `users` columns: +/// - `equivalent_domains` (TEXT JSON, default "[]") +/// - `excluded_globals` (TEXT JSON, default "[]") +/// and (optionally) embedding/updating the global domains dataset. +#[worker::send] +pub async fn get_domains(_claims: Claims) -> Result, AppError> { + Ok(Json(json!({ + "equivalentDomains": [], + "globalEquivalentDomains": [], + "object": "domains" + }))) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EquivDomainData { + #[allow(dead_code)] // stub endpoint doesn't persist these yet + pub excluded_global_equivalent_domains: Option>, + #[allow(dead_code)] // stub endpoint doesn't persist these yet + pub equivalent_domains: Option>>, +} + +/// POST /api/settings/domains +/// +/// Stub: accept payload but do not persist yet. +#[worker::send] +pub async fn post_domains( + _claims: Claims, + _payload: Json, +) -> Result, AppError> { + Ok(Json(json!({}))) +} + +/// PUT /api/settings/domains +/// +/// Stub: behaves like POST. +#[worker::send] +pub async fn put_domains( + claims: Claims, + payload: Json, +) -> Result, AppError> { + post_domains(claims, payload).await +} + + diff --git a/src/handlers/sync.rs b/src/handlers/sync.rs index 0498c6c..c436504 100644 --- a/src/handlers/sync.rs +++ b/src/handlers/sync.rs @@ -92,7 +92,7 @@ pub async fn get_sync_data( .map_err(|_| AppError::Internal)?; let response = format!( - r#"{{"profile":{},"folders":{},"collections":[],"policies":[],"ciphers":{},"domains":null,"sends":[],"userDecryption":{},"object":"sync"}}"#, + r#"{{"profile":{},"folders":{},"collections":[],"policies":[],"ciphers":{},"domains":{{"equivalentDomains":[],"globalEquivalentDomains":[],"object":"domains"}},"sends":[],"userDecryption":{},"object":"sync"}}"#, profile_json, folders_json, ciphers_json, user_decryption_json ); diff --git a/src/router.rs b/src/router.rs index 6697758..015ac7f 100644 --- a/src/router.rs +++ b/src/router.rs @@ -7,7 +7,7 @@ use worker::Env; use crate::handlers::{ accounts, attachments, ciphers, config, devices, emergency_access, folders, identity, import, - sync, twofactor, webauth, + meta, settings, sync, twofactor, webauth, }; pub fn api_router(env: Env) -> Router { @@ -138,6 +138,15 @@ pub fn api_router(env: Env) -> Router { .route("/api/folders/{id}", delete(folders::delete_folder)) .route("/api/folders/{id}/delete", post(folders::delete_folder)) .route("/api/config", get(config::config)) + // Meta endpoints (mirrors a subset of vaultwarden core/mod.rs) + .route("/api/alive", get(meta::alive)) + .route("/api/now", get(meta::now)) + .route("/api/version", get(meta::version)) + .route("/api/hibp/breach", get(meta::hibp_breach)) + // Settings (stubbed) + .route("/api/settings/domains", get(settings::get_domains)) + .route("/api/settings/domains", post(settings::post_domains)) + .route("/api/settings/domains", put(settings::put_domains)) // Emergency access (stub - returns empty lists, feature not supported) .route( "/api/emergency-access/trusted",