diff --git a/backend/api/account/views/UserDataView.py b/backend/api/account/views/UserDataView.py
index 21186da..0077b33 100644
--- a/backend/api/account/views/UserDataView.py
+++ b/backend/api/account/views/UserDataView.py
@@ -29,7 +29,7 @@ class UserDataView(APIView):
value={
"id": 1,
"email": "user@example.com",
- "account_type": "engieneer",
+ "account_type": "engineer",
"name": "Иван",
"surname": "Иванов",
"imageURL": "https://example.com/avatar.jpg",
diff --git a/backend/api/auth/views.py b/backend/api/auth/views.py
index b25a798..950e934 100644
--- a/backend/api/auth/views.py
+++ b/backend/api/auth/views.py
@@ -45,7 +45,7 @@ class LoginViewSet(AuthBaseViewSet):
"user": {
"id": 1,
"email": "user@example.com",
- "account_type": "engieneer",
+ "account_type": "engineer",
"name": "Иван",
"surname": "Иванов",
"imageURL": "https://example.com/avatar.jpg",
diff --git a/frontend/app/(protected)/layout.tsx b/frontend/app/(protected)/layout.tsx
index 6ff9672..3058dcd 100644
--- a/frontend/app/(protected)/layout.tsx
+++ b/frontend/app/(protected)/layout.tsx
@@ -1,4 +1,5 @@
import React from 'react'
+import AuthGuard from '@/components/auth/AuthGuard'
export default function ProtectedLayout({
children,
@@ -6,8 +7,10 @@ export default function ProtectedLayout({
children: React.ReactNode
}) {
return (
-
- {children}
-
+
+
+ {children}
+
+
)
}
\ No newline at end of file
diff --git a/frontend/app/(protected)/navigation/page.tsx b/frontend/app/(protected)/navigation/page.tsx
index 6233cf4..b2b527e 100644
--- a/frontend/app/(protected)/navigation/page.tsx
+++ b/frontend/app/(protected)/navigation/page.tsx
@@ -79,6 +79,8 @@ const NavigationPage: React.FC = () => {
const [detectorsData, setDetectorsData] = useState<{ detectors: Record }>({ detectors: {} })
const [detectorsError, setDetectorsError] = useState(null)
+ const [modelError, setModelError] = useState(null)
+ const [isModelReady, setIsModelReady] = useState(false)
const urlObjectId = searchParams.get('objectId')
const urlObjectTitle = searchParams.get('objectTitle')
@@ -86,10 +88,14 @@ const NavigationPage: React.FC = () => {
const objectTitle = currentObject.title || urlObjectTitle
const handleModelLoaded = useCallback(() => {
+ setIsModelReady(true)
+ setModelError(null)
}, [])
const handleModelError = useCallback((error: string) => {
console.error('Model loading error:', error)
+ setModelError(error)
+ setIsModelReady(false)
}, [])
useEffect(() => {
@@ -131,6 +137,12 @@ const NavigationPage: React.FC = () => {
serial_number: detector.serial_number,
})
+ // Проверяем, что детектор имеет необходимые данные
+ if (!detector || !detector.detector_id || !detector.serial_number) {
+ console.warn('[NavigationPage] Invalid detector data, skipping menu display:', detector)
+ return
+ }
+
if (selectedDetector?.detector_id === detector.detector_id && showDetectorMenu) {
setShowDetectorMenu(false)
setSelectedDetector(null)
@@ -197,6 +209,7 @@ const NavigationPage: React.FC = () => {
detectorsData={detectorsData}
onDetectorMenuClick={handleDetectorMenuClick}
onClose={closeFloorNavigation}
+ is3DReady={isModelReady && !modelError}
/>
@@ -263,24 +276,40 @@ const NavigationPage: React.FC = () => {
-
(
- selectedDetector && showDetectorMenu && anchor ? (
-
- ) : null
- )}
- />
+ {modelError ? (
+
+
+
+ Ошибка загрузки 3D модели
+
+
+ {modelError}
+
+
+ Используйте навигацию по этажам для просмотра детекторов
+
+
+
+ ) : (
+ (
+ selectedDetector && showDetectorMenu && anchor ? (
+
+ ) : null
+ )}
+ />
+ )}
diff --git a/frontend/app/(protected)/objects/page.tsx b/frontend/app/(protected)/objects/page.tsx
index f4dd0f7..0a982ca 100644
--- a/frontend/app/(protected)/objects/page.tsx
+++ b/frontend/app/(protected)/objects/page.tsx
@@ -93,14 +93,9 @@ const ObjectsPage: React.FC = () => {
- Ошибка загрузки
+ Ошибка загрузки данных
{error}
-
+ Если проблема повторяется, обратитесь к администратору
)
diff --git a/frontend/app/api/auth/logout/route.ts b/frontend/app/api/auth/logout/route.ts
new file mode 100644
index 0000000..630eb09
--- /dev/null
+++ b/frontend/app/api/auth/logout/route.ts
@@ -0,0 +1,53 @@
+import { getServerSession } from 'next-auth'
+import { authOptions } from '@/lib/auth'
+import { NextRequest } from 'next/server'
+import { getToken } from 'next-auth/jwt'
+
+export async function POST(req: NextRequest) {
+ try {
+ const session = await getServerSession(authOptions)
+ const secret = process.env.NEXTAUTH_SECRET
+ const token = await getToken({ req, secret }).catch(() => null)
+
+ const accessToken = (session as any)?.accessToken || (token as any)?.accessToken
+ const refreshToken = (session as any)?.refreshToken || (token as any)?.refreshToken
+
+ const backendUrl = process.env.BACKEND_URL
+ if (!backendUrl) {
+ return new Response(JSON.stringify({ success: false, error: 'BACKEND_URL is not configured' }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+
+ if (!refreshToken && !accessToken) {
+ return new Response(JSON.stringify({ success: false, error: 'Unauthorized' }), {
+ status: 401,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+
+ const payload = refreshToken ? { refresh: refreshToken } : {}
+ const headers: Record = { 'Content-Type': 'application/json' }
+ if (accessToken) headers['Authorization'] = `Bearer ${accessToken}`
+
+ const res = await fetch(`${backendUrl}/auth/logout/`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(payload),
+ })
+
+ const text = await res.text().catch(() => '')
+ const contentType = res.headers.get('Content-Type') || 'application/json'
+ return new Response(text || JSON.stringify({ success: res.ok }), {
+ status: res.status,
+ headers: { 'Content-Type': contentType },
+ })
+ } catch (error) {
+ console.error('Error in logout route:', error)
+ return new Response(JSON.stringify({ success: false, error: 'Failed to logout' }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/providers/AuthProvider.tsx b/frontend/app/providers/AuthProvider.tsx
index cc2fa7f..b9ea4d2 100644
--- a/frontend/app/providers/AuthProvider.tsx
+++ b/frontend/app/providers/AuthProvider.tsx
@@ -50,7 +50,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
email: userData.email || session.user.email || '',
image: userData.imageURL || userData.image,
account_type: userData.account_type,
- login: userData.login,
+ login: userData.login ?? session.user.name ?? '',
uuid: userData.uuid,
})
setAuthenticated(true)
diff --git a/frontend/app/store/userStore.ts b/frontend/app/store/userStore.ts
index bbaec70..b79c348 100644
--- a/frontend/app/store/userStore.ts
+++ b/frontend/app/store/userStore.ts
@@ -1,5 +1,4 @@
import { create } from 'zustand'
-import { persist } from 'zustand/middleware'
import { User, UserState } from '../types'
interface UserStore extends UserState {
@@ -16,28 +15,19 @@ interface UserStore extends UserState {
//! что пользователь может делать асинхронно?
}
-const useUserStore = create()(
- persist(
- set => ({
- // начальное состояние
- isAuthenticated: false,
- user: null,
- favorites: [],
+const useUserStore = create(set => ({
+ // начальное состояние
+ isAuthenticated: false,
+ user: null,
+ favorites: [],
- // синхронные действия
- setUser: user => set({ user }),
- setAuthenticated: isAuthenticated => set({ isAuthenticated }),
- logout: () =>
- set({
- isAuthenticated: false,
- user: null,
- }),
- }),
-
- //! асинхронщина?
- { name: 'user-store' }
- )
-)
+ // синхронные действия
+ setUser: user => set({ user }),
+ setAuthenticated: isAuthenticated => set({ isAuthenticated }),
+ logout: () => {
+ set({ isAuthenticated: false, user: null })
+ },
+}))
export default useUserStore
diff --git a/frontend/app/types.ts b/frontend/app/types.ts
index 5c8ae6d..c44ee82 100644
--- a/frontend/app/types.ts
+++ b/frontend/app/types.ts
@@ -1,9 +1,2 @@
- export interface ValidationRules {
- required?: boolean
- minLength?: number
- pattern?: RegExp
-}
-export type ValidationErrors = Record
-
-export type User = object
-export type UserState = object
\ No newline at end of file
+import type { ValidationRules, ValidationErrors, User, UserState } from './types/index'
+export type { ValidationRules, ValidationErrors, User, UserState }
\ No newline at end of file
diff --git a/frontend/components/auth/AuthGuard.tsx b/frontend/components/auth/AuthGuard.tsx
new file mode 100644
index 0000000..b7b135f
--- /dev/null
+++ b/frontend/components/auth/AuthGuard.tsx
@@ -0,0 +1,52 @@
+'use client'
+
+import { useSession } from 'next-auth/react'
+import { useRouter } from 'next/navigation'
+import { useEffect, useState } from 'react'
+
+interface AuthGuardProps {
+ children: React.ReactNode
+}
+
+export default function AuthGuard({ children }: AuthGuardProps) {
+ const { data: session, status } = useSession()
+ const router = useRouter()
+ const [isChecking, setIsChecking] = useState(true)
+
+ useEffect(() => {
+ if (status === 'loading') {
+ return // Ждем загрузки сессии
+ }
+
+ if (status === 'unauthenticated' || !session) {
+ console.log('[AuthGuard] Нет активной сессии, перенаправление на логин')
+ router.replace('/login')
+ return
+ }
+
+ if (status === 'authenticated' && session) {
+ console.log('[AuthGuard] Сессия активна, разрешаем доступ')
+ setIsChecking(false)
+ }
+ }, [session, status, router])
+
+ // Показываем загрузку пока проверяем аутентификацию
+ if (status === 'loading' || isChecking) {
+ return (
+
+
+
+
Проверка аутентификации...
+
+
+ )
+ }
+
+ // Если нет сессии, не рендерим детей (будет перенаправление)
+ if (status === 'unauthenticated' || !session) {
+ return null
+ }
+
+ // Если все в порядке, рендерим детей
+ return <>{children}>
+}
\ No newline at end of file
diff --git a/frontend/components/dashboard/Dashboard.tsx b/frontend/components/dashboard/Dashboard.tsx
index 637a8d1..35a8b31 100644
--- a/frontend/components/dashboard/Dashboard.tsx
+++ b/frontend/components/dashboard/Dashboard.tsx
@@ -215,28 +215,28 @@ const Dashboard: React.FC = () => {
title="Тренды детекторов"
subtitle="За последний месяц"
>
-
+ ({ value: d.value }))} />
-
+ ({ value: d.value }))} />
-
+ ({ value: d.value }))} />
-
+ ({ value: d.value }))} />
diff --git a/frontend/components/dashboard/DetectorChart.tsx b/frontend/components/dashboard/DetectorChart.tsx
index 19b5953..c286344 100644
--- a/frontend/components/dashboard/DetectorChart.tsx
+++ b/frontend/components/dashboard/DetectorChart.tsx
@@ -17,10 +17,11 @@ interface DetectorChartProps {
const DetectorChart: React.FC = ({
className = '',
+ data,
type = 'line'
}) => {
if (type === 'bar') {
- const barData = [
+ const defaultBarData = [
{ value: 85, label: 'Янв' },
{ value: 70, label: 'Фев' },
{ value: 90, label: 'Мар' },
@@ -29,6 +30,13 @@ const DetectorChart: React.FC = ({
{ value: 95, label: 'Июн' }
]
+ const barData = (Array.isArray(data) && data.length > 0)
+ ? data.slice(0, 6).map((d, i) => ({
+ value: d.value || 0,
+ label: d.label || defaultBarData[i]?.label || `${i + 1}`
+ }))
+ : defaultBarData
+
return (
)
}
diff --git a/frontend/components/navigation/FloorNavigation.tsx b/frontend/components/navigation/FloorNavigation.tsx
index 438732a..941cf9e 100644
--- a/frontend/components/navigation/FloorNavigation.tsx
+++ b/frontend/components/navigation/FloorNavigation.tsx
@@ -11,6 +11,7 @@ interface FloorNavigationProps {
detectorsData: DetectorsDataType
onDetectorMenuClick: (detector: DetectorType) => void
onClose?: () => void
+ is3DReady?: boolean
}
interface DetectorType {
@@ -33,7 +34,7 @@ interface DetectorType {
}>
}
-const FloorNavigation: React.FC = ({ objectId, detectorsData, onDetectorMenuClick, onClose }) => {
+const FloorNavigation: React.FC = ({ objectId, detectorsData, onDetectorMenuClick, onClose, is3DReady = true }) => {
const [expandedFloors, setExpandedFloors] = useState>(new Set())
const [searchTerm, setSearchTerm] = useState('')
@@ -95,6 +96,12 @@ const FloorNavigation: React.FC = ({ objectId, detectorsDa
}
const handleDetectorMenuClick = (detector: DetectorType) => {
+ // Проверяем валидность данных детектора перед передачей
+ if (!detector || !detector.detector_id || !detector.serial_number) {
+ console.warn('[FloorNavigation] Invalid detector data, skipping menu click:', detector)
+ return
+ }
+
onDetectorMenuClick(detector)
}
@@ -184,10 +191,20 @@ const FloorNavigation: React.FC = ({ objectId, detectorsDa
)}
diff --git a/frontend/components/navigation/Monitoring.tsx b/frontend/components/navigation/Monitoring.tsx
index c669d54..210555e 100644
--- a/frontend/components/navigation/Monitoring.tsx
+++ b/frontend/components/navigation/Monitoring.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
import Image from 'next/image';
interface MonitoringProps {
@@ -7,6 +7,8 @@ interface MonitoringProps {
}
const Monitoring: React.FC = ({ onClose }) => {
+ const [objectImageError, setObjectImageError] = useState(false);
+
return (
@@ -26,18 +28,26 @@ const Monitoring: React.FC
= ({ onClose }) => {
-
{
- const target = e.target as HTMLImageElement;
- target.style.display = 'none';
- }}
- />
+ {objectImageError ? (
+
+
+ Предпросмотр 3D недоступен
+
+
+ Изображение модели не найдено
+
+
+ ) : (
+ setObjectImageError(true)}
+ />
+ )}
diff --git a/frontend/components/ui/Sidebar.tsx b/frontend/components/ui/Sidebar.tsx
index bfeb086..92ce88f 100644
--- a/frontend/components/ui/Sidebar.tsx
+++ b/frontend/components/ui/Sidebar.tsx
@@ -6,6 +6,8 @@ import Image from 'next/image'
import useUIStore from '../../app/store/uiStore'
import useNavigationStore from '../../app/store/navigationStore'
import { useNavigationService } from '@/services/navigationService'
+import useUserStore from '../../app/store/userStore'
+import { signOut } from 'next-auth/react'
interface NavigationItem {
id: number
@@ -130,8 +132,8 @@ const navigationSubItems: NavigationItem[] = [
const Sidebar: React.FC = ({
logoSrc,
userInfo = {
- name: 'Александр',
- role: 'Администратор'
+ name: '—',
+ role: '—'
},
activeItem: propActiveItem,
onCustomItemClick
@@ -150,7 +152,35 @@ const Sidebar: React.FC = ({
setNavigationSubMenuExpanded: setShowNavigationSubItems,
toggleNavigationSubMenu
} = useUIStore()
-
+ const { user, logout } = useUserStore()
+
+ const roleLabelMap: Record = {
+ engineer: 'Инженер',
+ operator: 'Оператор',
+ admin: 'Администратор',
+ }
+ const fullName = [user?.name, user?.surname].filter(Boolean).join(' ').trim()
+
+ const uiUserInfo = {
+ name: fullName || user?.login || userInfo?.name || '—',
+ role: roleLabelMap[(user?.account_type ?? '').toLowerCase()] || userInfo?.role || '—',
+ avatar: user?.image || userInfo?.avatar,
+ }
+
+ const handleLogout = async () => {
+ try {
+ await fetch('/api/auth/logout', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ })
+ } catch (e) {
+ console.error('Logout request failed:', e)
+ } finally {
+ logout()
+ await signOut({ redirect: true, callbackUrl: '/login' })
+ }
+ }
const {
openMonitoring,
openFloorNavigation,
@@ -401,9 +431,9 @@ const Sidebar: React.FC = ({
role="img"
aria-label="User avatar"
>
- {userInfo.avatar && (
+ {uiUserInfo.avatar && (
= ({
- {userInfo.name}
+ {uiUserInfo.name}
- {userInfo.role}
+ {uiUserInfo.role}
diff --git a/frontend/lib/auth.ts b/frontend/lib/auth.ts
index ea87c93..c6882f6 100644
--- a/frontend/lib/auth.ts
+++ b/frontend/lib/auth.ts
@@ -41,7 +41,8 @@ interface GoogleToken extends JWT {
async function refreshAccessToken(token: GoogleToken): Promise {
try {
- const response = await fetch(`${process.env.BACKEND_URL}/auth/refresh/`, {
+ const BACKEND = process.env.BACKEND_URL || 'http://127.0.0.1:8000/api/v1'
+ const response = await fetch(`${BACKEND}/auth/refresh/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -52,8 +53,14 @@ async function refreshAccessToken(token: GoogleToken): Promise {
})
if (!response.ok) {
- const errorData = await response.json()
- throw errorData
+ const errorText = await response.text()
+ let errorData: { error?: string; [key: string]: unknown } = {}
+ try {
+ errorData = JSON.parse(errorText)
+ } catch {
+ errorData = { error: errorText }
+ }
+ throw new Error(errorData.error || 'Token refresh failed')
}
const refreshedTokens = await response.json()
@@ -85,7 +92,8 @@ export const authOptions: NextAuthOptions = {
},
async authorize(credentials) {
try {
- const res = await fetch(`${process.env.BACKEND_URL}/auth/login/`, {
+ const BACKEND = process.env.BACKEND_URL || 'http://127.0.0.1:8000/api/v1'
+ const res = await fetch(`${BACKEND}/auth/login/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -94,18 +102,34 @@ export const authOptions: NextAuthOptions = {
}),
})
- const data = await res.json()
+ const raw = await res.text()
+ let data: {
+ error?: string;
+ user?: {
+ id: string | number;
+ email: string;
+ name: string;
+ };
+ access?: string;
+ refresh?: string;
+ [key: string]: unknown
+ }
+ try {
+ data = JSON.parse(raw)
+ } catch {
+ data = { error: raw }
+ }
if (!res.ok) {
- throw new Error(data.error || 'Authentication failed')
+ throw new Error(data.error || `Authentication failed (${res.status})`)
}
return {
- id: data.user.id.toString(),
- email: data.user.email,
- name: data.user.firstName,
- accessToken: data.access,
- refreshToken: data.refresh,
+ id: data.user?.id?.toString?.() ?? String(data.user?.id),
+ email: data.user?.email ?? '',
+ name: data.user?.name ?? '', // backend uses `name`, not `firstName`
+ accessToken: data.access ?? '',
+ refreshToken: data.refresh ?? '',
}
} catch (error) {
console.error('Login error:', error)
diff --git a/frontend/public/models/AerBIM-Monitor_ASM-HT-Viewer_Expo2017Astana_20250908_L_+1430.glb b/frontend/public/models/AerBIM-Monitor_ASM-HT-Viewer_Expo2017Astana_20250908_L_+1430.glb
deleted file mode 100644
index 9919c34..0000000
Binary files a/frontend/public/models/AerBIM-Monitor_ASM-HT-Viewer_Expo2017Astana_20250908_L_+1430.glb and /dev/null differ