package handler import ( "encoding/json" "errors" "log/slog" "net/http" "strings" "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 } // viewerContextFromHeaders — собирает ViewerContext из portal-gateway- // прокинутых headers'ов. Пустые поля = соответствующие access_grants // не сматчатся (что корректно: «у меня нет роли X» → ресурсы выданные // роли X мне не видны). func viewerContextFromHeaders(r *http.Request, uid uuid.UUID) repository.ViewerContext { ctx := repository.ViewerContext{UserID: uid} // X-User-Roles: CSV ролевых UUID'ов. if v := r.Header.Get("X-User-Roles"); v != "" { for _, raw := range strings.Split(v, ",") { raw = strings.TrimSpace(raw) if raw == "" { continue } if id, err := uuid.Parse(raw); err == nil { ctx.RoleIDs = append(ctx.RoleIDs, id) } } } if v := r.Header.Get("X-User-Department-Id"); v != "" { if id, err := uuid.Parse(strings.TrimSpace(v)); err == nil { ctx.DepartmentID = &id } } if v := r.Header.Get("X-User-Position-Id"); v != "" { if id, err := uuid.Parse(strings.TrimSpace(v)); err == nil { ctx.PositionID = &id } } return ctx }