mobile update

This commit is contained in:
arrelin
2026-03-10 14:11:35 +03:00
parent 265c29d542
commit 45eefeb1f5
12 changed files with 107 additions and 68 deletions

View File

@@ -18,7 +18,12 @@ use time::Duration;
use tower_http::cors::CorsLayer;
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 services;

View File

@@ -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<MobileTokenStore>,
Query(query): Query<GoogleAuthQuery>,
) -> Result<Json<OAuthUrlResponse>, 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<MobileTokenStore>,
Query(query): Query<GoogleCallbackQuery>,
) -> Result<Response, StatusCode> {
let stored_csrf: Option<String> = session
let session_csrf: Option<String> = 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::<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
.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::<String>(CSRF_TOKEN_KEY).await.ok();
.unwrap_or(None);
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();
@@ -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#"<!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
@@ -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#"<!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())
}
@@ -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})))
}
}