From 514ced6e19d000f6793d531892ad1150a45e20c9 Mon Sep 17 00:00:00 2001 From: arrelin Date: Sat, 17 Jan 2026 13:10:03 +0300 Subject: [PATCH] fix --- backend/src/routes/invite_link.rs | 15 +++ frontend/src/App.tsx | 12 ++- frontend/src/pages/InvitePage.tsx | 153 ++++++++++++++++++++++++++++++ frontend/src/pages/NoFamily.tsx | 47 ++++++++- 4 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/InvitePage.tsx diff --git a/backend/src/routes/invite_link.rs b/backend/src/routes/invite_link.rs index 401cb11..cb077fc 100644 --- a/backend/src/routes/invite_link.rs +++ b/backend/src/routes/invite_link.rs @@ -6,6 +6,7 @@ use axum::{ use axum_login::AuthSession; use sea_orm::DatabaseConnection; use serde::{Deserialize, Serialize}; +use tower_sessions::Session; use utoipa::ToSchema; use crate::auth::AuthBackend; @@ -243,6 +244,7 @@ pub async fn validate_invite_link( )] pub async fn join_family_via_invite( auth_session: AuthSession, + session: Session, State(db): State, Path(token): Path, ) -> Result, StatusCode> { @@ -266,6 +268,19 @@ pub async fn join_family_via_invite( _ => StatusCode::INTERNAL_SERVER_ERROR, })?; + let mut authorized_families: Vec = session + .get("authorized_families") + .await + .unwrap_or(None) + .unwrap_or_default(); + if !authorized_families.contains(&invite.family_id) { + authorized_families.push(invite.family_id); + session + .insert("authorized_families", authorized_families) + .await + .ok(); + } + Ok(Json(JoinFamilyResponse { success: true, family_id: invite.family_id, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 186778e..b1d8c3d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,15 +1,17 @@ import { useEffect } from 'react'; -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import Login from './pages/Login'; import FamilyView from './pages/FamilyView'; import AdminPanel from './pages/AdminPanel'; import NoFamily from './pages/NoFamily'; +import InvitePage from './pages/InvitePage'; import { useStore } from './store/useStore'; import { authApi } from './api/client'; import { Loader2 } from 'lucide-react'; function AppContent() { const { user, isAuthenticated, isLoading, setUser, setIsLoading } = useStore(); + const location = useLocation(); useEffect(() => { checkAuth(); @@ -26,6 +28,14 @@ function AppContent() { } }; + if (location.pathname.startsWith('/invite/')) { + return ( + + } /> + + ); + } + if (isLoading) { return (
diff --git a/frontend/src/pages/InvitePage.tsx b/frontend/src/pages/InvitePage.tsx new file mode 100644 index 0000000..9368dd2 --- /dev/null +++ b/frontend/src/pages/InvitePage.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { inviteLinkApi, authApi } from '../api/client'; +import { useStore } from '../store/useStore'; +import { Loader2, Users, UserPlus, AlertCircle } from 'lucide-react'; + +export default function InvitePage() { + const { token } = useParams<{ token: string }>(); + const navigate = useNavigate(); + const { isAuthenticated, setUser } = useStore(); + + const [loading, setLoading] = useState(true); + const [joining, setJoining] = useState(false); + const [error, setError] = useState(''); + const [familyName, setFamilyName] = useState(null); + const [isValid, setIsValid] = useState(false); + + useEffect(() => { + if (token) { + validateInvite(); + } + }, [token]); + + useEffect(() => { + if (isAuthenticated && isValid && token) { + joinFamily(); + } + }, [isAuthenticated, isValid]); + + const validateInvite = async () => { + if (!token) return; + + try { + setLoading(true); + const response = await inviteLinkApi.validate(token); + if (response.data.valid) { + setIsValid(true); + setFamilyName(response.data.family_name); + } else { + setError('Ссылка недействительна или срок её действия истёк'); + } + } catch (err) { + setError('Ссылка не найдена'); + } finally { + setLoading(false); + } + }; + + const joinFamily = async () => { + if (!token) return; + + try { + setJoining(true); + const response = await inviteLinkApi.join(token); + if (response.data.success) { + const meResponse = await authApi.me(); + setUser(meResponse.data); + navigate(`/family/${response.data.family_id}`); + } else { + setError(response.data.message); + } + } catch (err: any) { + if (err.response?.status === 400) { + setError('Вы уже состоите в семье'); + } else { + setError('Ошибка при присоединении к семье'); + } + } finally { + setJoining(false); + } + }; + + const handleGoogleLogin = async () => { + if (token) { + localStorage.setItem('pendingInviteToken', token); + } + try { + const response = await authApi.getGoogleAuthUrl(window.location.href); + window.location.href = response.data.url; + } catch (err) { + setError('Ошибка при получении ссылки для авторизации'); + } + }; + + if (loading) { + return ( +
+
+ + Проверка приглашения... +
+
+ ); + } + + if (joining) { + return ( +
+
+ + Присоединение к семье... +
+
+ ); + } + + if (error) { + return ( +
+
+
+ +
+

Ошибка

+

{error}

+ +
+
+ ); + } + + return ( +
+
+
+ +
+

+ Приглашение в семью +

+

+ {familyName} +

+

+ Вас пригласили присоединиться к семейному бюджету. + Войдите через Google, чтобы принять приглашение. +

+ +
+
+ ); +} diff --git a/frontend/src/pages/NoFamily.tsx b/frontend/src/pages/NoFamily.tsx index 4034fb2..ac8dcb5 100644 --- a/frontend/src/pages/NoFamily.tsx +++ b/frontend/src/pages/NoFamily.tsx @@ -1,7 +1,7 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useStore } from '../store/useStore'; -import { authApi, familyApi } from '../api/client'; +import { authApi, familyApi, inviteLinkApi } from '../api/client'; import { Users, LogOut, Settings, Plus, Loader2, Eye, EyeOff } from 'lucide-react'; export default function NoFamily() { @@ -13,6 +13,38 @@ export default function NoFamily() { const [showPassword, setShowPassword] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); + const [joiningFamily, setJoiningFamily] = useState(false); + + useEffect(() => { + checkPendingInvite(); + }, []); + + const checkPendingInvite = async () => { + const pendingToken = localStorage.getItem('pendingInviteToken'); + if (!pendingToken) return; + + localStorage.removeItem('pendingInviteToken'); + + try { + setJoiningFamily(true); + const response = await inviteLinkApi.join(pendingToken); + if (response.data.success) { + const meResponse = await authApi.me(); + setUser(meResponse.data); + navigate(`/family/${response.data.family_id}`); + } else { + setError(response.data.message); + } + } catch (err: any) { + if (err.response?.status === 400) { + setError('Ссылка-приглашение недействительна или истекла'); + } else { + setError('Ошибка при присоединении к семье'); + } + } finally { + setJoiningFamily(false); + } + }; const handleLogout = async () => { try { @@ -68,6 +100,17 @@ export default function NoFamily() { } }; + if (joiningFamily) { + return ( +
+
+ + Присоединение к семье... +
+
+ ); + } + return (