diff --git a/api/app/controllers/backup_router.py b/api/app/controllers/backup_router.py index 96fffcf..34272c5 100644 --- a/api/app/controllers/backup_router.py +++ b/api/app/controllers/backup_router.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, File, UploadFile from sqlalchemy.ext.asyncio import AsyncSession from starlette.responses import FileResponse -from app.database.session import get_db +from app.database.session import get_db, engine from app.domain.entities.responses.backup import BackupResponseEntity from app.infrastructure.backup_service import BackupService from app.infrastructure.dependencies import require_admin @@ -79,6 +79,27 @@ async def upload_backup( return await backup_service.upload_backup(file, user) +@router.post( + '/{backup_id}/restore/', + response_model=BackupResponseEntity, + summary='Restore a backup', + description='Restore a backup by ID, overwriting existing data', +) +async def restore_backup( + backup_id: int, + db: AsyncSession = Depends(get_db), + user=Depends(require_admin), +): + backup_service = BackupService( + db, + settings.BACKUP_DB_URL, + settings.FILE_UPLOAD_DIR, + settings.BACKUP_DIR, + settings.PG_DUMP_PATH, + ) + return await backup_service.restore_backup(backup_id, engine) + + @router.delete( '/{backup_id}/', response_model=BackupResponseEntity, diff --git a/api/app/domain/models/appointment_files.py b/api/app/domain/models/appointment_files.py index 895b712..c9ba32f 100644 --- a/api/app/domain/models/appointment_files.py +++ b/api/app/domain/models/appointment_files.py @@ -2,14 +2,16 @@ from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class AppointmentFile(BaseModel): __tablename__ = 'appointment_files' + __table_args__ = {"schema": settings.SCHEMA} file_path = Column(String, nullable=False) file_title = Column(String, nullable=False) - appointment_id = Column(Integer, ForeignKey('appointments.id'), nullable=False) + appointment_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointments.id'), nullable=False) appointment = relationship('Appointment', back_populates='files') \ No newline at end of file diff --git a/api/app/domain/models/appointment_types.py b/api/app/domain/models/appointment_types.py index 8dc68f3..f35c976 100644 --- a/api/app/domain/models/appointment_types.py +++ b/api/app/domain/models/appointment_types.py @@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class AppointmentType(BaseModel): __tablename__ = 'appointment_types' + __table_args__ = {"schema": settings.SCHEMA} title = Column(VARCHAR(150), nullable=False, unique=True) diff --git a/api/app/domain/models/appointments.py b/api/app/domain/models/appointments.py index e57bfac..43ed4b8 100644 --- a/api/app/domain/models/appointments.py +++ b/api/app/domain/models/appointments.py @@ -3,18 +3,20 @@ from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.domain.models.base import BaseModel +from app.settings import settings class Appointment(BaseModel): __tablename__ = 'appointments' + __table_args__ = {"schema": settings.SCHEMA} results = Column(String) days_until_the_next_appointment = Column(Integer) appointment_datetime = Column(DateTime, nullable=False, server_default=func.now()) - patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False) - doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False) - type_id = Column(Integer, ForeignKey('appointment_types.id'), nullable=False) + patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False) + doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) + type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointment_types.id'), nullable=False) patient = relationship('Patient', back_populates='appointments') doctor = relationship('User', back_populates='appointments') diff --git a/api/app/domain/models/backups.py b/api/app/domain/models/backups.py index 163fc5e..07058f2 100644 --- a/api/app/domain/models/backups.py +++ b/api/app/domain/models/backups.py @@ -3,14 +3,16 @@ import datetime from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, Boolean from app.domain.models.base import BaseModel +from app.settings import settings class Backup(BaseModel): __tablename__ = 'backups' + __table_args__ = {"schema": settings.SCHEMA} timestamp = Column(DateTime, nullable=False, default=datetime.datetime.now) path = Column(String, nullable=False) filename = Column(String, nullable=False) is_by_user = Column(Boolean, nullable=False, default=False) - user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + user_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) diff --git a/api/app/domain/models/lens.py b/api/app/domain/models/lens.py index 6c311fc..8a94c5d 100644 --- a/api/app/domain/models/lens.py +++ b/api/app/domain/models/lens.py @@ -4,6 +4,7 @@ from sqlalchemy import Column, Integer, ForeignKey, Float, Enum, Boolean from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class SideEnum(PyEnum): @@ -13,6 +14,7 @@ class SideEnum(PyEnum): class Lens(BaseModel): __tablename__ = 'lens' + __table_args__ = {"schema": settings.SCHEMA} tor = Column(Float, nullable=False) trial = Column(Float, nullable=False) @@ -24,7 +26,7 @@ class Lens(BaseModel): side = Column(Enum(SideEnum), nullable=False) issued = Column(Boolean, nullable=False, default=False) - type_id = Column(Integer, ForeignKey('lens_types.id'), nullable=False) + type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens_types.id'), nullable=False) type = relationship('LensType', back_populates='lenses') diff --git a/api/app/domain/models/lens_issues.py b/api/app/domain/models/lens_issues.py index b55f749..23a0712 100644 --- a/api/app/domain/models/lens_issues.py +++ b/api/app/domain/models/lens_issues.py @@ -2,16 +2,18 @@ from sqlalchemy import Column, Integer, ForeignKey, Date from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class LensIssue(BaseModel): __tablename__ = 'lens_issues' + __table_args__ = {"schema": settings.SCHEMA} issue_date = Column(Date, nullable=False) - patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False) - doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False) - lens_id = Column(Integer, ForeignKey('lens.id'), nullable=False) + patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False) + doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) + lens_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens.id'), nullable=False) patient = relationship('Patient', back_populates='lens_issues') doctor = relationship('User', back_populates='lens_issues') diff --git a/api/app/domain/models/lens_types.py b/api/app/domain/models/lens_types.py index 37cd69a..6b1abdd 100644 --- a/api/app/domain/models/lens_types.py +++ b/api/app/domain/models/lens_types.py @@ -2,10 +2,12 @@ from sqlalchemy import Column, Integer, VARCHAR from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class LensType(BaseModel): __tablename__ = 'lens_types' + __table_args__ = {"schema": settings.SCHEMA} title = Column(VARCHAR(150), nullable=False, unique=True) diff --git a/api/app/domain/models/mailing.py b/api/app/domain/models/mailing.py index 1913348..dda55d7 100644 --- a/api/app/domain/models/mailing.py +++ b/api/app/domain/models/mailing.py @@ -3,16 +3,18 @@ from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.domain.models.base import BaseModel +from app.settings import settings class Mailing(BaseModel): __tablename__ = 'mailing' + __table_args__ = {"schema": settings.SCHEMA} text = Column(String, nullable=False) title = Column(String, nullable=False) datetime = Column(DateTime, nullable=False, default=func.utcnow) - user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + user_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) user = relationship('User', back_populates='mailing') diff --git a/api/app/domain/models/mailing_delivery_methods.py b/api/app/domain/models/mailing_delivery_methods.py index 0584957..70dac3b 100644 --- a/api/app/domain/models/mailing_delivery_methods.py +++ b/api/app/domain/models/mailing_delivery_methods.py @@ -2,10 +2,12 @@ from sqlalchemy import Column, Integer, VARCHAR from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class MailingDeliveryMethod(BaseModel): __tablename__ = 'mailing_delivery_methods' + __table_args__ = {"schema": settings.SCHEMA} title = Column(VARCHAR(200), nullable=False) diff --git a/api/app/domain/models/mailing_options.py b/api/app/domain/models/mailing_options.py index 40a3a92..7e68412 100644 --- a/api/app/domain/models/mailing_options.py +++ b/api/app/domain/models/mailing_options.py @@ -2,13 +2,15 @@ from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class MailingOption(BaseModel): __tablename__ = 'mailing_options' + __table_args__ = {"schema": settings.SCHEMA} - option_id = Column(Integer, ForeignKey('mailing_delivery_methods.id'), nullable=False) - mailing_id = Column(Integer, ForeignKey('mailing.id'), nullable=False) + option_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing_delivery_methods.id'), nullable=False) + mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id'), nullable=False) method = relationship('MailingDeliveryMethod', back_populates='mailing') mailing = relationship('Mailing', back_populates='mailing_options') diff --git a/api/app/domain/models/patients.py b/api/app/domain/models/patients.py index 75146e8..f8be359 100644 --- a/api/app/domain/models/patients.py +++ b/api/app/domain/models/patients.py @@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR, Date, String from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class Patient(BaseModel): __tablename__ = 'patients' + __table_args__ = {"schema": settings.SCHEMA} first_name = Column(VARCHAR(200), nullable=False) last_name = Column(VARCHAR(200), nullable=False) diff --git a/api/app/domain/models/recipients.py b/api/app/domain/models/recipients.py index 46fbce9..65c8cc2 100644 --- a/api/app/domain/models/recipients.py +++ b/api/app/domain/models/recipients.py @@ -2,13 +2,15 @@ from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class Recipient(BaseModel): __tablename__ = 'recipients' + __table_args__ = {"schema": settings.SCHEMA} - patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False) - mailing_id = Column(Integer, ForeignKey('mailing.id'), nullable=False) + patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False) + mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id'), nullable=False) patient = relationship('Patient', back_populates='mailing') mailing = relationship('Mailing', back_populates='recipients') diff --git a/api/app/domain/models/roles.py b/api/app/domain/models/roles.py index 5506610..1ec1816 100644 --- a/api/app/domain/models/roles.py +++ b/api/app/domain/models/roles.py @@ -2,10 +2,12 @@ from sqlalchemy import Column, VARCHAR from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class Role(BaseModel): __tablename__ = 'roles' + __table_args__ = {"schema": settings.SCHEMA} title = Column(VARCHAR(150), nullable=False, unique=True) diff --git a/api/app/domain/models/scheduled_appointments.py b/api/app/domain/models/scheduled_appointments.py index 106ef8e..62e53ee 100644 --- a/api/app/domain/models/scheduled_appointments.py +++ b/api/app/domain/models/scheduled_appointments.py @@ -3,17 +3,19 @@ from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.domain.models.base import BaseModel +from app.settings import settings class ScheduledAppointment(BaseModel): __tablename__ = 'scheduled_appointments' + __table_args__ = {"schema": settings.SCHEMA} scheduled_datetime = Column(DateTime, nullable=False, server_default=func.now()) is_canceled = Column(Boolean, nullable=False, default=False, server_default='false') - patient_id = Column(Integer, ForeignKey('patients.id'), nullable=False) - doctor_id = Column(Integer, ForeignKey('users.id'), nullable=False) - type_id = Column(Integer, ForeignKey('appointment_types.id'), nullable=False) + patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id'), nullable=False) + doctor_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) + type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.appointment_types.id'), nullable=False) patient = relationship('Patient', back_populates='scheduled_appointments') doctor = relationship('User', back_populates='scheduled_appointments') diff --git a/api/app/domain/models/set_contents.py b/api/app/domain/models/set_contents.py index c221731..6bee858 100644 --- a/api/app/domain/models/set_contents.py +++ b/api/app/domain/models/set_contents.py @@ -3,10 +3,12 @@ from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel from app.domain.models.lens import SideEnum +from app.settings import settings class SetContent(BaseModel): __tablename__ = 'set_contents' + __table_args__ = {"schema": settings.SCHEMA} tor = Column(Float, nullable=False) trial = Column(Float, nullable=False) @@ -18,8 +20,8 @@ class SetContent(BaseModel): side = Column(Enum(SideEnum), nullable=False) count = Column(Integer, nullable=False) - type_id = Column(Integer, ForeignKey('lens_types.id'), nullable=False) - set_id = Column(Integer, ForeignKey('sets.id'), nullable=False) + type_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.lens_types.id'), nullable=False) + set_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.sets.id'), nullable=False) type = relationship('LensType', back_populates='contents') set = relationship('Set', back_populates='contents') diff --git a/api/app/domain/models/sets.py b/api/app/domain/models/sets.py index 859959e..4f09678 100644 --- a/api/app/domain/models/sets.py +++ b/api/app/domain/models/sets.py @@ -2,10 +2,12 @@ from sqlalchemy import Column, Integer, VARCHAR from sqlalchemy.orm import relationship from app.domain.models.base import BaseModel +from app.settings import settings class Set(BaseModel): __tablename__ = 'sets' + __table_args__ = {"schema": settings.SCHEMA} title = Column(VARCHAR(150), nullable=False, unique=True) diff --git a/api/app/domain/models/users.py b/api/app/domain/models/users.py index a6758b8..b0baf20 100644 --- a/api/app/domain/models/users.py +++ b/api/app/domain/models/users.py @@ -3,10 +3,12 @@ from sqlalchemy.orm import relationship from werkzeug.security import check_password_hash, generate_password_hash from app.domain.models.base import BaseModel +from app.settings import settings class User(BaseModel): __tablename__ = 'users' + __table_args__ = {"schema": settings.SCHEMA} first_name = Column(VARCHAR(200), nullable=False) last_name = Column(VARCHAR(200), nullable=False) @@ -15,7 +17,7 @@ class User(BaseModel): password = Column(String, nullable=False) is_blocked = Column(Boolean, nullable=False, default=False, server_default='false') - role_id = Column(Integer, ForeignKey('roles.id'), nullable=False) + role_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.roles.id'), nullable=False) role = relationship('Role', back_populates='users') diff --git a/api/app/infrastructure/backup_service.py b/api/app/infrastructure/backup_service.py index 2543416..863514e 100644 --- a/api/app/infrastructure/backup_service.py +++ b/api/app/infrastructure/backup_service.py @@ -1,14 +1,16 @@ -import io -import subprocess -import os -import tarfile import datetime -from typing import Any, Coroutine, Optional +import io +import os +import shutil +import subprocess +import tarfile +from typing import Optional +from fastapi_maintenance import maintenance_mode_on import aiofiles from fastapi import HTTPException, UploadFile from magic import magic -from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine from starlette.responses import FileResponse from werkzeug.utils import secure_filename @@ -69,6 +71,58 @@ class BackupService: except subprocess.CalledProcessError as e: raise HTTPException(500, f"Ошибка создания бэкапа: {e}") + async def restore_backup(self, backup_id: int, engine: AsyncEngine) -> BackupResponseEntity: + try: + async with maintenance_mode_on(): + backup = await self.backup_repository.get_by_id(backup_id) + if not backup: + raise HTTPException(404, 'Резервная копия с таким id не найдена') + + if not os.path.exists(backup.path): + raise HTTPException(404, 'Файл не найден на диске') + + with tarfile.open(backup.path, "r:gz") as tar: + members = tar.getnames() + if "db_dump.sql" not in members or not any( + name.startswith(os.path.basename(self.app_files_dir)) for name in members): + raise HTTPException(400, "Неверная структура архива резервной копии") + + if os.path.exists(self.app_files_dir): + shutil.rmtree(self.app_files_dir) + os.makedirs(self.app_files_dir, exist_ok=True) + + await engine.dispose() + + psql_path = self.pg_dump_path.replace("pg_dump", "psql") + drop_cmd = ( + f'"{psql_path}" ' + f'-d "{self.db_url}" ' + f'-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"' + ) + subprocess.run(drop_cmd, shell=True, check=True) + + temp_dir = os.path.join(self.backup_dir, f"temp_restore_{backup_id}") + os.makedirs(temp_dir, exist_ok=True) + with tarfile.open(backup.path, "r:gz") as tar: + tar.extractall(temp_dir) + + db_dump_path = os.path.join(temp_dir, "db_dump.sql") + restore_cmd = f'"{self.pg_dump_path.replace("pg_dump", "pg_restore")}" -d {self.db_url} --no-owner --no-privileges -Fc "{db_dump_path}"' + subprocess.run(restore_cmd, shell=True, check=True) + + extracted_app_files_dir = os.path.join(temp_dir, os.path.basename(self.app_files_dir)) + if os.path.exists(extracted_app_files_dir): + shutil.copytree(extracted_app_files_dir, self.app_files_dir, dirs_exist_ok=True) + + shutil.rmtree(temp_dir) + + return self.model_to_entity(backup) + + except subprocess.CalledProcessError as e: + raise HTTPException(500, f"Ошибка восстановления бэкапа: {e}") + except Exception as e: + raise HTTPException(500, f"Ошибка сервера: {str(e)}") + async def get_backup_file_by_id(self, backup_id: int) -> FileResponse: backup = await self.backup_repository.get_by_id(backup_id) diff --git a/api/app/main.py b/api/app/main.py index 72d0021..54cfe24 100644 --- a/api/app/main.py +++ b/api/app/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi_maintenance import MaintenanceModeMiddleware from starlette.middleware.cors import CORSMiddleware from app.controllers.appointment_files_router import router as appointment_files_router @@ -29,6 +30,7 @@ def start_app(): allow_methods=['*'], allow_headers=['*'], ) + # api_app.add_middleware(MaintenanceModeMiddleware, enable_maintenance=False) api_app.include_router(appointment_files_router, prefix=f'{settings.APP_PREFIX}/appointment_files', tags=['appointment_files']) diff --git a/api/app/settings.py b/api/app/settings.py index b0f4abd..890d6d9 100644 --- a/api/app/settings.py +++ b/api/app/settings.py @@ -12,6 +12,7 @@ class Settings(BaseSettings): BACKUP_DIR: str = 'backups' BACKUP_DB_URL: str PG_DUMP_PATH: str + SCHEMA: str = 'public' class Config: env_file = '.env' diff --git a/api/req.txt b/api/req.txt index f3c1b10..7fd94e6 100644 --- a/api/req.txt +++ b/api/req.txt @@ -11,4 +11,5 @@ Werkzeug==3.1.3 pyjwt==2.10.1 python-magic==0.4.27 aiofiles==24.1.0 -python-multipart==0.0.20 \ No newline at end of file +python-multipart==0.0.20 +fastapi-maintenance==0.0.4 \ No newline at end of file diff --git a/web-app/src/Api/backupsApi.js b/web-app/src/Api/backupsApi.js index 20a74d3..c9c9314 100644 --- a/web-app/src/Api/backupsApi.js +++ b/web-app/src/Api/backupsApi.js @@ -40,7 +40,12 @@ export const backupsApi = createApi({ }, invalidatesTags: ['Backup'], }), - + restoreBackup: builder.mutation({ + query: (backupId) => ({ + url: `/backups/${backupId}/restore/`, + method: 'POST', + }), + }), }), }); @@ -49,4 +54,5 @@ export const { useCreateBackupMutation, useDeleteBackupMutation, useUploadBackupMutation, + useRestoreBackupMutation, } = backupsApi; diff --git a/web-app/src/Components/Dummies/AppointmentViewModal/useAppointmentView.js b/web-app/src/Components/Dummies/AppointmentViewModal/useAppointmentView.js index 6667fe0..57b4daf 100644 --- a/web-app/src/Components/Dummies/AppointmentViewModal/useAppointmentView.js +++ b/web-app/src/Components/Dummies/AppointmentViewModal/useAppointmentView.js @@ -1,29 +1,29 @@ -import { useDispatch, useSelector } from "react-redux"; -import { setSelectedAppointment } from "../../../Redux/Slices/appointmentsSlice.js"; +import {useDispatch, useSelector} from "react-redux"; +import {setSelectedAppointment} from "../../../Redux/Slices/appointmentsSlice.js"; import dayjs from "dayjs"; -import { useState } from "react"; -import { useGetAppointmentFilesQuery, useDeleteAppointmentFileMutation } from "../../../Api/appointmentFilesApi.js"; -import { baseQueryWithAuth } from "../../../Api/baseQuery.js"; -import { notification } from "antd"; +import {useState} from "react"; +import {useGetAppointmentFilesQuery, useDeleteAppointmentFileMutation} from "../../../Api/appointmentFilesApi.js"; +import {notification} from "antd"; +import CONFIG from "../../../Core/сonfig.js"; const useAppointmentView = () => { const dispatch = useDispatch(); - const { selectedAppointment } = useSelector((state) => state.appointmentsUI); + const {selectedAppointment} = useSelector((state) => state.appointmentsUI); - const { data: files = [], isLoading: isFilesLoading } = useGetAppointmentFilesQuery( + const {data: files = [], isLoading: isFilesLoading} = useGetAppointmentFilesQuery( selectedAppointment?.id, - { skip: !selectedAppointment?.id } + {skip: !selectedAppointment?.id} ); - const [deleteAppointmentFile, { isLoading: isDeletingFile }] = useDeleteAppointmentFileMutation(); + const [deleteAppointmentFile, {isLoading: isDeletingFile}] = useDeleteAppointmentFileMutation(); const [downloadingFiles, setDownloadingFiles] = useState({}); const [deletingFiles, setDeletingFiles] = useState({}); const modalWidth = 700; - const blockStyle = { marginBottom: 16 }; - const footerRowStyle = { marginTop: 16 }; - const footerButtonStyle = { marginRight: 8 }; + const blockStyle = {marginBottom: 16}; + const footerRowStyle = {marginTop: 16}; + const footerButtonStyle = {marginRight: 8}; const labels = { title: "Просмотр приема", @@ -81,54 +81,84 @@ const useAppointmentView = () => { const downloadFile = async (fileId, fileName) => { try { setDownloadingFiles((prev) => ({ ...prev, [fileId]: true })); - const { url, ...options } = await baseQueryWithAuth( - { - url: `/appointment_files/${fileId}/file/`, - method: 'GET', - credentials: 'include', - }, - {}, - {} - ); - const response = await fetch(url, { - ...options, - method: 'GET', - credentials: 'include', - }); - - if (!response.ok) { + const token = localStorage.getItem('access_token'); + if (!token) { notification.error({ - message: "Ошибка при скачивании файла", - description: "Не удалось загрузить файл.", + message: "Ошибка", + description: "Токен не найден", placement: "topRight", }); + return; + } + + const response = await fetch(`${CONFIG.BASE_URL}appointment_files/${fileId}/file/`, { + method: 'GET', + credentials: 'include', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + + if (!response.ok) { + const errorText = await response.text(); + notification.error({ + message: "Ошибка", + description: errorText || "Не удалось скачать файл", + placement: "topRight", + }); + return; + } + + const contentType = response.headers.get('content-type'); + if (!contentType || contentType.includes('text/html')) { + const errorText = await response.text(); + notification.error({ + message: "Ошибка", + description: errorText || "Не удалось скачать файл", + placement: "topRight", + }); + return; + } + + let safeFileName = fileName || "file"; + if (!safeFileName.match(/\.[a-zA-Z0-9]+$/)) { + if (contentType.includes('application/pdf')) { + safeFileName += '.pdf'; + } else if (contentType.includes('image/jpeg')) { + safeFileName += '.jpg'; + } else if (contentType.includes('image/png')) { + safeFileName += '.png'; + } else { + safeFileName += '.bin'; + } } const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = downloadUrl; - link.setAttribute("download", fileName || "file"); + link.setAttribute("download", safeFileName); document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(downloadUrl); } catch (error) { - console.error("Error downloading file:", error); + console.error("Error downloading file:", error); // Отладка notification.error({ - message: "Ошибка при скачивании файлов", - description: "Не удалось загрузить файл.", + message: "Ошибка", + description: error.message || "Не удалось скачать файл", placement: "topRight", }); } finally { - setDownloadingFiles((prev) => ({ ...prev, [fileId]: false })); + setDownloadingFiles((prev) => ({...prev, [fileId]: false})); } }; const deleteFile = async (fileId, fileName) => { try { - setDeletingFiles((prev) => ({ ...prev, [fileId]: true })); + setDeletingFiles((prev) => ({...prev, [fileId]: true})); await deleteAppointmentFile(fileId).unwrap(); notification.success({ message: "Файл удален", @@ -143,7 +173,7 @@ const useAppointmentView = () => { placement: "topRight", }); } finally { - setDeletingFiles((prev) => ({ ...prev, [fileId]: false })); + setDeletingFiles((prev) => ({...prev, [fileId]: false})); } }; diff --git a/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/BackupManageTab.jsx b/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/BackupManageTab.jsx index dacd98f..101108d 100644 --- a/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/BackupManageTab.jsx +++ b/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/BackupManageTab.jsx @@ -1,5 +1,5 @@ -import {Button, Divider, List, Result, Space, Tooltip, Typography, Upload} from "antd"; -import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined} from "@ant-design/icons"; +import {Button, Divider, List, Popconfirm, Result, Space, Tooltip, Typography, Upload} from "antd"; +import {CloudDownloadOutlined, DeleteOutlined, UploadOutlined, ReloadOutlined} from "@ant-design/icons"; import useBackupManageTab from "./useBackupManageTab.js"; import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; @@ -11,7 +11,7 @@ const BackupManageTab = () => { } if (backupManageTabData.isErrorBackups) { - return + return ; } return ( @@ -22,7 +22,6 @@ const BackupManageTab = () => { icon={} onClick={backupManageTabData.createBackupHandler} loading={backupManageTabData.isCreatingBackup} - > Создать резервную копию @@ -46,33 +45,47 @@ const BackupManageTab = () => { renderItem={(backup) => ( {backup.filename} + {backup.is_by_user ? "Загружена" : "Создана"}: {new Date(backup.timestamp).toLocaleString()} - - - - - - + + + + + + backupManageTabData.restoreBackupHandler(backup.id)}> + + + + + + + )} /> - ); }; diff --git a/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js b/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js index dcbd356..ceaaf22 100644 --- a/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js +++ b/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js @@ -1,7 +1,9 @@ import { useCreateBackupMutation, useDeleteBackupMutation, - useGetBackupsQuery, useUploadBackupMutation + useGetBackupsQuery, + useUploadBackupMutation, + useRestoreBackupMutation, } from "../../../../../Api/backupsApi.js"; import {useDispatch} from "react-redux"; import {notification} from "antd"; @@ -10,13 +12,13 @@ import CONFIG from "../../../../../Core/сonfig.js"; const useBackupManageTab = () => { - const dispatch = useDispatch(); const {data: backups, isLoading: isLoadingBackups, isError: isErrorBackups} = useGetBackupsQuery(undefined, { pollingInterval: 60000, }); const [createBackup, {isLoading: isCreatingBackup}] = useCreateBackupMutation(); const [deleteBackup, {isLoading: isDeletingBackup}] = useDeleteBackupMutation(); const [uploadBackup, {isLoading: isUploadingBackup}] = useUploadBackupMutation(); + const [restoreBackup, {isLoading: isRestoringBackup}] = useRestoreBackupMutation(); const [isDownloadingBackup, setDownloadingFiles] = useState(false); @@ -107,6 +109,20 @@ const useBackupManageTab = () => { } }; + const restoreBackupHandler = async (backupId) => { + try { + await restoreBackup(backupId).unwrap(); + notification.success({ + message: "Успех", description: "Резервная копия успешно восстановлена", placement: "topRight", + }); + } catch (e) { + notification.error({ + message: "Ошибка", + description: e?.data?.detail || "Не удалось восстановить резервную копию", + placement: "topRight", + }); + } + }; const deleteBackupHandler = async (backupId) => { try { @@ -142,6 +158,7 @@ const useBackupManageTab = () => { placement: "topRight", }); } catch (e) { + console.error("Upload error:", e); // Отладка notification.error({ message: "Ошибка", description: e.message || e?.data?.detail || "Не удалось загрузить резервную копию", @@ -158,11 +175,13 @@ const useBackupManageTab = () => { isDownloadingBackup, isDeletingBackup, isUploadingBackup, + isRestoringBackup, createBackupHandler, downloadBackupHandler, + restoreBackupHandler, deleteBackupHandler, uploadBackupHandler, - } + }; }; export default useBackupManageTab; \ No newline at end of file