mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
Enhance CI workflow with environment variables for build and add artifact upload step. Update ProtectedRoute and AdminRoute to save return URL for authentication. Introduce cabinet notification settings in admin ticket management and update localization files for new notification strings.
This commit is contained in:
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -1,5 +1,3 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, dev]
|
branches: [main, dev]
|
||||||
@@ -31,3 +29,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
|
||||||
|
|||||||
13
src/App.tsx
13
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 { 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) {
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ export interface TicketSettings {
|
|||||||
sla_check_interval_seconds: number
|
sla_check_interval_seconds: number
|
||||||
sla_reminder_cooldown_minutes: number
|
sla_reminder_cooldown_minutes: number
|
||||||
support_system_mode: string // tickets, contact, both
|
support_system_mode: string // tickets, contact, both
|
||||||
|
cabinet_user_notifications_enabled: boolean
|
||||||
|
cabinet_admin_notifications_enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketSettingsUpdate {
|
export interface TicketSettingsUpdate {
|
||||||
@@ -67,6 +69,8 @@ export interface TicketSettingsUpdate {
|
|||||||
sla_check_interval_seconds?: number
|
sla_check_interval_seconds?: number
|
||||||
sla_reminder_cooldown_minutes?: number
|
sla_reminder_cooldown_minutes?: number
|
||||||
support_system_mode?: string
|
support_system_mode?: string
|
||||||
|
cabinet_user_notifications_enabled?: boolean
|
||||||
|
cabinet_admin_notifications_enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminTicketListResponse {
|
export interface AdminTicketListResponse {
|
||||||
|
|||||||
@@ -35,6 +35,20 @@
|
|||||||
"info": "Info",
|
"info": "Info",
|
||||||
"wheel": "Fortune Wheel"
|
"wheel": "Fortune Wheel"
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"ticketNotifications": "Ticket Notifications",
|
||||||
|
"markAllRead": "Mark all read",
|
||||||
|
"noNotifications": "No notifications",
|
||||||
|
"justNow": "Just now",
|
||||||
|
"minutesAgo": "{{count}} min ago",
|
||||||
|
"hoursAgo": "{{count}} h ago",
|
||||||
|
"daysAgo": "{{count}} d ago",
|
||||||
|
"viewAll": "View all tickets",
|
||||||
|
"newNotification": "New notification",
|
||||||
|
"clickToView": "Click to view",
|
||||||
|
"newTicket": "New ticket: {{title}}",
|
||||||
|
"newReply": "New reply in ticket"
|
||||||
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
@@ -732,7 +746,12 @@
|
|||||||
"reminderCooldown": "Reminder interval (minutes)",
|
"reminderCooldown": "Reminder interval (minutes)",
|
||||||
"reminderCooldownDesc": "Minimum time between reminders (1-120 minutes)",
|
"reminderCooldownDesc": "Minimum time between reminders (1-120 minutes)",
|
||||||
"settingsUpdateError": "Error saving settings",
|
"settingsUpdateError": "Error saving settings",
|
||||||
"copyTelegramId": "Click to copy Telegram ID"
|
"copyTelegramId": "Click to copy Telegram ID",
|
||||||
|
"cabinetNotifications": "Cabinet Notifications",
|
||||||
|
"userNotificationsEnabled": "User Notifications",
|
||||||
|
"userNotificationsEnabledDesc": "Send notifications to users about admin replies",
|
||||||
|
"adminNotificationsEnabled": "Admin Notifications",
|
||||||
|
"adminNotificationsEnabledDesc": "Send notifications to admins about new tickets and replies"
|
||||||
},
|
},
|
||||||
"tariffs": {
|
"tariffs": {
|
||||||
"title": "Tariff Management",
|
"title": "Tariff Management",
|
||||||
|
|||||||
@@ -35,6 +35,20 @@
|
|||||||
"info": "Информация",
|
"info": "Информация",
|
||||||
"wheel": "Колесо удачи"
|
"wheel": "Колесо удачи"
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"ticketNotifications": "Уведомления о тикетах",
|
||||||
|
"markAllRead": "Прочитать все",
|
||||||
|
"noNotifications": "Нет уведомлений",
|
||||||
|
"justNow": "Только что",
|
||||||
|
"minutesAgo": "{{count}} мин. назад",
|
||||||
|
"hoursAgo": "{{count}} ч. назад",
|
||||||
|
"daysAgo": "{{count}} д. назад",
|
||||||
|
"viewAll": "Все тикеты",
|
||||||
|
"newNotification": "Новое уведомление",
|
||||||
|
"clickToView": "Нажмите для просмотра",
|
||||||
|
"newTicket": "Новый тикет: {{title}}",
|
||||||
|
"newReply": "Новый ответ в тикете"
|
||||||
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "Вход",
|
"login": "Вход",
|
||||||
"register": "Регистрация",
|
"register": "Регистрация",
|
||||||
@@ -732,7 +746,12 @@
|
|||||||
"reminderCooldown": "Интервал напоминаний (минуты)",
|
"reminderCooldown": "Интервал напоминаний (минуты)",
|
||||||
"reminderCooldownDesc": "Минимальное время между напоминаниями (1-120 минут)",
|
"reminderCooldownDesc": "Минимальное время между напоминаниями (1-120 минут)",
|
||||||
"settingsUpdateError": "Ошибка сохранения настроек",
|
"settingsUpdateError": "Ошибка сохранения настроек",
|
||||||
"copyTelegramId": "Нажмите чтобы скопировать Telegram ID"
|
"copyTelegramId": "Нажмите чтобы скопировать Telegram ID",
|
||||||
|
"cabinetNotifications": "Уведомления в кабинете",
|
||||||
|
"userNotificationsEnabled": "Уведомления для пользователей",
|
||||||
|
"userNotificationsEnabledDesc": "Отправлять уведомления пользователям об ответах админа",
|
||||||
|
"adminNotificationsEnabled": "Уведомления для админов",
|
||||||
|
"adminNotificationsEnabledDesc": "Отправлять уведомления админам о новых тикетах и ответах"
|
||||||
},
|
},
|
||||||
"tariffs": {
|
"tariffs": {
|
||||||
"title": "Управление тарифами",
|
"title": "Управление тарифами",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -460,6 +460,8 @@ function TicketSettingsModal({ onClose }: { onClose: () => void }) {
|
|||||||
sla_check_interval_seconds: settings?.sla_check_interval_seconds ?? 60,
|
sla_check_interval_seconds: settings?.sla_check_interval_seconds ?? 60,
|
||||||
sla_reminder_cooldown_minutes: settings?.sla_reminder_cooldown_minutes ?? 15,
|
sla_reminder_cooldown_minutes: settings?.sla_reminder_cooldown_minutes ?? 15,
|
||||||
support_system_mode: settings?.support_system_mode ?? 'both',
|
support_system_mode: settings?.support_system_mode ?? 'both',
|
||||||
|
cabinet_user_notifications_enabled: settings?.cabinet_user_notifications_enabled ?? true,
|
||||||
|
cabinet_admin_notifications_enabled: settings?.cabinet_admin_notifications_enabled ?? true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update form when settings load
|
// Update form when settings load
|
||||||
@@ -471,6 +473,8 @@ function TicketSettingsModal({ onClose }: { onClose: () => void }) {
|
|||||||
sla_check_interval_seconds: settings.sla_check_interval_seconds,
|
sla_check_interval_seconds: settings.sla_check_interval_seconds,
|
||||||
sla_reminder_cooldown_minutes: settings.sla_reminder_cooldown_minutes,
|
sla_reminder_cooldown_minutes: settings.sla_reminder_cooldown_minutes,
|
||||||
support_system_mode: settings.support_system_mode,
|
support_system_mode: settings.support_system_mode,
|
||||||
|
cabinet_user_notifications_enabled: settings.cabinet_user_notifications_enabled ?? true,
|
||||||
|
cabinet_admin_notifications_enabled: settings.cabinet_admin_notifications_enabled ?? true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [settings])
|
}, [settings])
|
||||||
@@ -526,6 +530,43 @@ function TicketSettingsModal({ onClose }: { onClose: () => void }) {
|
|||||||
<p className="text-xs text-dark-500 mt-1">{t('admin.tickets.supportModeDesc')}</p>
|
<p className="text-xs text-dark-500 mt-1">{t('admin.tickets.supportModeDesc')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Cabinet Notifications */}
|
||||||
|
<div className="border-t border-dark-800/50 pt-6">
|
||||||
|
<h3 className="text-lg font-semibold text-dark-100 mb-4">{t('admin.tickets.cabinetNotifications')}</h3>
|
||||||
|
|
||||||
|
{/* User Notifications */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.cabinet_user_notifications_enabled}
|
||||||
|
onChange={(e) => setFormData({ ...formData, cabinet_user_notifications_enabled: e.target.checked })}
|
||||||
|
className="w-5 h-5 rounded border-dark-700 bg-dark-800 text-accent-500 focus:ring-2 focus:ring-accent-500 focus:ring-offset-0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="text-dark-100 font-medium">{t('admin.tickets.userNotificationsEnabled')}</div>
|
||||||
|
<div className="text-sm text-dark-500">{t('admin.tickets.userNotificationsEnabledDesc')}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Admin Notifications */}
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.cabinet_admin_notifications_enabled}
|
||||||
|
onChange={(e) => setFormData({ ...formData, cabinet_admin_notifications_enabled: e.target.checked })}
|
||||||
|
className="w-5 h-5 rounded border-dark-700 bg-dark-800 text-accent-500 focus:ring-2 focus:ring-accent-500 focus:ring-offset-0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="text-dark-100 font-medium">{t('admin.tickets.adminNotificationsEnabled')}</div>
|
||||||
|
<div className="text-sm text-dark-500">{t('admin.tickets.adminNotificationsEnabledDesc')}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-dark-800/50 pt-6">
|
<div className="border-t border-dark-800/50 pt-6">
|
||||||
<h3 className="text-lg font-semibold text-dark-100 mb-4">{t('admin.tickets.slaSettings')}</h3>
|
<h3 className="text-lg font-semibold text-dark-100 mb-4">{t('admin.tickets.slaSettings')}</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -517,6 +517,7 @@ export interface ManualCheckResponse {
|
|||||||
new_status: string | null
|
new_status: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
// Ticket notification types
|
// Ticket notification types
|
||||||
export interface TicketNotification {
|
export interface TicketNotification {
|
||||||
id: number
|
id: number
|
||||||
@@ -525,13 +526,42 @@ export interface TicketNotification {
|
|||||||
message: string
|
message: string
|
||||||
is_read: boolean
|
is_read: boolean
|
||||||
created_at: string
|
created_at: string
|
||||||
|
=======
|
||||||
|
// Ticket notifications types
|
||||||
|
export interface TicketNotification {
|
||||||
|
id: number
|
||||||
|
ticket_id: number
|
||||||
|
notification_type: 'new_ticket' | 'admin_reply' | 'user_reply'
|
||||||
|
message: string | null
|
||||||
|
is_read: boolean
|
||||||
|
created_at: string
|
||||||
|
read_at: string | null
|
||||||
|
>>>>>>> af948391078db4bb97a333feb7e0ddfcf638b076
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketNotificationList {
|
export interface TicketNotificationList {
|
||||||
items: TicketNotification[]
|
items: TicketNotification[]
|
||||||
|
<<<<<<< HEAD
|
||||||
total: number
|
total: number
|
||||||
|
=======
|
||||||
|
unread_count: number
|
||||||
|
>>>>>>> af948391078db4bb97a333feb7e0ddfcf638b076
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnreadCountResponse {
|
export interface UnreadCountResponse {
|
||||||
unread_count: number
|
unread_count: number
|
||||||
}
|
}
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
|
||||||
|
// Extended TicketSettings with cabinet notifications
|
||||||
|
export interface TicketSettings {
|
||||||
|
sla_enabled: boolean
|
||||||
|
sla_minutes: number
|
||||||
|
sla_check_interval_seconds: number
|
||||||
|
sla_reminder_cooldown_minutes: number
|
||||||
|
support_system_mode: string
|
||||||
|
cabinet_user_notifications_enabled: boolean
|
||||||
|
cabinet_admin_notifications_enabled: boolean
|
||||||
|
}
|
||||||
|
>>>>>>> af948391078db4bb97a333feb7e0ddfcf638b076
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user