From 45eefeb1f545c9acf7c677d4ecf92c76dbf29905 Mon Sep 17 00:00:00 2001 From: arrelin Date: Tue, 10 Mar 2026 14:11:35 +0300 Subject: [PATCH] mobile update --- backend/src/lib.rs | 7 +- backend/src/routes/oauth.rs | 116 ++++++++++-------- frontend/package-lock.json | 10 ++ frontend/package.json | 1 + frontend/src/pages/Login.tsx | 8 +- frontend/vite.config.ts | 4 +- mobile/src-tauri/Cargo.toml | 1 - mobile/src-tauri/capabilities/default.json | 2 - .../android/app/src/main/AndroidManifest.xml | 13 ++ .../app/src/main/res/values/strings.xml | 6 +- mobile/src-tauri/src/lib.rs | 1 - mobile/src-tauri/tauri.conf.json | 6 +- 12 files changed, 107 insertions(+), 68 deletions(-) diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 4a3b03a..9b06193 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -18,7 +18,12 @@ use time::Duration; use tower_http::cors::CorsLayer; use axum::http::{Method, HeaderValue}; -pub type MobileTokenStore = Arc>>; +pub enum MobileStoreEntry { + Csrf { created_at: Instant }, + AuthToken { user_id: i32, created_at: Instant }, +} + +pub type MobileTokenStore = Arc>>; pub mod models; pub mod services; diff --git a/backend/src/routes/oauth.rs b/backend/src/routes/oauth.rs index 1ab5905..ad5b156 100644 --- a/backend/src/routes/oauth.rs +++ b/backend/src/routes/oauth.rs @@ -14,7 +14,7 @@ use utoipa::ToSchema; use crate::auth::AuthBackend; use crate::models::User; use crate::services::OAuthService; -use crate::MobileTokenStore; +use crate::{MobileStoreEntry, MobileTokenStore}; const CSRF_TOKEN_KEY: &str = "oauth_csrf_token"; const FRONTEND_URL_KEY: &str = "oauth_frontend_url"; @@ -49,28 +49,30 @@ pub struct OAuthUrlResponse { )] pub async fn google_auth( session: Session, + Extension(token_store): Extension, Query(query): Query, ) -> Result, StatusCode> { let oauth_service = OAuthService::new(); let (auth_url, csrf_token) = oauth_service.get_auth_url(); - session - .insert(CSRF_TOKEN_KEY, csrf_token.secret().clone()) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - if let Some(redirect_url) = query.redirect_url { - session - .insert(FRONTEND_URL_KEY, redirect_url) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - } - if query.mobile.unwrap_or(false) { + let mut store = token_store.lock().unwrap(); + store.insert( + format!("csrf:{}", csrf_token.secret()), + MobileStoreEntry::Csrf { created_at: std::time::Instant::now() }, + ); + } else { session - .insert("oauth_mobile", true) + .insert(CSRF_TOKEN_KEY, csrf_token.secret().clone()) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + if let Some(redirect_url) = query.redirect_url { + session + .insert(FRONTEND_URL_KEY, redirect_url) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + } } Ok(Json(OAuthUrlResponse { url: auth_url })) @@ -92,29 +94,38 @@ pub async fn google_callback( Extension(token_store): Extension, Query(query): Query, ) -> Result { - let stored_csrf: Option = session + let session_csrf: Option = session .get(CSRF_TOKEN_KEY) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .unwrap_or(None); + + let is_mobile; + let csrf_valid; + + if let Some(csrf) = session_csrf { + is_mobile = false; + csrf_valid = csrf == query.state; + session.remove::(CSRF_TOKEN_KEY).await.ok(); + } else { + let key = format!("csrf:{}", &query.state); + let mut store = token_store.lock().unwrap(); + csrf_valid = matches!( + store.get(&key), + Some(MobileStoreEntry::Csrf { created_at }) if created_at.elapsed().as_secs() < 300 + ); + store.remove(&key); + is_mobile = csrf_valid; + } + + if !csrf_valid { + return Err(StatusCode::UNAUTHORIZED); + } let frontend_url: Option = session .get(FRONTEND_URL_KEY) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let is_mobile: bool = session - .get("oauth_mobile") - .await - .unwrap_or(None) - .unwrap_or(false); - - session.remove::(CSRF_TOKEN_KEY).await.ok(); + .unwrap_or(None); session.remove::(FRONTEND_URL_KEY).await.ok(); - session.remove::("oauth_mobile").await.ok(); - - if stored_csrf.as_deref() != Some(&query.state) { - return Err(StatusCode::UNAUTHORIZED); - } let oauth_service = OAuthService::new(); @@ -133,6 +144,23 @@ pub async fn google_callback( .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + if is_mobile { + let token = uuid::Uuid::new_v4().to_string(); + { + let mut store = token_store.lock().unwrap(); + store.insert( + token.clone(), + MobileStoreEntry::AuthToken { user_id: user.id, created_at: std::time::Instant::now() }, + ); + } + let deep_link = format!("com.arrelin.family-budget-android://auth?token={}", token); + let html = format!( + r#""#, + deep_link + ); + return Ok(Html(html).into_response()); + } + auth_session .login(&user) .await @@ -146,28 +174,10 @@ pub async fn google_callback( .unwrap_or_default(); if !authorized_families.contains(&family_id) { authorized_families.push(family_id); - session - .insert("authorized_families", authorized_families) - .await - .ok(); + session.insert("authorized_families", authorized_families).await.ok(); } } - if is_mobile { - let token = uuid::Uuid::new_v4().to_string(); - { - let mut store = token_store.lock().unwrap(); - store.insert(token.clone(), (user.id, std::time::Instant::now())); - } - let deep_link = format!("com.arrelin.family-budget-android://auth?token={}", token); - let html = format!( - r#" - "#, - deep_link - ); - return Ok(Html(html).into_response()); - } - let redirect_url = frontend_url.unwrap_or_else(|| "http://localhost:3000".to_string()); Ok(Redirect::temporary(&redirect_url).into_response()) } @@ -186,8 +196,10 @@ pub async fn mobile_callback( let user_id = { let mut store = token_store.lock().unwrap(); match store.get(&query.token) { - Some((uid, created_at)) if created_at.elapsed().as_secs() < 300 => { - let uid = *uid; + Some(MobileStoreEntry::AuthToken { user_id, created_at }) + if created_at.elapsed().as_secs() < 300 => + { + let uid = *user_id; store.remove(&query.token); uid } @@ -210,4 +222,4 @@ pub async fn mobile_callback( .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(serde_json::json!({"success": true}))) -} \ No newline at end of file +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e5d2f5b..7be9213 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@tailwindcss/postcss": "^4.1.18", "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-deep-link": "^2.4.7", + "@tauri-apps/plugin-opener": "^2.5.3", "@tauri-apps/plugin-shell": "^2.3.5", "axios": "^1.13.2", "i18next": "^25.8.0", @@ -1630,6 +1631,15 @@ "@tauri-apps/api": "^2.10.1" } }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", + "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tauri-apps/plugin-shell": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index e58e3be..0f7ebfc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "@tailwindcss/postcss": "^4.1.18", "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-deep-link": "^2.4.7", + "@tauri-apps/plugin-opener": "^2.5.3", "@tauri-apps/plugin-shell": "^2.3.5", "axios": "^1.13.2", "i18next": "^25.8.0", diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 800f712..777b248 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -51,17 +51,17 @@ export default function Login() { setError(''); if (isTauriEnv()) { - const { open } = await import('@tauri-apps/plugin-shell'); + const { openUrl } = await import('@tauri-apps/plugin-opener'); const response = await authApi.getGoogleAuthUrl(undefined, true); - await open(response.data.url); + await openUrl(response.data.url); return; } const currentUrl = window.location.origin; const response = await authApi.getGoogleAuthUrl(currentUrl); window.location.href = response.data.url; - } catch (err) { - setError(t('login.authError')); + } catch (err: any) { + setError(String(err?.message || err)); console.error(err); setLoading(false); } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 3c13057..d02704b 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -8,7 +8,9 @@ export default defineConfig({ host: process.env.TAURI_DEV_HOST || 'localhost', proxy: { '/api': { - target: 'http://localhost:8080', + target: process.env.TAURI_DEV_HOST + ? 'https://family-budget.duckdns.org' + : 'http://localhost:8080', changeOrigin: true, } } diff --git a/mobile/src-tauri/Cargo.toml b/mobile/src-tauri/Cargo.toml index a1f5117..1e0cf9c 100644 --- a/mobile/src-tauri/Cargo.toml +++ b/mobile/src-tauri/Cargo.toml @@ -21,7 +21,6 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = [] } tauri-plugin-opener = "2" tauri-plugin-deep-link = "2" -tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/mobile/src-tauri/capabilities/default.json b/mobile/src-tauri/capabilities/default.json index 7075013..ec2e275 100644 --- a/mobile/src-tauri/capabilities/default.json +++ b/mobile/src-tauri/capabilities/default.json @@ -6,8 +6,6 @@ "permissions": [ "core:default", "opener:default", - "shell:default", - "shell:allow-open", "deep-link:default" ] } diff --git a/mobile/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/mobile/src-tauri/gen/android/app/src/main/AndroidManifest.xml index 50562b2..8e5c1c3 100644 --- a/mobile/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/mobile/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,19 @@ + + + + + + + + + + + + + - family-budget-android - family-budget-android - \ No newline at end of file + Family Budget + Family Budget + diff --git a/mobile/src-tauri/src/lib.rs b/mobile/src-tauri/src/lib.rs index 265ce67..c4fe7a0 100644 --- a/mobile/src-tauri/src/lib.rs +++ b/mobile/src-tauri/src/lib.rs @@ -2,7 +2,6 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) - .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_deep_link::init()) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/mobile/src-tauri/tauri.conf.json b/mobile/src-tauri/tauri.conf.json index 44e322f..5a8138d 100644 --- a/mobile/src-tauri/tauri.conf.json +++ b/mobile/src-tauri/tauri.conf.json @@ -4,8 +4,8 @@ "version": "0.1.0", "identifier": "com.arrelin.family-budget-android", "build": { - "beforeDevCommand": "", - "devUrl": "https://family-budget.duckdns.org", + "beforeDevCommand": "npm run dev --prefix ../frontend", + "devUrl": "http://localhost:5173", "beforeBuildCommand": "npm run build --prefix ../frontend", "frontendDist": "../frontend/dist" }, @@ -36,7 +36,7 @@ "deep-link": { "mobile": [ { - "scheme": "com.arrelin.family-budget-android", + "scheme": ["com.arrelin.family-budget-android"], "host": "auth" } ] -- 2.49.1