diff --git a/Dockerfile b/Dockerfile.api similarity index 100% rename from Dockerfile rename to Dockerfile.api diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 0000000..1e6fb46 --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,3 @@ +FROM nginx:alpine +COPY . /usr/share/nginx/html +EXPOSE 80 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index a3959a7..f454995 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ services: api: - build: . + build: + context: . + dockerfile: Dockerfile.api ports: - "8000:8000" environment: @@ -19,3 +21,13 @@ services: - REDIS_PASSWORD=CNXpuhMdxXHo7ZK8bhtXDvgXVZcjRn volumes: - ./data:/app/data + web: + build: + context: ./web + dockerfile: ../Dockerfile.web + ports: + - "3000:80" + depends_on: + - api + volumes: + - ./web:/usr/share/nginx/html:ro diff --git a/main.py b/main.py index ccc41f0..4d74a51 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,8 @@ import os import tomllib from pathlib import Path from fastapi import FastAPI -from route import base, holland_crud, init_data_base, groups_crud, organizations_crud, users_crud, question_crud, choice_crud, response_crud, poll_crud, ui +from fastapi.middleware.cors import CORSMiddleware +from route import base, holland_crud, init_data_base, groups_crud, organizations_crud, users_crud, question_crud, choice_crud, response_crud, poll_crud import uvicorn # Читаем версию из pyproject.toml @@ -20,6 +21,14 @@ except Exception as e: APP_PATH = os.getenv("APP_PATH", "/home/user/api-copp") app = FastAPI(title="api", version=VERSION) +# Разрешаем CORS для фронтенда (dev). При необходимости сузить список origins. +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) app.include_router(base.router) app.include_router(init_data_base.router) app.include_router(groups_crud.router) @@ -30,7 +39,7 @@ app.include_router(choice_crud.router) app.include_router(response_crud.router) app.include_router(poll_crud.router) app.include_router(holland_crud.router) -app.include_router(ui.router) + # Доступна переменная APP_PATH для использования в приложении diff --git a/route/init_data_base.py b/route/init_data_base.py index 12b9732..d387e82 100644 --- a/route/init_data_base.py +++ b/route/init_data_base.py @@ -7,6 +7,7 @@ from typing import List from bd import Settings from sqlalchemy import create_engine from sqlalchemy.exc import SQLAlchemyError +from pydantic import BaseModel router = APIRouter(tags=["db"]) @@ -67,3 +68,40 @@ async def create_tables(): return {"status": "ok", "detail": f"Created {len(unique)} metadata groups"} except SQLAlchemyError as e: raise HTTPException(status_code=500, detail=str(e)) + + +class ClearDBIn(BaseModel): + confirm: bool + + +@router.post("/db/clear") +async def clear_tables(payload: ClearDBIn): + """Полная очистка всех таблиц, описанных в `bd.tables`. + + Требуется явное подтверждение: POST с телом {"confirm": true}. + """ + if not payload.confirm: + raise HTTPException(status_code=400, detail="Confirmation required") + + settings = Settings() + db_url = settings.DATABASE_URL_syncpg + engine = create_engine(db_url, future=True) + + metadatas = collect_metadatas() + if not metadatas: + raise HTTPException(status_code=400, detail="No table metadata found in db.tables") + + # deduplicate metadata objects + unique = [] + seen = set() + for md in metadatas: + if id(md) not in seen: + seen.add(id(md)) + unique.append(md) + + try: + for md in unique: + md.drop_all(bind=engine) + return {"status": "ok", "detail": f"Dropped {len(unique)} metadata groups"} + except SQLAlchemyError as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/route/response_crud.py b/route/response_crud.py index 46b5387..57f7e92 100644 --- a/route/response_crud.py +++ b/route/response_crud.py @@ -95,3 +95,15 @@ def get_response(response_id: str): if not r: raise HTTPException(status_code=404, detail="Response not found") return {"id": str(r.id), "poll_id": str(r.poll_id), "user_id": str(r.user_id) if r.user_id else None, "submitted_at": r.submitted_at.isoformat(), "answers": [{"id": str(a.id), "question_id": str(a.question_id), "choice_id": str(a.choice_id) if a.choice_id else None, "text": a.text} for a in r.answers]} + + +@router.get("/users/{user_id}/responses") +def list_responses_by_user(user_id: str): + Session = get_session() + try: + uid = uuid.UUID(user_id) + except Exception: + raise HTTPException(status_code=400, detail="Invalid user UUID") + with Session() as session: + rows = session.query(Response).filter(Response.user_id == uid).all() + return [{"id": str(r.id), "poll_id": str(r.poll_id), "submitted_at": r.submitted_at.isoformat()} for r in rows] diff --git a/route/ui.py b/route/ui.py deleted file mode 100644 index 5ce96b2..0000000 --- a/route/ui.py +++ /dev/null @@ -1,111 +0,0 @@ -from fastapi import APIRouter, Request - -router = APIRouter() - - -@router.get("/ui/create-user") -def create_user_page(request: Request): - return """ - - -
- -Нажмите продолжить, чтобы зарегистрироваться и пройти тест.
`); + document.getElementById('agree').onclick = ()=> showRegister(); +} + +function showRegister(){ + render(`Не удалось загрузить тест
'); return } + const poll = await res.json(); + // simple rendering: list questions and choices (if present) + const html = [`