Merge pull request 'mobile update' (#33) from bugfix/iro4ka into master
All checks were successful
Build and Publish Images / build-and-push (push) Successful in 43s
All checks were successful
Build and Publish Images / build-and-push (push) Successful in 43s
Reviewed-on: http://192.168.31.100:3847/Arrelin/family_budget/pulls/33
This commit was merged in pull request #33.
This commit is contained in:
@@ -18,7 +18,12 @@ use time::Duration;
|
|||||||
use tower_http::cors::CorsLayer;
|
use tower_http::cors::CorsLayer;
|
||||||
use axum::http::{Method, HeaderValue};
|
use axum::http::{Method, HeaderValue};
|
||||||
|
|
||||||
pub type MobileTokenStore = Arc<Mutex<HashMap<String, (i32, Instant)>>>;
|
pub enum MobileStoreEntry {
|
||||||
|
Csrf { created_at: Instant },
|
||||||
|
AuthToken { user_id: i32, created_at: Instant },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type MobileTokenStore = Arc<Mutex<HashMap<String, MobileStoreEntry>>>;
|
||||||
|
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod services;
|
pub mod services;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use utoipa::ToSchema;
|
|||||||
use crate::auth::AuthBackend;
|
use crate::auth::AuthBackend;
|
||||||
use crate::models::User;
|
use crate::models::User;
|
||||||
use crate::services::OAuthService;
|
use crate::services::OAuthService;
|
||||||
use crate::MobileTokenStore;
|
use crate::{MobileStoreEntry, MobileTokenStore};
|
||||||
|
|
||||||
const CSRF_TOKEN_KEY: &str = "oauth_csrf_token";
|
const CSRF_TOKEN_KEY: &str = "oauth_csrf_token";
|
||||||
const FRONTEND_URL_KEY: &str = "oauth_frontend_url";
|
const FRONTEND_URL_KEY: &str = "oauth_frontend_url";
|
||||||
@@ -49,28 +49,30 @@ pub struct OAuthUrlResponse {
|
|||||||
)]
|
)]
|
||||||
pub async fn google_auth(
|
pub async fn google_auth(
|
||||||
session: Session,
|
session: Session,
|
||||||
|
Extension(token_store): Extension<MobileTokenStore>,
|
||||||
Query(query): Query<GoogleAuthQuery>,
|
Query(query): Query<GoogleAuthQuery>,
|
||||||
) -> Result<Json<OAuthUrlResponse>, StatusCode> {
|
) -> Result<Json<OAuthUrlResponse>, StatusCode> {
|
||||||
let oauth_service = OAuthService::new();
|
let oauth_service = OAuthService::new();
|
||||||
let (auth_url, csrf_token) = oauth_service.get_auth_url();
|
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) {
|
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
|
session
|
||||||
.insert("oauth_mobile", true)
|
.insert(CSRF_TOKEN_KEY, csrf_token.secret().clone())
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.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 }))
|
Ok(Json(OAuthUrlResponse { url: auth_url }))
|
||||||
@@ -92,29 +94,38 @@ pub async fn google_callback(
|
|||||||
Extension(token_store): Extension<MobileTokenStore>,
|
Extension(token_store): Extension<MobileTokenStore>,
|
||||||
Query(query): Query<GoogleCallbackQuery>,
|
Query(query): Query<GoogleCallbackQuery>,
|
||||||
) -> Result<Response, StatusCode> {
|
) -> Result<Response, StatusCode> {
|
||||||
let stored_csrf: Option<String> = session
|
let session_csrf: Option<String> = session
|
||||||
.get(CSRF_TOKEN_KEY)
|
.get(CSRF_TOKEN_KEY)
|
||||||
.await
|
.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::<String>(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<String> = session
|
let frontend_url: Option<String> = session
|
||||||
.get(FRONTEND_URL_KEY)
|
.get(FRONTEND_URL_KEY)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.unwrap_or(None);
|
||||||
|
|
||||||
let is_mobile: bool = session
|
|
||||||
.get("oauth_mobile")
|
|
||||||
.await
|
|
||||||
.unwrap_or(None)
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
session.remove::<String>(CSRF_TOKEN_KEY).await.ok();
|
|
||||||
session.remove::<String>(FRONTEND_URL_KEY).await.ok();
|
session.remove::<String>(FRONTEND_URL_KEY).await.ok();
|
||||||
session.remove::<bool>("oauth_mobile").await.ok();
|
|
||||||
|
|
||||||
if stored_csrf.as_deref() != Some(&query.state) {
|
|
||||||
return Err(StatusCode::UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
let oauth_service = OAuthService::new();
|
let oauth_service = OAuthService::new();
|
||||||
|
|
||||||
@@ -133,6 +144,23 @@ pub async fn google_callback(
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.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#"<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;url={0}"></head><body><script>window.location="{0}"</script></body></html>"#,
|
||||||
|
deep_link
|
||||||
|
);
|
||||||
|
return Ok(Html(html).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
auth_session
|
auth_session
|
||||||
.login(&user)
|
.login(&user)
|
||||||
.await
|
.await
|
||||||
@@ -146,28 +174,10 @@ pub async fn google_callback(
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if !authorized_families.contains(&family_id) {
|
if !authorized_families.contains(&family_id) {
|
||||||
authorized_families.push(family_id);
|
authorized_families.push(family_id);
|
||||||
session
|
session.insert("authorized_families", authorized_families).await.ok();
|
||||||
.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#"<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;url={0}"></head>
|
|
||||||
<body><script>window.location="{0}"</script></body></html>"#,
|
|
||||||
deep_link
|
|
||||||
);
|
|
||||||
return Ok(Html(html).into_response());
|
|
||||||
}
|
|
||||||
|
|
||||||
let redirect_url = frontend_url.unwrap_or_else(|| "http://localhost:3000".to_string());
|
let redirect_url = frontend_url.unwrap_or_else(|| "http://localhost:3000".to_string());
|
||||||
Ok(Redirect::temporary(&redirect_url).into_response())
|
Ok(Redirect::temporary(&redirect_url).into_response())
|
||||||
}
|
}
|
||||||
@@ -186,8 +196,10 @@ pub async fn mobile_callback(
|
|||||||
let user_id = {
|
let user_id = {
|
||||||
let mut store = token_store.lock().unwrap();
|
let mut store = token_store.lock().unwrap();
|
||||||
match store.get(&query.token) {
|
match store.get(&query.token) {
|
||||||
Some((uid, created_at)) if created_at.elapsed().as_secs() < 300 => {
|
Some(MobileStoreEntry::AuthToken { user_id, created_at })
|
||||||
let uid = *uid;
|
if created_at.elapsed().as_secs() < 300 =>
|
||||||
|
{
|
||||||
|
let uid = *user_id;
|
||||||
store.remove(&query.token);
|
store.remove(&query.token);
|
||||||
uid
|
uid
|
||||||
}
|
}
|
||||||
|
|||||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@tauri-apps/api": "^2.10.1",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
"@tauri-apps/plugin-deep-link": "^2.4.7",
|
"@tauri-apps/plugin-deep-link": "^2.4.7",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"i18next": "^25.8.0",
|
"i18next": "^25.8.0",
|
||||||
@@ -1630,6 +1631,15 @@
|
|||||||
"@tauri-apps/api": "^2.10.1"
|
"@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": {
|
"node_modules/@tauri-apps/plugin-shell": {
|
||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@tauri-apps/api": "^2.10.1",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
"@tauri-apps/plugin-deep-link": "^2.4.7",
|
"@tauri-apps/plugin-deep-link": "^2.4.7",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"i18next": "^25.8.0",
|
"i18next": "^25.8.0",
|
||||||
|
|||||||
@@ -51,17 +51,17 @@ export default function Login() {
|
|||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
if (isTauriEnv()) {
|
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);
|
const response = await authApi.getGoogleAuthUrl(undefined, true);
|
||||||
await open(response.data.url);
|
await openUrl(response.data.url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUrl = window.location.origin;
|
const currentUrl = window.location.origin;
|
||||||
const response = await authApi.getGoogleAuthUrl(currentUrl);
|
const response = await authApi.getGoogleAuthUrl(currentUrl);
|
||||||
window.location.href = response.data.url;
|
window.location.href = response.data.url;
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
setError(t('login.authError'));
|
setError(String(err?.message || err));
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export default defineConfig({
|
|||||||
host: process.env.TAURI_DEV_HOST || 'localhost',
|
host: process.env.TAURI_DEV_HOST || 'localhost',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8080',
|
target: process.env.TAURI_DEV_HOST
|
||||||
|
? 'https://family-budget.duckdns.org'
|
||||||
|
: 'http://localhost:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ tauri-build = { version = "2", features = [] }
|
|||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-deep-link = "2"
|
tauri-plugin-deep-link = "2"
|
||||||
tauri-plugin-shell = "2"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"shell:default",
|
|
||||||
"shell:allow-open",
|
|
||||||
"deep-link:default"
|
"deep-link:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,19 @@
|
|||||||
<!-- AndroidTV support -->
|
<!-- AndroidTV support -->
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||||
|
<intent-filter >
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="com.arrelin.family-budget-android" />
|
||||||
|
<data android:host="auth" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">family-budget-android</string>
|
<string name="app_name">Family Budget</string>
|
||||||
<string name="main_activity_title">family-budget-android</string>
|
<string name="main_activity_title">Family Budget</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.arrelin.family-budget-android",
|
"identifier": "com.arrelin.family-budget-android",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "",
|
"beforeDevCommand": "npm run dev --prefix ../frontend",
|
||||||
"devUrl": "https://family-budget.duckdns.org",
|
"devUrl": "http://localhost:5173",
|
||||||
"beforeBuildCommand": "npm run build --prefix ../frontend",
|
"beforeBuildCommand": "npm run build --prefix ../frontend",
|
||||||
"frontendDist": "../frontend/dist"
|
"frontendDist": "../frontend/dist"
|
||||||
},
|
},
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"deep-link": {
|
"deep-link": {
|
||||||
"mobile": [
|
"mobile": [
|
||||||
{
|
{
|
||||||
"scheme": "com.arrelin.family-budget-android",
|
"scheme": ["com.arrelin.family-budget-android"],
|
||||||
"host": "auth"
|
"host": "auth"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user