Vault + registration
This commit is contained in:
parent
0112286623
commit
2cb2004840
6 changed files with 366 additions and 34 deletions
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue