20 changed files with 812 additions and 2 deletions
			
			
		| @ -0,0 +1 @@ | |||||
|  | DROP TABLE sends; | ||||
| @ -0,0 +1,25 @@ | |||||
|  | CREATE TABLE sends ( | ||||
|  |   uuid              CHAR(36) NOT NULL   PRIMARY KEY, | ||||
|  |   user_uuid         CHAR(36)            REFERENCES users (uuid), | ||||
|  |   organization_uuid CHAR(36)            REFERENCES organizations (uuid), | ||||
|  | 
 | ||||
|  |   name              TEXT    NOT NULL, | ||||
|  |   notes             TEXT, | ||||
|  | 
 | ||||
|  |   atype             INTEGER NOT NULL, | ||||
|  |   data              TEXT    NOT NULL, | ||||
|  |   key               TEXT    NOT NULL, | ||||
|  |   password_hash     BLOB, | ||||
|  |   password_salt     BLOB, | ||||
|  |   password_iter     INTEGER, | ||||
|  | 
 | ||||
|  |   max_access_count  INTEGER, | ||||
|  |   access_count      INTEGER NOT NULL, | ||||
|  | 
 | ||||
|  |   creation_date     DATETIME NOT NULL, | ||||
|  |   revision_date     DATETIME NOT NULL, | ||||
|  |   expiration_date   DATETIME, | ||||
|  |   deletion_date     DATETIME NOT NULL, | ||||
|  | 
 | ||||
|  |   disabled          BOOLEAN NOT NULL | ||||
|  | ); | ||||
| @ -0,0 +1 @@ | |||||
|  | DROP TABLE sends; | ||||
| @ -0,0 +1,25 @@ | |||||
|  | CREATE TABLE sends ( | ||||
|  |   uuid              CHAR(36) NOT NULL   PRIMARY KEY, | ||||
|  |   user_uuid         CHAR(36)            REFERENCES users (uuid), | ||||
|  |   organization_uuid CHAR(36)            REFERENCES organizations (uuid), | ||||
|  | 
 | ||||
|  |   name              TEXT    NOT NULL, | ||||
|  |   notes             TEXT, | ||||
|  | 
 | ||||
|  |   atype             INTEGER NOT NULL, | ||||
|  |   data              TEXT    NOT NULL, | ||||
|  |   key               TEXT    NOT NULL, | ||||
|  |   password_hash     BYTEA, | ||||
|  |   password_salt     BYTEA, | ||||
|  |   password_iter     INTEGER, | ||||
|  | 
 | ||||
|  |   max_access_count  INTEGER, | ||||
|  |   access_count      INTEGER NOT NULL, | ||||
|  | 
 | ||||
|  |   creation_date     TIMESTAMP NOT NULL, | ||||
|  |   revision_date     TIMESTAMP NOT NULL, | ||||
|  |   expiration_date   TIMESTAMP, | ||||
|  |   deletion_date     TIMESTAMP NOT NULL, | ||||
|  | 
 | ||||
|  |   disabled          BOOLEAN NOT NULL | ||||
|  | ); | ||||
| @ -0,0 +1 @@ | |||||
|  | DROP TABLE sends; | ||||
| @ -0,0 +1,25 @@ | |||||
|  | CREATE TABLE sends ( | ||||
|  |   uuid              TEXT NOT NULL   PRIMARY KEY, | ||||
|  |   user_uuid         TEXT            REFERENCES users (uuid), | ||||
|  |   organization_uuid TEXT            REFERENCES organizations (uuid), | ||||
|  | 
 | ||||
|  |   name              TEXT    NOT NULL, | ||||
|  |   notes             TEXT, | ||||
|  | 
 | ||||
|  |   atype             INTEGER NOT NULL, | ||||
|  |   data              TEXT    NOT NULL, | ||||
|  |   key               TEXT    NOT NULL, | ||||
|  |   password_hash     BLOB, | ||||
|  |   password_salt     BLOB, | ||||
|  |   password_iter     INTEGER, | ||||
|  | 
 | ||||
|  |   max_access_count  INTEGER, | ||||
|  |   access_count      INTEGER NOT NULL, | ||||
|  | 
 | ||||
|  |   creation_date     DATETIME NOT NULL, | ||||
|  |   revision_date     DATETIME NOT NULL, | ||||
|  |   expiration_date   DATETIME, | ||||
|  |   deletion_date     DATETIME NOT NULL, | ||||
|  | 
 | ||||
|  |   disabled          BOOLEAN NOT NULL | ||||
|  | ); | ||||
| @ -0,0 +1,383 @@ | |||||
|  | use std::{io::Read, path::Path}; | ||||
|  | 
 | ||||
|  | use chrono::{DateTime, Duration, Utc}; | ||||
|  | use multipart::server::{save::SavedData, Multipart, SaveResult}; | ||||
|  | use rocket::{http::ContentType, Data}; | ||||
|  | use rocket_contrib::json::Json; | ||||
|  | use serde_json::Value; | ||||
|  | 
 | ||||
|  | use crate::{ | ||||
|  |     api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, | ||||
|  |     auth::{Headers, Host}, | ||||
|  |     db::{models::*, DbConn}, | ||||
|  |     CONFIG, | ||||
|  | }; | ||||
|  | 
 | ||||
|  | pub fn routes() -> Vec<rocket::Route> { | ||||
|  |     routes![ | ||||
|  |         post_send, | ||||
|  |         post_send_file, | ||||
|  |         post_access, | ||||
|  |         post_access_file, | ||||
|  |         put_send, | ||||
|  |         delete_send, | ||||
|  |         put_remove_password | ||||
|  |     ] | ||||
|  | } | ||||
|  | 
 | ||||
|  | #[derive(Deserialize)] | ||||
|  | #[allow(non_snake_case)] | ||||
|  | pub struct SendData { | ||||
|  |     pub Type: i32, | ||||
|  |     pub Key: String, | ||||
|  |     pub Password: Option<String>, | ||||
|  |     pub MaxAccessCount: Option<i32>, | ||||
|  |     pub ExpirationDate: Option<DateTime<Utc>>, | ||||
|  |     pub DeletionDate: DateTime<Utc>, | ||||
|  |     pub Disabled: bool, | ||||
|  | 
 | ||||
|  |     // Data field
 | ||||
|  |     pub Name: String, | ||||
|  |     pub Notes: Option<String>, | ||||
|  |     pub Text: Option<Value>, | ||||
|  |     pub File: Option<Value>, | ||||
|  | } | ||||
|  | 
 | ||||
|  | fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> { | ||||
|  |     let data_val = if data.Type == SendType::Text as i32 { | ||||
|  |         data.Text | ||||
|  |     } else if data.Type == SendType::File as i32 { | ||||
|  |         data.File | ||||
|  |     } else { | ||||
|  |         err!("Invalid Send type") | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     let data_str = if let Some(mut d) = data_val { | ||||
|  |         d.as_object_mut().and_then(|o| o.remove("Response")); | ||||
|  |         serde_json::to_string(&d)? | ||||
|  |     } else { | ||||
|  |         err!("Send data not provided"); | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     if data.DeletionDate > Utc::now() + Duration::days(31) { | ||||
|  |         err!( | ||||
|  |             "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." | ||||
|  |         ); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     let mut send = Send::new(data.Type, data.Name, data_str, data.Key, data.DeletionDate.naive_utc()); | ||||
|  |     send.user_uuid = Some(user_uuid); | ||||
|  |     send.notes = data.Notes; | ||||
|  |     send.max_access_count = data.MaxAccessCount; | ||||
|  |     send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); | ||||
|  |     send.disabled = data.Disabled; | ||||
|  |     send.atype = data.Type; | ||||
|  | 
 | ||||
|  |     send.set_password(data.Password.as_deref()); | ||||
|  | 
 | ||||
|  |     Ok(send) | ||||
|  | } | ||||
|  | 
 | ||||
|  | #[post("/sends", data = "<data>")] | ||||
|  | fn post_send(data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { | ||||
|  |     let data: SendData = data.into_inner().data; | ||||
|  | 
 | ||||
|  |     if data.Type == SendType::File as i32 { | ||||
|  |         err!("File sends should use /api/sends/file") | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     let mut send = create_send(data, headers.user.uuid.clone())?; | ||||
|  |     send.save(&conn)?; | ||||
|  |     nt.send_user_update(UpdateType::SyncSendCreate, &headers.user); | ||||
|  | 
 | ||||
|  |     Ok(Json(send.to_json())) | ||||
|  | } | ||||
|  | 
 | ||||
|  | #[post("/sends/file", format = "multipart/form-data", data = "<data>")] | ||||
|  | fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { | ||||
|  |     let boundary = content_type.params().next().expect("No boundary provided").1; | ||||
|  | 
 | ||||
|  |     let mut mpart = Multipart::with_body(data.open(), boundary); | ||||
|  | 
 | ||||
|  |     // First entry is the SendData JSON
 | ||||
|  |     let mut model_entry = match mpart.read_entry()? { | ||||
|  |         Some(e) if &*e.headers.name == "model" => e, | ||||
|  |         Some(_) => err!("Invalid entry name"), | ||||
|  |         None => err!("No model entry present"), | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     let mut buf = String::new(); | ||||
|  |     model_entry.data.read_to_string(&mut buf)?; | ||||
|  |     let data = serde_json::from_str::<crate::util::UpCase<SendData>>(&buf)?; | ||||
|  | 
 | ||||
|  |     // Get the file length and add an extra 10% to avoid issues
 | ||||
|  |     const SIZE_110_MB: u64 = 115_343_360; | ||||
|  | 
 | ||||
|  |     let size_limit = match CONFIG.user_attachment_limit() { | ||||
|  |         Some(0) => err!("File uploads are disabled"), | ||||
|  |         Some(limit_kb) => { | ||||
|  |             let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &conn); | ||||
|  |             if left <= 0 { | ||||
|  |                 err!("Attachment size limit reached! Delete some files to open space") | ||||
|  |             } | ||||
|  |             std::cmp::Ord::max(left as u64, SIZE_110_MB) | ||||
|  |         } | ||||
|  |         None => SIZE_110_MB, | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     // Create the Send
 | ||||
|  |     let mut send = create_send(data.data, headers.user.uuid.clone())?; | ||||
|  |     let file_id: String = data_encoding::HEXLOWER.encode(&crate::crypto::get_random(vec![0; 32])); | ||||
|  | 
 | ||||
|  |     if send.atype != SendType::File as i32 { | ||||
|  |         err!("Send content is not a file"); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     let file_path = Path::new(&CONFIG.sends_folder()).join(&send.uuid).join(&file_id); | ||||
|  | 
 | ||||
|  |     // Read the data entry and save the file
 | ||||
|  |     let mut data_entry = match mpart.read_entry()? { | ||||
|  |         Some(e) if &*e.headers.name == "data" => e, | ||||
|  |         Some(_) => err!("Invalid entry name"), | ||||
|  |         None => err!("No model entry present"), | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     let size = match data_entry | ||||
|  |         .data | ||||
|  |         .save() | ||||
|  |         .memory_threshold(0) | ||||
|  |         .size_limit(size_limit) | ||||
|  |         .with_path(&file_path) | ||||
|  |     { | ||||
|  |         SaveResult::Full(SavedData::File(_, size)) => size as i32, | ||||
|  |         SaveResult::Full(other) => { | ||||
|  |             std::fs::remove_file(&file_path).ok(); | ||||
|  |             err!(format!("Attachment is not a file: {:?}", other)); | ||||
|  |         } | ||||
|  |         SaveResult::Partial(_, reason) => { | ||||
|  |             std::fs::remove_file(&file_path).ok(); | ||||
|  |             err!(format!("Attachment size limit exceeded with this file: {:?}", reason)); | ||||
|  |         } | ||||
|  |         SaveResult::Error(e) => { | ||||
|  |             std::fs::remove_file(&file_path).ok(); | ||||
|  |             err!(format!("Error: {:?}", e)); | ||||
|  |         } | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     // Set ID and sizes
 | ||||
|  |     let mut data_value: Value = serde_json::from_str(&send.data)?; | ||||
|  |     if let Some(o) = data_value.as_object_mut() { | ||||
|  |         o.insert(String::from("Id"), Value::String(file_id)); | ||||
|  |         o.insert(String::from("Size"), Value::Number(size.into())); | ||||
|  |         o.insert( | ||||
|  |             String::from("SizeName"), | ||||
|  |             Value::String(crate::util::get_display_size(size)), | ||||
|  |         ); | ||||
|  |     } | ||||
|  |     send.data = serde_json::to_string(&data_value)?; | ||||
|  | 
 | ||||
|  |     // Save the changes in the database
 | ||||
|  |     send.save(&conn)?; | ||||
|  |     nt.send_user_update(UpdateType::SyncSendCreate, &headers.user); | ||||
|  | 
 | ||||
|  |     Ok(Json(send.to_json())) | ||||
|  | } | ||||
|  | 
 | ||||
|  | #[derive(Deserialize)] | ||||
|  | #[allow(non_snake_case)] | ||||
|  | pub struct SendAccessData { | ||||
|  |     pub Password: Option<String>, | ||||
|  | } | ||||
|  | 
 | ||||
|  | #[post("/sends/access/<access_id>", data = "<data>")] | ||||
|  | fn post_access(access_id: String, data: JsonUpcase<SendAccessData>, conn: DbConn) -> JsonResult { | ||||
|  |     let mut send = match Send::find_by_access_id(&access_id, &conn) { | ||||
|  |         Some(s) => s, | ||||
|  |         None => err_code!("Send not found", 404), | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     if let Some(max_access_count) = send.max_access_count { | ||||
|  |         if send.access_count > max_access_count { | ||||
|  |             err_code!("Max access count reached", 404); | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if let Some(expiration) = send.expiration_date { | ||||
|  |         if Utc::now().naive_utc() > expiration { | ||||
|  |             err_code!("Send has expired", 404) | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if Utc::now().naive_utc() > send.deletion_date { | ||||
|  |         err_code!("Send has been deleted", 404) | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if send.disabled { | ||||
|  |         err_code!("Send has been disabled", 404) | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if send.password_hash.is_some() { | ||||
|  |         match data.into_inner().data.Password { | ||||
|  |             Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } | ||||
|  |             Some(_) => err!("Invalid password."), | ||||
|  |             None => err_code!("Password not provided", 401), | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     // Files are incremented during the download
 | ||||
|  |     if send.atype == SendType::Text as i32 { | ||||
|  |         send.access_count += 1; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     send.save(&conn)?; | ||||
|  | 
 | ||||
|  |     Ok(Json(send.to_json())) | ||||
|  | } | ||||
|  | 
 | ||||
|  | #[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")] | ||||
|  | fn post_access_file( | ||||
|  |     send_id: String, | ||||
|  |     file_id: String, | ||||
|  |     data: JsonUpcase<SendAccessData>, | ||||
|  |     host: Host, | ||||
|  |     conn: DbConn, | ||||
|  | ) -> JsonResult { | ||||
|  |     let mut send = match Send::find_by_uuid(&send_id, &conn) { | ||||
|  |         Some(s) => s, | ||||
|  |         None => err_code!("Send not found", 404), | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     if let Some(max_access_count) = send.max_access_count { | ||||
|  |         if send.access_count > max_access_count { | ||||
|  |             err_code!("Max access count reached", 404); | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if let Some(expiration) = send.expiration_date { | ||||
|  |         if Utc::now().naive_utc() > expiration { | ||||
|  |             err_code!("Send has expired", 404) | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if Utc::now().naive_utc() > send.deletion_date { | ||||
|  |         err_code!("Send has been deleted", 404) | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if send.disabled { | ||||
|  |         err_code!("Send has been disabled", 404) | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if send.password_hash.is_some() { | ||||
|  |         match data.into_inner().data.Password { | ||||
|  |             Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } | ||||
|  |             Some(_) => err!("Invalid password."), | ||||
|  |             None => err_code!("Password not provided", 401), | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     send.access_count += 1; | ||||
|  | 
 | ||||
|  |     send.save(&conn)?; | ||||
|  | 
 | ||||
|  |     Ok(Json(json!({ | ||||
|  |         "Object": "send-fileDownload", | ||||
|  |         "Id": file_id, | ||||
|  |         "Url": format!("{}/sends/{}/{}", &host.host, send_id, file_id) | ||||
|  |     }))) | ||||
|  | } | ||||
|  | 
 | ||||
|  | #[put("/sends/<id>", data = "<data>")] | ||||
|  | fn put_send(id: String, data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { | ||||
|  |     let data: SendData = data.into_inner().data; | ||||
|  | 
 | ||||
|  |     let mut send = match Send::find_by_uuid(&id, &conn) { | ||||
|  |         Some(s) => s, | ||||
|  |         None => err!("Send not found"), | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     if send.user_uuid.as_ref() != Some(&headers.user.uuid) { | ||||
|  |         err!("Send is not owned by user") | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if send.atype != data.Type { | ||||
|  |         err!("Sends can't change type") | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     let data_val = if data.Type == SendType::Text as i32 { | ||||
|  |         data.Text | ||||
|  |     } else if data.Type == SendType::File as i32 { | ||||
|  |         data.File | ||||
|  |     } else { | ||||
|  |         err!("Invalid Send type") | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     let data_str = if let Some(mut d) = data_val { | ||||
|  |         d.as_object_mut().and_then(|d| d.remove("Response")); | ||||
|  |         serde_json::to_string(&d)? | ||||
|  |     } else { | ||||
|  |         err!("Send data not provided"); | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     if data.DeletionDate > Utc::now() + Duration::days(31) { | ||||
|  |         err!( | ||||
|  |             "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." | ||||
|  |         ); | ||||
|  |     } | ||||
|  |     send.data = data_str; | ||||
|  |     send.name = data.Name; | ||||
|  |     send.key = data.Key; | ||||
|  |     send.deletion_date = data.DeletionDate.naive_utc(); | ||||
|  |     send.notes = data.Notes; | ||||
|  |     send.max_access_count = data.MaxAccessCount; | ||||
|  |     send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); | ||||
|  |     send.disabled = data.Disabled; | ||||
|  | 
 | ||||
|  |     // Only change the value if it's present
 | ||||
|  |     if let Some(password) = data.Password { | ||||
|  |         send.set_password(Some(&password)); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     send.save(&conn)?; | ||||
|  |     nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user); | ||||
|  | 
 | ||||
|  |     Ok(Json(send.to_json())) | ||||
|  | } | ||||
|  | 
 | ||||
|  | #[delete("/sends/<id>")] | ||||
|  | fn delete_send(id: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { | ||||
|  |     let send = match Send::find_by_uuid(&id, &conn) { | ||||
|  |         Some(s) => s, | ||||
|  |         None => err!("Send not found"), | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     if send.user_uuid.as_ref() != Some(&headers.user.uuid) { | ||||
|  |         err!("Send is not owned by user") | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if send.atype == SendType::File as i32 { | ||||
|  |         std::fs::remove_dir_all(Path::new(&CONFIG.sends_folder()).join(&send.uuid)).ok(); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     send.delete(&conn)?; | ||||
|  |     nt.send_user_update(UpdateType::SyncSendDelete, &headers.user); | ||||
|  | 
 | ||||
|  |     Ok(()) | ||||
|  | } | ||||
|  | 
 | ||||
|  | #[put("/sends/<id>/remove-password")] | ||||
|  | fn put_remove_password(id: String, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { | ||||
|  |     let mut send = match Send::find_by_uuid(&id, &conn) { | ||||
|  |         Some(s) => s, | ||||
|  |         None => err!("Send not found"), | ||||
|  |     }; | ||||
|  | 
 | ||||
|  |     if send.user_uuid.as_ref() != Some(&headers.user.uuid) { | ||||
|  |         err!("Send is not owned by user") | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     send.set_password(None); | ||||
|  |     send.save(&conn)?; | ||||
|  |     nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user); | ||||
|  | 
 | ||||
|  |     Ok(Json(send.to_json())) | ||||
|  | } | ||||
| @ -0,0 +1,235 @@ | |||||
|  | use chrono::{NaiveDateTime, Utc}; | ||||
|  | use serde_json::Value; | ||||
|  | 
 | ||||
|  | use super::{Organization, User}; | ||||
|  | 
 | ||||
|  | db_object! { | ||||
|  |     #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] | ||||
|  |     #[table_name = "sends"] | ||||
|  |     #[changeset_options(treat_none_as_null="true")] | ||||
|  |     #[belongs_to(User, foreign_key = "user_uuid")] | ||||
|  |     #[belongs_to(Organization, foreign_key = "organization_uuid")] | ||||
|  |     #[primary_key(uuid)] | ||||
|  |     pub struct Send { | ||||
|  |         pub uuid: String, | ||||
|  | 
 | ||||
|  |         pub user_uuid: Option<String>, | ||||
|  |         pub organization_uuid: Option<String>, | ||||
|  | 
 | ||||
|  | 
 | ||||
|  |         pub name: String, | ||||
|  |         pub notes: Option<String>, | ||||
|  | 
 | ||||
|  |         pub atype: i32, | ||||
|  |         pub data: String, | ||||
|  |         pub key: String, | ||||
|  |         pub password_hash: Option<Vec<u8>>, | ||||
|  |         password_salt: Option<Vec<u8>>, | ||||
|  |         password_iter: Option<i32>, | ||||
|  | 
 | ||||
|  |         pub max_access_count: Option<i32>, | ||||
|  |         pub access_count: i32, | ||||
|  | 
 | ||||
|  |         pub creation_date: NaiveDateTime, | ||||
|  |         pub revision_date: NaiveDateTime, | ||||
|  |         pub expiration_date: Option<NaiveDateTime>, | ||||
|  |         pub deletion_date: NaiveDateTime, | ||||
|  | 
 | ||||
|  |         pub disabled: bool, | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | #[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)] | ||||
|  | pub enum SendType { | ||||
|  |     Text = 0, | ||||
|  |     File = 1, | ||||
|  | } | ||||
|  | 
 | ||||
|  | impl Send { | ||||
|  |     pub fn new(atype: i32, name: String, data: String, key: String, deletion_date: NaiveDateTime) -> Self { | ||||
|  |         let now = Utc::now().naive_utc(); | ||||
|  | 
 | ||||
|  |         Self { | ||||
|  |             uuid: crate::util::get_uuid(), | ||||
|  |             user_uuid: None, | ||||
|  |             organization_uuid: None, | ||||
|  | 
 | ||||
|  |             name, | ||||
|  |             notes: None, | ||||
|  | 
 | ||||
|  |             atype, | ||||
|  |             data, | ||||
|  |             key, | ||||
|  |             password_hash: None, | ||||
|  |             password_salt: None, | ||||
|  |             password_iter: None, | ||||
|  | 
 | ||||
|  |             max_access_count: None, | ||||
|  |             access_count: 0, | ||||
|  | 
 | ||||
|  |             creation_date: now, | ||||
|  |             revision_date: now, | ||||
|  |             expiration_date: None, | ||||
|  |             deletion_date, | ||||
|  | 
 | ||||
|  |             disabled: false, | ||||
|  |         } | ||||
|  |     } | ||||
|  |     
 | ||||
|  |     pub fn set_password(&mut self, password: Option<&str>) { | ||||
|  |         const PASSWORD_ITER: i32 = 100_000; | ||||
|  | 
 | ||||
|  |         if let Some(password) = password { | ||||
|  |             self.password_iter = Some(PASSWORD_ITER); | ||||
|  |             let salt = crate::crypto::get_random_64(); | ||||
|  |             let hash = crate::crypto::hash_password(password.as_bytes(), &salt, PASSWORD_ITER as u32); | ||||
|  |             self.password_salt = Some(salt); | ||||
|  |             self.password_hash = Some(hash); | ||||
|  |         } else { | ||||
|  |             self.password_iter = None; | ||||
|  |             self.password_salt = None; | ||||
|  |             self.password_hash = None; | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     pub fn check_password(&self, password: &str) -> bool { | ||||
|  |         match (&self.password_hash, &self.password_salt, self.password_iter) { | ||||
|  |             (Some(hash), Some(salt), Some(iter)) => { | ||||
|  |                 crate::crypto::verify_password_hash(password.as_bytes(), salt, hash, iter as u32) | ||||
|  |             } | ||||
|  |             _ => false, | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     pub fn to_json(&self) -> Value { | ||||
|  |         use crate::util::format_date; | ||||
|  |         use data_encoding::BASE64URL_NOPAD; | ||||
|  |         use uuid::Uuid; | ||||
|  | 
 | ||||
|  |         let data: Value = serde_json::from_str(&self.data).unwrap_or_default(); | ||||
|  | 
 | ||||
|  |         json!({ | ||||
|  |             "Id": self.uuid, | ||||
|  |             "AccessId": BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes()), | ||||
|  |             "Type": self.atype, | ||||
|  | 
 | ||||
|  |             "Name": self.name, | ||||
|  |             "Notes": self.notes, | ||||
|  |             "Text": if self.atype == SendType::Text as i32 { Some(&data) } else { None }, | ||||
|  |             "File": if self.atype == SendType::File as i32 { Some(&data) } else { None }, | ||||
|  | 
 | ||||
|  |             "Key": self.key, | ||||
|  |             "MaxAccessCount": self.max_access_count, | ||||
|  |             "AccessCount": self.access_count, | ||||
|  |             "Password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)), | ||||
|  |             "Disabled": self.disabled, | ||||
|  | 
 | ||||
|  |             "RevisionDate": format_date(&self.revision_date), | ||||
|  |             "ExpirationDate": self.expiration_date.as_ref().map(format_date), | ||||
|  |             "DeletionDate": format_date(&self.deletion_date), | ||||
|  |             "Object": "send", | ||||
|  |         }) | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | use crate::db::DbConn; | ||||
|  | 
 | ||||
|  | use crate::api::EmptyResult; | ||||
|  | use crate::error::MapResult; | ||||
|  | 
 | ||||
|  | impl Send { | ||||
|  |     pub fn save(&mut self, conn: &DbConn) -> EmptyResult { | ||||
|  |         // self.update_users_revision(conn);
 | ||||
|  |         self.revision_date = Utc::now().naive_utc(); | ||||
|  | 
 | ||||
|  |         db_run! { conn: | ||||
|  |             sqlite, mysql { | ||||
|  |                 match diesel::replace_into(sends::table) | ||||
|  |                     .values(SendDb::to_db(self)) | ||||
|  |                     .execute(conn) | ||||
|  |                 { | ||||
|  |                     Ok(_) => Ok(()), | ||||
|  |                     // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
 | ||||
|  |                     Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { | ||||
|  |                         diesel::update(sends::table) | ||||
|  |                             .filter(sends::uuid.eq(&self.uuid)) | ||||
|  |                             .set(SendDb::to_db(self)) | ||||
|  |                             .execute(conn) | ||||
|  |                             .map_res("Error saving send") | ||||
|  |                     } | ||||
|  |                     Err(e) => Err(e.into()), | ||||
|  |                 }.map_res("Error saving send") | ||||
|  |             } | ||||
|  |             postgresql { | ||||
|  |                 let value = SendDb::to_db(self); | ||||
|  |                 diesel::insert_into(sends::table) | ||||
|  |                     .values(&value) | ||||
|  |                     .on_conflict(sends::uuid) | ||||
|  |                     .do_update() | ||||
|  |                     .set(&value) | ||||
|  |                     .execute(conn) | ||||
|  |                     .map_res("Error saving send") | ||||
|  |             } | ||||
|  |         } | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     pub fn delete(&self, conn: &DbConn) -> EmptyResult { | ||||
|  |         // self.update_users_revision(conn);
 | ||||
|  | 
 | ||||
|  |         db_run! { conn: { | ||||
|  |             diesel::delete(sends::table.filter(sends::uuid.eq(&self.uuid))) | ||||
|  |                 .execute(conn) | ||||
|  |                 .map_res("Error deleting send") | ||||
|  |         }} | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { | ||||
|  |         for send in Self::find_by_user(user_uuid, &conn) { | ||||
|  |             send.delete(&conn)?; | ||||
|  |         } | ||||
|  |         Ok(()) | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     pub fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option<Self> { | ||||
|  |         use data_encoding::BASE64URL_NOPAD; | ||||
|  |         use uuid::Uuid; | ||||
|  | 
 | ||||
|  |         let uuid_vec = match BASE64URL_NOPAD.decode(access_id.as_bytes()) { | ||||
|  |             Ok(v) => v, | ||||
|  |             Err(_) => return None, | ||||
|  |         }; | ||||
|  | 
 | ||||
|  |         let uuid = match Uuid::from_slice(&uuid_vec) { | ||||
|  |             Ok(u) => u.to_string(), | ||||
|  |             Err(_) => return None, | ||||
|  |         }; | ||||
|  | 
 | ||||
|  |         Self::find_by_uuid(&uuid, conn) | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { | ||||
|  |         db_run! {conn: { | ||||
|  |             sends::table | ||||
|  |                 .filter(sends::uuid.eq(uuid)) | ||||
|  |                 .first::<SendDb>(conn) | ||||
|  |                 .ok() | ||||
|  |                 .from_db() | ||||
|  |         }} | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { | ||||
|  |         db_run! {conn: { | ||||
|  |             sends::table | ||||
|  |                 .filter(sends::user_uuid.eq(user_uuid)) | ||||
|  |                 .load::<SendDb>(conn).expect("Error loading sends").from_db() | ||||
|  |         }} | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { | ||||
|  |         db_run! {conn: { | ||||
|  |             sends::table | ||||
|  |                 .filter(sends::organization_uuid.eq(org_uuid)) | ||||
|  |                 .load::<SendDb>(conn).expect("Error loading sends").from_db() | ||||
|  |         }} | ||||
|  |     } | ||||
|  | } | ||||
					Loading…
					
					
				
		Reference in new issue