Merge pull request #1 from PEDZEO/PEDZ

Pedz
This commit is contained in:
PEDZEO
2026-01-18 22:58:33 +03:00
committed by GitHub
5 changed files with 87 additions and 32 deletions

View File

@@ -1,5 +1,3 @@
name: CI
on: on:
push: push:
branches: [main, dev] branches: [main, dev]
@@ -11,16 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code @@ -24,39 +24,10 @@ jobs:
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci run: npm ci
- name: Run ESLint - name: Run ESLint
@@ -31,3 +20,15 @@ jobs:
- name: Build - name: Build
run: npm run 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

View File

@@ -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 { useAuthStore } from './store/auth'
import Layout from './components/layout/Layout' import Layout from './components/layout/Layout'
import PageLoader from './components/common/PageLoader' import PageLoader from './components/common/PageLoader'
import { saveReturnUrl } from './utils/token'
import Login from './pages/Login' import Login from './pages/Login'
import TelegramCallback from './pages/TelegramCallback' import TelegramCallback from './pages/TelegramCallback'
import TelegramRedirect from './pages/TelegramRedirect' import TelegramRedirect from './pages/TelegramRedirect'
@@ -36,13 +37,16 @@ import AdminRemnawave from './pages/AdminRemnawave'
function ProtectedRoute({ children }: { children: React.ReactNode }) { function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuthStore() const { isAuthenticated, isLoading } = useAuthStore()
const location = useLocation()
if (isLoading) { if (isLoading) {
return <PageLoader variant="dark" /> return <PageLoader variant="dark" />
} }
if (!isAuthenticated) { if (!isAuthenticated) {
return <Navigate to="/login" replace /> // Сохраняем текущий URL для возврата после авторизации
saveReturnUrl()
return <Navigate to="/login" replace state={{ from: location.pathname }} />
} }
return <Layout>{children}</Layout> return <Layout>{children}</Layout>
@@ -50,13 +54,16 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
function AdminRoute({ children }: { children: React.ReactNode }) { function AdminRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading, isAdmin } = useAuthStore() const { isAuthenticated, isLoading, isAdmin } = useAuthStore()
const location = useLocation()
if (isLoading) { if (isLoading) {
return <PageLoader variant="light" /> return <PageLoader variant="light" />
} }
if (!isAuthenticated) { if (!isAuthenticated) {
return <Navigate to="/login" replace /> // Сохраняем текущий URL для возврата после авторизации
saveReturnUrl()
return <Navigate to="/login" replace state={{ from: location.pathname }} />
} }
if (!isAdmin) { if (!isAdmin) {

View File

@@ -1,6 +1,5 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { import {
banSystemApi, banSystemApi,
type BanSystemStatus, type BanSystemStatus,
@@ -19,9 +18,9 @@ import {
} from '../api/banSystem' } from '../api/banSystem'
// Icons // Icons
const BackIcon = () => ( const ShieldIcon = () => (
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
</svg> </svg>
) )
@@ -397,12 +396,9 @@ export default function AdminBanSystem() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link <div className="p-3 bg-error-500/20 rounded-xl">
to="/admin" <ShieldIcon />
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors" </div>
>
<BackIcon />
</Link>
<div> <div>
<h1 className="text-2xl font-bold text-dark-100">{t('banSystem.title')}</h1> <h1 className="text-2xl font-bold text-dark-100">{t('banSystem.title')}</h1>
<p className="text-dark-400">{t('banSystem.subtitle')}</p> <p className="text-dark-400">{t('banSystem.subtitle')}</p>

View File

@@ -1,9 +1,10 @@
import { useState, useEffect, useMemo } from 'react' import { useState, useEffect, useMemo, useCallback } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useAuthStore } from '../store/auth' import { useAuthStore } from '../store/auth'
import { brandingApi, type BrandingInfo } from '../api/branding' import { brandingApi, type BrandingInfo } from '../api/branding'
import { getAndClearReturnUrl } from '../utils/token'
import LanguageSwitcher from '../components/LanguageSwitcher' import LanguageSwitcher from '../components/LanguageSwitcher'
import TelegramLoginButton from '../components/TelegramLoginButton' import TelegramLoginButton from '../components/TelegramLoginButton'
@@ -46,6 +47,7 @@ const cacheBranding = (data: BrandingInfo) => {
export default function Login() { export default function Login() {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation()
const { isAuthenticated, loginWithTelegram, loginWithEmail } = useAuthStore() const { isAuthenticated, loginWithTelegram, loginWithEmail } = useAuthStore()
const [activeTab, setActiveTab] = useState<'telegram' | 'email'>('telegram') const [activeTab, setActiveTab] = useState<'telegram' | 'email'>('telegram')
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
@@ -54,6 +56,22 @@ export default function Login() {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isTelegramWebApp, setIsTelegramWebApp] = 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 // Fetch branding
const cachedBranding = useMemo(() => getCachedBranding(), []) const cachedBranding = useMemo(() => getCachedBranding(), [])
@@ -82,9 +100,9 @@ export default function Login() {
useEffect(() => { useEffect(() => {
if (isAuthenticated) { if (isAuthenticated) {
navigate('/') navigate(getReturnUrl(), { replace: true })
} }
}, [isAuthenticated, navigate]) }, [isAuthenticated, navigate, getReturnUrl])
// Try Telegram WebApp authentication on mount // Try Telegram WebApp authentication on mount
useEffect(() => { useEffect(() => {
@@ -97,7 +115,7 @@ export default function Login() {
setIsLoading(true) setIsLoading(true)
try { try {
await loginWithTelegram(tg.initData) await loginWithTelegram(tg.initData)
navigate('/') navigate(getReturnUrl(), { replace: true })
} catch (err) { } catch (err) {
console.error('Telegram auth failed:', err) console.error('Telegram auth failed:', err)
setError(t('auth.telegramRequired')) setError(t('auth.telegramRequired'))
@@ -108,7 +126,7 @@ export default function Login() {
} }
tryTelegramAuth() tryTelegramAuth()
}, [loginWithTelegram, navigate, t]) }, [loginWithTelegram, navigate, t, getReturnUrl])
const handleEmailLogin = async (e: React.FormEvent) => { const handleEmailLogin = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
@@ -117,7 +135,7 @@ export default function Login() {
try { try {
await loginWithEmail(email, password) await loginWithEmail(email, password)
navigate('/') navigate(getReturnUrl(), { replace: true })
} catch (err: unknown) { } catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } } const error = err as { response?: { data?: { detail?: string } } }
setError(error.response?.data?.detail || t('common.error')) setError(error.response?.data?.detail || t('common.error'))

View File

@@ -252,9 +252,40 @@ class TokenRefreshManager {
export const tokenRefreshManager = new 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 чтобы предотвратить open redirect уязвимость
* Сохраняет текущий URL для возврата после авторизации
*/ */
export function safeRedirectToLogin(): void { export function safeRedirectToLogin(): void {
// Разрешённые пути для редиректа (относительные пути нашего приложения) // Разрешённые пути для редиректа (относительные пути нашего приложения)
@@ -262,6 +293,8 @@ export function safeRedirectToLogin(): void {
// Проверяем, что мы на том же origin // Проверяем, что мы на том же origin
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// Сохраняем текущий URL для возврата после логина
saveReturnUrl()
// Используем только относительный путь для безопасности // Используем только относительный путь для безопасности
window.location.href = loginPath window.location.href = loginPath
} }