Files
learning/internal/handler/helpers.go
Ilya 62519081e7 init: learning-service skeleton
Микросервис обучения портала: тесты, курсы, видео-уроки, доступы,
public-ссылки для кандидатов с email-валидацией.

В этой итерации:
- Skeleton (config, migrate, main, health) по паттерну tasks/candidates
- Migration 001_init: 10 таблиц (tests/questions/answers/attempts/
  attempt_answers + courses/lessons/lesson_progress + access_grants +
  public_tokens) с подробными комментариями why
- Tests: полный CRUD + вопросы/ответы; non-owner'у is_correct и
  explanation скрываются в выдаче
- Заглушки 501 для attempts / courses / lessons / video-stream /
  access / public-tokens — следующие итерации
- k8s: namespace, configmap, secrets, postgres, deployment с HPA,
  service с portal-discovery annotations
- Dockerfile, Makefile, .gitignore

См. README.md для полного списка отложенного и инструкций запуска.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 22:43:37 +03:00

64 lines
1.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"github.com/google/uuid"
"learning-service/internal/repository"
)
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]any{"error": msg})
}
// writeRepoError — маппит repository.Err* на HTTP-коды. Для остальных
// логируем и отдаём 500 без деталей (наружу не светим внутренние ошибки).
func writeRepoError(w http.ResponseWriter, r *http.Request, err error, action string) {
switch {
case errors.Is(err, repository.ErrNotFound):
writeError(w, http.StatusNotFound, "not found")
case errors.Is(err, repository.ErrForbidden):
writeError(w, http.StatusForbidden, "forbidden")
default:
slog.Error("repo error", "action", action, "error", err, "path", r.URL.Path)
writeError(w, http.StatusInternalServerError, "internal error")
}
}
func decodeJSON(r *http.Request, v any) error {
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
return dec.Decode(v)
}
func parseUUID(s string) (uuid.UUID, error) {
if s == "" {
return uuid.Nil, errors.New("empty uuid")
}
return uuid.Parse(s)
}
// userIDFromHeader — portal-gateway проставляет X-User-Id после валидации JWT.
// Все НЕ-internal handler'ы читают отсюда; пустой = unauthorized.
func userIDFromHeader(r *http.Request) (uuid.UUID, bool) {
v := r.Header.Get("X-User-Id")
if v == "" {
return uuid.Nil, false
}
id, err := uuid.Parse(v)
if err != nil {
return uuid.Nil, false
}
return id, true
}