Compare commits
2 Commits
bugfix/iro
...
91f9ed5474
| Author | SHA1 | Date | |
|---|---|---|---|
| 91f9ed5474 | |||
| adad656df2 |
@@ -26,7 +26,3 @@ oauth2 = { version = "5.0.0", features = ["reqwest"] }
|
||||
reqwest = { version = "0.13.1", features = ["json"] }
|
||||
rand = "0.9.2"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
@@ -1,10 +1,14 @@
|
||||
use axum::{
|
||||
routing::{get, post, put, delete},
|
||||
Router, middleware as axum_middleware,
|
||||
Extension,
|
||||
};
|
||||
use sea_orm::{sqlx, Database, DatabaseConnection, DbErr};
|
||||
use sea_orm_migration::prelude::*;
|
||||
use std::net::SocketAddr;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
use tower_sessions::{Expiry, SessionManagerLayer, cookie::SameSite};
|
||||
@@ -14,6 +18,7 @@ use time::Duration;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use axum::http::{Method, HeaderValue};
|
||||
|
||||
pub type MobileTokenStore = Arc<Mutex<HashMap<String, (i32, Instant)>>>;
|
||||
|
||||
pub mod models;
|
||||
pub mod services;
|
||||
@@ -169,10 +174,13 @@ pub async fn create_app(db: DatabaseConnection) -> Result<Router, DbErr> {
|
||||
.layer(auth_layer.clone())
|
||||
.with_state(db.clone());
|
||||
|
||||
let mobile_token_store: MobileTokenStore = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let oauth_routes = Router::new()
|
||||
.route("/auth/google", get(routes::oauth::google_auth))
|
||||
.route("/auth/google/callback", get(routes::oauth::google_callback))
|
||||
.route("/auth/mobile-callback", get(routes::oauth::mobile_callback))
|
||||
.layer(Extension(mobile_token_store))
|
||||
.layer(auth_layer.clone())
|
||||
.with_state(db.clone());
|
||||
|
||||
@@ -231,7 +239,7 @@ pub async fn create_app(db: DatabaseConnection) -> Result<Router, DbErr> {
|
||||
.url("/api-docs/openapi.json", ApiDoc::openapi());
|
||||
|
||||
let allowed_origins = std::env::var("ALLOWED_ORIGINS")
|
||||
.unwrap_or_else(|_| "http://localhost:3000,http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:8080,http://localhost:1420,http://tauri.localhost,https://tauri.localhost".to_string());
|
||||
.unwrap_or_else(|_| "http://localhost:3000,http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:8080".to_string());
|
||||
|
||||
let origins: Vec<HeaderValue> = allowed_origins
|
||||
.split(',')
|
||||
|
||||
@@ -3,13 +3,6 @@ use sea_orm::DbErr;
|
||||
use sea_orm_migration::prelude::*;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), DbErr> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "family_budget=debug,info".parse().unwrap()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let db = establish_connection().await?;
|
||||
println!("Successfully connected to database!");
|
||||
|
||||
|
||||
@@ -2,82 +2,23 @@ use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
Extension,
|
||||
Json,
|
||||
};
|
||||
use axum_login::AuthSession;
|
||||
use sea_orm::{DatabaseConnection, EntityTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tower_sessions::Session;
|
||||
use tracing::{info, warn};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::auth::AuthBackend;
|
||||
use crate::models::User;
|
||||
use crate::services::OAuthService;
|
||||
use crate::MobileTokenStore;
|
||||
|
||||
const CSRF_TOKEN_KEY: &str = "oauth_csrf_token";
|
||||
const FRONTEND_URL_KEY: &str = "oauth_frontend_url";
|
||||
|
||||
fn mobile_secret() -> String {
|
||||
std::env::var("MOBILE_SECRET").unwrap_or_else(|_| "family-budget-mobile-secret".to_string())
|
||||
}
|
||||
|
||||
fn sign(data: &str) -> String {
|
||||
let secret = mobile_secret();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(format!("{}:{}", secret, data).as_bytes());
|
||||
hex::encode(hasher.finalize())
|
||||
}
|
||||
|
||||
fn make_mobile_csrf_state(nonce: &str) -> String {
|
||||
let sig = sign(&format!("csrf.mobile.{}", nonce));
|
||||
format!("mobile.{}.{}", nonce, sig)
|
||||
}
|
||||
|
||||
fn verify_mobile_csrf_state(state: &str) -> bool {
|
||||
let mut parts = state.splitn(3, '.');
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some("mobile"), Some(nonce), Some(sig)) => {
|
||||
sign(&format!("csrf.mobile.{}", nonce)) == sig
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_auth_token(user_id: i32) -> String {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
let payload = format!("{}.{}", user_id, timestamp);
|
||||
let sig = sign(&format!("auth.{}", payload));
|
||||
format!("{}.{}", payload, sig)
|
||||
}
|
||||
|
||||
fn verify_auth_token(token: &str) -> Option<i32> {
|
||||
let mut parts = token.splitn(3, '.');
|
||||
let user_id_str = parts.next()?;
|
||||
let timestamp_str = parts.next()?;
|
||||
let sig = parts.next()?;
|
||||
|
||||
let payload = format!("{}.{}", user_id_str, timestamp_str);
|
||||
if sign(&format!("auth.{}", payload)) != sig {
|
||||
return None;
|
||||
}
|
||||
|
||||
let timestamp: u64 = timestamp_str.parse().ok()?;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
if now.saturating_sub(timestamp) > 300 {
|
||||
return None;
|
||||
}
|
||||
|
||||
user_id_str.parse().ok()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct GoogleAuthQuery {
|
||||
pub redirect_url: Option<String>,
|
||||
@@ -100,8 +41,7 @@ pub struct OAuthUrlResponse {
|
||||
path = "/auth/google",
|
||||
tag = "auth",
|
||||
params(
|
||||
("redirect_url" = Option<String>, Query, description = "Frontend URL to redirect after auth"),
|
||||
("mobile" = Option<bool>, Query, description = "Mobile OAuth flow")
|
||||
("redirect_url" = Option<String>, Query, description = "Frontend URL to redirect after auth")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Returns Google OAuth URL", body = OAuthUrlResponse)
|
||||
@@ -112,15 +52,6 @@ pub async fn google_auth(
|
||||
Query(query): Query<GoogleAuthQuery>,
|
||||
) -> Result<Json<OAuthUrlResponse>, StatusCode> {
|
||||
let oauth_service = OAuthService::new();
|
||||
|
||||
if query.mobile.unwrap_or(false) {
|
||||
let nonce = uuid::Uuid::new_v4().to_string();
|
||||
let mobile_state = make_mobile_csrf_state(&nonce);
|
||||
let auth_url = oauth_service.get_auth_url_with_state(mobile_state);
|
||||
info!("mobile google_auth: generated signed state for nonce={}", nonce);
|
||||
return Ok(Json(OAuthUrlResponse { url: auth_url }));
|
||||
}
|
||||
|
||||
let (auth_url, csrf_token) = oauth_service.get_auth_url();
|
||||
|
||||
session
|
||||
@@ -135,6 +66,13 @@ pub async fn google_auth(
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
|
||||
if query.mobile.unwrap_or(false) {
|
||||
session
|
||||
.insert("oauth_mobile", true)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
|
||||
Ok(Json(OAuthUrlResponse { url: auth_url }))
|
||||
}
|
||||
|
||||
@@ -151,32 +89,32 @@ pub async fn google_callback(
|
||||
mut auth_session: AuthSession<AuthBackend>,
|
||||
session: Session,
|
||||
State(db): State<DatabaseConnection>,
|
||||
Extension(token_store): Extension<MobileTokenStore>,
|
||||
Query(query): Query<GoogleCallbackQuery>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let is_mobile = verify_mobile_csrf_state(&query.state);
|
||||
info!("google_callback: state={} is_mobile={}", &query.state[..query.state.len().min(20)], is_mobile);
|
||||
|
||||
if !is_mobile {
|
||||
let session_csrf: Option<String> = session
|
||||
let stored_csrf: Option<String> = session
|
||||
.get(CSRF_TOKEN_KEY)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
session.remove::<String>(CSRF_TOKEN_KEY).await.ok();
|
||||
|
||||
match session_csrf {
|
||||
Some(csrf) if csrf == query.state => {}
|
||||
_ => {
|
||||
warn!("google_callback: CSRF mismatch, session_csrf={:?}", session_csrf.as_deref().map(|s| &s[..s.len().min(10)]));
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
}
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let frontend_url: Option<String> = session
|
||||
.get(FRONTEND_URL_KEY)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
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::<bool>("oauth_mobile").await.ok();
|
||||
|
||||
if stored_csrf.as_deref() != Some(&query.state) {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
let oauth_service = OAuthService::new();
|
||||
|
||||
@@ -195,17 +133,6 @@ pub async fn google_callback(
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if is_mobile {
|
||||
let token = make_auth_token(user.id);
|
||||
info!("google_callback: mobile auth for user_id={}, token_prefix={}", user.id, &token[..token.len().min(20)]);
|
||||
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
|
||||
.login(&user)
|
||||
.await
|
||||
@@ -219,10 +146,28 @@ 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#"<!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());
|
||||
Ok(Redirect::temporary(&redirect_url).into_response())
|
||||
}
|
||||
@@ -234,19 +179,24 @@ pub struct MobileCallbackQuery {
|
||||
|
||||
pub async fn mobile_callback(
|
||||
mut auth_session: AuthSession<AuthBackend>,
|
||||
session: Session,
|
||||
State(db): State<DatabaseConnection>,
|
||||
Extension(token_store): Extension<MobileTokenStore>,
|
||||
Query(query): Query<MobileCallbackQuery>,
|
||||
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
info!("mobile_callback: received token_prefix={}", &query.token[..query.token.len().min(20)]);
|
||||
let user_id = match verify_auth_token(&query.token) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
warn!("mobile_callback: token verification failed for token={}", &query.token[..query.token.len().min(40)]);
|
||||
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;
|
||||
store.remove(&query.token);
|
||||
uid
|
||||
}
|
||||
_ => {
|
||||
store.remove(&query.token);
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
};
|
||||
info!("mobile_callback: token valid for user_id={}", user_id);
|
||||
|
||||
let user = User::find_by_id(user_id)
|
||||
.one(&db)
|
||||
@@ -259,17 +209,5 @@ pub async fn mobile_callback(
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
if let Some(family_id) = user.family_id {
|
||||
let mut authorized_families: Vec<i32> = session
|
||||
.get("authorized_families")
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
.unwrap_or_default();
|
||||
if !authorized_families.contains(&family_id) {
|
||||
authorized_families.push(family_id);
|
||||
session.insert("authorized_families", authorized_families).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(serde_json::json!({"success": true})))
|
||||
}
|
||||
@@ -44,26 +44,6 @@ impl OAuthService {
|
||||
(auth_url.to_string(), csrf_token)
|
||||
}
|
||||
|
||||
pub fn get_auth_url_with_state(&self, state: String) -> String {
|
||||
let client_id = std::env::var("GOOGLE_CLIENT_ID")
|
||||
.expect("GOOGLE_CLIENT_ID must be set");
|
||||
let client_secret = std::env::var("GOOGLE_CLIENT_SECRET")
|
||||
.expect("GOOGLE_CLIENT_SECRET must be set");
|
||||
let redirect_url = std::env::var("GOOGLE_REDIRECT_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:8080/api/auth/google/callback".to_string());
|
||||
|
||||
let client = Self::get_client(client_id, client_secret, redirect_url);
|
||||
|
||||
let (auth_url, _) = client
|
||||
.authorize_url(move || CsrfToken::new(state))
|
||||
.add_scope(Scope::new("openid".to_string()))
|
||||
.add_scope(Scope::new("email".to_string()))
|
||||
.add_scope(Scope::new("profile".to_string()))
|
||||
.url();
|
||||
|
||||
auth_url.to_string()
|
||||
}
|
||||
|
||||
pub async fn exchange_code(&self, code: String) -> Result<String, OAuthError> {
|
||||
let client_id = std::env::var("GOOGLE_CLIENT_ID")
|
||||
.expect("GOOGLE_CLIENT_ID must be set");
|
||||
|
||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -11,7 +11,6 @@
|
||||
"@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",
|
||||
@@ -1631,15 +1630,6 @@
|
||||
"@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",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"@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",
|
||||
|
||||
@@ -26,24 +26,16 @@ export default function Login() {
|
||||
const url = Array.isArray(urls) ? urls[0] : urls;
|
||||
if (!url.startsWith(DEEP_LINK_SCHEME)) return;
|
||||
|
||||
let token: string | null;
|
||||
try {
|
||||
token = new URL(url).searchParams.get('token');
|
||||
} catch {
|
||||
setError(t('login.authError'));
|
||||
return;
|
||||
}
|
||||
if (!token) { setError(t('login.authError')); return; }
|
||||
const token = new URL(url).searchParams.get('token');
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await authApi.mobileCallback(token);
|
||||
const me = await authApi.me();
|
||||
setUser(me.data);
|
||||
} catch (err: any) {
|
||||
const status = err?.response?.status;
|
||||
const msg = err?.response?.data ? JSON.stringify(err.response.data) : err?.message;
|
||||
setError(`${status ?? 'network'}: ${msg}`);
|
||||
} catch {
|
||||
setError(t('login.authError'));
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
@@ -59,17 +51,17 @@ export default function Login() {
|
||||
setError('');
|
||||
|
||||
if (isTauriEnv()) {
|
||||
const { openUrl } = await import('@tauri-apps/plugin-opener');
|
||||
const { open } = await import('@tauri-apps/plugin-shell');
|
||||
const response = await authApi.getGoogleAuthUrl(undefined, true);
|
||||
await openUrl(response.data.url);
|
||||
await open(response.data.url);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUrl = window.location.origin;
|
||||
const response = await authApi.getGoogleAuthUrl(currentUrl);
|
||||
window.location.href = response.data.url;
|
||||
} catch (err: any) {
|
||||
setError(String(err?.message || err));
|
||||
} catch (err) {
|
||||
setError(t('login.authError'));
|
||||
console.error(err);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -8,24 +8,8 @@ export default defineConfig({
|
||||
host: process.env.TAURI_DEV_HOST || 'localhost',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.TAURI_DEV_HOST
|
||||
? 'https://family-budget.duckdns.org'
|
||||
: 'http://localhost:8080',
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
if (process.env.TAURI_DEV_HOST) {
|
||||
proxy.on('proxyRes', (proxyRes) => {
|
||||
const cookies = proxyRes.headers['set-cookie'];
|
||||
if (cookies) {
|
||||
proxyRes.headers['set-cookie'] = cookies.map(cookie =>
|
||||
cookie
|
||||
.replace(/;\s*Secure/gi, '')
|
||||
.replace(/;\s*Domain=[^;]*/gi, '')
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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"
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"shell:default",
|
||||
"shell:allow-open",
|
||||
"deep-link:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,19 +22,6 @@
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</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>
|
||||
|
||||
<provider
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">Family Budget</string>
|
||||
<string name="main_activity_title">Family Budget</string>
|
||||
<string name="app_name">family-budget-android</string>
|
||||
<string name="main_activity_title">family-budget-android</string>
|
||||
</resources>
|
||||
@@ -2,6 +2,7 @@
|
||||
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");
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.arrelin.family-budget-android",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev --prefix ../frontend",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeBuildCommand": "VITE_API_BASE_URL=https://family-budget.duckdns.org/api npm run build --prefix ../frontend",
|
||||
"beforeDevCommand": "",
|
||||
"devUrl": "https://family-budget.duckdns.org",
|
||||
"beforeBuildCommand": "npm run build --prefix ../frontend",
|
||||
"frontendDist": "../frontend/dist"
|
||||
},
|
||||
"app": {
|
||||
@@ -36,7 +36,7 @@
|
||||
"deep-link": {
|
||||
"mobile": [
|
||||
{
|
||||
"scheme": ["com.arrelin.family-budget-android"],
|
||||
"scheme": "com.arrelin.family-budget-android",
|
||||
"host": "auth"
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user