diff --git a/API/app/application/contest_files_repository.py b/API/app/application/contest_files_repository.py new file mode 100644 index 0000000..0c626d9 --- /dev/null +++ b/API/app/application/contest_files_repository.py @@ -0,0 +1,32 @@ +from typing import Optional, Sequence + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.domain.models import ContestFile + + +class ContestFilesRepository: + def __init__(self, db: AsyncSession): + self.db = db + + async def get_by_id(self, file_id: int) -> Optional[ContestFile]: + stmt = select(ContestFile).filter_by(id=file_id) + result = await self.db.execute(stmt) + return result.scalars().first() + + async def get_by_contest_id(self, contest_id: int) -> Sequence[ContestFile]: + stmt = select(ContestFile).filter_by(contest_id=contest_id) + result = await self.db.execute(stmt) + return result.scalars().all() + + async def create(self, contest_file: ContestFile) -> ContestFile: + self.db.add(contest_file) + await self.db.commit() + await self.db.refresh(contest_file) + return contest_file + + async def delete(self, contest_file: ContestFile) -> ContestFile: + await self.db.delete(contest_file) + await self.db.commit() + return contest_file \ No newline at end of file diff --git a/API/app/contollers/contest_files_router.py b/API/app/contollers/contest_files_router.py new file mode 100644 index 0000000..82e89f1 --- /dev/null +++ b/API/app/contollers/contest_files_router.py @@ -0,0 +1,69 @@ +from fastapi import Depends, File, UploadFile, APIRouter +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.responses import FileResponse + +from app.database.session import get_db +from app.domain.entities.contest_file import ContestFileEntity +from app.infrastructure.dependencies import require_admin +from app.infrastructure.contest_files_service import ContestFilesService + +router = APIRouter() + + +@router.get( + "/contests/{contest_id}/", + response_model=list[ContestFileEntity], + summary="Get all contest files", + description="Returns metadata of all files uploaded for the specified contest." +) +async def get_files_by_contest_id( + contest_id: int, + db: AsyncSession = Depends(get_db), +): + service = ContestFilesService(db) + return await service.get_files_by_contest_id(contest_id) + + +@router.get( + "/{file_id}/file", + response_class=FileResponse, + summary="Download contest file by ID", + description="Returns the file for the specified file ID." +) +async def download_contest_file( + file_id: int, + db: AsyncSession = Depends(get_db), +): + service = ContestFilesService(db) + return await service.get_file_by_id(file_id) + + +@router.post( + "/contests/{contest_id}/upload", + response_model=ContestFileEntity, + summary="Upload a new file for the contest", + description="Uploads a new file and associates it with the specified contest." +) +async def upload_contest_file( + contest_id: int, + file: UploadFile = File(...), + db: AsyncSession = Depends(get_db), + user=Depends(require_admin), +): + service = ContestFilesService(db) + return await service.upload_file(contest_id, file, user) + + +@router.delete( + "/{file_id}/", + response_model=ContestFileEntity, + summary="Delete a contest file by ID", + description="Deletes the file and its database entry." +) +async def delete_contest_file( + file_id: int, + db: AsyncSession = Depends(get_db), + user=Depends(require_admin), +): + service = ContestFilesService(db) + return await service.delete_file(file_id, user) \ No newline at end of file diff --git a/API/app/contollers/project_files_router.py b/API/app/contollers/project_files_router.py index 0bdefc6..25d9c11 100644 --- a/API/app/contollers/project_files_router.py +++ b/API/app/contollers/project_files_router.py @@ -4,7 +4,7 @@ from starlette.responses import FileResponse from app.database.session import get_db from app.domain.entities.project_file import ProjectFileEntity -from app.infrastructure.dependencies import get_current_user, require_admin +from app.infrastructure.dependencies import require_admin from app.infrastructure.project_files_service import ProjectFilesService router = APIRouter() diff --git a/API/app/database/migrations/versions/de2777da99c9_0005_добавил_поле_filename_у_файлов_.py b/API/app/database/migrations/versions/de2777da99c9_0005_добавил_поле_filename_у_файлов_.py new file mode 100644 index 0000000..177d98b --- /dev/null +++ b/API/app/database/migrations/versions/de2777da99c9_0005_добавил_поле_filename_у_файлов_.py @@ -0,0 +1,32 @@ +"""0005_добавил_поле_filename_у_файлов_конкурсов + +Revision ID: de2777da99c9 +Revises: e53896c51cf8 +Create Date: 2025-06-03 10:23:08.013605 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'de2777da99c9' +down_revision: Union[str, None] = 'e53896c51cf8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('contest_files', sa.Column('filename', sa.String(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('contest_files', 'filename') + # ### end Alembic commands ### diff --git a/API/app/domain/entities/contest_file.py b/API/app/domain/entities/contest_file.py new file mode 100644 index 0000000..f6de705 --- /dev/null +++ b/API/app/domain/entities/contest_file.py @@ -0,0 +1,10 @@ +from typing import Optional + +from pydantic import BaseModel + + +class ContestFileEntity(BaseModel): + id: Optional[int] = None + filename: str + file_path: str + contest_id: int \ No newline at end of file diff --git a/API/app/domain/entities/profile_photo.py b/API/app/domain/entities/profile_photo.py index f01fb2a..19e9e8c 100644 --- a/API/app/domain/entities/profile_photo.py +++ b/API/app/domain/entities/profile_photo.py @@ -1,8 +1,10 @@ +from typing import Optional + from pydantic import BaseModel class ProfilePhotoEntity(BaseModel): - id: int + id: Optional[int] = None filename: str file_path: str profile_id: int diff --git a/API/app/domain/entities/project_file.py b/API/app/domain/entities/project_file.py index 9392fed..be8e37e 100644 --- a/API/app/domain/entities/project_file.py +++ b/API/app/domain/entities/project_file.py @@ -1,8 +1,10 @@ +from typing import Optional + from pydantic import BaseModel class ProjectFileEntity(BaseModel): - id: int + id: Optional[int] = None filename: str file_path: str project_id: int diff --git a/API/app/domain/models/contest_files.py b/API/app/domain/models/contest_files.py index e26eb58..49d47ba 100644 --- a/API/app/domain/models/contest_files.py +++ b/API/app/domain/models/contest_files.py @@ -7,6 +7,7 @@ from app.domain.models.base import AdvancedBaseModel class ContestFile(AdvancedBaseModel): __tablename__ = 'contest_files' + filename = Column(String, nullable=False) file_path = Column(String, nullable=False) contest_id = Column(Integer, ForeignKey('contests.id'), nullable=False) diff --git a/API/app/infrastructure/contest_carousel_photos_service.py b/API/app/infrastructure/contest_carousel_photos_service.py index d073f43..a008862 100644 --- a/API/app/infrastructure/contest_carousel_photos_service.py +++ b/API/app/infrastructure/contest_carousel_photos_service.py @@ -29,8 +29,8 @@ class ContestCarouselPhotosService: return FileResponse( photo.file_path, - media_type=self.get_media_type(photo.file_path), # Use file_path to infer type - filename=os.path.basename(photo.file_path), # Extract filename from path + media_type=self.get_media_type(photo.file_path), + filename=os.path.basename(photo.file_path), ) async def get_photos_by_contest_id(self, contest_id: int) -> list[ContestCarouselPhotoEntity]: @@ -40,7 +40,7 @@ class ContestCarouselPhotosService: for photo in photos ] - async def upload_photo(self, contest_id: int, file: UploadFile): # Removed 'user: User' for simplicity, add if needed + async def upload_photo(self, contest_id: int, file: UploadFile): self.validate_file_type(file) filename = self.generate_filename(file) @@ -89,8 +89,7 @@ class ContestCarouselPhotosService: @staticmethod def generate_filename(file: UploadFile): - original_filename = secure_filename(file.filename) - return f"{uuid.uuid4()}_{original_filename}" + return secure_filename(f"{uuid.uuid4()}_{file.filename}") @staticmethod def model_to_entity(photo_model: ContestCarouselPhoto) -> ContestCarouselPhotoEntity: diff --git a/API/app/infrastructure/contest_files_service.py b/API/app/infrastructure/contest_files_service.py new file mode 100644 index 0000000..e30dd18 --- /dev/null +++ b/API/app/infrastructure/contest_files_service.py @@ -0,0 +1,134 @@ +import os +import uuid + +import aiofiles +import magic +from fastapi import HTTPException, UploadFile +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.responses import FileResponse +from werkzeug.utils import secure_filename + +from app.application.contest_files_repository import ContestFilesRepository +from app.application.contests_repository import ContestsRepository +from app.domain.entities.contest_file import ContestFileEntity +from app.domain.models import ContestFile, User + + +class ContestFilesService: + def __init__(self, db: AsyncSession): + self.contest_files_repository = ContestFilesRepository(db) + self.contests_repository = ContestsRepository(db) + + async def get_file_by_id(self, file_id: int) -> FileResponse: + contest_file = await self.contest_files_repository.get_by_id(file_id) + + if not contest_file: + raise HTTPException(404, "Файл не найден") + + if not os.path.exists(contest_file.file_path): + raise HTTPException(404, "Файл не найден на диске") + + return FileResponse( + contest_file.file_path, + media_type=self.get_media_type(contest_file.file_path), + filename=os.path.basename(contest_file.file_path), + ) + + async def get_files_by_contest_id(self, contest_id: int) -> list[ContestFileEntity]: + files = await self.contest_files_repository.get_by_contest_id(contest_id) + return [self.model_to_entity(file) for file in files] + + async def upload_file(self, contest_id: int, file: UploadFile, user: User) -> ContestFileEntity: + contest = await self.contests_repository.get_by_id(contest_id) + if not contest: + raise HTTPException(404, "Конкурс не найден") + + self.validate_file_type(file) + file_path = await self.save_file(file) + + contest_file = ContestFile( + filename=file.filename, + file_path=file_path, + contest_id=contest_id, + ) + + return self.model_to_entity( + await self.contest_files_repository.create(contest_file) + ) + + async def delete_file(self, file_id: int, user: User) -> ContestFileEntity: + contest_file = await self.contest_files_repository.get_by_id(file_id) + if not contest_file: + raise HTTPException(404, "Файл не найден") + + if os.path.exists(contest_file.file_path): + os.remove(contest_file.file_path) + + return self.model_to_entity( + await self.contest_files_repository.delete(contest_file) + ) + + async def save_file(self, file: UploadFile, upload_dir: str = "uploads/contest_files") -> str: + os.makedirs(upload_dir, exist_ok=True) + filename = self.generate_filename(file) + file_path = os.path.join(upload_dir, filename) + + async with aiofiles.open(file_path, 'wb') as out_file: + content = await file.read() + await out_file.write(content) + return file_path + + @staticmethod + def validate_file_type(file: UploadFile): + mime = magic.Magic(mime=True) + file_type = mime.from_buffer(file.file.read(1024)) + file.file.seek(0) + + allowed_types = [ + "application/pdf", + "image/jpeg", + "image/png", + "application/zip", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/plain", + ] + + if file_type not in allowed_types: + raise HTTPException(400, f"Недопустимый тип файла: {file_type}") + + @staticmethod + def generate_filename(file: UploadFile) -> str: + return secure_filename(f"{uuid.uuid4()}_{file.filename}") + + @staticmethod + def model_to_entity(contest_file_model: ContestFile) -> ContestFileEntity: + return ContestFileEntity( + id=contest_file_model.id, + filename=contest_file_model.filename, + file_path=contest_file_model.file_path, + contest_id=contest_file_model.contest_id, + ) + + @staticmethod + def get_media_type(filename: str) -> str: + extension = filename.split('.')[-1].lower() + if extension in ['jpeg', 'jpg', 'png']: + return f"image/{extension}" + if extension == 'pdf': + return "application/pdf" + if extension in ['zip']: + return "application/zip" + if extension in ['doc', 'docx']: + return "application/msword" + if extension in ['xls', 'xlsx']: + return "application/vnd.ms-excel" + if extension in ['ppt', 'pptx']: + return "application/vnd.ms-powerpoint" + if extension in ['txt']: + return "text/plain" + return "application/octet-stream" \ No newline at end of file diff --git a/API/app/main.py b/API/app/main.py index c05b33b..2207555 100644 --- a/API/app/main.py +++ b/API/app/main.py @@ -13,6 +13,7 @@ from app.contollers.teams_router import router as team_router from app.contollers.users_router import router as users_router from app.contollers.contests_router import router as contest_router from app.contollers.contest_carousel_photos_router import router as contest_carousel_photos_router +from app.contollers.contest_files_router import router as contest_files_router from app.settings import settings @@ -41,6 +42,7 @@ def start_app(): api_app.include_router(contest_router, prefix=f'{settings.PREFIX}/contests', tags=['contests']) api_app.include_router(contest_carousel_photos_router, prefix=f'{settings.PREFIX}/contest_carousel_photos', tags=['contest_carousel_photos']) + api_app.include_router(contest_files_router, prefix=f'{settings.PREFIX}/contest_files', tags=['contest_files']) return api_app diff --git a/WEB/src/api/contests/contest_carousel_photos/deleteContestPhoto.js b/WEB/src/api/contests/contest_carousel_photos/deleteContestPhoto.js new file mode 100644 index 0000000..5437d99 --- /dev/null +++ b/WEB/src/api/contests/contest_carousel_photos/deleteContestPhoto.js @@ -0,0 +1,27 @@ +import axios from 'axios' +import CONFIG from '@/core/config.js' + + +const deleteContestCarouselPhoto = async (photoId) => { + try { + const token = localStorage.getItem('access_token') + + const response = await axios.delete( + `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/`, // Изменено здесь + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}` + } + } + ) + + return response.data + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message + console.error(`Ошибка удаления фотографии карусели конкурса с ID ${photoId}:`, errorMessage) + throw new Error(`Не удалось удалить фотографию карусели конкурса: ${errorMessage}`) + } +} + +export default deleteContestCarouselPhoto \ No newline at end of file diff --git a/WEB/src/api/contests/contest_carousel_photos/downloadContestPhotoFile.js b/WEB/src/api/contests/contest_carousel_photos/downloadContestPhotoFile.js new file mode 100644 index 0000000..0d3babc --- /dev/null +++ b/WEB/src/api/contests/contest_carousel_photos/downloadContestPhotoFile.js @@ -0,0 +1,28 @@ +import axios from 'axios' +import CONFIG from '@/core/config.js' + + +const downloadContestCarouselPhotoFile = async (photoId) => { + try { + const token = localStorage.getItem('access_token') + + const response = await axios.get( + `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}` + }, + responseType: 'blob' // Важно для загрузки файлов + } + ) + + return response.data + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message + console.error(`Ошибка загрузки файла фотографии карусели конкурса с ID ${photoId}:`, errorMessage) + throw new Error(`Не удалось загрузить файл фотографии карусели конкурса: ${errorMessage}`) + } +} + +export default downloadContestCarouselPhotoFile \ No newline at end of file diff --git a/WEB/src/api/contests/contest_carousel_photos/getContestPhotoFileById.js b/WEB/src/api/contests/contest_carousel_photos/getContestPhotoFileById.js new file mode 100644 index 0000000..1bf8442 --- /dev/null +++ b/WEB/src/api/contests/contest_carousel_photos/getContestPhotoFileById.js @@ -0,0 +1,27 @@ +import axios from 'axios' +import CONFIG from '@/core/config.js' + + +const getContestCarouselPhotosByContestId = async (contestId) => { + try { + const token = localStorage.getItem('access_token') + + const response = await axios.get( + `${CONFIG.BASE_URL}/contest_carousel_photos/contests/${contestId}/`, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}` + } + } + ) + + return response.data + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message + console.error(`Ошибка получения фотографий карусели конкурса для конкурса ${contestId}:`, errorMessage) + throw new Error(`Не удалось загрузить фотографии карусели конкурса: ${errorMessage}`) + } +} + +export default getContestCarouselPhotosByContestId \ No newline at end of file diff --git a/WEB/src/api/contests/contest_carousel_photos/uploadContestProfilePhoto.js b/WEB/src/api/contests/contest_carousel_photos/uploadContestProfilePhoto.js new file mode 100644 index 0000000..b202ae3 --- /dev/null +++ b/WEB/src/api/contests/contest_carousel_photos/uploadContestProfilePhoto.js @@ -0,0 +1,33 @@ +import axios from 'axios' +import CONFIG from '@/core/config.js' + + +const uploadContestCarouselPhoto = async (contestId, file) => { + try { + const token = localStorage.getItem('access_token') + + const formData = new FormData() + formData.append('file', file) + + formData.append('contest_id', contestId); + + const response = await axios.post( + `${CONFIG.BASE_URL}/contest_carousel_photos/contests/${contestId}/upload`, + formData, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}`, + } + } + ) + + return response.data + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message + console.error(`Ошибка загрузки фотографии в карусель конкурса с ID ${contestId}:`, errorMessage) + throw new Error(`Не удалось загрузить фотографию в карусель конкурса: ${errorMessage}`) + } +} + +export default uploadContestCarouselPhoto \ No newline at end of file diff --git a/WEB/src/api/contests/contest_files/deleteContestFile.js b/WEB/src/api/contests/contest_files/deleteContestFile.js new file mode 100644 index 0000000..211789b --- /dev/null +++ b/WEB/src/api/contests/contest_files/deleteContestFile.js @@ -0,0 +1,31 @@ +import axios from "axios"; +import CONFIG from "@/core/config.js"; + +const deleteContestFile = async (fileId) => { + try { + const token = localStorage.getItem('access_token'); // Получаем токен из localStorage + + const response = await axios.delete( + `${CONFIG.BASE_URL}/contest_files/${fileId}/`, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + return response.data; + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message; + console.error(`Ошибка удаления файла конкурса с ID ${fileId}:`, errorMessage); + if (error.response?.status === 401) { + throw new Error("Недостаточно прав для удаления файла (401)"); + } + if (error.response?.status === 404) { + throw new Error("Файл не найден (404)"); + } + throw new Error(errorMessage); + } +}; + +export default deleteContestFile; \ No newline at end of file diff --git a/WEB/src/api/contests/contest_files/downloadContestFile.js b/WEB/src/api/contests/contest_files/downloadContestFile.js new file mode 100644 index 0000000..16363ca --- /dev/null +++ b/WEB/src/api/contests/contest_files/downloadContestFile.js @@ -0,0 +1,39 @@ +import axios from "axios"; +import CONFIG from "@/core/config.js"; + +const downloadContestFile = async (fileId) => { + try { + const response = await axios.get(`${CONFIG.BASE_URL}/contest_files/${fileId}/file`, { + responseType: 'blob', + withCredentials: true, + }); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + const contentDisposition = response.headers['content-disposition']; + let filename = 'download'; + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename="(.+)"/); + if (filenameMatch && filenameMatch[1]) { + filename = filenameMatch[1]; + } + } + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + return filename; + } catch (error) { + if (error.response?.status === 401) { + throw new Error("Нет доступа для скачивания файла (401)"); + } + if (error.response?.status === 404) { + throw new Error("Файл не найден (404)"); + } + throw new Error(error.message); + } +}; + +export default downloadContestFile; \ No newline at end of file diff --git a/WEB/src/api/contests/contest_files/getContestFiles.js b/WEB/src/api/contests/contest_files/getContestFiles.js new file mode 100644 index 0000000..57bbd0c --- /dev/null +++ b/WEB/src/api/contests/contest_files/getContestFiles.js @@ -0,0 +1,21 @@ +import axios from "axios"; +import CONFIG from "@/core/config.js"; + +const getContestFiles = async (contestId) => { + try { + const response = await axios.get(`${CONFIG.BASE_URL}/contest_files/contests/${contestId}/`, { + withCredentials: true, + }); + return response.data; + } catch (error) { + if (error.response?.status === 401) { + throw new Error("Нет доступа к файлам конкурса (401)"); + } + if (error.response?.status === 404) { + throw new Error("Конкурс не найден (404)"); + } + throw new Error(error.message); + } +}; + +export default getContestFiles; \ No newline at end of file diff --git a/WEB/src/api/contests/contest_files/uploadContestFile.js b/WEB/src/api/contests/contest_files/uploadContestFile.js new file mode 100644 index 0000000..0ac8e79 --- /dev/null +++ b/WEB/src/api/contests/contest_files/uploadContestFile.js @@ -0,0 +1,38 @@ +import axios from "axios"; +import CONFIG from "@/core/config.js"; + +const uploadContestFile = async (contestId, file) => { + try { + const token = localStorage.getItem('access_token'); + + const formData = new FormData(); + formData.append("file", file); + + const response = await axios.post( + `${CONFIG.BASE_URL}/contest_files/contests/${contestId}/upload`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + } + ); + return response.data; + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message; + if (error.response?.status === 401) { + throw new Error("Недостаточно прав для загрузки файла (401)"); + } + if (error.response?.status === 400) { + throw new Error(`Ошибка загрузки: ${errorMessage}`); + } + if (error.response?.status === 404) { + throw new Error("Конкурс не найден (404)"); + } + throw new Error(errorMessage); + } +}; + +export default uploadContestFile; \ No newline at end of file diff --git a/WEB/src/api/projects/project_files/deleteProjectFile.js b/WEB/src/api/projects/project_files/deleteProjectFile.js new file mode 100644 index 0000000..646e548 --- /dev/null +++ b/WEB/src/api/projects/project_files/deleteProjectFile.js @@ -0,0 +1,30 @@ +import axios from 'axios'; +import CONFIG from '@/core/config.js'; + +const deleteProjectFile = async (fileId) => { + try { + const token = localStorage.getItem('access_token'); + const response = await axios.delete( + `${CONFIG.BASE_URL}/project_files/${fileId}/`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + } + ); + return response.data; + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message; + console.error(`Ошибка удаления файла проекта с ID ${fileId}:`, errorMessage); + if (error.response?.status === 401) { + throw new Error("Недостаточно прав для удаления файла (401)"); + } + if (error.response?.status === 404) { + throw new Error("Файл не найден (404)"); + } + throw new Error(errorMessage); + } +}; + +export default deleteProjectFile; \ No newline at end of file diff --git a/WEB/src/api/projects/project_files/downloadProjectFile.js b/WEB/src/api/projects/project_files/downloadProjectFile.js new file mode 100644 index 0000000..11b1347 --- /dev/null +++ b/WEB/src/api/projects/project_files/downloadProjectFile.js @@ -0,0 +1,51 @@ +import axios from 'axios'; +import CONFIG from '@/core/config.js'; + +const downloadProjectFile = async (fileId) => { + try { + const token = localStorage.getItem('access_token'); + const response = await axios.get( + `${CONFIG.BASE_URL}/project_files/${fileId}/download`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + responseType: 'blob', + withCredentials: true, + } + ); + + // Создаем ссылку для скачивания + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + + const contentDisposition = response.headers['content-disposition']; + let filename = `project_file_${fileId}`; + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename="([^"]+)"/); + if (filenameMatch && filenameMatch[1]) { + filename = decodeURIComponent(filenameMatch[1]); + } + } + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + return { success: true, filename: filename }; + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message; + console.error(`Ошибка скачивания файла проекта с ID ${fileId}:`, errorMessage); + if (error.response?.status === 401) { + throw new Error("Недостаточно прав для скачивания файла (401)"); + } + if (error.response?.status === 404) { + throw new Error("Файл не найден (404)"); + } + throw new Error(errorMessage); + } +}; + +export default downloadProjectFile; \ No newline at end of file diff --git a/WEB/src/api/projects/project_files/getProjectFiles.js b/WEB/src/api/projects/project_files/getProjectFiles.js new file mode 100644 index 0000000..17b19fd --- /dev/null +++ b/WEB/src/api/projects/project_files/getProjectFiles.js @@ -0,0 +1,24 @@ +import axios from 'axios'; +import CONFIG from '@/core/config.js'; + +const getProjectFiles = async (projectId) => { + try { + const token = localStorage.getItem('access_token'); + const response = await axios.get( + `${CONFIG.BASE_URL}/project_files/projects/${projectId}/`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + } + ); + return response.data; + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message; + console.error(`Ошибка загрузки файлов проекта с ID ${projectId}:`, errorMessage); + throw new Error(`Не удалось загрузить файлы проекта: ${errorMessage}`); + } +}; + +export default getProjectFiles; \ No newline at end of file diff --git a/WEB/src/api/projects/project_files/uploadProjectFile.js b/WEB/src/api/projects/project_files/uploadProjectFile.js new file mode 100644 index 0000000..7a5605f --- /dev/null +++ b/WEB/src/api/projects/project_files/uploadProjectFile.js @@ -0,0 +1,39 @@ +import axios from 'axios'; +import CONFIG from '@/core/config.js'; + +const uploadProjectFile = async (projectId, file) => { + try { + const token = localStorage.getItem('access_token'); + + const formData = new FormData(); + formData.append("file", file); + + const response = await axios.post( + `${CONFIG.BASE_URL}/project_files/projects/${projectId}/upload`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + Authorization: `Bearer ${token}`, + }, + withCredentials: true, + } + ); + return response.data; + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message; + console.error(`Ошибка загрузки файла проекта с ID ${projectId}:`, errorMessage); + if (error.response?.status === 401) { + throw new Error("Недостаточно прав для загрузки файла (401)"); + } + if (error.response?.status === 400) { + throw new Error(`Ошибка загрузки: ${errorMessage}`); + } + if (error.response?.status === 404) { + throw new Error("Проект не найден (404)"); + } + throw new Error(errorMessage); + } +}; + +export default uploadProjectFile; \ No newline at end of file diff --git a/WEB/src/pages/AdminPage.vue b/WEB/src/pages/AdminPage.vue index 48b00e2..3e3b08b 100644 --- a/WEB/src/pages/AdminPage.vue +++ b/WEB/src/pages/AdminPage.vue @@ -168,7 +168,7 @@