mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-05-12 17:09:39 -06:00
OpenDAL S3 storage accepts bucket and root path data today, but serverless deployments also need URI query parameters to describe provider behavior in one DATA_FOLDER value. Update OpenDAL to 0.56.0 and build S3 operators with S3Config::from_uri(). Keep Vaultwarden's AWS SDK credential chain by installing a reqsign provider when the URI does not explicitly request OpenDAL-native credential handling. Move path handling and operator construction into storage.rs so S3-specific parsing, credential setup, and URI path manipulation stay out of configuration handling. Local filesystem behavior is unchanged, and S3 child paths are derived before query strings.
705 lines
22 KiB
Rust
705 lines
22 KiB
Rust
use std::{path::Path, sync::LazyLock, time::Duration};
|
|
|
|
use chrono::{DateTime, TimeDelta, Utc};
|
|
use num_traits::ToPrimitive;
|
|
use rocket::{
|
|
form::Form,
|
|
fs::{NamedFile, TempFile},
|
|
serde::json::Json,
|
|
};
|
|
use serde_json::Value;
|
|
|
|
use crate::{
|
|
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},
|
|
auth::{ClientIp, Headers, Host},
|
|
config::PathType,
|
|
db::{
|
|
models::{Device, OrgPolicy, OrgPolicyType, Send, SendFileId, SendId, SendType, UserId},
|
|
DbConn, DbPool,
|
|
},
|
|
util::{save_temp_file, NumberOrString},
|
|
CONFIG,
|
|
};
|
|
|
|
const SEND_INACCESSIBLE_MSG: &str = "Send does not exist or is no longer available";
|
|
static ANON_PUSH_DEVICE: LazyLock<Device> = LazyLock::new(|| {
|
|
let dt = crate::util::parse_date("1970-01-01T00:00:00.000000Z");
|
|
Device {
|
|
uuid: String::from("00000000-0000-0000-0000-000000000000").into(),
|
|
created_at: dt,
|
|
updated_at: dt,
|
|
user_uuid: String::from("00000000-0000-0000-0000-000000000000").into(),
|
|
name: String::new(),
|
|
atype: 14, // 14 == Unknown Browser
|
|
push_uuid: Some(String::from("00000000-0000-0000-0000-000000000000").into()),
|
|
push_token: None,
|
|
refresh_token: String::new(),
|
|
twofactor_remember: None,
|
|
}
|
|
});
|
|
|
|
// The max file size allowed by Bitwarden clients and add an extra 5% to avoid issues
|
|
const SIZE_525_MB: i64 = 550_502_400;
|
|
|
|
pub fn routes() -> Vec<rocket::Route> {
|
|
routes![
|
|
get_sends,
|
|
get_send,
|
|
post_send,
|
|
post_send_file,
|
|
post_access,
|
|
post_access_file,
|
|
put_send,
|
|
delete_send,
|
|
put_remove_password,
|
|
download_send,
|
|
post_send_file_v2,
|
|
post_send_file_v2_data
|
|
]
|
|
}
|
|
|
|
pub async fn purge_sends(pool: DbPool) {
|
|
debug!("Purging sends");
|
|
if let Ok(conn) = pool.get().await {
|
|
Send::purge(&conn).await;
|
|
} else {
|
|
error!("Failed to get DB connection while purging sends")
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SendData {
|
|
r#type: i32,
|
|
key: String,
|
|
password: Option<String>,
|
|
max_access_count: Option<NumberOrString>,
|
|
expiration_date: Option<DateTime<Utc>>,
|
|
deletion_date: DateTime<Utc>,
|
|
disabled: bool,
|
|
hide_email: Option<bool>,
|
|
|
|
// Data field
|
|
name: String,
|
|
notes: Option<String>,
|
|
text: Option<Value>,
|
|
file: Option<Value>,
|
|
file_length: Option<NumberOrString>,
|
|
|
|
// Used for key rotations
|
|
pub id: Option<SendId>,
|
|
}
|
|
|
|
/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to
|
|
/// an org with this policy enabled isn't allowed to create new Sends or
|
|
/// modify existing ones, but is allowed to delete them.
|
|
///
|
|
/// Ref: https://bitwarden.com/help/article/policies/#disable-send
|
|
///
|
|
/// There is also a Vaultwarden-specific `sends_allowed` config setting that
|
|
/// controls this policy globally.
|
|
async fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult {
|
|
let user_id = &headers.user.uuid;
|
|
if !CONFIG.sends_allowed()
|
|
|| OrgPolicy::is_applicable_to_user(user_id, OrgPolicyType::DisableSend, None, conn).await
|
|
{
|
|
err!("Due to an Enterprise Policy, you are only able to delete an existing Send.")
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Enforces the `DisableHideEmail` option of the `Send Options` policy.
|
|
/// A non-owner/admin user belonging to an org with this option enabled isn't
|
|
/// allowed to hide their email address from the recipient of a Bitwarden Send,
|
|
/// but is allowed to remove this option from an existing Send.
|
|
///
|
|
/// Ref: https://bitwarden.com/help/article/policies/#send-options
|
|
async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &DbConn) -> EmptyResult {
|
|
let user_id = &headers.user.uuid;
|
|
let hide_email = data.hide_email.unwrap_or(false);
|
|
if hide_email && OrgPolicy::is_hide_email_disabled(user_id, conn).await {
|
|
err!(
|
|
"Due to an Enterprise Policy, you are not allowed to hide your email address \
|
|
from recipients when creating or editing a Send."
|
|
)
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn create_send(data: SendData, user_id: UserId) -> ApiResult<Send> {
|
|
let data_val = if data.r#type == SendType::Text as i32 {
|
|
data.text
|
|
} else if data.r#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.deletion_date > Utc::now() + TimeDelta::try_days(31).unwrap() {
|
|
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.r#type, data.name, data_str, data.key, data.deletion_date.naive_utc());
|
|
send.user_uuid = Some(user_id);
|
|
send.notes = data.notes;
|
|
send.max_access_count = match data.max_access_count {
|
|
Some(m) => Some(m.into_i32()?),
|
|
_ => None,
|
|
};
|
|
send.expiration_date = data.expiration_date.map(|d| d.naive_utc());
|
|
send.disabled = data.disabled;
|
|
send.hide_email = data.hide_email;
|
|
send.atype = data.r#type;
|
|
|
|
send.set_password(data.password.as_deref());
|
|
|
|
Ok(send)
|
|
}
|
|
|
|
#[get("/sends")]
|
|
async fn get_sends(headers: Headers, conn: DbConn) -> Json<Value> {
|
|
let sends = Send::find_by_user(&headers.user.uuid, &conn);
|
|
let sends_json: Vec<Value> = sends.await.iter().map(|s| s.to_json()).collect();
|
|
|
|
Json(json!({
|
|
"data": sends_json,
|
|
"object": "list",
|
|
"continuationToken": null
|
|
}))
|
|
}
|
|
|
|
#[get("/sends/<send_id>")]
|
|
async fn get_send(send_id: SendId, headers: Headers, conn: DbConn) -> JsonResult {
|
|
match Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await {
|
|
Some(send) => Ok(Json(send.to_json())),
|
|
None => err!("Send not found", "Invalid send uuid or does not belong to user"),
|
|
}
|
|
}
|
|
|
|
#[post("/sends", data = "<data>")]
|
|
async fn post_send(data: Json<SendData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
|
enforce_disable_send_policy(&headers, &conn).await?;
|
|
|
|
let data: SendData = data.into_inner();
|
|
enforce_disable_hide_email_policy(&data, &headers, &conn).await?;
|
|
|
|
if data.r#type == SendType::File as i32 {
|
|
err!("File sends should use /api/sends/file")
|
|
}
|
|
|
|
let mut send = create_send(data, headers.user.uuid)?;
|
|
send.save(&conn).await?;
|
|
nt.send_send_update(
|
|
UpdateType::SyncSendCreate,
|
|
&send,
|
|
&send.update_users_revision(&conn).await,
|
|
&headers.device,
|
|
&conn,
|
|
)
|
|
.await;
|
|
|
|
Ok(Json(send.to_json()))
|
|
}
|
|
|
|
#[derive(FromForm)]
|
|
struct UploadData<'f> {
|
|
model: Json<SendData>,
|
|
data: TempFile<'f>,
|
|
}
|
|
|
|
#[derive(FromForm)]
|
|
struct UploadDataV2<'f> {
|
|
data: TempFile<'f>,
|
|
}
|
|
|
|
// @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads (v2).
|
|
// This method still exists to support older clients, probably need to remove it sometime.
|
|
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167
|
|
// 2025: This endpoint doesn't seem to exists anymore in the latest version
|
|
// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs
|
|
#[post("/sends/file", format = "multipart/form-data", data = "<data>")]
|
|
async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
|
enforce_disable_send_policy(&headers, &conn).await?;
|
|
|
|
let UploadData {
|
|
model,
|
|
data,
|
|
} = data.into_inner();
|
|
let model = model.into_inner();
|
|
|
|
let Some(size) = data.len().to_i64() else {
|
|
err!("Invalid send size");
|
|
};
|
|
if size < 0 {
|
|
err!("Send size can't be negative")
|
|
}
|
|
|
|
enforce_disable_hide_email_policy(&model, &headers, &conn).await?;
|
|
|
|
let size_limit = match CONFIG.user_send_limit() {
|
|
Some(0) => err!("File uploads are disabled"),
|
|
Some(limit_kb) => {
|
|
let Some(already_used) = Send::size_by_user(&headers.user.uuid, &conn).await else {
|
|
err!("Existing sends overflow")
|
|
};
|
|
let Some(left) = limit_kb.checked_mul(1024).and_then(|l| l.checked_sub(already_used)) else {
|
|
err!("Send size overflow");
|
|
};
|
|
if left <= 0 {
|
|
err!("Send storage limit reached! Delete some sends to free up space")
|
|
}
|
|
i64::clamp(left, 0, SIZE_525_MB)
|
|
}
|
|
None => SIZE_525_MB,
|
|
};
|
|
|
|
if size > size_limit {
|
|
err!("Send storage limit exceeded with this file");
|
|
}
|
|
|
|
let mut send = create_send(model, headers.user.uuid)?;
|
|
if send.atype != SendType::File as i32 {
|
|
err!("Send content is not a file");
|
|
}
|
|
|
|
let file_id = crate::crypto::generate_send_file_id();
|
|
|
|
save_temp_file(&PathType::Sends, &format!("{}/{file_id}", send.uuid), data, true).await?;
|
|
|
|
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).await?;
|
|
nt.send_send_update(
|
|
UpdateType::SyncSendCreate,
|
|
&send,
|
|
&send.update_users_revision(&conn).await,
|
|
&headers.device,
|
|
&conn,
|
|
)
|
|
.await;
|
|
|
|
Ok(Json(send.to_json()))
|
|
}
|
|
|
|
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L165
|
|
#[post("/sends/file/v2", data = "<data>")]
|
|
async fn post_send_file_v2(data: Json<SendData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
enforce_disable_send_policy(&headers, &conn).await?;
|
|
|
|
let data = data.into_inner();
|
|
|
|
if data.r#type != SendType::File as i32 {
|
|
err!("Send content is not a file");
|
|
}
|
|
|
|
enforce_disable_hide_email_policy(&data, &headers, &conn).await?;
|
|
|
|
let file_length = match &data.file_length {
|
|
Some(m) => m.into_i64()?,
|
|
_ => err!("Invalid send length"),
|
|
};
|
|
if file_length < 0 {
|
|
err!("Send size can't be negative")
|
|
}
|
|
|
|
let size_limit = match CONFIG.user_send_limit() {
|
|
Some(0) => err!("File uploads are disabled"),
|
|
Some(limit_kb) => {
|
|
let Some(already_used) = Send::size_by_user(&headers.user.uuid, &conn).await else {
|
|
err!("Existing sends overflow")
|
|
};
|
|
let Some(left) = limit_kb.checked_mul(1024).and_then(|l| l.checked_sub(already_used)) else {
|
|
err!("Send size overflow");
|
|
};
|
|
if left <= 0 {
|
|
err!("Send storage limit reached! Delete some sends to free up space")
|
|
}
|
|
i64::clamp(left, 0, SIZE_525_MB)
|
|
}
|
|
None => SIZE_525_MB,
|
|
};
|
|
|
|
if file_length > size_limit {
|
|
err!("Send storage limit exceeded with this file");
|
|
}
|
|
|
|
let mut send = create_send(data, headers.user.uuid)?;
|
|
|
|
let file_id = crate::crypto::generate_send_file_id();
|
|
|
|
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.clone()));
|
|
o.insert(String::from("size"), Value::Number(file_length.into()));
|
|
o.insert(String::from("sizeName"), Value::String(crate::util::get_display_size(file_length)));
|
|
}
|
|
send.data = serde_json::to_string(&data_value)?;
|
|
send.save(&conn).await?;
|
|
|
|
Ok(Json(json!({
|
|
"fileUploadType": 0, // 0 == Direct | 1 == Azure
|
|
"object": "send-fileUpload",
|
|
"url": format!("/sends/{}/file/{file_id}", send.uuid),
|
|
"sendResponse": send.to_json()
|
|
})))
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[allow(non_snake_case)]
|
|
pub struct SendFileData {
|
|
id: SendFileId,
|
|
size: u64,
|
|
fileName: String,
|
|
}
|
|
|
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L195
|
|
#[post("/sends/<send_id>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
|
|
async fn post_send_file_v2_data(
|
|
send_id: SendId,
|
|
file_id: SendFileId,
|
|
data: Form<UploadDataV2<'_>>,
|
|
headers: Headers,
|
|
conn: DbConn,
|
|
nt: Notify<'_>,
|
|
) -> EmptyResult {
|
|
enforce_disable_send_policy(&headers, &conn).await?;
|
|
|
|
let data = data.into_inner();
|
|
|
|
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {
|
|
err!("Send not found. Unable to save the file.", "Invalid send uuid or does not belong to user.")
|
|
};
|
|
|
|
if send.atype != SendType::File as i32 {
|
|
err!("Send is not a file type send.");
|
|
}
|
|
|
|
let Ok(send_data) = serde_json::from_str::<SendFileData>(&send.data) else {
|
|
err!("Unable to decode send data as json.")
|
|
};
|
|
|
|
match data.data.raw_name() {
|
|
Some(raw_file_name)
|
|
if raw_file_name.dangerous_unsafe_unsanitized_raw() == send_data.fileName
|
|
// be less strict only if using CLI, cf. https://github.com/dani-garcia/vaultwarden/issues/5614
|
|
|| (headers.device.is_cli() && send_data.fileName.ends_with(raw_file_name.dangerous_unsafe_unsanitized_raw().as_str())
|
|
) => {}
|
|
Some(raw_file_name) => err!(
|
|
"Send file name does not match.",
|
|
format!(
|
|
"Expected file name '{}' got '{}'",
|
|
send_data.fileName,
|
|
raw_file_name.dangerous_unsafe_unsanitized_raw()
|
|
)
|
|
),
|
|
_ => err!("Send file name does not match or is not provided."),
|
|
}
|
|
|
|
if file_id != send_data.id {
|
|
err!("Send file does not match send data.", format!("Expected id {} got {file_id}", send_data.id));
|
|
}
|
|
|
|
let Some(size) = data.data.len().to_u64() else {
|
|
err!("Send file size overflow.");
|
|
};
|
|
|
|
if size != send_data.size {
|
|
err!("Send file size does not match.", format!("Expected a file size of {} got {size}", send_data.size));
|
|
}
|
|
|
|
let file_path = format!("{send_id}/{file_id}");
|
|
|
|
save_temp_file(&PathType::Sends, &file_path, data.data, false).await?;
|
|
|
|
nt.send_send_update(
|
|
UpdateType::SyncSendCreate,
|
|
&send,
|
|
&send.update_users_revision(&conn).await,
|
|
&headers.device,
|
|
&conn,
|
|
)
|
|
.await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SendAccessData {
|
|
pub password: Option<String>,
|
|
}
|
|
|
|
#[post("/sends/access/<access_id>", data = "<data>")]
|
|
async fn post_access(
|
|
access_id: &str,
|
|
data: Json<SendAccessData>,
|
|
conn: DbConn,
|
|
ip: ClientIp,
|
|
nt: Notify<'_>,
|
|
) -> JsonResult {
|
|
let Some(mut send) = Send::find_by_access_id(access_id, &conn).await else {
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
|
};
|
|
|
|
if let Some(max_access_count) = send.max_access_count {
|
|
if send.access_count >= max_access_count {
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404);
|
|
}
|
|
}
|
|
|
|
if let Some(expiration) = send.expiration_date {
|
|
if Utc::now().naive_utc() >= expiration {
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
|
}
|
|
}
|
|
|
|
if Utc::now().naive_utc() >= send.deletion_date {
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
|
}
|
|
|
|
if send.disabled {
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
|
}
|
|
|
|
if send.password_hash.is_some() {
|
|
match data.into_inner().password {
|
|
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }
|
|
Some(_) => err!("Invalid password", format!("IP: {}.", ip.ip)),
|
|
None => err_code!("Password not provided", format!("IP: {}.", ip.ip), 401),
|
|
}
|
|
}
|
|
|
|
// Files are incremented during the download
|
|
if send.atype == SendType::Text as i32 {
|
|
send.access_count += 1;
|
|
}
|
|
|
|
send.save(&conn).await?;
|
|
|
|
nt.send_send_update(
|
|
UpdateType::SyncSendUpdate,
|
|
&send,
|
|
&send.update_users_revision(&conn).await,
|
|
&ANON_PUSH_DEVICE,
|
|
&conn,
|
|
)
|
|
.await;
|
|
|
|
Ok(Json(send.to_json_access(&conn).await))
|
|
}
|
|
|
|
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
|
async fn post_access_file(
|
|
send_id: SendId,
|
|
file_id: SendFileId,
|
|
data: Json<SendAccessData>,
|
|
host: Host,
|
|
conn: DbConn,
|
|
nt: Notify<'_>,
|
|
) -> JsonResult {
|
|
let Some(mut send) = Send::find_by_uuid(&send_id, &conn).await else {
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
|
};
|
|
|
|
if let Some(max_access_count) = send.max_access_count {
|
|
if send.access_count >= max_access_count {
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
|
}
|
|
}
|
|
|
|
if let Some(expiration) = send.expiration_date {
|
|
if Utc::now().naive_utc() >= expiration {
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
|
}
|
|
}
|
|
|
|
if Utc::now().naive_utc() >= send.deletion_date {
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
|
}
|
|
|
|
if send.disabled {
|
|
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
|
}
|
|
|
|
if send.password_hash.is_some() {
|
|
match data.into_inner().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).await?;
|
|
|
|
nt.send_send_update(
|
|
UpdateType::SyncSendUpdate,
|
|
&send,
|
|
&send.update_users_revision(&conn).await,
|
|
&ANON_PUSH_DEVICE,
|
|
&conn,
|
|
)
|
|
.await;
|
|
|
|
Ok(Json(json!({
|
|
"object": "send-fileDownload",
|
|
"id": file_id,
|
|
"url": download_url(&host, &send_id, &file_id).await?,
|
|
})))
|
|
}
|
|
|
|
async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Result<String, crate::Error> {
|
|
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;
|
|
|
|
if crate::storage::is_fs_operator(&operator) {
|
|
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
|
|
let token = crate::auth::encode_jwt(&token_claims);
|
|
|
|
Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host))
|
|
} else {
|
|
Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_mins(5)).await?.uri().to_string())
|
|
}
|
|
}
|
|
|
|
#[get("/sends/<send_id>/<file_id>?<t>")]
|
|
async fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option<NamedFile> {
|
|
if let Ok(claims) = crate::auth::decode_send(t) {
|
|
if claims.sub == format!("{send_id}/{file_id}") {
|
|
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok();
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[put("/sends/<send_id>", data = "<data>")]
|
|
async fn put_send(send_id: SendId, data: Json<SendData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
|
enforce_disable_send_policy(&headers, &conn).await?;
|
|
|
|
let data: SendData = data.into_inner();
|
|
enforce_disable_hide_email_policy(&data, &headers, &conn).await?;
|
|
|
|
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {
|
|
err!("Send not found", "Send send_id is invalid or does not belong to user")
|
|
};
|
|
|
|
update_send_from_data(&mut send, data, &headers, &conn, &nt, UpdateType::SyncSendUpdate).await?;
|
|
|
|
Ok(Json(send.to_json()))
|
|
}
|
|
|
|
pub async fn update_send_from_data(
|
|
send: &mut Send,
|
|
data: SendData,
|
|
headers: &Headers,
|
|
conn: &DbConn,
|
|
nt: &Notify<'_>,
|
|
ut: UpdateType,
|
|
) -> EmptyResult {
|
|
if send.user_uuid.as_ref() != Some(&headers.user.uuid) {
|
|
err!("Send is not owned by user")
|
|
}
|
|
|
|
if send.atype != data.r#type {
|
|
err!("Sends can't change type")
|
|
}
|
|
|
|
if data.deletion_date > Utc::now() + TimeDelta::try_days(31).unwrap() {
|
|
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."
|
|
);
|
|
}
|
|
|
|
// When updating a file Send, we receive nulls in the File field, as it's immutable,
|
|
// so we only need to update the data field in the Text case
|
|
if data.r#type == SendType::Text as i32 {
|
|
let data_str = if let Some(mut d) = data.text {
|
|
d.as_object_mut().and_then(|d| d.remove("response"));
|
|
serde_json::to_string(&d)?
|
|
} else {
|
|
err!("Send data not provided");
|
|
};
|
|
send.data = data_str;
|
|
}
|
|
|
|
send.name = data.name;
|
|
send.akey = data.key;
|
|
send.deletion_date = data.deletion_date.naive_utc();
|
|
send.notes = data.notes;
|
|
send.max_access_count = match data.max_access_count {
|
|
Some(m) => Some(m.into_i32()?),
|
|
_ => None,
|
|
};
|
|
send.expiration_date = data.expiration_date.map(|d| d.naive_utc());
|
|
send.hide_email = data.hide_email;
|
|
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).await?;
|
|
if ut != UpdateType::None {
|
|
nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device, conn).await;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[delete("/sends/<send_id>")]
|
|
async fn delete_send(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
|
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {
|
|
err!("Send not found", "Invalid send uuid, or does not belong to user")
|
|
};
|
|
|
|
send.delete(&conn).await?;
|
|
nt.send_send_update(
|
|
UpdateType::SyncSendDelete,
|
|
&send,
|
|
&send.update_users_revision(&conn).await,
|
|
&headers.device,
|
|
&conn,
|
|
)
|
|
.await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[put("/sends/<send_id>/remove-password")]
|
|
async fn put_remove_password(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
|
enforce_disable_send_policy(&headers, &conn).await?;
|
|
|
|
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else {
|
|
err!("Send not found", "Invalid send uuid, or does not belong to user")
|
|
};
|
|
|
|
send.set_password(None);
|
|
send.save(&conn).await?;
|
|
nt.send_send_update(
|
|
UpdateType::SyncSendUpdate,
|
|
&send,
|
|
&send.update_users_revision(&conn).await,
|
|
&headers.device,
|
|
&conn,
|
|
)
|
|
.await;
|
|
|
|
Ok(Json(send.to_json()))
|
|
}
|