diff --git a/backend/sync_server/src/app_state/database.rs b/backend/sync_server/src/app_state/database.rs index 570355c..1e26e14 100644 --- a/backend/sync_server/src/app_state/database.rs +++ b/backend/sync_server/src/app_state/database.rs @@ -143,7 +143,9 @@ impl Database { document_id as "document_id: Hyphenated", relative_path, updated_date as "updated_date: chrono::DateTime", - is_deleted + is_deleted, + user_id, + device_id from latest_document_versions order by vault_update_id "#, @@ -175,7 +177,9 @@ impl Database { document_id as "document_id: Hyphenated", relative_path, updated_date as "updated_date: chrono::DateTime", - is_deleted + is_deleted, + user_id, + device_id from latest_document_versions where vault_update_id > ? order by vault_update_id @@ -233,7 +237,9 @@ impl Database { relative_path, updated_date as "updated_date: chrono::DateTime", content, - is_deleted + is_deleted, + user_id, + device_id from latest_document_versions where relative_path = ? order by vault_update_id desc -- `latest_document_versions` only contains a single latest version of each document, however, @@ -270,7 +276,9 @@ impl Database { relative_path, updated_date as "updated_date: chrono::DateTime", content, - is_deleted + is_deleted, + user_id, + device_id from latest_document_versions where document_id = ? "#, @@ -302,7 +310,9 @@ impl Database { relative_path, updated_date as "updated_date: chrono::DateTime", content, - is_deleted + is_deleted, + user_id, + device_id from documents where vault_update_id = ?"#, vault_update_id @@ -333,16 +343,20 @@ impl Database { relative_path, updated_date, content, - is_deleted + is_deleted, + user_id, + device_id ) - values (?, ?, ?, ?, ?, ?) + values (?, ?, ?, ?, ?, ?, ?, ?) "#, version.vault_update_id, document_id, version.relative_path, version.updated_date, version.content, - version.is_deleted + version.is_deleted, + version.user_id, + version.device_id ); if let Some(transaction) = transaction { diff --git a/backend/sync_server/src/app_state/database/migrations/20250522192949_add_provenance_columns.sql b/backend/sync_server/src/app_state/database/migrations/20250522192949_add_provenance_columns.sql new file mode 100644 index 0000000..0686017 --- /dev/null +++ b/backend/sync_server/src/app_state/database/migrations/20250522192949_add_provenance_columns.sql @@ -0,0 +1,2 @@ +ALTER TABLE documents ADD COLUMN user_id TEXT NOT NULL DEFAULT ""; +ALTER TABLE documents ADD COLUMN device_id TEXT NOT NULL DEFAULT ""; diff --git a/backend/sync_server/src/app_state/database/models.rs b/backend/sync_server/src/app_state/database/models.rs index 55079c8..9f896ac 100644 --- a/backend/sync_server/src/app_state/database/models.rs +++ b/backend/sync_server/src/app_state/database/models.rs @@ -6,6 +6,7 @@ use sync_lib::bytes_to_base64; pub type VaultId = String; pub type VaultUpdateId = i64; pub type DocumentId = uuid::Uuid; +pub type UserId = String; pub type DeviceId = String; #[derive(Debug, Clone)] @@ -16,6 +17,8 @@ pub struct StoredDocumentVersion { pub updated_date: DateTime, pub content: Vec, pub is_deleted: bool, + pub user_id: UserId, + pub device_id: DeviceId, } impl PartialEq for StoredDocumentVersion { @@ -30,6 +33,8 @@ pub struct DocumentVersionWithoutContent { pub relative_path: String, pub updated_date: DateTime, pub is_deleted: bool, + pub user_id: UserId, + pub device_id: DeviceId, } impl From for DocumentVersionWithoutContent { @@ -40,6 +45,8 @@ impl From for DocumentVersionWithoutContent { relative_path: value.relative_path, updated_date: value.updated_date, is_deleted: value.is_deleted, + user_id: value.user_id, + device_id: value.device_id, } } } @@ -53,6 +60,8 @@ pub struct DocumentVersion { pub updated_date: DateTime, pub content_base64: String, pub is_deleted: bool, + pub user_id: UserId, + pub device_id: DeviceId, } impl From for DocumentVersion { @@ -64,6 +73,8 @@ impl From for DocumentVersion { updated_date: value.updated_date, content_base64: bytes_to_base64(&value.content), is_deleted: value.is_deleted, + user_id: value.user_id, + device_id: value.device_id, } } } diff --git a/backend/sync_server/src/server/create_document.rs b/backend/sync_server/src/server/create_document.rs index ebbcac2..0fc9dc3 100644 --- a/backend/sync_server/src/server/create_document.rs +++ b/backend/sync_server/src/server/create_document.rs @@ -1,6 +1,10 @@ use aide_axum_typed_multipart::TypedMultipart; use anyhow::Context as _; -use axum::extract::{Path, State}; +use axum::{ + Extension, + extract::{Path, State}, +}; +use axum_extra::{TypedHeader, headers::UserAgent}; use axum_jsonschema::Json; use schemars::JsonSchema; use serde::Deserialize; @@ -15,6 +19,7 @@ use crate::{ DeviceId, DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId, }, }, + config::user_config::User, errors::{SyncServerError, client_error, server_error}, utils::{normalize::normalize, sanitize_path::sanitize_path}, }; @@ -32,12 +37,16 @@ pub struct CreateDocumentPathParams { #[axum::debug_handler] pub async fn create_document_multipart( Path(CreateDocumentPathParams { vault_id }): Path, + Extension(user): Extension, + TypedHeader(user_agent): TypedHeader, State(state): State, TypedMultipart(axum_typed_multipart::TypedMultipart(request)): TypedMultipart< CreateDocumentVersionMultipart, >, ) -> Result, SyncServerError> { internal_create_document( + user, + user_agent, state, vault_id, request.document_id, @@ -54,6 +63,8 @@ pub async fn create_document_multipart( #[axum::debug_handler] pub async fn create_document_json( Path(CreateDocumentPathParams { vault_id }): Path, + Extension(user): Extension, + TypedHeader(user_agent): TypedHeader, State(state): State, Json(request): Json, ) -> Result, SyncServerError> { @@ -62,6 +73,8 @@ pub async fn create_document_json( .map_err(client_error)?; internal_create_document( + user, + user_agent, state, vault_id, request.document_id, @@ -72,7 +85,10 @@ pub async fn create_document_json( .await } +#[allow(clippy::too_many_arguments)] async fn internal_create_document( + user: User, + user_agent: UserAgent, state: AppState, vault_id: VaultId, document_id: Option, @@ -120,6 +136,8 @@ async fn internal_create_document( content, updated_date: chrono::Utc::now(), is_deleted: false, + user_id: user.name, + device_id: user_agent.to_string(), }; state diff --git a/backend/sync_server/src/server/delete_document.rs b/backend/sync_server/src/server/delete_document.rs index 3329e7f..9735e80 100644 --- a/backend/sync_server/src/server/delete_document.rs +++ b/backend/sync_server/src/server/delete_document.rs @@ -1,5 +1,9 @@ use anyhow::Context as _; -use axum::extract::{Path, State}; +use axum::{ + Extension, + extract::{Path, State}, +}; +use axum_extra::{TypedHeader, headers::UserAgent}; use axum_jsonschema::Json; use schemars::JsonSchema; use serde::Deserialize; @@ -13,6 +17,7 @@ use crate::{ DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId, }, }, + config::user_config::User, errors::{SyncServerError, server_error}, utils::{normalize::normalize, sanitize_path::sanitize_path}, }; @@ -32,6 +37,8 @@ pub async fn delete_document( vault_id, document_id, }): Path, + Extension(user): Extension, + TypedHeader(user_agent): TypedHeader, State(state): State, Json(request): Json, ) -> Result, SyncServerError> { @@ -54,6 +61,8 @@ pub async fn delete_document( content: vec![], updated_date: chrono::Utc::now(), is_deleted: true, + user_id: user.name, + device_id: user_agent.to_string(), }; state diff --git a/backend/sync_server/src/server/update_document.rs b/backend/sync_server/src/server/update_document.rs index 60a4cab..ded4dd0 100644 --- a/backend/sync_server/src/server/update_document.rs +++ b/backend/sync_server/src/server/update_document.rs @@ -1,6 +1,10 @@ use aide_axum_typed_multipart::TypedMultipart; use anyhow::{Context as _, anyhow}; -use axum::extract::{Path, State}; +use axum::{ + Extension, + extract::{Path, State}, +}; +use axum_extra::{TypedHeader, headers::UserAgent}; use axum_jsonschema::Json; use log::info; use schemars::JsonSchema; @@ -17,6 +21,7 @@ use crate::{ broadcasts::VaultUpdate, database::models::{DeviceId, DocumentId, StoredDocumentVersion, VaultId, VaultUpdateId}, }, + config::user_config::User, errors::{SyncServerError, client_error, not_found_error, server_error}, utils::{dedup_paths::dedup_paths, normalize::normalize, sanitize_path::sanitize_path}, }; @@ -36,12 +41,16 @@ pub async fn update_document_multipart( vault_id, document_id, }): Path, + Extension(user): Extension, + TypedHeader(user_agent): TypedHeader, State(state): State, TypedMultipart(axum_typed_multipart::TypedMultipart(request)): TypedMultipart< UpdateDocumentVersionMultipart, >, ) -> Result, SyncServerError> { internal_update_document( + user, + user_agent, state, vault_id, document_id, @@ -59,6 +68,8 @@ pub async fn update_document_json( vault_id, document_id, }): Path, + Extension(user): Extension, + TypedHeader(user_agent): TypedHeader, State(state): State, Json(request): Json, ) -> Result, SyncServerError> { @@ -67,6 +78,8 @@ pub async fn update_document_json( .map_err(client_error)?; internal_update_document( + user, + user_agent, state, vault_id, document_id, @@ -80,6 +93,8 @@ pub async fn update_document_json( #[allow(clippy::too_many_arguments, clippy::too_many_lines)] async fn internal_update_document( + user: User, + user_agent: UserAgent, state: AppState, vault_id: VaultId, document_id: DocumentId, @@ -198,6 +213,8 @@ async fn internal_update_document( content: merged_content, updated_date: chrono::Utc::now(), is_deleted: false, + user_id: user.name, + device_id: user_agent.to_string(), }; state