From 1140a6a96b52dfb25c621142ed2c5d27fe17e596 Mon Sep 17 00:00:00 2001 From: iqubik Date: Mon, 30 Mar 2026 01:29:04 +0300 Subject: [PATCH] http warning --- client/src/components/Layout.tsx | 10 +- client/src/components/SecurityWarning.tsx | 114 ++++++++++++ client/src/utils/useSecureConnection.ts | 18 ++ .../test/components/SecurityWarning.spec.tsx | 175 ++++++++++++++++++ client/test/utils/useSecureConnection.spec.ts | 91 +++++++++ server/Dockerfile | 4 +- 6 files changed, 407 insertions(+), 5 deletions(-) create mode 100644 client/src/components/SecurityWarning.tsx create mode 100644 client/src/utils/useSecureConnection.ts create mode 100644 client/test/components/SecurityWarning.spec.tsx create mode 100644 client/test/utils/useSecureConnection.spec.ts diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index 8ce644a..29b7269 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -1,6 +1,6 @@ -import { - Toolbar, Drawer, List, ListItem, - ListItemButton, ListItemIcon, ListItemText, Box, useMediaQuery, useTheme +import { + Toolbar, Drawer, List, ListItem, + ListItemButton, ListItemIcon, ListItemText, Box, useMediaQuery, useTheme } from '@mui/material'; import { People, Settings, Dns, SwapHoriz } from '@mui/icons-material'; import { useNavigate, useLocation, Outlet } from 'react-router-dom'; @@ -8,6 +8,8 @@ import { useState } from 'react'; import Header from './Header'; import Footer from './Footer'; +import SecurityWarning from './SecurityWarning'; +import { useSecureConnection } from '../utils/useSecureConnection'; const drawerWidth = 240; @@ -17,6 +19,7 @@ export default function Layout() { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const [mobileOpen, setMobileOpen] = useState(false); + const { isSecure } = useSecureConnection(); const handleDrawerToggle = () => { setMobileOpen(!mobileOpen); @@ -81,6 +84,7 @@ export default function Layout() { }} > + {!isSecure && } diff --git a/client/src/components/SecurityWarning.tsx b/client/src/components/SecurityWarning.tsx new file mode 100644 index 0000000..65de83a --- /dev/null +++ b/client/src/components/SecurityWarning.tsx @@ -0,0 +1,114 @@ +import { Alert, AlertTitle, Box, Button, Collapse, IconButton, Snackbar, useMediaQuery, useTheme } from '@mui/material'; +import { Close, ContentCopy } from '@mui/icons-material'; +import { useState } from 'react'; + +const INSTALL_COMMAND = 'bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main/install.sh)'; + +export default function SecurityWarning() { + const [copied, setCopied] = useState(false); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(INSTALL_COMMAND); + setCopied(true); + } catch { + // Fallback для старых браузеров + const textArea = document.createElement('textarea'); + textArea.value = INSTALL_COMMAND; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + setCopied(true); + } + }; + + return ( + <> + + + + 3DP-MANAGER работает в небезопасном режиме (HTTP) + + + Не вводите реальные пароли от 3x-ui панели и не меняйте пароль администратора в режиме работы по HTTP!{' '} + Для безопасной работы переустановите 3DP-MANAGER с SSL-сертификатами. Все ваши настройки сохранятся. + + + + {INSTALL_COMMAND} + + + + + + + setCopied(false)} + message="Скопировано" + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + action={ + setCopied(false)}> + + + } + /> + + ); +} diff --git a/client/src/utils/useSecureConnection.ts b/client/src/utils/useSecureConnection.ts new file mode 100644 index 0000000..01c3b82 --- /dev/null +++ b/client/src/utils/useSecureConnection.ts @@ -0,0 +1,18 @@ +import { useMemo } from 'react'; + +/** + * Хук для определения безопасного соединения (HTTPS) + * @returns {isSecure: boolean} - true если соединение по HTTPS + */ +export function useSecureConnection() { + const isSecure = useMemo(() => { + // Проверка в браузере + if (typeof window !== 'undefined' && window.location) { + return window.location.protocol === 'https:'; + } + // SSR fallback - считаем небезопасным + return false; + }, []); + + return { isSecure }; +} diff --git a/client/test/components/SecurityWarning.spec.tsx b/client/test/components/SecurityWarning.spec.tsx new file mode 100644 index 0000000..019dc47 --- /dev/null +++ b/client/test/components/SecurityWarning.spec.tsx @@ -0,0 +1,175 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import SecurityWarning from '../../src/components/SecurityWarning' + +// Мок для navigator.clipboard +const mockWriteText = vi.fn() +Object.assign(navigator, { + clipboard: { + writeText: mockWriteText, + }, +}) + +// Мок для document.execCommand (fallback для старых браузеров) +const mockExecCommand = vi.fn() +beforeEach(() => { + vi.clearAllMocks() + document.execCommand = mockExecCommand +}) + +describe('SecurityWarning', () => { + const INSTALL_COMMAND = 'bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main/install.sh)' + + const renderWarning = () => { + return render() + } + + describe('рендеринг', () => { + it('должен рендериться с заголовком предупреждения', () => { + renderWarning() + expect( + screen.getByText(/3DP-MANAGER работает в небезопасном режиме/i) + ).toBeInTheDocument() + }) + + it('должен отображать предупреждение о паролях', () => { + renderWarning() + expect( + screen.getByText(/Не вводите реальные пароли от 3x-ui панели/i) + ).toBeInTheDocument() + }) + + it('должен отображать инструкцию по переустановке', () => { + renderWarning() + expect( + screen.getByText(/Для безопасной работы переустановите 3DP-MANAGER с SSL-сертификатами/i) + ).toBeInTheDocument() + }) + + it('должен отображать команду установки', () => { + renderWarning() + expect(screen.getByText(INSTALL_COMMAND)).toBeInTheDocument() + }) + + it('должен отображать кнопку копирования команды', () => { + renderWarning() + expect(screen.getByText('Копировать')).toBeInTheDocument() + }) + + it('должен отображать иконку копирования', () => { + renderWarning() + expect(screen.getByTestId('icon-ContentCopy')).toBeInTheDocument() + }) + + it('должен иметь семантически правильный Alert', () => { + renderWarning() + const alert = screen.getByRole('alert') + expect(alert).toBeInTheDocument() + }) + }) + + describe('копирование команды', () => { + it('должен копировать команду в буфер обмена при клике', async () => { + mockWriteText.mockResolvedValue(undefined) + + renderWarning() + const copyButton = screen.getByText('Копировать') + fireEvent.click(copyButton) + + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalledWith(INSTALL_COMMAND) + }) + }) + + it('должен показывать уведомление "Скопировано" после копирования', async () => { + mockWriteText.mockResolvedValue(undefined) + + renderWarning() + const copyButton = screen.getByText('Копировать') + fireEvent.click(copyButton) + + await waitFor(() => { + expect(screen.getByText('Скопировано')).toBeInTheDocument() + }) + }) + + it('должен использовать fallback при отсутствии navigator.clipboard', async () => { + // Мок ошибки clipboard API + mockWriteText.mockRejectedValue(new Error('Not supported')) + + renderWarning() + const copyButton = screen.getByText('Копировать') + fireEvent.click(copyButton) + + await waitFor(() => { + expect(mockExecCommand).toHaveBeenCalledWith('copy') + }) + }) + + it('должен показывать уведомление "Скопировано" при использовании fallback', async () => { + mockWriteText.mockRejectedValue(new Error('Not supported')) + mockExecCommand.mockReturnValue(true) + + renderWarning() + const copyButton = screen.getByText('Копировать') + fireEvent.click(copyButton) + + await waitFor(() => { + expect(screen.getByText('Скопировано')).toBeInTheDocument() + }) + }) + }) + + describe('Snackbar уведомление', () => { + it('должен показывать уведомление после копирования', async () => { + mockWriteText.mockResolvedValue(undefined) + + renderWarning() + const copyButton = screen.getByText('Копировать') + fireEvent.click(copyButton) + + await waitFor(() => { + expect(screen.getByText('Скопировано')).toBeInTheDocument() + }) + }) + + it('должен закрывать уведомление при клике на кнопку закрытия', async () => { + mockWriteText.mockResolvedValue(undefined) + + renderWarning() + const copyButton = screen.getByText('Копировать') + fireEvent.click(copyButton) + + // Ждём появления уведомления + await waitFor(() => { + expect(screen.getByText('Скопировано')).toBeInTheDocument() + }) + + // Находим кнопку закрытия по иконке Close + const closeButton = screen.getByTestId('icon-Close').closest('button') + if (closeButton) { + fireEvent.click(closeButton) + } + + // Уведомление закрыто + await waitFor(() => { + expect(screen.queryByText('Скопировано')).not.toBeInTheDocument() + }) + }) + }) + + describe('стили компонента', () => { + it('должен иметь variant="filled"', () => { + renderWarning() + const alert = screen.getByRole('alert') + // Проверяем что Alert имеет filled стиль (проверка через классы MUI) + expect(alert).toHaveClass('MuiAlert-filledWarning') + }) + + it('должен отображать код в monospace шрифте', () => { + renderWarning() + const codeBlock = screen.getByText(INSTALL_COMMAND) + expect(codeBlock.tagName).toBe('CODE') + }) + }) +}) diff --git a/client/test/utils/useSecureConnection.spec.ts b/client/test/utils/useSecureConnection.spec.ts new file mode 100644 index 0000000..35d4f90 --- /dev/null +++ b/client/test/utils/useSecureConnection.spec.ts @@ -0,0 +1,91 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { renderHook } from '@testing-library/react' +import { useSecureConnection } from '../../src/utils/useSecureConnection' + +describe('useSecureConnection', () => { + const originalLocation = window.location + + beforeEach(() => { + // Очищаем моки перед каждым тестом + vi.clearAllMocks() + }) + + afterEach(() => { + // Восстанавливаем оригинальный location + Object.defineProperty(window, 'location', { + value: originalLocation, + writable: true, + configurable: true, + }) + }) + + describe('при HTTPS соединении', () => { + it('должен возвращать isSecure: true когда protocol https:', () => { + // Мок для HTTPS + Object.defineProperty(window, 'location', { + value: { + protocol: 'https:', + }, + writable: true, + configurable: true, + }) + + const { result } = renderHook(() => useSecureConnection()) + + expect(result.current.isSecure).toBe(true) + }) + }) + + describe('при HTTP соединении', () => { + it('должен возвращать isSecure: false когда protocol http:', () => { + // Мок для HTTP + Object.defineProperty(window, 'location', { + value: { + protocol: 'http:', + }, + writable: true, + configurable: true, + }) + + const { result } = renderHook(() => useSecureConnection()) + + expect(result.current.isSecure).toBe(false) + }) + }) + + describe('при localhost', () => { + it('должен возвращать isSecure: false для localhost без HTTPS', () => { + Object.defineProperty(window, 'location', { + value: { + protocol: 'http:', + hostname: 'localhost', + }, + writable: true, + configurable: true, + }) + + const { result } = renderHook(() => useSecureConnection()) + + expect(result.current.isSecure).toBe(false) + }) + }) + + describe('мемозация', () => { + it('должен возвращать одно и то же значение при повторных рендерах', () => { + Object.defineProperty(window, 'location', { + value: { + protocol: 'https:', + }, + writable: true, + configurable: true, + }) + + const { result, rerender } = renderHook(() => useSecureConnection()) + const firstValue = result.current.isSecure + + rerender() + + expect(result.current.isSecure).toBe(firstValue) + }) + }) +}) diff --git a/server/Dockerfile b/server/Dockerfile index 651c986..60020ed 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY package*.json ./ -RUN npm ci +RUN npm ci --legacy-peer-deps COPY . . @@ -32,7 +32,7 @@ FROM node:24-alpine WORKDIR /app COPY package*.json ./ -RUN npm ci --only=production +RUN npm ci --only=production --legacy-peer-deps # Runtime tools required for checker/scanner integration scripts. RUN apk add --no-cache \