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 (
@@ -69,9 +77,40 @@ const DetectorChart: React.FC = ({ ) } + // Line chart implementation + const defaultLineData = [ + { value: 150 }, + { value: 120 }, + { value: 100 }, + { value: 80 }, + { value: 90 }, + { value: 70 }, + { value: 60 } + ] + + const lineData = (Array.isArray(data) && data.length > 0) + ? data.slice(0, 7) + : defaultLineData + + const maxVal = Math.max(...lineData.map(d => d.value || 0), 1) + const width = 400 + const height = 200 + const padding = 20 + const plotHeight = height - 40 + const stepX = lineData.length > 1 ? (width - 2 * padding) / (lineData.length - 1) : 0 + + const points = lineData.map((d, i) => { + const x = padding + i * stepX + const y = height - padding - ((d.value || 0) / maxVal) * plotHeight + return { x, y } + }) + + const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ') + const areaPath = `${linePath} L${width - padding},${height - padding} L${padding},${height - padding} Z` + return (
- + @@ -79,22 +118,18 @@ const DetectorChart: React.FC = ({ - - - - - - - + {points.map((p, i) => ( + + ))}
) diff --git a/frontend/components/model/ModelViewer.tsx b/frontend/components/model/ModelViewer.tsx index 7bc1f3f..3566eb1 100644 --- a/frontend/components/model/ModelViewer.tsx +++ b/frontend/components/model/ModelViewer.tsx @@ -141,7 +141,14 @@ const ModelViewer: React.FC = ({ }, []) useEffect(() => { - if (!isInitializedRef.current || !modelPath || isDisposedRef.current) { + if (!isInitializedRef.current || isDisposedRef.current) { + return + } + + // Check if modelPath is provided + if (!modelPath) { + console.warn('[ModelViewer] No model path provided') + onError?.('Путь к 3D модели не задан') return } @@ -209,13 +216,14 @@ const ModelViewer: React.FC = ({ }, 500) } else { console.warn('No meshes found in model') - onError?.('No geometry found in model') + onError?.('В модели не найдена геометрия') setIsLoading(false) } } catch (error) { clearInterval(progressInterval) console.error('Error loading GLTF model:', error) - onError?.(`Failed to load model: ${error}`) + const errorMessage = error instanceof Error ? error.message : String(error) + onError?.(`Ошибка загрузки модели: ${errorMessage}`) setIsLoading(false) } } @@ -239,7 +247,25 @@ const ModelViewer: React.FC = ({ } const allMeshes = importedMeshesRef.current || [] - const sensorMeshes = allMeshes.filter((m: any) => ((m.id ?? '').includes('IfcSensor') || (m.name ?? '').includes('IfcSensor'))) + + // Safeguard: Check if we have any meshes at all + if (allMeshes.length === 0) { + console.warn('[ModelViewer] No meshes available for sensor matching') + highlightLayerRef.current?.removeAllMeshes() + chosenMeshRef.current = null + setOverlayPos(null) + setOverlayData(null) + return + } + + const sensorMeshes = allMeshes.filter((m: any) => { + try { + return ((m.id ?? '').includes('IfcSensor') || (m.name ?? '').includes('IfcSensor')) + } catch (error) { + console.warn('[ModelViewer] Error filtering sensor mesh:', error) + return false + } + }) const chosen = sensorMeshes.find((m: any) => { try { @@ -248,7 +274,8 @@ const ModelViewer: React.FC = ({ const sid = extras?.Sensor_ID ?? extras?.sensor_id ?? extras?.SERIAL_NUMBER ?? extras?.serial_number if (sid == null) return false return String(sid).trim() === sensorId - } catch { + } catch (error) { + console.warn('[ModelViewer] Error matching sensor mesh:', error) return false } }) @@ -264,44 +291,52 @@ const ModelViewer: React.FC = ({ const scene = sceneRef.current! if (chosen) { - const camera = scene.activeCamera as ArcRotateCamera - const bbox = (typeof chosen.getHierarchyBoundingVectors === 'function') - ? chosen.getHierarchyBoundingVectors() - : { min: chosen.getBoundingInfo().boundingBox.minimumWorld, max: chosen.getBoundingInfo().boundingBox.maximumWorld } - const center = bbox.min.add(bbox.max).scale(0.5) - const size = bbox.max.subtract(bbox.min) - const maxDimension = Math.max(size.x, size.y, size.z) - const targetRadius = Math.max(camera.lowerRadiusLimit ?? 2, maxDimension * 1.5) + try { + const camera = scene.activeCamera as ArcRotateCamera + const bbox = (typeof chosen.getHierarchyBoundingVectors === 'function') + ? chosen.getHierarchyBoundingVectors() + : { min: chosen.getBoundingInfo().boundingBox.minimumWorld, max: chosen.getBoundingInfo().boundingBox.maximumWorld } + const center = bbox.min.add(bbox.max).scale(0.5) + const size = bbox.max.subtract(bbox.min) + const maxDimension = Math.max(size.x, size.y, size.z) + const targetRadius = Math.max(camera.lowerRadiusLimit ?? 2, maxDimension * 1.5) - scene.stopAnimation(camera) + scene.stopAnimation(camera) - const ease = new CubicEase() - ease.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT) - const frameRate = 60 - const durationMs = 600 - const totalFrames = Math.round((durationMs / 1000) * frameRate) + const ease = new CubicEase() + ease.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT) + const frameRate = 60 + const durationMs = 600 + const totalFrames = Math.round((durationMs / 1000) * frameRate) - Animation.CreateAndStartAnimation('camTarget', camera, 'target', frameRate, totalFrames, camera.target.clone(), center.clone(), Animation.ANIMATIONLOOPMODE_CONSTANT, ease) - Animation.CreateAndStartAnimation('camRadius', camera, 'radius', frameRate, totalFrames, camera.radius, targetRadius, Animation.ANIMATIONLOOPMODE_CONSTANT, ease) + Animation.CreateAndStartAnimation('camTarget', camera, 'target', frameRate, totalFrames, camera.target.clone(), center.clone(), Animation.ANIMATIONLOOPMODE_CONSTANT, ease) + Animation.CreateAndStartAnimation('camRadius', camera, 'radius', frameRate, totalFrames, camera.radius, targetRadius, Animation.ANIMATIONLOOPMODE_CONSTANT, ease) - const hl = highlightLayerRef.current - if (hl) { - hl.removeAllMeshes() - if (chosen instanceof Mesh) { - hl.addMesh(chosen, new Color3(1, 1, 0)) - } else if (chosen instanceof InstancedMesh) { - hl.addMesh(chosen.sourceMesh, new Color3(1, 1, 0)) - } else { - const children = typeof (chosen as any)?.getChildMeshes === 'function' ? (chosen as any).getChildMeshes() : [] - for (const cm of children) { - if (cm instanceof Mesh) { - hl.addMesh(cm, new Color3(1, 1, 0)) + const hl = highlightLayerRef.current + if (hl) { + hl.removeAllMeshes() + if (chosen instanceof Mesh) { + hl.addMesh(chosen, new Color3(1, 1, 0)) + } else if (chosen instanceof InstancedMesh) { + hl.addMesh(chosen.sourceMesh, new Color3(1, 1, 0)) + } else { + const children = typeof (chosen as any)?.getChildMeshes === 'function' ? (chosen as any).getChildMeshes() : [] + for (const cm of children) { + if (cm instanceof Mesh) { + hl.addMesh(cm, new Color3(1, 1, 0)) + } } } } + chosenMeshRef.current = chosen + setOverlayData({ name: chosen.name, sensorId }) + } catch (error) { + console.error('[ModelViewer] Error focusing on sensor mesh:', error) + highlightLayerRef.current?.removeAllMeshes() + chosenMeshRef.current = null + setOverlayPos(null) + setOverlayData(null) } - chosenMeshRef.current = chosen - setOverlayData({ name: chosen.name, sensorId }) } else { highlightLayerRef.current?.removeAllMeshes() chosenMeshRef.current = null @@ -316,15 +351,22 @@ const ModelViewer: React.FC = ({ const observer = scene.onAfterRenderObservable.add(() => { const chosen = chosenMeshRef.current if (!chosen) return - const engine = scene.getEngine() - const cam = scene.activeCamera - if (!cam) return - const center = chosen.getBoundingInfo().boundingBox.centerWorld - const world = Matrix.IdentityReadOnly - const transform = scene.getTransformMatrix() - const viewport = new Viewport(0, 0, engine.getRenderWidth(), engine.getRenderHeight()) - const projected = Vector3.Project(center, world, transform, viewport) - setOverlayPos({ left: projected.x, top: projected.y }) + + try { + const engine = scene.getEngine() + const cam = scene.activeCamera + if (!cam) return + + const center = chosen.getBoundingInfo().boundingBox.centerWorld + const world = Matrix.IdentityReadOnly + const transform = scene.getTransformMatrix() + const viewport = new Viewport(0, 0, engine.getRenderWidth(), engine.getRenderHeight()) + const projected = Vector3.Project(center, world, transform, viewport) + setOverlayPos({ left: projected.x, top: projected.y }) + } catch (error) { + console.warn('[ModelViewer] Error updating overlay position:', error) + setOverlayPos(null) + } }) return () => { scene.onAfterRenderObservable.remove(observer) @@ -333,20 +375,50 @@ const ModelViewer: React.FC = ({ return (
- - {isLoading && ( -
- + {!modelPath ? ( +
+
+
+ 3D модель недоступна +
+
+ Путь к 3D модели не задан +
+
+ Обратитесь к администратору для настройки модели +
+
+ ) : ( + <> + + {isLoading && ( +
+ +
+ )} + {!modelReady && !isLoading && ( +
+
+
+ 3D модель не загружена +
+
+ Модель не готова к отображению +
+
+
+ )} + )} {renderOverlay ? renderOverlay({ anchor: overlayPos, info: overlayData }) @@ -357,7 +429,8 @@ const ModelViewer: React.FC = ({ {overlayData.sensorId &&
ID: {overlayData.sensorId}
}
- ))} + )) + }
) } 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 }) => {
- Object Model { - const target = e.target as HTMLImageElement; - target.style.display = 'none'; - }} - /> + {objectImageError ? ( +
+
+ Предпросмотр 3D недоступен +
+
+ Изображение модели не найдено +
+
+ ) : ( + Object Model 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 && ( User 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