{t('banSystem.title')}
{t('banSystem.subtitle')}
diff --git a/src/pages/AdminTickets.tsx b/src/pages/AdminTickets.tsx
index 9d6b622..1206b13 100644
--- a/src/pages/AdminTickets.tsx
+++ b/src/pages/AdminTickets.tsx
@@ -460,6 +460,8 @@ function TicketSettingsModal({ onClose }: { onClose: () => void }) {
sla_check_interval_seconds: settings?.sla_check_interval_seconds ?? 60,
sla_reminder_cooldown_minutes: settings?.sla_reminder_cooldown_minutes ?? 15,
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
@@ -471,6 +473,8 @@ function TicketSettingsModal({ onClose }: { onClose: () => void }) {
sla_check_interval_seconds: settings.sla_check_interval_seconds,
sla_reminder_cooldown_minutes: settings.sla_reminder_cooldown_minutes,
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])
@@ -526,6 +530,43 @@ function TicketSettingsModal({ onClose }: { onClose: () => void }) {
{t('admin.tickets.supportModeDesc')}
{t('admin.tickets.slaSettings')}
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/types/index.ts b/src/types/index.ts
index d040af7..099f748 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -517,6 +517,7 @@ export interface ManualCheckResponse {
new_status: string | null
}
+<<<<<<< HEAD
// Ticket notification types
export interface TicketNotification {
id: number
@@ -525,13 +526,42 @@ export interface TicketNotification {
message: string
is_read: boolean
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 {
items: TicketNotification[]
+<<<<<<< HEAD
total: number
+=======
+ unread_count: number
+>>>>>>> af948391078db4bb97a333feb7e0ddfcf638b076
}
export interface UnreadCountResponse {
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
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
}