diff --git a/services/backup/src/http/handlers/log.rs b/services/backup/src/http/handlers/log.rs index 9b9743996..043f7db92 100644 --- a/services/backup/src/http/handlers/log.rs +++ b/services/backup/src/http/handlers/log.rs @@ -1,342 +1,372 @@ use crate::constants::WS_FRAME_SIZE; use crate::database::{log_item::LogItem, DatabaseClient}; use actix::fut::ready; use actix::{Actor, ActorContext, ActorFutureExt, AsyncContext, StreamHandler}; use actix_http::ws::{CloseCode, Item}; use actix_web::{ web::{self, Bytes, BytesMut}, Error, HttpRequest, HttpResponse, }; use actix_web_actors::ws::{self, WebsocketContext}; -use comm_lib::auth::UserIdentity; +use comm_lib::auth::{AuthService, AuthServiceError, UserIdentity}; use comm_lib::{ backup::{ DownloadLogsRequest, LogWSRequest, LogWSResponse, UploadLogRequest, }, blob::{ client::{BlobServiceClient, BlobServiceError}, types::BlobInfo, }, database::{self, blob::BlobOrDBContent}, }; use std::future::Future; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tracing::{error, info, instrument, warn}; pub async fn handle_ws( req: HttpRequest, stream: web::Payload, blob_client: web::Data, db_client: web::Data, + auth_service: AuthService, ) -> Result { ws::WsResponseBuilder::new( LogWSActor { - user: None, + user: Arc::new(Mutex::new(None)), blob_client: blob_client.as_ref().clone(), db_client: db_client.as_ref().clone(), + auth_service, last_msg_time: Instant::now(), buffer: BytesMut::new(), }, &req, stream, ) .frame_size(WS_FRAME_SIZE) .start() } #[derive( Debug, derive_more::From, derive_more::Display, derive_more::Error, )] enum LogWSError { Bincode(bincode::Error), Blob(BlobServiceError), DB(database::Error), + Auth(AuthServiceError), } struct LogWSActor { - user: Option, + user: Arc>>, blob_client: BlobServiceClient, db_client: DatabaseClient, - + auth_service: AuthService, last_msg_time: Instant, buffer: BytesMut, } impl LogWSActor { const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); fn handle_msg_sync( &mut self, ctx: &mut WebsocketContext, bytes: Bytes, ) { match bincode::deserialize(&bytes) { Ok(request) => { if let LogWSRequest::Authenticate(user) = request { - self.user.replace(user); + Self::spawn_response_future( + ctx, + Self::handle_auth_msg( + self.auth_service.clone(), + Arc::clone(&self.user), + user, + ), + ); return; } - let Some(user) = &self.user else { + let user_guard = self.user.lock().expect("user mutex poisoned"); + let Some(user) = user_guard.as_ref() else { Self::spawn_response_future( ctx, ready(Ok(vec![LogWSResponse::Unauthenticated])), ); return; }; Self::spawn_response_future( ctx, Self::handle_msg( user.user_id.clone(), self.blob_client.clone().with_user_identity(user.clone()), self.db_client.clone(), request, ), ); } Err(err) => { error!("Error: {err:?}"); Self::spawn_response_future( ctx, ready(Ok(vec![LogWSResponse::ServerError])), ); } }; } fn spawn_response_future( ctx: &mut WebsocketContext, future: impl Future, LogWSError>> + 'static, ) { let fut = actix::fut::wrap_future(future).map( |responses, _: &mut LogWSActor, ctx: &mut WebsocketContext| { let responses = match responses { Ok(responses) => responses, Err(err) => { error!("Error: {err:?}"); vec![LogWSResponse::ServerError] } }; for response in responses { match bincode::serialize(&response) { Ok(bytes) => ctx.binary(bytes), Err(error) => { error!( "Error serializing a response: {response:?}. Error: {error}" ); } }; } }, ); ctx.spawn(fut); } + async fn handle_auth_msg( + auth_service: AuthService, + current_user: Arc>>, + user_to_verify: UserIdentity, + ) -> Result, LogWSError> { + use comm_lib::auth::AuthorizationCredential; + let credential = AuthorizationCredential::UserToken(user_to_verify.clone()); + let user_valid = auth_service.verify_auth_credential(&credential).await?; + + if user_valid { + *current_user.lock().expect("mutex poisoned") = Some(user_to_verify); + Ok(vec![LogWSResponse::AuthSuccess]) + } else { + tracing::debug!("Invalid credentials"); + Ok(vec![LogWSResponse::Unauthenticated]) + } + } + async fn handle_msg( user_id: String, blob_client: BlobServiceClient, db_client: DatabaseClient, request: LogWSRequest, ) -> Result, LogWSError> { match request { LogWSRequest::UploadLog(UploadLogRequest { backup_id, log_id, content, attachments, }) => { let mut attachment_blob_infos = Vec::new(); for attachment in attachments.unwrap_or_default() { let blob_info = Self::create_attachment(&blob_client, attachment).await?; attachment_blob_infos.push(blob_info); } let mut log_item = LogItem { user_id, backup_id: backup_id.clone(), log_id, content: BlobOrDBContent::new(content), attachments: attachment_blob_infos, }; log_item.ensure_size_constraints(&blob_client).await?; db_client.put_log_item(log_item, &blob_client).await?; Ok(vec![LogWSResponse::LogUploaded { backup_id, log_id }]) } LogWSRequest::DownloadLogs(DownloadLogsRequest { backup_id, from_id, }) => { let (log_items, last_id) = db_client .fetch_log_items(&user_id, &backup_id, from_id) .await?; let mut messages = vec![]; for LogItem { log_id, content, attachments, .. } in log_items { let content = content.fetch_bytes(&blob_client).await?; let attachments: Vec = attachments.into_iter().map(|att| att.blob_hash).collect(); let attachments = if attachments.is_empty() { None } else { Some(attachments) }; messages.push(LogWSResponse::LogDownload { log_id, content, attachments, }) } messages.push(LogWSResponse::LogDownloadFinished { last_log_id: last_id, }); Ok(messages) } LogWSRequest::Authenticate(_) => { warn!("LogWSRequest::Authenticate should have been handled earlier."); Ok(Vec::new()) } } } async fn create_attachment( blob_client: &BlobServiceClient, attachment: String, ) -> Result { let blob_info = BlobInfo { blob_hash: attachment, holder: uuid::Uuid::new_v4().to_string(), }; if !blob_client .assign_holder(&blob_info.blob_hash, &blob_info.holder) .await? { warn!( "Blob attachment with hash {:?} doesn't exist", blob_info.blob_hash ); } Ok(blob_info) } } impl Actor for LogWSActor { type Context = ws::WebsocketContext; #[instrument(skip_all)] fn started(&mut self, ctx: &mut Self::Context) { info!("Socket opened"); ctx.run_interval(Self::HEARTBEAT_INTERVAL, |actor, ctx| { if Instant::now().duration_since(actor.last_msg_time) > Self::CONNECTION_TIMEOUT { warn!("Socket timeout, closing connection"); ctx.stop(); return; } ctx.ping(&[]); }); } #[instrument(skip_all)] fn stopped(&mut self, _: &mut Self::Context) { info!("Socket closed"); } } impl StreamHandler> for LogWSActor { #[instrument(skip_all)] fn handle( &mut self, msg: Result, ctx: &mut Self::Context, ) { let msg = match msg { Ok(msg) => msg, Err(err) => { warn!("Error during socket message handling: {err}"); ctx.close(Some(CloseCode::Error.into())); ctx.stop(); return; } }; self.last_msg_time = Instant::now(); match msg { ws::Message::Binary(bytes) => self.handle_msg_sync(ctx, bytes), // Continuations - this is mostly boilerplate code. Some websocket // clients may split a message into these ones ws::Message::Continuation(Item::FirstBinary(bytes)) => { if !self.buffer.is_empty() { warn!("Socket received continuation before previous was completed"); ctx.close(Some(CloseCode::Error.into())); ctx.stop(); return; } self.buffer.extend_from_slice(&bytes); } ws::Message::Continuation(Item::Continue(bytes)) => { if self.buffer.is_empty() { warn!("Socket received continuation message before it was started"); ctx.close(Some(CloseCode::Error.into())); ctx.stop(); return; } self.buffer.extend_from_slice(&bytes); } ws::Message::Continuation(Item::Last(bytes)) => { if self.buffer.is_empty() { warn!( "Socket received last continuation message before it was started" ); ctx.close(Some(CloseCode::Error.into())); ctx.stop(); return; } self.buffer.extend_from_slice(&bytes); let bytes = self.buffer.split(); self.handle_msg_sync(ctx, bytes.into()); } // Heartbeat ws::Message::Ping(message) => ctx.pong(&message), ws::Message::Pong(_) => (), // Other ws::Message::Text(_) | ws::Message::Continuation(Item::FirstText(_)) => { warn!("Socket received unsupported message"); ctx.close(Some(CloseCode::Unsupported.into())); ctx.stop(); } ws::Message::Close(reason) => { info!("Socket was closed"); ctx.close(reason); ctx.stop(); } ws::Message::Nop => (), } } } diff --git a/services/commtest/tests/backup_integration_test.rs b/services/commtest/tests/backup_integration_test.rs index d57f4add9..4cd2d7777 100644 --- a/services/commtest/tests/backup_integration_test.rs +++ b/services/commtest/tests/backup_integration_test.rs @@ -1,214 +1,211 @@ use backup_client::{ BackupClient, BackupData, BackupDescriptor, DownloadedLog, Error as BackupClientError, LogUploadConfirmation, RequestedData, SinkExt, StreamExt, TryStreamExt, }; use bytesize::ByteSize; use comm_lib::{ auth::UserIdentity, backup::{LatestBackupIDResponse, UploadLogRequest}, }; use commtest::{ service_addr, tools::{generate_stable_nbytes, Error}, }; use reqwest::StatusCode; use std::collections::HashSet; use uuid::Uuid; #[tokio::test] async fn backup_integration_test() -> Result<(), Error> { let backup_client = BackupClient::new(service_addr::BACKUP_SERVICE_HTTP)?; let user_identity = UserIdentity { user_id: "1".to_string(), access_token: "dummy access token".to_string(), device_id: "dummy device_id".to_string(), }; let backup_datas = generate_backup_data(); // Upload backups for (backup_data, log_datas) in &backup_datas { backup_client .upload_backup(&user_identity, backup_data.clone()) .await?; - let (tx, rx) = backup_client.upload_logs(&user_identity).await?; - - tokio::pin!(tx); - tokio::pin!(rx); + let (mut tx, rx) = backup_client.upload_logs(&user_identity).await?; for log_data in log_datas { tx.send(log_data.clone()).await?; } let result: HashSet = rx.take(log_datas.len()).try_collect().await?; let expected = log_datas .iter() .map(|data| LogUploadConfirmation { backup_id: data.backup_id.clone(), log_id: data.log_id, }) .collect(); assert_eq!(result, expected); } // Test direct lookup let (backup_data, log_datas) = &backup_datas[1]; let second_backup_descriptor = BackupDescriptor::BackupID { backup_id: backup_data.backup_id.clone(), user_identity: user_identity.clone(), }; let user_keys = backup_client .download_backup_data(&second_backup_descriptor, RequestedData::UserKeys) .await?; assert_eq!(user_keys, backup_data.user_keys); let user_data = backup_client .download_backup_data(&second_backup_descriptor, RequestedData::UserData) .await?; assert_eq!(user_data, backup_data.user_data); // Test latest backup lookup let latest_backup_descriptor = BackupDescriptor::Latest { // Initial version of the backup service uses `user_id` in place of a username username: "1".to_string(), }; let backup_id_response = backup_client .download_backup_data(&latest_backup_descriptor, RequestedData::BackupID) .await?; let response: LatestBackupIDResponse = serde_json::from_slice(&backup_id_response)?; assert_eq!(response.backup_id, backup_data.backup_id); let user_keys = backup_client .download_backup_data(&latest_backup_descriptor, RequestedData::UserKeys) .await?; assert_eq!(user_keys, backup_data.user_keys); // Test log download let log_stream = backup_client .download_logs(&user_identity, &backup_data.backup_id) .await; let downloaded_logs: Vec = log_stream.try_collect().await?; let expected_logs: Vec = log_datas .iter() .map(|data| DownloadedLog { content: data.content.clone(), attachments: data.attachments.clone(), }) .collect(); assert_eq!(downloaded_logs, expected_logs); // Test backup cleanup let (removed_backup, _) = &backup_datas[0]; let removed_backup_descriptor = BackupDescriptor::BackupID { backup_id: removed_backup.backup_id.clone(), user_identity: user_identity.clone(), }; let response = backup_client .download_backup_data(&removed_backup_descriptor, RequestedData::UserKeys) .await; let Err(BackupClientError::ReqwestError(error)) = response else { panic!("First backup should have been removed, instead got response: {response:?}"); }; assert_eq!( error.status(), Some(StatusCode::NOT_FOUND), "Expected status 'not found'" ); // Test log cleanup let log_stream = backup_client .download_logs(&user_identity, &removed_backup.backup_id) .await; let downloaded_logs: Vec = log_stream.try_collect().await?; if !downloaded_logs.is_empty() { panic!( "Logs for first backup should have been removed, \ instead got: {downloaded_logs:?}" ) } Ok(()) } fn generate_backup_data() -> [(BackupData, Vec); 2] { [ ( BackupData { backup_id: "b1".to_string(), user_keys: generate_stable_nbytes( ByteSize::kib(4).as_u64() as usize, Some(b'a'), ), user_data: generate_stable_nbytes( ByteSize::mib(4).as_u64() as usize, Some(b'A'), ), attachments: vec![], siwe_backup_msg: None, }, generate_log_data("b1", b'a'), ), ( BackupData { backup_id: "b2".to_string(), user_keys: generate_stable_nbytes( ByteSize::kib(4).as_u64() as usize, Some(b'b'), ), user_data: generate_stable_nbytes( ByteSize::mib(4).as_u64() as usize, Some(b'B'), ), attachments: vec![], siwe_backup_msg: None, }, generate_log_data("b2", b'b'), ), ] } fn generate_log_data(backup_id: &str, value: u8) -> Vec { const IN_DB_SIZE: usize = ByteSize::kib(4).as_u64() as usize; const IN_BLOB_SIZE: usize = ByteSize::kib(400).as_u64() as usize; (1..30) .map(|log_id| { let size = if log_id % 2 == 0 { IN_DB_SIZE } else { IN_BLOB_SIZE }; let attachments = if log_id % 10 == 0 { Some(vec![Uuid::new_v4().to_string()]) } else { None }; let mut content = generate_stable_nbytes(size, Some(value)); let unique_suffix = log_id.to_string(); content.extend(unique_suffix.as_bytes()); UploadLogRequest { backup_id: backup_id.to_string(), log_id, content, attachments, } }) .collect() } diff --git a/shared/backup_client/src/lib.rs b/shared/backup_client/src/lib.rs index f36df93f8..038cfa720 100644 --- a/shared/backup_client/src/lib.rs +++ b/shared/backup_client/src/lib.rs @@ -1,376 +1,385 @@ #[cfg(target_arch = "wasm32")] mod web; use async_stream::{stream, try_stream}; pub use comm_lib::auth::UserIdentity; pub use comm_lib::backup::{ DownloadLogsRequest, LatestBackupIDResponse, LogWSRequest, LogWSResponse, UploadLogRequest, }; pub use futures_util::{Sink, SinkExt, Stream, StreamExt, TryStreamExt}; use hex::ToHex; use reqwest::{ header::InvalidHeaderValue, multipart::{Form, Part}, Body, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::time::Duration; use tokio_tungstenite_wasm::{ connect, Error as TungsteniteError, Message::Binary, }; const LOG_DOWNLOAD_RETRY_DELAY: Duration = Duration::from_secs(5); const LOG_DOWNLOAD_MAX_RETRY: usize = 3; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::wasm_bindgen; #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] #[derive(Debug, Clone)] pub struct BackupClient { url: reqwest::Url, } impl BackupClient { pub fn new>(url: T) -> Result { Ok(BackupClient { url: url.try_into()?, }) } } /// Backup functions impl BackupClient { pub async fn upload_backup( &self, user_identity: &UserIdentity, backup_data: BackupData, ) -> Result<(), Error> { let BackupData { backup_id, user_keys, user_data, attachments, siwe_backup_msg, } = backup_data; let client = reqwest::Client::new(); let mut form = Form::new() .text("backup_id", backup_id) .text( "user_keys_hash", Sha256::digest(&user_keys).encode_hex::(), ) .part("user_keys", Part::stream(Body::from(user_keys))) .text( "user_data_hash", Sha256::digest(&user_data).encode_hex::(), ) .part("user_data", Part::stream(Body::from(user_data))) .text("attachments", attachments.join("\n")); if let Some(siwe_backup_msg_value) = siwe_backup_msg { form = form.text("siwe_backup_msg", siwe_backup_msg_value); } let response = client .post(self.url.join("backups")?) .bearer_auth(user_identity.as_authorization_token()?) .multipart(form) .send() .await?; response.error_for_status()?; Ok(()) } pub async fn download_backup_data( &self, backup_descriptor: &BackupDescriptor, requested_data: RequestedData, ) -> Result, Error> { let client = reqwest::Client::new(); let url = self.url.join("backups/")?; let url = match backup_descriptor { BackupDescriptor::BackupID { backup_id, .. } => { url.join(&format!("{backup_id}/"))? } BackupDescriptor::Latest { username } => { url.join(&format!("latest/{username}/"))? } }; let url = match &requested_data { RequestedData::BackupID => url.join("backup_id")?, RequestedData::UserKeys => url.join("user_keys")?, RequestedData::UserData => url.join("user_data")?, }; let mut request = client.get(url); if let BackupDescriptor::BackupID { user_identity, .. } = backup_descriptor { request = request.bearer_auth(user_identity.as_authorization_token()?) } let response = request.send().await?; let result = response.error_for_status()?.bytes().await?.to_vec(); Ok(result) } } /// Log functions impl BackupClient { pub async fn upload_logs( &self, user_identity: &UserIdentity, ) -> Result< ( impl Sink, impl Stream>, ), Error, > { let (tx, rx) = self.create_log_ws_connection(user_identity).await?; let rx = rx.map(|response| match response? { LogWSResponse::LogUploaded { backup_id, log_id } => { Ok(LogUploadConfirmation { backup_id, log_id }) } LogWSResponse::ServerError => Err(Error::ServerError), msg => Err(Error::InvalidBackupMessage(msg)), }); Ok((tx, rx)) } /// Handles complete log download. /// It will try and retry download a few times, but if the issues persist /// the next item returned will be the last received error and the stream /// will be closed. pub async fn download_logs<'this>( &'this self, user_identity: &'this UserIdentity, backup_id: &'this str, ) -> impl Stream> + 'this { stream! { let mut last_downloaded_log = None; let mut fail_count = 0; 'retry: loop { let stream = self.log_download_stream(user_identity, backup_id, &mut last_downloaded_log).await; let mut stream = Box::pin(stream); while let Some(item) = stream.next().await { match item { Ok(log) => yield Ok(log), Err(err) => { println!("Error when downloading logs: {err:?}"); fail_count += 1; if fail_count >= LOG_DOWNLOAD_MAX_RETRY { yield Err(err); break 'retry; } #[cfg(target_arch = "wasm32")] let _ = web::sleep(LOG_DOWNLOAD_RETRY_DELAY).await; #[cfg(not(target_arch = "wasm32"))] tokio::time::sleep(LOG_DOWNLOAD_RETRY_DELAY).await; continue 'retry; } } } // Everything downloaded return; } println!("Log download failed!"); } } /// Handles singular connection websocket connection. Returns error in case /// anything goes wrong e.g. missing log or connection error. async fn log_download_stream<'stream>( &'stream self, user_identity: &'stream UserIdentity, backup_id: &'stream str, last_downloaded_log: &'stream mut Option, ) -> impl Stream> + 'stream { try_stream! { - let (tx, rx) = self.create_log_ws_connection(user_identity).await?; - - let mut tx = Box::pin(tx); - let mut rx = Box::pin(rx); + let (mut tx, mut rx) = self.create_log_ws_connection(user_identity).await?; tx.send(DownloadLogsRequest { backup_id: backup_id.to_string(), from_id: *last_downloaded_log, }) .await?; while let Some(response) = rx.try_next().await? { let expected_log_id = last_downloaded_log.unwrap_or(0); match response { LogWSResponse::LogDownload { content, attachments, log_id, } if log_id == expected_log_id + 1 => { *last_downloaded_log = Some(log_id); yield DownloadedLog { content, attachments, }; } LogWSResponse::LogDownload { .. } => { Err(Error::LogMissing)?; } LogWSResponse::LogDownloadFinished { last_log_id: Some(log_id), } if log_id == expected_log_id => { tx.send(DownloadLogsRequest { backup_id: backup_id.to_string(), from_id: *last_downloaded_log, }) .await? } LogWSResponse::LogDownloadFinished { last_log_id: None } => return, LogWSResponse::LogDownloadFinished { .. } => { Err(Error::LogMissing)?; } msg => Err(Error::InvalidBackupMessage(msg))?, } } Err(Error::WSClosed)?; } } async fn create_log_ws_connection>( &self, user_identity: &UserIdentity, ) -> Result< ( impl Sink, impl Stream>, ), Error, > { let url = self.create_ws_url()?; let stream = connect(url).await?; let (mut tx, rx) = stream.split(); tx.send(Binary(bincode::serialize(&LogWSRequest::Authenticate( user_identity.clone(), ))?)) .await?; let tx = tx.with(|request: Request| async { let request: LogWSRequest = request.into(); let request = bincode::serialize(&request)?; Ok(Binary(request)) }); let rx = rx.filter_map(|msg| async { let bytes = match msg { Ok(Binary(bytes)) => bytes, Ok(_) => return Some(Err(Error::InvalidWSMessage)), Err(err) => return Some(Err(err.into())), }; match bincode::deserialize(&bytes) { Ok(response) => Some(Ok(response)), Err(err) => Some(Err(err.into())), } }); + let tx = Box::pin(tx); + let mut rx = Box::pin(rx); + + if let Some(response) = rx.try_next().await? { + match response { + LogWSResponse::AuthSuccess => {} + LogWSResponse::Unauthenticated => Err(Error::Unauthenticated)?, + msg => Err(Error::InvalidBackupMessage(msg))?, + } + } + Ok((tx, rx)) } fn create_ws_url(&self) -> Result { let mut url = self.url.clone(); match url.scheme() { "http" => url.set_scheme("ws").map_err(|_| Error::UrlSchemaError)?, "https" => url.set_scheme("wss").map_err(|_| Error::UrlSchemaError)?, _ => (), }; let url = url.join("logs")?; Ok(url) } } #[derive(Debug, Clone)] pub struct BackupData { pub backup_id: String, pub user_keys: Vec, pub user_data: Vec, pub attachments: Vec, pub siwe_backup_msg: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum BackupDescriptor { BackupID { #[serde(rename = "backupID")] backup_id: String, #[serde(rename = "userIdentity")] user_identity: UserIdentity, }, Latest { username: String, }, } #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] #[derive(Debug, Clone)] pub enum RequestedData { BackupID, UserKeys, UserData, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct LogUploadConfirmation { pub backup_id: String, pub log_id: usize, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct DownloadedLog { pub content: Vec, pub attachments: Option>, } #[derive(Debug, derive_more::Display, derive_more::From)] pub enum Error { InvalidAuthorizationHeader, UrlSchemaError, UrlError(url::ParseError), ReqwestError(reqwest::Error), TungsteniteError(TungsteniteError), JsonError(serde_json::Error), BincodeError(bincode::Error), InvalidWSMessage, #[display(fmt = "Error::InvalidBackupMessage({:?})", _0)] InvalidBackupMessage(LogWSResponse), ServerError, LogMissing, WSClosed, + Unauthenticated, } impl std::error::Error for Error {} impl From for Error { fn from(_: InvalidHeaderValue) -> Self { Self::InvalidAuthorizationHeader } } diff --git a/shared/comm-lib/src/backup/mod.rs b/shared/comm-lib/src/backup/mod.rs index 2f207af98..27e3b5a4b 100644 --- a/shared/comm-lib/src/backup/mod.rs +++ b/shared/comm-lib/src/backup/mod.rs @@ -1,50 +1,51 @@ use crate::auth::UserIdentity; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LatestBackupIDResponse { #[serde(rename = "backupID")] pub backup_id: String, pub siwe_backup_msg: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UploadLogRequest { pub backup_id: String, pub log_id: usize, pub content: Vec, pub attachments: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DownloadLogsRequest { pub backup_id: String, pub from_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, derive_more::From)] pub enum LogWSRequest { Authenticate(UserIdentity), UploadLog(UploadLogRequest), DownloadLogs(DownloadLogsRequest), } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum LogWSResponse { LogUploaded { backup_id: String, log_id: usize, }, LogDownload { log_id: usize, content: Vec, attachments: Option>, }, LogDownloadFinished { last_log_id: Option, }, ServerError, + AuthSuccess, Unauthenticated, }