diff --git a/desktop/app.vue b/desktop/app.vue
index 4f900fba..4218fe2d 100644
--- a/desktop/app.vue
+++ b/desktop/app.vue
@@ -10,7 +10,7 @@ import { invoke } from "@tauri-apps/api/core";
import { AppStatus, type AppState } from "./types.d.ts";
import { listen } from "@tauri-apps/api/event";
import { useAppState } from "./composables/app-state.js";
-import {useRouter} from "#vue-router";
+import { useRouter } from "#vue-router";
const router = useRouter();
@@ -23,7 +23,9 @@ router.beforeEach(async () => {
switch (state.value.status) {
case AppStatus.NotConfigured:
- router.push({ path: "/setup" }).then(() => {console.log("Pushed Setup")});
+ router.push({ path: "/setup" }).then(() => {
+ console.log("Pushed Setup");
+ });
break;
case AppStatus.SignedOut:
router.push("/auth");
@@ -31,6 +33,9 @@ switch (state.value.status) {
case AppStatus.SignedInNeedsReauth:
router.push("/auth/signedout");
break;
+ case AppStatus.ServerUnavailable:
+ router.push("/error/serverunavailable");
+ break;
default:
router.push("/store");
}
@@ -48,6 +53,6 @@ listen("auth/finished", () => {
});
useHead({
- title: "Drop"
-})
+ title: "Drop",
+});
diff --git a/desktop/components/InitiateAuthModule.vue b/desktop/components/InitiateAuthModule.vue
index ba263d10..5f7158cb 100644
--- a/desktop/components/InitiateAuthModule.vue
+++ b/desktop/components/InitiateAuthModule.vue
@@ -40,23 +40,20 @@
Sign in with your browser →
+
-
-
-
-
-
-
-
- {{ error }}
-
-
+
+
+
+
+
+
+
+ {{ error }}
+
-
+
diff --git a/desktop/pages/error.vue b/desktop/pages/error.vue
new file mode 100644
index 00000000..d50e70c2
--- /dev/null
+++ b/desktop/pages/error.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+ Unrecoverable error
+
+
+ Drop encountered an error that it couldn't handle. Please restart the
+ application and file a bug report.
+
+
+
+
+
+

+
+
+
+
+
diff --git a/desktop/pages/error/serverunavailable.vue b/desktop/pages/error/serverunavailable.vue
new file mode 100644
index 00000000..d0a25159
--- /dev/null
+++ b/desktop/pages/error/serverunavailable.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+ Server is unavailable
+
+
+ We were unable to contact your Drop instance. See if you can open it
+ in your web browser, or contact your server admin for help.
+
+
+
+ Connect to different instance →
+
+
+
+
+
+
+

+
+
+
+
+
diff --git a/desktop/plugins/global-error-handler.ts b/desktop/plugins/global-error-handler.ts
new file mode 100644
index 00000000..1ad9b59e
--- /dev/null
+++ b/desktop/plugins/global-error-handler.ts
@@ -0,0 +1,11 @@
+export default defineNuxtPlugin((nuxtApp) => {
+ nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
+ // handle error, e.g. report to a service
+ };
+
+ // Also possible
+ nuxtApp.hook("vue:error", (error, instance, info) => {
+ const router = useRouter();
+ router.replace("/error");
+ });
+});
diff --git a/desktop/src-tauri/src/auth.rs b/desktop/src-tauri/src/auth.rs
index c27e2e45..7d6f4aa8 100644
--- a/desktop/src-tauri/src/auth.rs
+++ b/desktop/src-tauri/src/auth.rs
@@ -1,55 +1,44 @@
use std::{
env,
- sync::Mutex, time::{SystemTime, UNIX_EPOCH},
+ fmt::{Display, Formatter},
+ sync::Mutex,
+ time::{SystemTime, UNIX_EPOCH},
};
use log::{info, warn};
-use openssl::{
- ec::EcKey,
- hash::MessageDigest,
- pkey::PKey,
- sign::{Signer},
-};
+use openssl::{ec::EcKey, hash::MessageDigest, pkey::PKey, sign::Signer};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, Manager};
-use url::Url;
+use url::{ParseError, Url};
-use crate::{db::{DatabaseAuth, DatabaseImpls}, AppState, AppStatus, User, DB};
+use crate::{
+ db::{DatabaseAuth, DatabaseImpls},
+ remote::RemoteAccessError,
+ AppState, AppStatus, User, DB,
+};
#[derive(Serialize)]
-#[serde(rename_all="camelCase")]
+#[serde(rename_all = "camelCase")]
struct InitiateRequestBody {
name: String,
platform: String,
}
#[derive(Serialize)]
-#[serde(rename_all="camelCase")]
+#[serde(rename_all = "camelCase")]
struct HandshakeRequestBody {
client_id: String,
token: String,
}
#[derive(Deserialize)]
-#[serde(rename_all="camelCase")]
+#[serde(rename_all = "camelCase")]
struct HandshakeResponse {
private: String,
certificate: String,
id: String,
}
-macro_rules! unwrap_or_return {
- ( $e:expr, $app:expr ) => {
- match $e {
- Ok(x) => x,
- Err(_) => {
- $app.emit("auth/failed", ()).unwrap();
- return;
- }
- }
- };
-}
-
pub fn sign_nonce(private_key: String, nonce: String) -> Result {
let client_private_key = EcKey::private_key_from_pem(private_key.as_bytes()).unwrap();
let pkey_private_key = PKey::from_ec_key(client_private_key).unwrap();
@@ -80,42 +69,39 @@ pub fn generate_authorization_header() -> String {
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
}
-pub fn fetch_user() -> Result {
+pub fn fetch_user() -> Result {
let base_url = DB.fetch_base_url();
- let endpoint = base_url.join("/api/v1/client/user").unwrap();
+ let endpoint = base_url.join("/api/v1/client/user")?;
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let response = client
.get(endpoint.to_string())
.header("Authorization", header)
- .send()
- .unwrap();
+ .send()?;
if response.status() != 200 {
- warn!("Failed to fetch user: {}", response.status());
- return Err(());
+ return Err(response.status().as_u16().into());
}
- let user = response.json::().unwrap();
+ let user = response.json::()?;
Ok(user)
}
-pub fn recieve_handshake(app: AppHandle, path: String) {
- // Tell the app we're processing
- app.emit("auth/processing", ()).unwrap();
-
+fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
let path_chunks: Vec<&str> = path.split("/").collect();
if path_chunks.len() != 3 {
app.emit("auth/failed", ()).unwrap();
- return;
+ return Err(RemoteAccessError::GenericErrror(
+ "Invalid number of handshake chunks".to_string().into(),
+ ));
}
let base_url = {
let handle = DB.borrow_data().unwrap();
- Url::parse(handle.base_url.as_str()).unwrap()
+ Url::parse(handle.base_url.as_str())?
};
let client_id = path_chunks.get(1).unwrap();
@@ -125,11 +111,11 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
token: token.to_string(),
};
- let endpoint = unwrap_or_return!(base_url.join("/api/v1/client/auth/handshake"), app);
+ let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
let client = reqwest::blocking::Client::new();
- let response = unwrap_or_return!(client.post(endpoint).json(&body).send(), app);
+ let response = client.post(endpoint).json(&body).send()?;
info!("server responded with {}", response.status());
- let response_struct = unwrap_or_return!(response.json::(), app);
+ let response_struct = response.json::()?;
{
let mut handle = DB.borrow_data_mut().unwrap();
@@ -146,43 +132,62 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
let app_state = app.state::>();
let mut app_state_handle = app_state.lock().unwrap();
app_state_handle.status = AppStatus::SignedIn;
- app_state_handle.user = Some(fetch_user().unwrap());
+ app_state_handle.user = Some(fetch_user()?);
+ }
+
+ return Ok(());
+}
+
+pub fn recieve_handshake(app: AppHandle, path: String) {
+ // Tell the app we're processing
+ app.emit("auth/processing", ()).unwrap();
+
+ let handshake_result = recieve_handshake_logic(&app, path);
+ if handshake_result.is_err() {
+ app.emit("auth/failed", ()).unwrap();
+ return;
}
app.emit("auth/finished", ()).unwrap();
}
-#[tauri::command]
-pub async fn auth_initiate<'a>() -> Result<(), String> {
+async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
let base_url = {
let db_lock = DB.borrow_data().unwrap();
- Url::parse(&db_lock.base_url.clone()).unwrap()
+ Url::parse(&db_lock.base_url.clone())?
};
- let endpoint = base_url.join("/api/v1/client/auth/initiate").unwrap();
+ let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
let body = InitiateRequestBody {
name: "Drop Desktop Client".to_string(),
platform: env::consts::OS.to_string(),
};
let client = reqwest::Client::new();
- let response = client
- .post(endpoint.to_string())
- .json(&body)
- .send()
- .await
- .unwrap();
+ let response = client.post(endpoint.to_string()).json(&body).send().await?;
if response.status() != 200 {
- return Err("Failed to create redirect URL. Please try again later.".to_string());
+ return Err("Failed to create redirect URL. Please try again later."
+ .to_string()
+ .into());
}
- let redir_url = response.text().await.unwrap();
- let complete_redir_url = base_url.join(&redir_url).unwrap();
+ let redir_url = response.text().await?;
+ let complete_redir_url = base_url.join(&redir_url)?;
info!("opening web browser to continue authentication");
webbrowser::open(complete_redir_url.as_ref()).unwrap();
+ return Ok(());
+}
+
+#[tauri::command]
+pub async fn auth_initiate<'a>() -> Result<(), String> {
+ let result = auth_initiate_wrapper().await;
+ if result.is_err() {
+ return Err(result.err().unwrap().to_string());
+ }
+
Ok(())
}
@@ -193,7 +198,14 @@ pub fn setup() -> Result<(AppStatus, Option), ()> {
if data.auth.is_some() {
let user_result = fetch_user();
if user_result.is_err() {
- return Ok((AppStatus::SignedInNeedsReauth, None));
+ let error = user_result.err().unwrap();
+ warn!("auth setup failed with: {}", error);
+ match error {
+ RemoteAccessError::FetchError(_) => {
+ return Ok((AppStatus::ServerUnavailable, None))
+ }
+ _ => return Ok((AppStatus::SignedInNeedsReauth, None)),
+ }
}
return Ok((AppStatus::SignedIn, Some(user_result.unwrap())));
}
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index 44198c13..75fcdc3d 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -6,6 +6,7 @@ mod downloads;
#[cfg(test)]
mod tests;
+use crate::db::DatabaseImpls;
use auth::{auth_initiate, generate_authorization_header, recieve_handshake};
use db::{DatabaseInterface, DATA_ROOT_DIR};
use downloads::download_commands::{queue_game_download, start_game_downloads, stop_specific_game_download};
@@ -16,7 +17,8 @@ use log::info;
use remote::{gen_drop_url, use_remote};
use serde::{Deserialize, Serialize};
use std::{
- collections::HashMap, sync::{LazyLock, Mutex}
+ collections::HashMap,
+ sync::{LazyLock, Mutex},
};
use std::sync::Arc;
use tauri_plugin_deep_link::DeepLinkExt;
@@ -26,12 +28,14 @@ use crate::downloads::download_agent::{GameDownloadAgent};
#[derive(Clone, Copy, Serialize)]
pub enum AppStatus {
NotConfigured,
+ ServerError,
SignedOut,
SignedIn,
SignedInNeedsReauth,
+ ServerUnavailable,
}
#[derive(Clone, Serialize, Deserialize)]
-#[serde(rename_all="camelCase")]
+#[serde(rename_all = "camelCase")]
pub struct User {
id: String,
username: String,
@@ -41,7 +45,7 @@ pub struct User {
}
#[derive(Clone, Serialize)]
-#[serde(rename_all="camelCase")]
+#[serde(rename_all = "camelCase")]
pub struct AppState {
status: AppStatus,
user: Option,
@@ -72,10 +76,10 @@ fn setup() -> AppState {
};
}
- let auth_result = auth::setup().unwrap();
+ let (app_status, user) = auth::setup().unwrap();
AppState {
- status: auth_result.0,
- user: auth_result.1,
+ status: app_status,
+ user: user,
games: HashMap::new(),
game_downloads: HashMap::new(),
}
@@ -144,7 +148,9 @@ pub fn run() {
info!("handling drop:// url");
let binding = event.urls();
let url = binding.first().unwrap();
- if url.host_str().unwrap() == "handshake" { recieve_handshake(handle.clone(), url.path().to_string()) }
+ if url.host_str().unwrap() == "handshake" {
+ recieve_handshake(handle.clone(), url.path().to_string())
+ }
});
Ok(())
diff --git a/desktop/src-tauri/src/library.rs b/desktop/src-tauri/src/library.rs
index 1c81870d..80e41340 100644
--- a/desktop/src-tauri/src/library.rs
+++ b/desktop/src-tauri/src/library.rs
@@ -4,9 +4,10 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use tauri::{AppHandle, Manager};
-use crate::{auth::generate_authorization_header, AppState, DB};
-use crate::db::DatabaseImpls;
use crate::db::DatabaseGameStatus;
+use crate::db::DatabaseImpls;
+use crate::remote::RemoteAccessError;
+use crate::{auth::generate_authorization_header, AppState, DB};
#[derive(serde::Serialize)]
struct FetchGameStruct {
@@ -29,10 +30,9 @@ pub struct Game {
m_image_library: Vec,
}
-#[tauri::command]
-pub fn fetch_library(app: AppHandle) -> Result {
+fn fetch_library_logic(app: AppHandle) -> Result {
let base_url = DB.fetch_base_url();
- let library_url = base_url.join("/api/v1/client/user/library").unwrap();
+ let library_url = base_url.join("/api/v1/client/user/library")?;
let header = generate_authorization_header();
@@ -40,18 +40,13 @@ pub fn fetch_library(app: AppHandle) -> Result {
let response = client
.get(library_url.to_string())
.header("Authorization", header)
- .send()
- .unwrap();
+ .send()?;
if response.status() != 200 {
- return Err(format!(
- "Library fetch request failed with {}",
- response.status()
- ));
+ return Err(response.status().as_u16().into());
}
- // Keep as string
- let games = response.json::>().unwrap();
+ let games = response.json::>()?;
let state = app.state::>();
let mut handle = state.lock().unwrap();
@@ -74,20 +69,48 @@ pub fn fetch_library(app: AppHandle) -> Result {
}
#[tauri::command]
-pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result {
+pub fn fetch_library(app: AppHandle) -> Result {
+ let result = fetch_library_logic(app);
+
+ if result.is_err() {
+ return Err(result.err().unwrap().to_string());
+ }
+
+ return Ok(result.unwrap());
+}
+
+fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result {
let state = app.state::>();
let handle = state.lock().unwrap();
+
let game = handle.games.get(&id);
if let Some(game) = game {
let db_handle = DB.borrow_data().unwrap();
let data = FetchGameStruct {
game: game.clone(),
- status: db_handle.games.games_statuses.get(&game.id).unwrap().clone(),
+ status: db_handle
+ .games
+ .games_statuses
+ .get(&game.id)
+ .unwrap()
+ .clone(),
};
return Ok(json!(data).to_string());
}
+ // TODO request games that aren't found from remote server
- Err("".to_string())
+ Err("".to_string().into())
+}
+
+#[tauri::command]
+pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result {
+ let result = fetch_game_logic(id, app);
+
+ if result.is_err() {
+ return Err(result.err().unwrap().to_string());
+ }
+
+ Ok(result.unwrap())
}
diff --git a/desktop/src-tauri/src/remote.rs b/desktop/src-tauri/src/remote.rs
index fdf6a736..5691ac47 100644
--- a/desktop/src-tauri/src/remote.rs
+++ b/desktop/src-tauri/src/remote.rs
@@ -1,48 +1,80 @@
-use std::sync::Mutex;
+use std::{
+ fmt::{write, Display, Formatter},
+ sync::Mutex,
+};
use log::{info, warn};
use serde::Deserialize;
-use url::Url;
+use url::{ParseError, Url};
use crate::{AppState, AppStatus, DB};
-macro_rules! unwrap_or_return {
- ( $e:expr ) => {
- match $e {
- Ok(x) => x,
- Err(e) => {
- return Err(format!(
- "Invalid URL or Drop is inaccessible ({})",
- e.to_string()
- ))
- }
- }
- };
+#[derive(Debug)]
+pub enum RemoteAccessError {
+ FetchError(reqwest::Error),
+ ParsingError(ParseError),
+ InvalidCodeError(u16),
+ GenericErrror(String),
}
+impl Display for RemoteAccessError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ RemoteAccessError::FetchError(error) => write!(f, "{}", error),
+ RemoteAccessError::GenericErrror(error) => write!(f, "{}", error),
+ RemoteAccessError::ParsingError(parse_error) => {
+ write!(f, "{}", parse_error)
+ }
+ RemoteAccessError::InvalidCodeError(error) => write!(f, "HTTP {}", error),
+ }
+ }
+}
+
+impl From for RemoteAccessError {
+ fn from(err: reqwest::Error) -> Self {
+ RemoteAccessError::FetchError(err)
+ }
+}
+impl From for RemoteAccessError {
+ fn from(err: String) -> Self {
+ RemoteAccessError::GenericErrror(err)
+ }
+}
+impl From for RemoteAccessError {
+ fn from(err: ParseError) -> Self {
+ RemoteAccessError::ParsingError(err)
+ }
+}
+impl From for RemoteAccessError {
+ fn from(err: u16) -> Self {
+ RemoteAccessError::InvalidCodeError(err)
+ }
+}
+
+impl std::error::Error for RemoteAccessError {}
+
#[derive(Deserialize)]
-#[serde(rename_all="camelCase")]
+#[serde(rename_all = "camelCase")]
struct DropHealthcheck {
app_name: String,
}
-#[tauri::command]
-pub async fn use_remote<'a>(
+async fn use_remote_logic<'a>(
url: String,
state: tauri::State<'_, Mutex>,
-) -> Result<(), String> {
+) -> Result<(), RemoteAccessError> {
info!("connecting to url {}", url);
- let base_url = unwrap_or_return!(Url::parse(&url));
+ let base_url = Url::parse(&url)?;
// Test Drop url
- let test_endpoint = base_url.join("/api/v1").unwrap();
- let response = unwrap_or_return!(reqwest::get(test_endpoint.to_string()).await);
+ let test_endpoint = base_url.join("/api/v1")?;
+ let response = reqwest::get(test_endpoint.to_string()).await?;
- let result = response.json::().await.unwrap();
+ let result = response.json::().await?;
if result.app_name != "Drop" {
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
- return Err("Not a valid Drop endpoint".to_string());
+ return Err("Not a valid Drop endpoint".to_string().into());
}
let mut app_state = state.lock().unwrap();
@@ -58,6 +90,20 @@ pub async fn use_remote<'a>(
Ok(())
}
+#[tauri::command]
+pub async fn use_remote<'a>(
+ url: String,
+ state: tauri::State<'_, Mutex>,
+) -> Result<(), String> {
+ let result = use_remote_logic(url, state).await;
+
+ if result.is_err() {
+ return Err(result.err().unwrap().to_string());
+ }
+
+ Ok(())
+}
+
#[tauri::command]
pub fn gen_drop_url(path: String) -> Result {
let base_url = {
diff --git a/desktop/types.d.ts b/desktop/types.d.ts
index f6e5c1bd..b63d9043 100644
--- a/desktop/types.d.ts
+++ b/desktop/types.d.ts
@@ -22,6 +22,7 @@ export enum AppStatus {
SignedOut = "SignedOut",
SignedIn = "SignedIn",
SignedInNeedsReauth = "SignedInNeedsReauth",
+ ServerUnavailable = "ServerUnavailable",
}
export enum GameStatus {