Vault + registration

This commit is contained in:
Deep 2025-09-21 15:39:41 +05:30
parent 0112286623
commit 2cb2004840
6 changed files with 366 additions and 34 deletions

View file

@ -1,5 +1,6 @@
use axum::{extract::State, Json};
use chrono::Utc;
use serde_json::{json, Value};
use std::sync::Arc;
use uuid::Uuid;
use worker::{query, D1Database, Env};
@ -39,7 +40,7 @@ pub async fn prelogin(
pub async fn register(
State(env): State<Arc<Env>>,
Json(payload): Json<RegisterRequest>,
) -> Result<Json<()>, AppError> {
) -> Result<Json<Value>, AppError> {
log::info!("Get db");
let db = db::get_db(&env)?;
log::info!("db got");
@ -88,5 +89,10 @@ pub async fn register(
AppError::Database
})?;
Ok(Json(()))
Ok(Json(json!({})))
}
#[worker::send]
pub async fn send_verification_email() -> String {
"fixed-token-to-mock".to_string()
}

View file

@ -2,40 +2,79 @@ use axum::{extract::State, Json};
use chrono::Utc;
use std::sync::Arc;
use uuid::Uuid;
use worker::Env;
use worker::{query, Env};
use crate::{auth::Claims, db, error::AppError, models::cipher::Cipher};
use crate::auth::Claims;
use crate::db;
use crate::error::AppError;
use crate::models::cipher::{Cipher, CipherData, CreateCipherRequest};
#[worker::send]
pub async fn create_cipher(
claims: Claims,
State(env): State<Arc<Env>>,
Json(mut payload): Json<Cipher>,
Json(payload): Json<CreateCipherRequest>,
) -> Result<Json<Cipher>, AppError> {
let db = db::get_db(&env)?;
let now = Utc::now().to_rfc3339();
let now = Utc::now();
let now = now.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
let cipher_data_req = payload.cipher;
payload.id = Uuid::new_v4().to_string();
payload.user_id = Some(claims.sub);
payload.created_at = now.clone();
payload.updated_at = now;
let cipher_data = CipherData {
name: cipher_data_req.name,
notes: cipher_data_req.notes,
login: cipher_data_req.login,
card: cipher_data_req.card,
identity: cipher_data_req.identity,
secure_note: cipher_data_req.secure_note,
fields: cipher_data_req.fields,
password_history: cipher_data_req.password_history,
reprompt: cipher_data_req.reprompt,
};
db.prepare(
"INSERT INTO ciphers (id, user_id, type, data, favorite, folder_id, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
)
.bind(&[
payload.id.clone().into(),
payload.user_id.clone().into(),
payload.r#type.into(),
serde_json::to_string(&payload.data).unwrap().into(),
payload.favorite.into(),
payload.folder_id.clone().into(),
payload.created_at.clone().into(),
payload.updated_at.clone().into(),
])?
let data_value = serde_json::to_value(&cipher_data).map_err(|_| AppError::Internal)?;
let cipher = Cipher {
id: Uuid::new_v4().to_string(),
user_id: Some(claims.sub.clone()),
organization_id: cipher_data_req.organization_id.clone(),
r#type: cipher_data_req.r#type,
data: data_value,
favorite: cipher_data_req.favorite,
folder_id: cipher_data_req.folder_id.clone(),
deleted_at: None,
created_at: now.clone(),
updated_at: now.clone(),
object: "cipher".to_string(),
organization_use_totp: false,
edit: true,
view_password: true,
collection_ids: if payload.collection_ids.is_empty() {
None
} else {
Some(payload.collection_ids)
},
};
let data = serde_json::to_string(&cipher.data).map_err(|_| AppError::Internal)?;
query!(
&db,
"INSERT INTO ciphers (id, user_id, organization_id, type, data, favorite, folder_id, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
cipher.id,
cipher.user_id,
cipher.organization_id,
cipher.r#type,
data,
cipher.favorite,
cipher.folder_id,
cipher.created_at,
cipher.updated_at,
).map_err(|_|AppError::Database)?
.run()
.await?;
Ok(Json(payload))
Ok(Json(cipher))
}

View file

@ -1,4 +1,5 @@
use axum::{extract::State, Json};
use serde_json::Value;
use std::sync::Arc;
use worker::Env;
@ -7,7 +8,7 @@ use crate::{
db,
error::AppError,
models::{
cipher::Cipher,
cipher::{Cipher, CipherDBModel},
folder::Folder,
sync::{Profile, SyncResponse},
user::User,
@ -22,6 +23,7 @@ pub async fn get_sync_data(
let user_id = claims.sub;
let db = db::get_db(&env)?;
log::info!("Fetch user");
// Fetch profile
let user: User = db
.prepare("SELECT * FROM users WHERE id = ?1")
@ -30,6 +32,7 @@ pub async fn get_sync_data(
.await?
.ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
log::info!("Fetch folders");
// Fetch folders
let folders: Vec<Folder> = db
.prepare("SELECT * FROM folders WHERE user_id = ?1")
@ -38,14 +41,30 @@ pub async fn get_sync_data(
.await?
.results()?;
log::info!("Fetch ciphers");
// Fetch ciphers
let ciphers: Vec<Cipher> = db
let ciphers: Vec<Value> = db
.prepare("SELECT * FROM ciphers WHERE user_id = ?1")
.bind(&[user_id.clone().into()])?
.all()
.await?
.results()?;
let ciphers = ciphers
.into_iter()
.filter_map(
|cipher| match serde_json::from_value::<CipherDBModel>(cipher.clone()) {
Ok(cipher) => Some(cipher),
Err(err) => {
log::warn!("Cannot parse {err:?} {cipher:?}");
None
}
},
)
.map(|cipher| cipher.into())
.collect::<Vec<Cipher>>();
log::info!("Fetch time");
let time = chrono::DateTime::parse_from_rfc3339(&user.created_at)
.map_err(|_| AppError::Internal)?
.to_rfc3339_opts(chrono::SecondsFormat::Micros, true);

View file

@ -1,7 +1,74 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{json, Map, Value};
#[derive(Debug, Serialize, Deserialize)]
// This struct represents the data stored in the `data` column of the `ciphers` table.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CipherData {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub login: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub card: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub identity: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secure_note: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password_history: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reprompt: Option<i32>,
}
// Custom deserialization function for booleans
fn deserialize_bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
// A visitor is used to handle different data types
struct BoolOrIntVisitor;
impl<'de> de::Visitor<'de> for BoolOrIntVisitor {
type Value = bool;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a boolean or an integer 0 or 1")
}
// Handles boolean values
fn visit_bool<E>(self, value: bool) -> Result<bool, E>
where
E: de::Error,
{
Ok(value)
}
// Handles integer values (0 or 1)
fn visit_u64<E>(self, value: u64) -> Result<bool, E>
where
E: de::Error,
{
match value {
0 => Ok(false),
1 => Ok(true),
_ => Err(de::Error::invalid_value(
de::Unexpected::Unsigned(value),
&"0 or 1",
)),
}
}
}
deserializer.deserialize_any(BoolOrIntVisitor)
}
// 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)]
#[serde(rename_all = "camelCase")]
pub struct Cipher {
pub id: String,
@ -11,9 +78,8 @@ pub struct Cipher {
pub organization_id: Option<String>,
#[serde(rename = "type")]
pub r#type: i32,
// Store all other fields as a raw JSON value.
// The server doesn't need to understand the encrypted contents.
pub data: Value,
#[serde(deserialize_with = "deserialize_bool_from_int")]
pub favorite: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub folder_id: Option<String>,
@ -21,4 +87,199 @@ pub struct Cipher {
pub deleted_at: Option<String>,
pub created_at: String,
pub updated_at: String,
// Bitwarden specific field for API responses
#[serde(default = "default_object")]
pub object: String,
#[serde(default)]
#[serde(deserialize_with = "deserialize_bool_from_int")]
pub organization_use_totp: bool,
#[serde(default = "default_true")]
#[serde(deserialize_with = "deserialize_bool_from_int")]
pub edit: bool,
#[serde(default = "default_true")]
#[serde(deserialize_with = "deserialize_bool_from_int")]
pub view_password: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub collection_ids: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CipherDBModel {
pub id: String,
pub user_id: String,
pub organization_id: Option<String>,
pub r#type: i32,
pub data: String,
pub favorite: i32,
pub folder_id: Option<String>,
pub deleted_at: Option<String>,
pub created_at: String,
pub updated_at: String,
}
impl Into<Cipher> for CipherDBModel {
fn into(self) -> Cipher {
Cipher {
id: self.id,
user_id: Some(self.user_id),
organization_id: self.organization_id,
r#type: self.r#type,
data: serde_json::from_str(&self.data).unwrap_or_default(),
favorite: match self.favorite {
0 => false,
_ => true,
},
folder_id: self.folder_id,
deleted_at: self.deleted_at,
created_at: self.created_at,
updated_at: self.updated_at,
object: "default_object".to_string(),
organization_use_totp: false,
edit: true,
view_password: true,
collection_ids: None,
}
}
}
impl Serialize for Cipher {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut response_map = Map::new();
response_map.insert("object".to_string(), json!(self.object));
response_map.insert("id".to_string(), json!(self.id));
if self.user_id.is_some() {
response_map.insert("userId".to_string(), json!(self.user_id));
}
response_map.insert("organizationId".to_string(), json!(self.organization_id));
response_map.insert("folderId".to_string(), json!(self.folder_id));
response_map.insert("type".to_string(), json!(self.r#type));
response_map.insert("favorite".to_string(), json!(self.favorite));
response_map.insert("edit".to_string(), json!(self.edit));
response_map.insert("viewPassword".to_string(), json!(self.view_password));
response_map.insert(
"organizationUseTotp".to_string(),
json!(self.organization_use_totp),
);
response_map.insert("collectionIds".to_string(), json!(self.collection_ids));
response_map.insert("revisionDate".to_string(), json!(self.updated_at));
response_map.insert("creationDate".to_string(), json!(self.created_at));
response_map.insert("deletedDate".to_string(), json!(self.deleted_at));
log::debug!("Response data is {:?}", self.data);
if let Some(data_obj) = self.data.as_object() {
let data_clone = data_obj.clone();
response_map.insert(
"name".to_string(),
data_clone.get("name").cloned().unwrap_or(Value::Null),
);
response_map.insert(
"notes".to_string(),
data_clone.get("notes").cloned().unwrap_or(Value::Null),
);
response_map.insert(
"fields".to_string(),
data_clone.get("fields").cloned().unwrap_or(Value::Null),
);
response_map.insert(
"passwordHistory".to_string(),
data_clone
.get("passwordHistory")
.cloned()
.unwrap_or(Value::Null),
);
response_map.insert(
"reprompt".to_string(),
data_clone
.get("reprompt")
.cloned()
.unwrap_or(Value::Number(serde_json::Number::from_f64(0.0).unwrap())),
);
let mut login = Value::Null;
let mut secure_note = Value::Null;
let mut card = Value::Null;
let mut identity = Value::Null;
match self.r#type {
1 => login = data_clone.get("login").cloned().unwrap_or(Value::Null),
2 => secure_note = data_clone.get("secureNote").cloned().unwrap_or(Value::Null),
3 => card = data_clone.get("card").cloned().unwrap_or(Value::Null),
4 => identity = data_clone.get("identity").cloned().unwrap_or(Value::Null),
_ => {}
}
response_map.insert("login".to_string(), login);
response_map.insert("secureNote".to_string(), secure_note);
response_map.insert("card".to_string(), card);
response_map.insert("identity".to_string(), identity);
} else {
response_map.insert("name".to_string(), Value::Null);
response_map.insert("notes".to_string(), Value::Null);
response_map.insert("fields".to_string(), Value::Null);
response_map.insert("passwordHistory".to_string(), Value::Null);
response_map.insert("reprompt".to_string(), Value::Null);
response_map.insert("login".to_string(), Value::Null);
response_map.insert("secureNote".to_string(), Value::Null);
response_map.insert("card".to_string(), Value::Null);
response_map.insert("identity".to_string(), Value::Null);
}
Value::Object(response_map).serialize(serializer)
}
}
fn default_object() -> String {
"cipher".to_string()
}
fn default_true() -> bool {
true
}
// Represents the "Cipher" object within the incoming request payload.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CipherRequestData {
#[serde(rename = "type")]
pub r#type: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub folder_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_id: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(default)]
pub favorite: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub login: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub card: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub identity: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secure_note: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password_history: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reprompt: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_known_revision_date: Option<String>,
}
// Represents the full request payload for creating a cipher.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CreateCipherRequest {
pub cipher: CipherRequestData,
#[serde(default)]
pub collection_ids: Vec<String>,
}

View file

@ -37,7 +37,7 @@ pub struct SyncResponse {
pub profile: Profile,
#[serde(rename = "Folders")]
pub folders: Vec<Folder>,
#[serde(rename = "Ciphers")]
#[serde(rename = "ciphers")]
pub ciphers: Vec<Cipher>,
#[serde(rename = "Domains")]
pub domains: Value,

View file

@ -13,8 +13,15 @@ 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),
)
.route("/identity/connect/token", post(identity::token))
.route(
"/identity/accounts/register/send-verification-email",
post(accounts::send_verification_email),
)
// Main data sync route
.route("/api/sync", get(sync::get_sync_data))
// Ciphers CRUD