mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-06-03 09:10:15 +03:00
Add support for archiving items (#6916)
* Add archiving * Update Diesel macros and remove unnecessary SUPPORTED_FEATURE_FLAG * Add IF EXISTS to down.sql migratinos * Rename migration folders, separate logic based on PR threads
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
|
|
||||||
|
CREATE TABLE archives (
|
||||||
|
user_uuid CHAR(36) NOT NULL,
|
||||||
|
cipher_uuid CHAR(36) NOT NULL,
|
||||||
|
archived_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_uuid, cipher_uuid),
|
||||||
|
FOREIGN KEY (user_uuid) REFERENCES users (uuid) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (cipher_uuid) REFERENCES ciphers (uuid) ON DELETE CASCADE
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
|
|
||||||
|
CREATE TABLE archives (
|
||||||
|
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid) ON DELETE CASCADE,
|
||||||
|
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid) ON DELETE CASCADE,
|
||||||
|
archived_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_uuid, cipher_uuid)
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE IF EXISTS archives;
|
||||||
|
|
||||||
|
CREATE TABLE archives (
|
||||||
|
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid) ON DELETE CASCADE,
|
||||||
|
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid) ON DELETE CASCADE,
|
||||||
|
archived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_uuid, cipher_uuid)
|
||||||
|
);
|
||||||
+170
-5
@@ -19,9 +19,9 @@ use crate::{
|
|||||||
crypto,
|
crypto,
|
||||||
db::{
|
db::{
|
||||||
models::{
|
models::{
|
||||||
Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId,
|
Archive, Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup,
|
||||||
CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership, MembershipType,
|
CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership,
|
||||||
OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId,
|
MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId,
|
||||||
},
|
},
|
||||||
DbConn, DbPool,
|
DbConn, DbPool,
|
||||||
},
|
},
|
||||||
@@ -96,6 +96,10 @@ pub fn routes() -> Vec<Route> {
|
|||||||
post_collections_update,
|
post_collections_update,
|
||||||
post_collections_admin,
|
post_collections_admin,
|
||||||
put_collections_admin,
|
put_collections_admin,
|
||||||
|
archive_cipher_put,
|
||||||
|
archive_cipher_selected,
|
||||||
|
unarchive_cipher_put,
|
||||||
|
unarchive_cipher_selected,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +297,7 @@ pub struct CipherData {
|
|||||||
// when using older client versions, or if the operation doesn't involve
|
// when using older client versions, or if the operation doesn't involve
|
||||||
// updating an existing cipher.
|
// updating an existing cipher.
|
||||||
last_known_revision_date: Option<String>,
|
last_known_revision_date: Option<String>,
|
||||||
|
archived_date: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -534,6 +539,13 @@ pub async fn update_cipher_from_data(
|
|||||||
cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?;
|
cipher.move_to_folder(data.folder_id, &headers.user.uuid, conn).await?;
|
||||||
cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?;
|
cipher.set_favorite(data.favorite, &headers.user.uuid, conn).await?;
|
||||||
|
|
||||||
|
if let Some(dt_str) = data.archived_date {
|
||||||
|
match NaiveDateTime::parse_from_str(&dt_str, "%+") {
|
||||||
|
Ok(dt) => cipher.set_archived_at(dt, &headers.user.uuid, conn).await?,
|
||||||
|
Err(err) => warn!("Error parsing ArchivedDate '{dt_str}': {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ut != UpdateType::None {
|
if ut != UpdateType::None {
|
||||||
// Only log events for organizational ciphers
|
// Only log events for organizational ciphers
|
||||||
if let Some(org_id) = &cipher.organization_uuid {
|
if let Some(org_id) = &cipher.organization_uuid {
|
||||||
@@ -1715,6 +1727,36 @@ async fn purge_personal_vault(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/<cipher_id>/archive")]
|
||||||
|
async fn archive_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
|
archive_cipher(&cipher_id, &headers, false, &conn, &nt).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/archive", data = "<data>")]
|
||||||
|
async fn archive_cipher_selected(
|
||||||
|
data: Json<CipherIdsData>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
archive_multiple_ciphers(data, &headers, &conn, &nt).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/<cipher_id>/unarchive")]
|
||||||
|
async fn unarchive_cipher_put(cipher_id: CipherId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||||
|
unarchive_cipher(&cipher_id, &headers, false, &conn, &nt).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/ciphers/unarchive", data = "<data>")]
|
||||||
|
async fn unarchive_cipher_selected(
|
||||||
|
data: Json<CipherIdsData>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
nt: Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
unarchive_multiple_ciphers(data, &headers, &conn, &nt).await
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum CipherDeleteOptions {
|
pub enum CipherDeleteOptions {
|
||||||
SoftSingle,
|
SoftSingle,
|
||||||
@@ -1933,6 +1975,122 @@ async fn _delete_cipher_attachment_by_id(
|
|||||||
Ok(Json(json!({"cipher":cipher_json})))
|
Ok(Json(json!({"cipher":cipher_json})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn archive_cipher(
|
||||||
|
cipher_id: &CipherId,
|
||||||
|
headers: &Headers,
|
||||||
|
multi_archive: bool,
|
||||||
|
conn: &DbConn,
|
||||||
|
nt: &Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
let Some(cipher) = Cipher::find_by_uuid(cipher_id, conn).await else {
|
||||||
|
err!("Cipher doesn't exist")
|
||||||
|
};
|
||||||
|
|
||||||
|
if !cipher.is_accessible_to_user(&headers.user.uuid, conn).await {
|
||||||
|
err!("Cipher is not accessible for the current user")
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.set_archived_at(Utc::now().naive_utc(), &headers.user.uuid, conn).await?;
|
||||||
|
|
||||||
|
if !multi_archive {
|
||||||
|
nt.send_cipher_update(
|
||||||
|
UpdateType::SyncCipherUpdate,
|
||||||
|
&cipher,
|
||||||
|
&cipher.update_users_revision(conn).await,
|
||||||
|
&headers.device,
|
||||||
|
None,
|
||||||
|
conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unarchive_cipher(
|
||||||
|
cipher_id: &CipherId,
|
||||||
|
headers: &Headers,
|
||||||
|
multi_unarchive: bool,
|
||||||
|
conn: &DbConn,
|
||||||
|
nt: &Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
let Some(cipher) = Cipher::find_by_uuid(cipher_id, conn).await else {
|
||||||
|
err!("Cipher doesn't exist")
|
||||||
|
};
|
||||||
|
|
||||||
|
if !cipher.is_accessible_to_user(&headers.user.uuid, conn).await {
|
||||||
|
err!("Cipher is not accessible for the current user")
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.unarchive(&headers.user.uuid, conn).await?;
|
||||||
|
|
||||||
|
if !multi_unarchive {
|
||||||
|
nt.send_cipher_update(
|
||||||
|
UpdateType::SyncCipherUpdate,
|
||||||
|
&cipher,
|
||||||
|
&cipher.update_users_revision(conn).await,
|
||||||
|
&headers.device,
|
||||||
|
None,
|
||||||
|
conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn archive_multiple_ciphers(
|
||||||
|
data: Json<CipherIdsData>,
|
||||||
|
headers: &Headers,
|
||||||
|
conn: &DbConn,
|
||||||
|
nt: &Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
let data = data.into_inner();
|
||||||
|
|
||||||
|
let mut ciphers: Vec<Value> = Vec::new();
|
||||||
|
for cipher_id in data.ids {
|
||||||
|
match archive_cipher(&cipher_id, headers, true, conn, nt).await {
|
||||||
|
Ok(json) => ciphers.push(json.into_inner()),
|
||||||
|
err => return err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi archive does not send out a push for each cipher, we need to send a general sync here
|
||||||
|
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"data": ciphers,
|
||||||
|
"object": "list",
|
||||||
|
"continuationToken": null
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unarchive_multiple_ciphers(
|
||||||
|
data: Json<CipherIdsData>,
|
||||||
|
headers: &Headers,
|
||||||
|
conn: &DbConn,
|
||||||
|
nt: &Notify<'_>,
|
||||||
|
) -> JsonResult {
|
||||||
|
let data = data.into_inner();
|
||||||
|
|
||||||
|
let mut ciphers: Vec<Value> = Vec::new();
|
||||||
|
for cipher_id in data.ids {
|
||||||
|
match unarchive_cipher(&cipher_id, headers, true, conn, nt).await {
|
||||||
|
Ok(json) => ciphers.push(json.into_inner()),
|
||||||
|
err => return err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi unarchive does not send out a push for each cipher, we need to send a general sync here
|
||||||
|
nt.send_user_update(UpdateType::SyncCiphers, &headers.user, &headers.device.push_uuid, conn).await;
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"data": ciphers,
|
||||||
|
"object": "list",
|
||||||
|
"continuationToken": null
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
/// This will hold all the necessary data to improve a full sync of all the ciphers
|
/// This will hold all the necessary data to improve a full sync of all the ciphers
|
||||||
/// It can be used during the `Cipher::to_json()` call.
|
/// It can be used during the `Cipher::to_json()` call.
|
||||||
/// It will prevent the so called N+1 SQL issue by running just a few queries which will hold all the data needed.
|
/// It will prevent the so called N+1 SQL issue by running just a few queries which will hold all the data needed.
|
||||||
@@ -1942,6 +2100,7 @@ pub struct CipherSyncData {
|
|||||||
pub cipher_folders: HashMap<CipherId, FolderId>,
|
pub cipher_folders: HashMap<CipherId, FolderId>,
|
||||||
pub cipher_favorites: HashSet<CipherId>,
|
pub cipher_favorites: HashSet<CipherId>,
|
||||||
pub cipher_collections: HashMap<CipherId, Vec<CollectionId>>,
|
pub cipher_collections: HashMap<CipherId, Vec<CollectionId>>,
|
||||||
|
pub cipher_archives: HashMap<CipherId, NaiveDateTime>,
|
||||||
pub members: HashMap<OrganizationId, Membership>,
|
pub members: HashMap<OrganizationId, Membership>,
|
||||||
pub user_collections: HashMap<CollectionId, CollectionUser>,
|
pub user_collections: HashMap<CollectionId, CollectionUser>,
|
||||||
pub user_collections_groups: HashMap<CollectionId, CollectionGroup>,
|
pub user_collections_groups: HashMap<CollectionId, CollectionGroup>,
|
||||||
@@ -1958,20 +2117,25 @@ impl CipherSyncData {
|
|||||||
pub async fn new(user_id: &UserId, sync_type: CipherSyncType, conn: &DbConn) -> Self {
|
pub async fn new(user_id: &UserId, sync_type: CipherSyncType, conn: &DbConn) -> Self {
|
||||||
let cipher_folders: HashMap<CipherId, FolderId>;
|
let cipher_folders: HashMap<CipherId, FolderId>;
|
||||||
let cipher_favorites: HashSet<CipherId>;
|
let cipher_favorites: HashSet<CipherId>;
|
||||||
|
let cipher_archives: HashMap<CipherId, NaiveDateTime>;
|
||||||
match sync_type {
|
match sync_type {
|
||||||
// User Sync supports Folders and Favorites
|
// User Sync supports Folders, Favorites, and Archives
|
||||||
CipherSyncType::User => {
|
CipherSyncType::User => {
|
||||||
// Generate a HashMap with the Cipher UUID as key and the Folder UUID as value
|
// Generate a HashMap with the Cipher UUID as key and the Folder UUID as value
|
||||||
cipher_folders = FolderCipher::find_by_user(user_id, conn).await.into_iter().collect();
|
cipher_folders = FolderCipher::find_by_user(user_id, conn).await.into_iter().collect();
|
||||||
|
|
||||||
// Generate a HashSet of all the Cipher UUID's which are marked as favorite
|
// Generate a HashSet of all the Cipher UUID's which are marked as favorite
|
||||||
cipher_favorites = Favorite::get_all_cipher_uuid_by_user(user_id, conn).await.into_iter().collect();
|
cipher_favorites = Favorite::get_all_cipher_uuid_by_user(user_id, conn).await.into_iter().collect();
|
||||||
|
|
||||||
|
// Generate a HashMap with the Cipher UUID as key and the archived date time as value
|
||||||
|
cipher_archives = Archive::find_by_user(user_id, conn).await.into_iter().collect();
|
||||||
}
|
}
|
||||||
// Organization Sync does not support Folders and Favorites.
|
// Organization Sync does not support Folders, Favorites, or Archives.
|
||||||
// If these are set, it will cause issues in the web-vault.
|
// If these are set, it will cause issues in the web-vault.
|
||||||
CipherSyncType::Organization => {
|
CipherSyncType::Organization => {
|
||||||
cipher_folders = HashMap::with_capacity(0);
|
cipher_folders = HashMap::with_capacity(0);
|
||||||
cipher_favorites = HashSet::with_capacity(0);
|
cipher_favorites = HashSet::with_capacity(0);
|
||||||
|
cipher_archives = HashMap::with_capacity(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2034,6 +2198,7 @@ impl CipherSyncData {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
cipher_archives,
|
||||||
cipher_attachments,
|
cipher_attachments,
|
||||||
cipher_folders,
|
cipher_folders,
|
||||||
cipher_favorites,
|
cipher_favorites,
|
||||||
|
|||||||
+2
-2
@@ -204,11 +204,11 @@ fn config() -> Json<Value> {
|
|||||||
// Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12
|
// Client (v2026.2.1): https://github.com/bitwarden/clients/blob/f96380c3138291a028bdd2c7a5fee540d5c98ba5/libs/common/src/enums/feature-flag.enum.ts#L12
|
||||||
// Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31
|
// Android (v2026.2.1): https://github.com/bitwarden/android/blob/6902c19c0093fa476bbf74ccaa70c9f14afbb82f/core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt#L31
|
||||||
// iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
|
// iOS (v2026.2.1): https://github.com/bitwarden/ios/blob/cdd9ba1770ca2ffc098d02d12cc3208e3a830454/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7
|
||||||
let feature_states = parse_experimental_client_feature_flags(
|
let mut feature_states = parse_experimental_client_feature_flags(
|
||||||
&CONFIG.experimental_client_feature_flags(),
|
&CONFIG.experimental_client_feature_flags(),
|
||||||
FeatureFlagFilter::ValidOnly,
|
FeatureFlagFilter::ValidOnly,
|
||||||
);
|
);
|
||||||
// Add default feature_states here if needed, currently no features are needed by default.
|
feature_states.insert("pm-19148-innovation-archive".to_string(), true);
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
// Note: The clients use this version to handle backwards compatibility concerns
|
// Note: The clients use this version to handle backwards compatibility concerns
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
use super::{CipherId, User, UserId};
|
||||||
|
use crate::api::EmptyResult;
|
||||||
|
use crate::db::schema::archives;
|
||||||
|
use crate::db::DbConn;
|
||||||
|
use crate::error::MapResult;
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Insertable)]
|
||||||
|
#[diesel(table_name = archives)]
|
||||||
|
#[diesel(primary_key(user_uuid, cipher_uuid))]
|
||||||
|
pub struct Archive {
|
||||||
|
pub user_uuid: UserId,
|
||||||
|
pub cipher_uuid: CipherId,
|
||||||
|
pub archived_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Archive {
|
||||||
|
// Returns the date the specified cipher was archived
|
||||||
|
pub async fn get_archived_at(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &DbConn) -> Option<NaiveDateTime> {
|
||||||
|
db_run! { conn: {
|
||||||
|
archives::table
|
||||||
|
.filter(archives::cipher_uuid.eq(cipher_uuid))
|
||||||
|
.filter(archives::user_uuid.eq(user_uuid))
|
||||||
|
.select(archives::archived_at)
|
||||||
|
.first::<NaiveDateTime>(conn).ok()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves (inserts or updates) an archive record with the provided timestamp
|
||||||
|
pub async fn save(
|
||||||
|
user_uuid: &UserId,
|
||||||
|
cipher_uuid: &CipherId,
|
||||||
|
archived_at: NaiveDateTime,
|
||||||
|
conn: &DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
|
User::update_uuid_revision(user_uuid, conn).await;
|
||||||
|
db_run! { conn:
|
||||||
|
sqlite, mysql {
|
||||||
|
diesel::replace_into(archives::table)
|
||||||
|
.values((
|
||||||
|
archives::user_uuid.eq(user_uuid),
|
||||||
|
archives::cipher_uuid.eq(cipher_uuid),
|
||||||
|
archives::archived_at.eq(archived_at),
|
||||||
|
))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving archive")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
diesel::insert_into(archives::table)
|
||||||
|
.values((
|
||||||
|
archives::user_uuid.eq(user_uuid),
|
||||||
|
archives::cipher_uuid.eq(cipher_uuid),
|
||||||
|
archives::archived_at.eq(archived_at),
|
||||||
|
))
|
||||||
|
.on_conflict((archives::user_uuid, archives::cipher_uuid))
|
||||||
|
.do_update()
|
||||||
|
.set(archives::archived_at.eq(archived_at))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving archive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes an archive record for a specific cipher
|
||||||
|
pub async fn delete_by_cipher(user_uuid: &UserId, cipher_uuid: &CipherId, conn: &DbConn) -> EmptyResult {
|
||||||
|
User::update_uuid_revision(user_uuid, conn).await;
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::delete(
|
||||||
|
archives::table
|
||||||
|
.filter(archives::user_uuid.eq(user_uuid))
|
||||||
|
.filter(archives::cipher_uuid.eq(cipher_uuid))
|
||||||
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error deleting archive")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a vec with (cipher_uuid, archived_at)
|
||||||
|
/// This is used during a full sync so we only need one query for all archive matches
|
||||||
|
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<(CipherId, NaiveDateTime)> {
|
||||||
|
db_run! { conn: {
|
||||||
|
archives::table
|
||||||
|
.filter(archives::user_uuid.eq(user_uuid))
|
||||||
|
.select((archives::cipher_uuid, archives::archived_at))
|
||||||
|
.load::<(CipherId, NaiveDateTime)>(conn)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-2
@@ -10,8 +10,8 @@ use diesel::prelude::*;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus,
|
Archive, Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership,
|
||||||
MembershipType, OrganizationId, User, UserId,
|
MembershipStatus, MembershipType, OrganizationId, User, UserId,
|
||||||
};
|
};
|
||||||
use crate::api::core::{CipherData, CipherSyncData, CipherSyncType};
|
use crate::api::core::{CipherData, CipherSyncData, CipherSyncType};
|
||||||
use macros::UuidFromParam;
|
use macros::UuidFromParam;
|
||||||
@@ -380,6 +380,11 @@ impl Cipher {
|
|||||||
} else {
|
} else {
|
||||||
self.is_favorite(user_uuid, conn).await
|
self.is_favorite(user_uuid, conn).await
|
||||||
});
|
});
|
||||||
|
json_object["archivedDate"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
|
||||||
|
cipher_sync_data.cipher_archives.get(&self.uuid).map_or(Value::Null, |d| Value::String(format_date(d)))
|
||||||
|
} else {
|
||||||
|
self.get_archived_at(user_uuid, conn).await.map_or(Value::Null, |d| Value::String(format_date(&d)))
|
||||||
|
});
|
||||||
// These values are true by default, but can be false if the
|
// These values are true by default, but can be false if the
|
||||||
// cipher belongs to a collection or group where the org owner has enabled
|
// cipher belongs to a collection or group where the org owner has enabled
|
||||||
// the "Read Only" or "Hide Passwords" restrictions for the user.
|
// the "Read Only" or "Hide Passwords" restrictions for the user.
|
||||||
@@ -742,6 +747,18 @@ impl Cipher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_archived_at(&self, user_uuid: &UserId, conn: &DbConn) -> Option<NaiveDateTime> {
|
||||||
|
Archive::get_archived_at(&self.uuid, user_uuid, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_archived_at(&self, archived_at: NaiveDateTime, user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
|
||||||
|
Archive::save(user_uuid, &self.uuid, archived_at, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unarchive(&self, user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
|
||||||
|
Archive::delete_by_cipher(user_uuid, &self.uuid, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &DbConn) -> Option<FolderId> {
|
pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &DbConn) -> Option<FolderId> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
folders_ciphers::table
|
folders_ciphers::table
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod archive;
|
||||||
mod attachment;
|
mod attachment;
|
||||||
mod auth_request;
|
mod auth_request;
|
||||||
mod cipher;
|
mod cipher;
|
||||||
@@ -17,6 +18,7 @@ mod two_factor_duo_context;
|
|||||||
mod two_factor_incomplete;
|
mod two_factor_incomplete;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
|
pub use self::archive::Archive;
|
||||||
pub use self::attachment::{Attachment, AttachmentId};
|
pub use self::attachment::{Attachment, AttachmentId};
|
||||||
pub use self::auth_request::{AuthRequest, AuthRequestId};
|
pub use self::auth_request::{AuthRequest, AuthRequestId};
|
||||||
pub use self::cipher::{Cipher, CipherId, RepromptType};
|
pub use self::cipher::{Cipher, CipherId, RepromptType};
|
||||||
|
|||||||
@@ -342,6 +342,16 @@ table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
archives (user_uuid, cipher_uuid) {
|
||||||
|
user_uuid -> Text,
|
||||||
|
cipher_uuid -> Text,
|
||||||
|
archived_at -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
joinable!(archives -> users (user_uuid));
|
||||||
|
joinable!(archives -> ciphers (cipher_uuid));
|
||||||
joinable!(attachments -> ciphers (cipher_uuid));
|
joinable!(attachments -> ciphers (cipher_uuid));
|
||||||
joinable!(ciphers -> organizations (organization_uuid));
|
joinable!(ciphers -> organizations (organization_uuid));
|
||||||
joinable!(ciphers -> users (user_uuid));
|
joinable!(ciphers -> users (user_uuid));
|
||||||
@@ -373,6 +383,7 @@ joinable!(auth_requests -> users (user_uuid));
|
|||||||
joinable!(sso_users -> users (user_uuid));
|
joinable!(sso_users -> users (user_uuid));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
|
archives,
|
||||||
attachments,
|
attachments,
|
||||||
ciphers,
|
ciphers,
|
||||||
ciphers_collections,
|
ciphers_collections,
|
||||||
|
|||||||
Reference in New Issue
Block a user