package handler import ( "net/http" "github.com/go-chi/chi/v5" "learning-service/internal/model" "learning-service/internal/repository" ) type AttemptHandler struct { repo *repository.AttemptRepository testRepo *repository.TestRepository } func NewAttemptHandler(repo *repository.AttemptRepository, testRepo *repository.TestRepository) *AttemptHandler { return &AttemptHandler{repo: repo, testRepo: testRepo} } // Start — POST /tests/{id}/attempts. Создаёт попытку прохождения для // текущего юзера. Респондент берётся из X-User-Id (заголовок ставит // portal-gateway). Для public-tokens — отдельный handler в public.go. func (h *AttemptHandler) Start(w http.ResponseWriter, r *http.Request) { uid, ok := userIDFromHeader(r) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } testID, err := parseUUID(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid test id") return } // Проверим, что тест опубликован — иначе только владелец может стартовать // (для preview). MVP: для не-владельца требуем is_published. t, err := h.testRepo.Get(r.Context(), testID) if err != nil { writeRepoError(w, r, err, "get test") return } if !t.IsPublished && t.OwnerUserID != uid { writeError(w, http.StatusForbidden, "test is not published") return } att, err := h.repo.Start(r.Context(), repository.StartParams{ TestID: testID, UserID: &uid, IP: r.RemoteAddr, UserAgent: r.UserAgent(), }) if err != nil { writeRepoError(w, r, err, "start attempt") return } writeJSON(w, http.StatusCreated, att) } // Get — GET /attempts/{id}. Возвращает попытку. Доступно: владелец попытки // (user_id), владелец теста (HR-просмотр), или admin (через bypass на портале). func (h *AttemptHandler) Get(w http.ResponseWriter, r *http.Request) { uid, ok := userIDFromHeader(r) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } id, err := parseUUID(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } a, err := h.repo.Get(r.Context(), id) if err != nil { writeRepoError(w, r, err, "get attempt") return } // Доступ: я респондент или я владелец теста. if a.UserID == nil || *a.UserID != uid { t, err := h.testRepo.Get(r.Context(), a.TestID) if err != nil || t.OwnerUserID != uid { writeError(w, http.StatusForbidden, "forbidden") return } } writeJSON(w, http.StatusOK, a) } // Submit — POST /attempts/{id}/submit. Применяет ответы и считает score. // После submit'а попытка переходит в submitted (если есть text-вопросы) // или сразу в graded. Идемпотентно НЕ делаем — повторный submit вернёт // ошибку «уже submitted». func (h *AttemptHandler) Submit(w http.ResponseWriter, r *http.Request) { uid, ok := userIDFromHeader(r) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } id, err := parseUUID(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } existing, err := h.repo.Get(r.Context(), id) if err != nil { writeRepoError(w, r, err, "get attempt") return } if existing.UserID == nil || *existing.UserID != uid { writeError(w, http.StatusForbidden, "not your attempt") return } var req model.SubmitAttemptRequest if err := decodeJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid body") return } updated, err := h.repo.SubmitAndGrade(r.Context(), id, req) if err != nil { writeRepoError(w, r, err, "submit attempt") return } writeJSON(w, http.StatusOK, updated) } // ListMine — GET /attempts?mine=true. История моих попыток. func (h *AttemptHandler) ListMine(w http.ResponseWriter, r *http.Request) { uid, ok := userIDFromHeader(r) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } items, err := h.repo.ListByUser(r.Context(), uid) if err != nil { writeRepoError(w, r, err, "list my attempts") return } writeJSON(w, http.StatusOK, map[string]any{"items": items}) } // ListByTest — GET /tests/{id}/attempts. Список попыток для HR (владелец теста). func (h *AttemptHandler) ListByTest(w http.ResponseWriter, r *http.Request) { uid, ok := userIDFromHeader(r) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } testID, err := parseUUID(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid test id") return } t, err := h.testRepo.Get(r.Context(), testID) if err != nil { writeRepoError(w, r, err, "get test") return } if t.OwnerUserID != uid { writeError(w, http.StatusForbidden, "only owner can view attempts") return } items, err := h.repo.ListByTest(r.Context(), testID) if err != nil { writeRepoError(w, r, err, "list test attempts") return } writeJSON(w, http.StatusOK, map[string]any{"items": items}) }