diff --git a/src/handlers/import.rs b/src/handlers/import.rs new file mode 100644 index 0000000..4669225 --- /dev/null +++ b/src/handlers/import.rs @@ -0,0 +1,113 @@ +use axum::{extract::State, Json}; +use chrono::Utc; +use std::sync::Arc; +use uuid::Uuid; +use worker::{query, Env}; + +use crate::auth::Claims; +use crate::db; +use crate::error::AppError; +use crate::models::cipher::{Cipher, CipherData}; +use crate::models::folder::Folder; +use crate::models::import::ImportRequest; + +#[worker::send] +pub async fn import_data( + claims: Claims, + State(env): State>, + Json(mut payload): Json, +) -> Result, AppError> { + let db = db::get_db(&env)?; + let now = Utc::now(); + let now = now.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); + + for import_folder in &payload.folders { + let folder = Folder { + id: import_folder.id.clone(), + user_id: claims.sub.clone(), + name: import_folder.name.clone(), + created_at: now.clone(), + updated_at: now.clone(), + }; + + query!( + &db, + "INSERT OR IGNORE INTO folders (id, user_id, name, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)", + folder.id, + folder.user_id, + folder.name, + folder.created_at, + folder.updated_at + ) + .map_err(|_| AppError::Database)? + .run() + .await?; + } + + for relationship in payload.folder_relationships { + if let Some(cipher) = payload.ciphers.get_mut(relationship.key) { + if let Some(folder) = payload.folders.get(relationship.value) { + cipher.folder_id = Some(folder.id.clone()); + } + } + } + + for import_cipher in payload.ciphers { + if import_cipher.encrypted_for != claims.sub { + return Err(AppError::BadRequest("Cipher encrypted for wrong user".to_string())); + } + + let cipher_data = CipherData { + name: import_cipher.name, + notes: import_cipher.notes, + login: import_cipher.login, + card: import_cipher.card, + identity: import_cipher.identity, + secure_note: import_cipher.secure_note, + fields: import_cipher.fields, + password_history: import_cipher.password_history, + reprompt: import_cipher.reprompt, + }; + + 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: import_cipher.organization_id.clone(), + r#type: import_cipher.r#type, + data: data_value, + favorite: import_cipher.favorite, + folder_id: import_cipher.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: None, + }; + + let data = serde_json::to_string(&cipher.data).map_err(|_| AppError::Internal)?; + + query!( + &db, + "INSERT OR IGNORE 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(())) +} \ No newline at end of file diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 9d5fbb4..d49bd56 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -4,3 +4,4 @@ pub mod config; pub mod identity; pub mod sync; pub mod folders; +pub mod import; diff --git a/src/models/import.rs b/src/models/import.rs new file mode 100644 index 0000000..0391a8e --- /dev/null +++ b/src/models/import.rs @@ -0,0 +1,47 @@ +use serde::Deserialize; +use serde_json::Value; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ImportCipher { + #[serde(rename = "type")] + pub r#type: i32, + pub folder_id: Option, + pub organization_id: Option, + pub name: String, + pub notes: Option, + pub favorite: bool, + pub login: Option, + pub card: Option, + pub identity: Option, + pub secure_note: Option, + pub fields: Option, + pub password_history: Option, + pub reprompt: Option, + pub last_known_revision_date: Option, + pub encrypted_for: String, +} + + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ImportFolder { + pub id: String, + pub name: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct FolderRelationship { + pub key: usize, + pub value: usize, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ImportRequest { + pub ciphers: Vec, + pub folders: Vec, + #[serde(default)] + pub folder_relationships: Vec, +} diff --git a/src/models/mod.rs b/src/models/mod.rs index a5a3a48..a2bd67b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,5 @@ +pub mod user; +pub mod sync; pub mod cipher; pub mod folder; -pub mod sync; -pub mod user; +pub mod import; diff --git a/src/router.rs b/src/router.rs index a21dd66..c8e4795 100644 --- a/src/router.rs +++ b/src/router.rs @@ -5,7 +5,7 @@ use axum::{ use std::sync::Arc; use worker::Env; -use crate::handlers::{accounts, ciphers, config, identity, sync, folders}; +use crate::handlers::{accounts, ciphers, config, identity, sync, folders, import}; pub fn api_router(env: Env) -> Router { let app_state = Arc::new(env); @@ -26,6 +26,7 @@ pub fn api_router(env: Env) -> Router { .route("/api/sync", get(sync::get_sync_data)) // Ciphers CRUD .route("/api/ciphers/create", post(ciphers::create_cipher)) + .route("/api/ciphers/import", post(import::import_data)) .route("/api/ciphers/{id}", put(ciphers::update_cipher)) .route("/api/ciphers/{id}/delete", put(ciphers::delete_cipher)) // Folders CRUD