Merge pull request #22 from BEDOLAGA-DEV/PEDZ

Pedz
This commit is contained in:
PEDZEO
2026-01-18 23:08:12 +03:00
committed by GitHub
17 changed files with 222 additions and 92 deletions

View File

@@ -1,11 +1,11 @@
on: on:
push: push:
branches: [main, develop] branches: [main, dev]
pull_request: pull_request:
branches: [main, develop] branches: [main, dev]
jobs: jobs:
lint-and-typecheck: lint-and-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -13,29 +13,12 @@ jobs:
run: npm ci run: npm ci
- name: Run ESLint - name: Run ESLint
run: npm run lint -- --max-warnings 50 run: npm run lint
- name: Type check - name: Run TypeScript check
run: npm run type-check run: npx tsc --noEmit
build: - name: Build
runs-on: ubuntu-latest
needs: lint-and-typecheck
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
run: npm ci
- name: Build application
run: npm run build run: npm run build
env: env:
VITE_API_URL: /api VITE_API_URL: /api
@@ -48,4 +31,4 @@ jobs:
with: with:
name: dist name: dist
path: dist/ path: dist/
retention-days: 7 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,7 @@
import { useState, useRef } from 'react' import { useState, useRef } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { import {
adminAppsApi, adminAppsApi,
AppDefinition, AppDefinition,
@@ -14,6 +15,12 @@ import {
} from '../api/adminApps' } from '../api/adminApps'
// Icons // Icons
const BackIcon = () => (
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
)
const AppsIcon = () => ( const AppsIcon = () => (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" /> <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
@@ -640,7 +647,12 @@ export default function AdminApps() {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between flex-wrap gap-3"> <div className="flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<AppsIcon /> <Link
to="/admin"
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"
>
<BackIcon />
</Link>
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('admin.apps.title')}</h1> <h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('admin.apps.title')}</h1>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { useState, useRef, useMemo } from 'react' import { useState, useRef, useMemo } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { import {
adminBroadcastsApi, adminBroadcastsApi,
Broadcast, Broadcast,
@@ -10,6 +11,12 @@ import {
} from '../api/adminBroadcasts' } from '../api/adminBroadcasts'
// Icons // Icons
const BackIcon = () => (
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
)
const BroadcastIcon = () => ( const BroadcastIcon = () => (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46" /> <path strokeLinecap="round" strokeLinejoin="round" d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46" />
@@ -618,9 +625,12 @@ export default function AdminBroadcasts() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-accent-500/20 rounded-lg text-accent-400"> <Link
<BroadcastIcon /> to="/admin"
</div> 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"
>
<BackIcon />
</Link>
<div> <div>
<h1 className="text-xl font-bold text-dark-100">{t('admin.broadcasts.title')}</h1> <h1 className="text-xl font-bold text-dark-100">{t('admin.broadcasts.title')}</h1>
<p className="text-sm text-dark-400">{t('admin.broadcasts.subtitle')}</p> <p className="text-sm text-dark-400">{t('admin.broadcasts.subtitle')}</p>

View File

@@ -1,5 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { import {
campaignsApi, campaignsApi,
CampaignListItem, CampaignListItem,
@@ -14,9 +15,9 @@ import {
} from '../api/campaigns' } from '../api/campaigns'
// Icons // Icons
const CampaignIcon = () => ( const BackIcon = () => (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" /> <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg> </svg>
) )
@@ -741,9 +742,12 @@ export default function AdminCampaigns() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-orange-500/20 rounded-lg text-orange-400"> <Link
<CampaignIcon /> to="/admin"
</div> 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"
>
<BackIcon />
</Link>
<div> <div>
<h1 className="text-xl font-semibold text-dark-100">Рекламные кампании</h1> <h1 className="text-xl font-semibold text-dark-100">Рекламные кампании</h1>
<p className="text-sm text-dark-400">Управление рекламными ссылками</p> <p className="text-sm text-dark-400">Управление рекламными ссылками</p>

View File

@@ -1,12 +1,13 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { statsApi, type DashboardStats, type NodeStatus } from '../api/admin' import { statsApi, type DashboardStats, type NodeStatus } from '../api/admin'
import { useCurrency } from '../hooks/useCurrency' import { useCurrency } from '../hooks/useCurrency'
// Icons // Icons
const ChartIcon = () => ( const BackIcon = () => (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg> </svg>
) )
@@ -292,9 +293,12 @@ export default function AdminDashboard() {
{/* 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">
<div className="p-3 bg-accent-500/20 rounded-xl"> <Link
<ChartIcon /> to="/admin"
</div> 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"
>
<BackIcon />
</Link>
<div> <div>
<h1 className="text-2xl font-bold text-dark-100">{t('adminDashboard.title')}</h1> <h1 className="text-2xl font-bold text-dark-100">{t('adminDashboard.title')}</h1>
<p className="text-dark-400">{t('adminDashboard.subtitle')}</p> <p className="text-dark-400">{t('adminDashboard.subtitle')}</p>

View File

@@ -1,5 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { import {
promoOffersApi, promoOffersApi,
PromoOfferTemplate, PromoOfferTemplate,
@@ -12,9 +13,9 @@ import {
} from '../api/promoOffers' } from '../api/promoOffers'
// Icons // Icons
const GiftIcon = () => ( const BackIcon = () => (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg> </svg>
) )
@@ -576,9 +577,12 @@ export default function AdminPromoOffers() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-orange-500/20 rounded-lg text-orange-400"> <Link
<GiftIcon /> to="/admin"
</div> 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"
>
<BackIcon />
</Link>
<div> <div>
<h1 className="text-xl font-semibold text-dark-100">Промопредложения</h1> <h1 className="text-xl font-semibold text-dark-100">Промопредложения</h1>
<p className="text-sm text-dark-400">Шаблоны, рассылка и логи предложений</p> <p className="text-sm text-dark-400">Шаблоны, рассылка и логи предложений</p>

View File

@@ -1,5 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { import {
promocodesApi, promocodesApi,
PromoCode, PromoCode,
@@ -13,9 +14,9 @@ import {
} from '../api/promocodes' } from '../api/promocodes'
// Icons // Icons
const TicketIcon = () => ( const BackIcon = () => (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg> </svg>
) )
@@ -800,9 +801,12 @@ export default function AdminPromocodes() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-violet-500/20 rounded-lg"> <Link
<TicketIcon /> to="/admin"
</div> 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"
>
<BackIcon />
</Link>
<div> <div>
<h1 className="text-xl font-semibold text-dark-100">Промокоды</h1> <h1 className="text-xl font-semibold text-dark-100">Промокоды</h1>
<p className="text-sm text-dark-400">Управление промокодами и группами скидок</p> <p className="text-sm text-dark-400">Управление промокодами и группами скидок</p>

View File

@@ -1,6 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { import {
serversApi, serversApi,
ServerListItem, ServerListItem,
@@ -9,9 +10,9 @@ import {
} from '../api/servers' } from '../api/servers'
// Icons // Icons
const ServerIcon = () => ( const BackIcon = () => (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg> </svg>
) )
@@ -322,9 +323,12 @@ export default function AdminServers() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-accent-500/20 rounded-lg"> <Link
<ServerIcon /> to="/admin"
</div> 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"
>
<BackIcon />
</Link>
<div> <div>
<h1 className="text-xl font-semibold text-dark-100">{t('admin.servers.title')}</h1> <h1 className="text-xl font-semibold text-dark-100">{t('admin.servers.title')}</h1>
<p className="text-sm text-dark-400">{t('admin.servers.subtitle')}</p> <p className="text-sm text-dark-400">{t('admin.servers.subtitle')}</p>

View File

@@ -1,6 +1,7 @@
import { useState, useMemo, useRef } from 'react' import { useState, useMemo, useRef } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { adminSettingsApi, SettingDefinition, SettingCategorySummary } from '../api/adminSettings' import { adminSettingsApi, SettingDefinition, SettingCategorySummary } from '../api/adminSettings'
import { brandingApi } from '../api/branding' import { brandingApi } from '../api/branding'
import { themeColorsApi } from '../api/themeColors' import { themeColorsApi } from '../api/themeColors'
@@ -10,6 +11,12 @@ import { applyThemeColors } from '../hooks/useThemeColors'
import { updateEnabledThemesCache } from '../hooks/useTheme' import { updateEnabledThemesCache } from '../hooks/useTheme'
// Icons // Icons
const BackIcon = () => (
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
)
const CogIcon = () => ( const CogIcon = () => (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
@@ -1033,7 +1040,12 @@ export default function AdminSettings() {
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<CogIcon /> <Link
to="/admin"
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"
>
<BackIcon />
</Link>
<div> <div>
<h1 className="text-2xl font-bold text-dark-50">{t('admin.settings.title')}</h1> <h1 className="text-2xl font-bold text-dark-50">{t('admin.settings.title')}</h1>
<p className="text-sm text-dark-400"> <p className="text-sm text-dark-400">

View File

@@ -1,6 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { import {
tariffsApi, tariffsApi,
TariffListItem, TariffListItem,
@@ -13,9 +14,9 @@ import {
} from '../api/tariffs' } from '../api/tariffs'
// Icons // Icons
const TariffIcon = () => ( const BackIcon = () => (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg> </svg>
) )
@@ -1187,9 +1188,12 @@ export default function AdminTariffs() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-accent-500/20 rounded-lg"> <Link
<TariffIcon /> to="/admin"
</div> 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"
>
<BackIcon />
</Link>
<div> <div>
<h1 className="text-xl font-semibold text-dark-100">{t('admin.tariffs.title')}</h1> <h1 className="text-xl font-semibold text-dark-100">{t('admin.tariffs.title')}</h1>
<p className="text-sm text-dark-400">{t('admin.tariffs.subtitle')}</p> <p className="text-sm text-dark-400">{t('admin.tariffs.subtitle')}</p>

View File

@@ -1,9 +1,16 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { adminApi, AdminTicket, AdminTicketDetail, AdminTicketMessage } from '../api/admin' import { adminApi, AdminTicket, AdminTicketDetail, AdminTicketMessage } from '../api/admin'
import { ticketsApi } from '../api/tickets' import { ticketsApi } from '../api/tickets'
const BackIcon = () => (
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
)
function AdminMessageMedia({ message, t }: { message: AdminTicketMessage; t: (key: string) => string }) { function AdminMessageMedia({ message, t }: { message: AdminTicketMessage; t: (key: string) => string }) {
const [imageLoaded, setImageLoaded] = useState(false) const [imageLoaded, setImageLoaded] = useState(false)
const [imageError, setImageError] = useState(false) const [imageError, setImageError] = useState(false)
@@ -175,7 +182,15 @@ export default function AdminTickets() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('admin.tickets.title')}</h1> <div className="flex items-center gap-3">
<Link
to="/admin"
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"
>
<BackIcon />
</Link>
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('admin.tickets.title')}</h1>
</div>
<button <button
onClick={() => setShowSettings(true)} onClick={() => setShowSettings(true)}
className="btn-secondary flex items-center gap-2" className="btn-secondary flex items-center gap-2"

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Link } from 'react-router-dom'
import { useCurrency } from '../hooks/useCurrency' import { useCurrency } from '../hooks/useCurrency'
import { import {
adminUsersApi, adminUsersApi,
@@ -11,12 +12,6 @@ import {
// ============ Icons ============ // ============ Icons ============
const UsersIcon = ({ className = "w-6 h-6" }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg>
)
const SearchIcon = () => ( const SearchIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
@@ -861,9 +856,12 @@ export default function AdminUsers() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2.5 bg-indigo-500/20 rounded-xl"> <Link
<UsersIcon className="w-7 h-7 text-indigo-400" /> to="/admin"
</div> 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"
>
<ChevronLeftIcon />
</Link>
<div> <div>
<h1 className="text-xl font-bold text-dark-100">Пользователи</h1> <h1 className="text-xl font-bold text-dark-100">Пользователи</h1>
<p className="text-sm text-dark-400">Управление пользователями бота</p> <p className="text-sm text-dark-400">Управление пользователями бота</p>

View File

@@ -1,9 +1,16 @@
import { useState } from 'react' import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { adminWheelApi, type WheelPrizeAdmin } from '../api/wheel' import { adminWheelApi, type WheelPrizeAdmin } from '../api/wheel'
// Icons // Icons
const BackIcon = () => (
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
)
const CogIcon = () => ( const CogIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
@@ -113,9 +120,17 @@ export default function AdminWheel() {
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50"> <div className="flex items-center gap-3">
{t('admin.wheel.title')} <Link
</h1> to="/admin"
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"
>
<BackIcon />
</Link>
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">
{t('admin.wheel.title')}
</h1>
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={`px-3 py-1 rounded-full text-sm ${ <span className={`px-3 py-1 rounded-full text-sm ${
config.is_enabled ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400' config.is_enabled ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'

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

@@ -145,6 +145,8 @@ export const useAuthStore = create<AuthState>()(
const newToken = await tokenRefreshManager.refreshAccessToken() const newToken = await tokenRefreshManager.refreshAccessToken()
if (newToken) { if (newToken) {
const user = await authApi.getMe() const user = await authApi.getMe()
// Сначала проверяем admin статус, потом снимаем isLoading
await get().checkAdminStatus()
set({ set({
accessToken: newToken, accessToken: newToken,
refreshToken, refreshToken,
@@ -152,7 +154,6 @@ export const useAuthStore = create<AuthState>()(
isAuthenticated: true, isAuthenticated: true,
isLoading: false, isLoading: false,
}) })
get().checkAdminStatus()
} else { } else {
tokenStorage.clearTokens() tokenStorage.clearTokens()
set({ set({
@@ -168,6 +169,8 @@ export const useAuthStore = create<AuthState>()(
try { try {
const user = await authApi.getMe() const user = await authApi.getMe()
// Сначала проверяем admin статус, потом снимаем isLoading
await get().checkAdminStatus()
set({ set({
accessToken, accessToken,
refreshToken, refreshToken,
@@ -175,13 +178,14 @@ export const useAuthStore = create<AuthState>()(
isAuthenticated: true, isAuthenticated: true,
isLoading: false, isLoading: false,
}) })
get().checkAdminStatus()
} catch { } catch {
// Token might be invalid on server, try to refresh // Token might be invalid on server, try to refresh
const newToken = await tokenRefreshManager.refreshAccessToken() const newToken = await tokenRefreshManager.refreshAccessToken()
if (newToken) { if (newToken) {
try { try {
const user = await authApi.getMe() const user = await authApi.getMe()
// Сначала проверяем admin статус, потом снимаем isLoading
await get().checkAdminStatus()
set({ set({
accessToken: newToken, accessToken: newToken,
refreshToken, refreshToken,
@@ -189,7 +193,6 @@ export const useAuthStore = create<AuthState>()(
isAuthenticated: true, isAuthenticated: true,
isLoading: false, isLoading: false,
}) })
get().checkAdminStatus()
} catch { } catch {
tokenStorage.clearTokens() tokenStorage.clearTokens()
set({ set({

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
} }