diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a593c22..29de04a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,3 @@ -name: CI - on: push: branches: [main, dev] @@ -11,16 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies + @@ -24,39 +24,10 @@ jobs: run: npm ci - name: Run ESLint @@ -31,3 +20,15 @@ jobs: - name: Build run: npm run build + env: + VITE_API_URL: /api + VITE_TELEGRAM_BOT_USERNAME: test_bot + VITE_APP_NAME: Cabinet + VITE_APP_LOGO: V + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 diff --git a/src/App.tsx b/src/App.tsx index c99b572..48f2c17 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,8 @@ -import { Routes, Route, Navigate } from 'react-router-dom' +import { Routes, Route, Navigate, useLocation } from 'react-router-dom' import { useAuthStore } from './store/auth' import Layout from './components/layout/Layout' import PageLoader from './components/common/PageLoader' +import { saveReturnUrl } from './utils/token' import Login from './pages/Login' import TelegramCallback from './pages/TelegramCallback' import TelegramRedirect from './pages/TelegramRedirect' @@ -36,13 +37,16 @@ import AdminRemnawave from './pages/AdminRemnawave' function ProtectedRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useAuthStore() + const location = useLocation() if (isLoading) { return } if (!isAuthenticated) { - return + // Сохраняем текущий URL для возврата после авторизации + saveReturnUrl() + return } return {children} @@ -50,13 +54,16 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { function AdminRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading, isAdmin } = useAuthStore() + const location = useLocation() if (isLoading) { return } if (!isAuthenticated) { - return + // Сохраняем текущий URL для возврата после авторизации + saveReturnUrl() + return } if (!isAdmin) { diff --git a/src/pages/AdminBanSystem.tsx b/src/pages/AdminBanSystem.tsx index 08a442f..99a87c7 100644 --- a/src/pages/AdminBanSystem.tsx +++ b/src/pages/AdminBanSystem.tsx @@ -1,6 +1,5 @@ import { useState, useEffect, useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { Link } from 'react-router-dom' import { banSystemApi, type BanSystemStatus, @@ -19,9 +18,9 @@ import { } from '../api/banSystem' // Icons -const BackIcon = () => ( - - +const ShieldIcon = () => ( + + ) @@ -397,12 +396,9 @@ export default function AdminBanSystem() { {/* Header */}
- - - +
+ +

{t('banSystem.title')}

{t('banSystem.subtitle')}

diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index fd75065..c6e48ca 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,9 +1,10 @@ -import { useState, useEffect, useMemo } from 'react' -import { useNavigate } from 'react-router-dom' +import { useState, useEffect, useMemo, useCallback } from 'react' +import { useNavigate, useLocation } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { useAuthStore } from '../store/auth' import { brandingApi, type BrandingInfo } from '../api/branding' +import { getAndClearReturnUrl } from '../utils/token' import LanguageSwitcher from '../components/LanguageSwitcher' import TelegramLoginButton from '../components/TelegramLoginButton' @@ -46,6 +47,7 @@ const cacheBranding = (data: BrandingInfo) => { export default function Login() { const { t } = useTranslation() const navigate = useNavigate() + const location = useLocation() const { isAuthenticated, loginWithTelegram, loginWithEmail } = useAuthStore() const [activeTab, setActiveTab] = useState<'telegram' | 'email'>('telegram') const [email, setEmail] = useState('') @@ -54,6 +56,22 @@ export default function Login() { const [isLoading, setIsLoading] = useState(false) const [isTelegramWebApp, setIsTelegramWebApp] = useState(false) + // Получаем URL для возврата после авторизации + const getReturnUrl = useCallback(() => { + // Сначала проверяем state от React Router + const stateFrom = (location.state as { from?: string })?.from + if (stateFrom && stateFrom !== '/login') { + return stateFrom + } + // Затем проверяем сохранённый URL в sessionStorage (от safeRedirectToLogin) + const savedUrl = getAndClearReturnUrl() + if (savedUrl && savedUrl !== '/login') { + return savedUrl + } + // По умолчанию на главную + return '/' + }, [location.state]) + // Fetch branding const cachedBranding = useMemo(() => getCachedBranding(), []) @@ -82,9 +100,9 @@ export default function Login() { useEffect(() => { if (isAuthenticated) { - navigate('/') + navigate(getReturnUrl(), { replace: true }) } - }, [isAuthenticated, navigate]) + }, [isAuthenticated, navigate, getReturnUrl]) // Try Telegram WebApp authentication on mount useEffect(() => { @@ -97,7 +115,7 @@ export default function Login() { setIsLoading(true) try { await loginWithTelegram(tg.initData) - navigate('/') + navigate(getReturnUrl(), { replace: true }) } catch (err) { console.error('Telegram auth failed:', err) setError(t('auth.telegramRequired')) @@ -108,7 +126,7 @@ export default function Login() { } tryTelegramAuth() - }, [loginWithTelegram, navigate, t]) + }, [loginWithTelegram, navigate, t, getReturnUrl]) const handleEmailLogin = async (e: React.FormEvent) => { e.preventDefault() @@ -117,7 +135,7 @@ export default function Login() { try { await loginWithEmail(email, password) - navigate('/') + navigate(getReturnUrl(), { replace: true }) } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } setError(error.response?.data?.detail || t('common.error')) diff --git a/src/store/auth.ts b/src/store/auth.ts index 9799c21..a31b075 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -145,6 +145,8 @@ export const useAuthStore = create()( const newToken = await tokenRefreshManager.refreshAccessToken() if (newToken) { const user = await authApi.getMe() + // Сначала проверяем admin статус, потом снимаем isLoading + await get().checkAdminStatus() set({ accessToken: newToken, refreshToken, @@ -152,7 +154,6 @@ export const useAuthStore = create()( isAuthenticated: true, isLoading: false, }) - get().checkAdminStatus() } else { tokenStorage.clearTokens() set({ @@ -168,6 +169,8 @@ export const useAuthStore = create()( try { const user = await authApi.getMe() + // Сначала проверяем admin статус, потом снимаем isLoading + await get().checkAdminStatus() set({ accessToken, refreshToken, @@ -175,13 +178,14 @@ export const useAuthStore = create()( isAuthenticated: true, isLoading: false, }) - get().checkAdminStatus() } catch { // Token might be invalid on server, try to refresh const newToken = await tokenRefreshManager.refreshAccessToken() if (newToken) { try { const user = await authApi.getMe() + // Сначала проверяем admin статус, потом снимаем isLoading + await get().checkAdminStatus() set({ accessToken: newToken, refreshToken, @@ -189,7 +193,6 @@ export const useAuthStore = create()( isAuthenticated: true, isLoading: false, }) - get().checkAdminStatus() } catch { tokenStorage.clearTokens() set({ diff --git a/src/utils/token.ts b/src/utils/token.ts index 62f32dc..eb0d254 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -252,9 +252,40 @@ class TokenRefreshManager { export const tokenRefreshManager = new TokenRefreshManager() +/** + * Ключ для сохранения URL для возврата после логина + */ +const RETURN_URL_KEY = 'auth_return_url' + +/** + * Сохраняет URL для возврата после авторизации + */ +export function saveReturnUrl(): void { + if (typeof window !== 'undefined') { + const currentPath = window.location.pathname + window.location.search + // Не сохраняем /login как return URL + if (currentPath && currentPath !== '/login') { + sessionStorage.setItem(RETURN_URL_KEY, currentPath) + } + } +} + +/** + * Получает и очищает сохранённый URL для возврата + */ +export function getAndClearReturnUrl(): string | null { + if (typeof window !== 'undefined') { + const url = sessionStorage.getItem(RETURN_URL_KEY) + sessionStorage.removeItem(RETURN_URL_KEY) + return url + } + return null +} + /** * Безопасный редирект на страницу логина * Валидирует URL чтобы предотвратить open redirect уязвимость + * Сохраняет текущий URL для возврата после авторизации */ export function safeRedirectToLogin(): void { // Разрешённые пути для редиректа (относительные пути нашего приложения) @@ -262,6 +293,8 @@ export function safeRedirectToLogin(): void { // Проверяем, что мы на том же origin if (typeof window !== 'undefined') { + // Сохраняем текущий URL для возврата после логина + saveReturnUrl() // Используем только относительный путь для безопасности window.location.href = loginPath }