committed by
							
								
								GitHub
							
						
					
				
				 43 changed files with 932 additions and 108 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, | 
				
			||||
 | 
					  akey              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 @@ | 
				
			|||||
 | 
					ALTER TABLE sends RENAME COLUMN key TO akey; | 
				
			||||
@ -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 @@ | 
				
			|||||
 | 
					ALTER TABLE sends RENAME COLUMN key TO akey; | 
				
			||||
@ -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.akey = 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 akey: 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, akey: 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, | 
				
			||||
 | 
					            akey, | 
				
			||||
 | 
					            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.akey, | 
				
			||||
 | 
					            "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