diff --git a/Cargo.lock b/Cargo.lock index 7f9753127..ad187a33b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1220,6 +1220,7 @@ dependencies = [ "tauri-plugin-window-state", "tauri-specta", "tempfile", + "thiserror 1.0.69", "tokio", "tokio-stream", "tokio-util", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 85046ece2..d17a17587 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -105,6 +105,7 @@ wgpu.workspace = true bytemuck = "1.23.1" kameo = "0.17.2" tauri-plugin-sentry = "0.5.0" +thiserror.workspace = true [target.'cfg(target_os = "macos")'.dependencies] core-graphics = "0.24.0" diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 701242654..cab3b7644 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -82,6 +82,7 @@ use tauri_specta::Event; use tokio::sync::{RwLock, oneshot}; use tracing::{error, trace}; use upload::{S3UploadMeta, create_or_get_video, upload_image, upload_video}; +use web_api::AuthedApiError; use web_api::ManagerExt as WebManagerExt; use windows::{CapWindowId, EditorWindowIds, ShowCapWindow, set_window_transparent}; @@ -1077,7 +1078,7 @@ async fn upload_exported_video( channel.send(UploadProgress { progress: 0.0 }).ok(); - let s3_config = async { + let s3_config = match async { let video_id = match mode { UploadMode::Initial { pre_created_video } => { if let Some(pre_created) = pre_created_video { @@ -1087,7 +1088,7 @@ async fn upload_exported_video( } UploadMode::Reupload => { let Some(sharing) = meta.sharing.clone() else { - return Err("No sharing metadata found".to_string()); + return Err("No sharing metadata found".into()); }; Some(sharing.id) @@ -1103,7 +1104,13 @@ async fn upload_exported_video( ) .await } - .await?; + .await + { + Ok(data) => data, + Err(AuthedApiError::InvalidAuthentication) => return Ok(UploadResult::NotAuthenticated), + Err(AuthedApiError::UpgradeRequired) => return Ok(UploadResult::UpgradeRequired), + Err(err) => return Err(err.to_string()), + }; let upload_id = s3_config.id().to_string(); @@ -1136,11 +1143,12 @@ async fn upload_exported_video( NotificationType::ShareableLinkCopied.send(&app); Ok(UploadResult::Success(uploaded_video.link)) } + Err(AuthedApiError::UpgradeRequired) => Ok(UploadResult::UpgradeRequired), Err(e) => { error!("Failed to upload video: {e}"); NotificationType::UploadFailed.send(&app); - Err(e) + Err(e.to_string().into()) } } } @@ -1547,16 +1555,10 @@ async fn check_upgraded_and_update(app: AppHandle) -> Result { .await .map_err(|e| { println!("Failed to fetch plan: {e}"); - format!("Failed to fetch plan: {e}") + e.to_string() })?; println!("Plan fetch response status: {}", response.status()); - if response.status() == reqwest::StatusCode::UNAUTHORIZED { - println!("Unauthorized response, clearing auth store"); - AuthStore::set(&app, None).map_err(|e| e.to_string())?; - return Ok(false); - } - let plan_data = response.json::().await.map_err(|e| { println!("Failed to parse plan response: {e}"); format!("Failed to parse plan response: {e}") diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index ba5209074..083a73ffc 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -265,7 +265,7 @@ fn normalize_thumbnail_dimensions(image: &image::RgbaImage) -> image::RgbaImage async fn capture_thumbnail_from_filter(filter: &cidre::sc::ContentFilter) -> Option { use cidre::{cv, sc}; use image::{ImageEncoder, RgbaImage, codecs::png::PngEncoder}; - use std::{io::Cursor, slice}; + use std::io::Cursor; let mut config = sc::StreamCfg::new(); config.set_width(THUMBNAIL_WIDTH as usize); diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index f6e923697..bc35604a8 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -1,6 +1,6 @@ // credit @filleduchaos -use crate::web_api::ManagerExt; +use crate::web_api::{AuthedApiError, ManagerExt}; use crate::{UploadProgress, VideoUploadInfo}; use cap_utils::spawn_actor; use ffmpeg::ffi::AV_TIME_BASE; @@ -216,7 +216,7 @@ pub async fn upload_video( screenshot_path: Option, meta: Option, channel: Option>, -) -> Result { +) -> Result { println!("Uploading video {video_id}..."); let client = reqwest::Client::new(); @@ -284,7 +284,7 @@ pub async fn upload_video( let (video_upload, screenshot_result): ( Result, - Option>, + Option>, ) = tokio::join!(video_upload.send(), async { if let Some(screenshot_req) = screenshot_upload { Some(screenshot_req.await) @@ -326,12 +326,13 @@ pub async fn upload_video( status, error_body ); - Err(format!( - "Failed to upload file. Status: {status}. Body: {error_body}" - )) + Err(format!("Failed to upload file. Status: {status}. Body: {error_body}").into()) } -pub async fn upload_image(app: &AppHandle, file_path: PathBuf) -> Result { +pub async fn upload_image( + app: &AppHandle, + file_path: PathBuf, +) -> Result { let file_name = file_path .file_name() .and_then(|name| name.to_str()) @@ -382,9 +383,7 @@ pub async fn upload_image(app: &AppHandle, file_path: PathBuf) -> Result, name: Option, meta: Option, -) -> Result { +) -> Result { + return Err(AuthedApiError::InvalidAuthentication); // TODO + let mut s3_config_url = if let Some(id) = video_id { format!("/api/desktop/video/create?recordingMode=desktopMP4&videoId={id}") } else if is_screenshot { @@ -417,23 +418,13 @@ pub async fn create_or_get_video( let response = app .authed_api_request(s3_config_url, |client, url| client.get(url)) - .await - .map_err(|e| format!("Failed to send request to Next.js handler: {e}"))?; - - if response.status() == StatusCode::UNAUTHORIZED { - return Err("Failed to authenticate request; please log in again".into()); - } + .await?; if response.status() != StatusCode::OK { if let Ok(error) = response.json::().await { if error.error == "upgrade_required" { - return Err( - "You must upgrade to Cap Pro to upload recordings over 5 minutes in length" - .into(), - ); + return Err(AuthedApiError::UpgradeRequired); } - - return Err(format!("server error: {}", error.error)); } return Err("Unknown error uploading video".into()); @@ -469,7 +460,10 @@ pub enum PresignedS3PutRequestMethod { Put, } -async fn presigned_s3_put(app: &AppHandle, body: PresignedS3PutRequest) -> Result { +async fn presigned_s3_put( + app: &AppHandle, + body: PresignedS3PutRequest, +) -> Result { #[derive(Deserialize, Debug)] struct Data { url: String, @@ -485,17 +479,9 @@ async fn presigned_s3_put(app: &AppHandle, body: PresignedS3PutRequest) -> Resul .authed_api_request("/api/upload/signed", |client, url| { client.post(url).json(&body) }) - .await - .map_err(|e| format!("Failed to send request to Next.js handler: {e}"))?; + .await?; - if response.status() == StatusCode::UNAUTHORIZED { - return Err("Failed to authenticate request; please log in again".into()); - } - - let Wrapper { presigned_put_data } = response - .json::() - .await - .map_err(|e| format!("Failed to deserialize server response: {e}"))?; + let Wrapper { presigned_put_data } = response.json::().await?; Ok(presigned_put_data.url) } @@ -556,7 +542,7 @@ pub async fn prepare_screenshot_upload( app: &AppHandle, s3_config: &S3UploadMeta, screenshot_path: PathBuf, -) -> Result { +) -> Result { let presigned_put = presigned_s3_put( app, PresignedS3PutRequest { @@ -576,7 +562,7 @@ pub async fn prepare_screenshot_upload( .body(compressed_image) .send() .await - .map_err(|e| format!("Error uploading screenshot: {e}")) + .map_err(Into::into) } async fn compress_image(path: PathBuf) -> Result, String> { diff --git a/apps/desktop/src-tauri/src/web_api.rs b/apps/desktop/src-tauri/src/web_api.rs index 2970d903f..76238b7b1 100644 --- a/apps/desktop/src-tauri/src/web_api.rs +++ b/apps/desktop/src-tauri/src/web_api.rs @@ -1,13 +1,42 @@ use reqwest::StatusCode; use tauri::{Emitter, Manager, Runtime}; use tauri_specta::Event; -use tracing::error; +use thiserror::Error; +use tracing::{error, warn}; use crate::{ ArcLock, auth::{AuthSecret, AuthStore, AuthenticationInvalid}, }; +#[derive(Error, Debug)] +pub enum AuthedApiError { + #[error("User is not authenticated or credentials have expired!")] + InvalidAuthentication, + #[error("User needs to upgrade their account to use this feature!")] + UpgradeRequired, + #[error("AuthedApiError/AuthStore: {0}")] + AuthStore(String), + #[error("AuthedApiError/Request: {0}")] + Request(#[from] reqwest::Error), + #[error("AuthedApiError/Deserialization: {0}")] + Deserialization(#[from] serde_json::Error), + #[error("AuthedApiError/Other: {0}")] + Other(String), +} + +impl From<&'static str> for AuthedApiError { + fn from(value: &'static str) -> Self { + AuthedApiError::Other(value.into()) + } +} + +impl From for AuthedApiError { + fn from(value: String) -> Self { + AuthedApiError::Other(value) + } +} + async fn do_authed_request( auth: &AuthStore, build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder, @@ -28,7 +57,7 @@ async fn do_authed_request( ) .header("X-Desktop-Version", env!("CARGO_PKG_VERSION")); - if let Some(s) = std::option_env!("VITE_VERCEL_AUTOMATION_BYPASS_SECRET") { + if let Ok(s) = std::env::var("VITE_VERCEL_AUTOMATION_BYPASS_SECRET") { req = req.header("x-vercel-protection-bypass", s); } @@ -40,7 +69,7 @@ pub trait ManagerExt: Manager { &self, path: impl Into, build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder, - ) -> Result; + ) -> Result; async fn make_app_url(&self, pathname: impl AsRef) -> String; } @@ -50,26 +79,21 @@ impl + Emitter, R: Runtime> ManagerExt for T { &self, path: impl Into, build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder, - ) -> Result { - let Some(auth) = AuthStore::get(self.app_handle())? else { - println!("Not logged in"); - + ) -> Result { + let Some(auth) = AuthStore::get(self.app_handle()).map_err(AuthedApiError::AuthStore)? + else { + warn!("Not logged in"); AuthenticationInvalid.emit(self).ok(); - - return Err("Unauthorized".to_string()); + return Err(AuthedApiError::InvalidAuthentication); }; let url = self.make_app_url(path.into()).await; - let response = do_authed_request(&auth, build, url) - .await - .map_err(|e| e.to_string())?; + let response = do_authed_request(&auth, build, url).await?; if response.status() == StatusCode::UNAUTHORIZED { error!("Authentication expired. Please log in again."); - AuthenticationInvalid.emit(self).ok(); - - return Err("Unauthorized".to_string()); + return Err(AuthedApiError::InvalidAuthentication); } Ok(response)