Files
test_deploy/internal/http/handlers.go
Alex Shevchuk 61fc0d2747
Some checks failed
Deploy Production / Deploy to Staging (push) Has been skipped
Go Linter / Run golangci-lint (api_gateway) (push) Failing after 2m31s
Go Linter / Build golang services (api_gateway) (push) Has been skipped
Go Linter / Tag Commit (push) Has been skipped
Go Linter / Push Docker Images (api_gateway) (push) Has been skipped
71
2025-09-17 14:32:06 +03:00

829 lines
26 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 http_router
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/feed"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/object_storage"
notification "github.com/AlexOreL-272/ProtoMolva/go/gen/notifications"
"github.com/aws/smithy-go/ptr"
"google.golang.org/protobuf/proto"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth/keycloak"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
"github.com/google/uuid"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/broker"
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
formgenerator "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/form_generator"
rmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/types"
"github.com/gorilla/mux"
)
// @Summary Получить персональную ссылку
// @Description Получение персональной ссылки агента для вакансии
// @Tags agents
// @Accept json
// @Produce json
// @Param agent_id path string true "ID агента"
// @Param vacancy_id path string true "ID вакансии"
// @Success 200 {object} types.PersonalLinkResponse "Персональная ссылка"
// @Failure 400 {object} map[string]string "Неверные данные запроса"
// @Failure 401 {object} map[string]string "Неавторизованный доступ"
// @Failure 500 {object} map[string]string "Внутренняя ошибка сервера"
// @Security BearerAuth
// @Router /api/v1/agents/{agent_id}/vacancies/{vacancy_id} [get]
func (h *handler) getPersonalLinkHandler(w http.ResponseWriter, r *http.Request) {
handlerName := "getPersonalLinkHandler"
vars := mux.Vars(r)
agentId := vars["agent_id"]
vacancyId := vars["vacancy_id"]
// formatting string
linkParams := fmt.Sprintf("%s|%s", agentId, vacancyId)
// encryption string
encryptedLink, err := h.urlShortener.Shorten(linkParams)
if err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error while encrypting link: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
personalLink := fmt.Sprintf("%s/api/v1/anketa?link=%s", r.Host, encryptedLink)
resp := types.PersonalLinkResponse{
Link: personalLink,
}
w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error while marshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
}
// @Summary Получить анкету
// @Description Получение HTML формы анкеты для клиента
// @Tags clients
// @Accept json
// @Produce text/html
// @Param link query string true "Зашифрованная ссылка с параметрами"
// @Success 200 {string} string "HTML форма анкеты"
// @Failure 400 {object} map[string]string "Неверные данные запроса"
// @Failure 404 {object} map[string]string "Вакансия не найдена"
// @Failure 500 {object} map[string]string "Внутренняя ошибка сервера"
// @Router /api/v1/anketa [get]
//
//nolint:funlen // TODO: make it sudo super clean
func (h *handler) getAnketaHandler(w http.ResponseWriter, r *http.Request) {
handlerName := "getAnketaHandler"
encodedLink := r.URL.Query().Get("link")
if encodedLink == "" {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("anketa link is empty: ",
slog.String("handler", handlerName))
return
}
link, err := h.urlShortener.Unshorten(encodedLink)
if err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error while unshorting link: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
linkParams := strings.Split(link, "|")
if len(linkParams) != 2 {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("Invalid link",
slog.String("handler", handlerName))
return
}
agentId, vacancyId := linkParams[0], linkParams[1]
h.logger.Debug("serving client form: ",
slog.String("agentID", agentId),
slog.String("vacancyID", vacancyId),
slog.String("handler", handlerName),
)
resp, err := h.dbClient.GetVacancyList(r.Context(), &dbtypes.VacancyListGetRequest{
Filters: &dbtypes.VacancyListFilters{
VacancyId: &vacancyId,
},
})
if err != nil {
h.handleDBError(w, err)
h.logger.Error("error getting vacancy info",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
if len(resp.Vacancies) == 0 {
// maybe still serve the form but without vacancy details?
http.Error(w, constants.ErrNotFound.Error(), http.StatusNotFound)
h.logger.Error("Vacancy info not found",
slog.String("handler", handlerName))
return
}
vacancyInfo := resp.Vacancies[0]
h.logger.Debug("found vacancy info: ",
slog.Any("vacancy", vacancyInfo),
slog.String("handler", handlerName),
)
formVacancyInfo := map[string]string{
"AgentId": agentId,
"VacancyId": vacancyId,
"VacancyName": vacancyInfo.Name,
"Address": vacancyInfo.Address,
"WorkFormat": vacancyInfo.WorkFormat,
"SalaryTop": fmt.Sprintf("%d", vacancyInfo.SalaryTop),
"SalaryBottom": fmt.Sprintf("%d", vacancyInfo.SalaryBottom),
}
if vacancyInfo.Requirements != nil {
formVacancyInfo["Requirements"] = *vacancyInfo.Requirements
}
if vacancyInfo.Responsibilities != nil {
formVacancyInfo["Responsibilities"] = *vacancyInfo.Responsibilities
}
if vacancyInfo.ExtraInfo != nil {
formVacancyInfo["Description"] = *vacancyInfo.ExtraInfo
}
formData, err := formgenerator.GenerateForm(formVacancyInfo)
if err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error generating form",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
w.Header().Set("Content-Type", "text/html")
if _, err := w.Write([]byte(formData)); err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error writing response: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
}
func (h *handler) getEmployeesData(ctx context.Context, staff []string) ([]types.Employee, error) {
employees := make([]types.Employee, len(staff))
handlerName := "getEmployeesData"
for i, uid := range staff {
userInfo, err := h.authManager.GetUserInfo(ctx, uid)
if err != nil {
h.logger.Error("Error retrieving user info",
slog.String("error", err.Error()),
slog.String("source", handlerName))
return nil, err
}
permissions, err := h.authManager.GetPermissionsByUsersId(ctx, uid)
if err != nil {
return nil, err
}
employees[i] = types.Employee{
UID: uid,
FirstName: userInfo.FirstName,
LastName: userInfo.SecondName,
Email: userInfo.Email,
Permissions: h.extractPermissions(permissions),
}
if userInfo.Patronymic != nil {
employees[i].MiddleName = *userInfo.Patronymic
}
}
return employees, nil
}
func (h *handler) extractPermissions(permMap *auth.GetPermissionsByUsersIdResponse) map[constants.PermissionType]constants.PermissionValue {
perm := map[constants.PermissionType]constants.PermissionValue{
keycloak.PermissionProfile: constants.PermissionValue(permMap.Profile),
keycloak.PermissionEmployees: constants.PermissionValue(permMap.Employees),
keycloak.PermissionCompany: constants.PermissionValue(permMap.Company),
keycloak.PermissionVacancies: constants.PermissionValue(permMap.Vacancies),
keycloak.PermissionBalance: constants.PermissionValue(permMap.Balance),
keycloak.PermissionSubmissions: constants.PermissionValue(permMap.Submissions),
}
return perm
}
// @Summary Получить список сотрудников компании
// @Description Получение списка сотрудников компании
// @Tags employees
// @Accept json
// @Produce json
// @Param company_id path string true "ID компании"
// @Param uid query string true "ID пользователя"
// @Success 200 {object} rmodel.EmployeeResponse "Список сотрудников"
// @Failure 400 {object} map[string]string "Неверные данные запроса"
// @Failure 401 {object} map[string]string "Неавторизованный доступ"
// @Failure 500 {object} map[string]string "Внутренняя ошибка сервера"
// @Security BearerAuth
// @Router /api/v1/companies/{company_id}/employees [get]
func (h *handler) getEmployeesHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getEmployeesHandler"
var (
uid = r.URL.Query().Get("uid")
vars = mux.Vars(r)
companyId = vars["company_id"]
)
// TODO: rewrite to request struct
if uid == "" {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("UID is required",
slog.String("handler", handlerName),
slog.String("source", "getEmployeesHandler.checkUserPermissions"))
return
}
company, err := h.distributorService.GetCompanyInfoById(r.Context(), &rmodel.CompanyByIdGetRequest{
UserId: uid,
CompanyId: companyId,
})
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting company info",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
employees, err := h.getEmployeesData(r.Context(), company.Company.Staff)
if err != nil {
h.handleKeycloakError(w, err)
h.logger.Error("error getting employees data",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
response := rmodel.EmployeeResponse{
CompanyID: companyId,
Employees: employees,
}
w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error encoding response",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
}
}
// @Summary Получить статус валидации пользователя
// @Description Получение статуса валидации пользователя по UID
// @Tags system
// @Accept json
// @Produce json
// @Param uid path string true "UID пользователя"
// @Success 200 {object} map[string]string "Статус валидации"
// @Failure 400 {object} map[string]string "Неверные параметры запроса"
// @Failure 500 {object} map[string]string "Внутренняя ошибка сервера"
// @Router /api/v1/{uid}/validation [get]
func (h *handler) getUserValidationStatusHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getValidationStatusHandler"
var (
vars = mux.Vars(r)
uid = vars["uid"]
)
if _, err := uuid.Parse(uid); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("Invalid uid",
slog.String("error", constants.ErrBadRequest.Error()),
slog.String("handler", handlerName))
return
}
resp, err := h.dbClient.GetClientValidation(r.Context(), &dbtypes.ClientValidationGetRequest{
UserId: uid,
})
if err != nil {
h.handleDBError(w, err)
h.logger.Error("error getting user validation",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error encoding response",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
}
}
// @Summary Получить файл документа
// @Description Получение файла документа по имени
// @Tags system
// @Accept json
// @Produce application/octet-stream
// @Param file path string true "Имя файла"
// @Success 200 {file} file "Файл документа"
// @Failure 404 {object} map[string]string "Файл не найден"
// @Failure 500 {object} map[string]string "Внутренняя ошибка сервера"
// @Router /api/v1/docs/{file} [get]
func (h *handler) getFileHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
fileName := vars["file"]
if fileName == "" {
http.Error(w, "file parameter is required", http.StatusBadRequest)
return
}
link, err := h.cacheClient.Get(r.Context(), fileName, cache.DocumentsValueType)
if err != nil && !errors.Is(err, cache.ErrKeyNotFound) {
h.logger.Error("error getting file link from cache",
slog.String("error", err.Error()))
}
if errors.Is(err, cache.ErrKeyNotFound) {
newLink, err := h.objectStorageClient.GetPresignedLink(r.Context(),
fileName, object_storage.DocumentCategory, object_storage.LinkOptions{
TTL: ptr.Duration(time.Minute * 30),
})
if err != nil {
if errors.Is(err, object_storage.ErrObjectNotFound) {
http.Error(w, constants.ErrNotFound.Error(), http.StatusNotFound)
return
}
h.logger.Error("error getting file link from object storage",
slog.String("error", err.Error()))
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
return
}
body, err := json.Marshal(types.GetDocumentsResponse{
Link: newLink,
ExpiresIn: time.Now().Add(time.Minute * 25),
})
if err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
return
}
if err := h.cacheClient.Set(r.Context(), fileName, cache.DocumentsValueType, string(body), time.Minute*25); err != nil {
h.logger.Error("error setting file link to cache",
slog.String("error", err.Error()))
}
link = string(body)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(link))
}
// @Summary Подтверждение email
// @Description Подтверждение email адреса пользователя по токену
// @Tags auth
// @Accept json
// @Produce json
// @Param uid query string true "ID пользователя"
// @Param token query string true "Токен подтверждения"
// @Success 200 {object} map[string]string "Email успешно подтвержден"
// @Failure 400 {object} map[string]string "Неверные параметры запроса"
// @Failure 500 {object} map[string]string "Внутренняя ошибка сервера"
// @Router /api/v1/verify_email [patch]
func (h *handler) verifyEmailHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "verifyEmailHandler"
userID := r.URL.Query().Get("uid")
token := r.URL.Query().Get("token")
if userID == "" || token == "" {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("missing parameters: uid or token",
slog.String("handler", handlerName))
return
}
if err := h.authManager.VerifyEmail(r.Context(), userID, token); err != nil {
h.handleKeycloakError(w, err)
h.logger.Error("error verifying email",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
userInfo, err := h.authManager.GetUserInfo(r.Context(), userID)
if err != nil {
h.logger.Error("error getting user info after verification",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
w.WriteHeader(http.StatusOK)
return
}
userName := h.formatUserFullName(userInfo)
if err := h.sendWelcomeEmail(userInfo.Email, userName); err != nil {
h.logger.Error("error sending welcome email to user",
slog.String("error", err.Error()),
slog.String("user_id", userID),
slog.String("handler", handlerName))
}
if err := h.sendNewUserNotificationToAdmin(userInfo, userName); err != nil {
h.logger.Error("error sending new user notification to admin",
slog.String("error", err.Error()),
slog.String("user_id", userID),
slog.String("handler", handlerName))
}
h.logger.Info("successfully sent new user notification to admin",
slog.String("user_email", userInfo.Email),
slog.String("user_type", getUserTypeName(userInfo.UserType)),
slog.String("admin_email", constants.AdminNotificationEmail))
w.WriteHeader(http.StatusOK)
}
func (h *handler) sendWelcomeEmail(email, userName string) error {
msg, err := proto.Marshal(&notification.SendEmailRequest{
To: []string{email},
Subject: constants.RegistrationNotificationMessageSubject,
ContentType: constants.TextNotificationContentType,
Body: []byte(fmt.Sprintf(
constants.RegistrationNotificationText,
userName,
)),
})
if err != nil {
return fmt.Errorf("marshaling welcome email: %w", err)
}
if err := broker.SendNotification(
broker.NotificationQueue,
constants.EmailNotificationMessageType,
msg,
h.logger,
); err != nil {
return fmt.Errorf("sending welcome email: %w", err)
}
h.logger.Info("successfully sent welcome email after verification",
slog.String("user_email", email))
return nil
}
func getUserTypeName(userType int32) string {
switch userType {
case keycloak.UserAgentType:
return constants.UserTypeAgentName
case keycloak.UserDistributorType:
return constants.UserTypeDistributorName
default:
return fmt.Sprintf("Неизвестный тип (%d)", userType)
}
}
func (h *handler) formatUserFullName(userInfo *auth.UserInfo) string {
fullName := fmt.Sprintf("%s %s", userInfo.SecondName, userInfo.FirstName)
if userInfo.Patronymic != nil && *userInfo.Patronymic != "" {
fullName = fmt.Sprintf("%s %s", fullName, *userInfo.Patronymic)
}
return fullName
}
func (h *handler) sendNewUserNotificationToAdmin(userInfo *auth.UserInfo, userName string) error {
userTypeName := getUserTypeName(userInfo.UserType)
emailBody := fmt.Sprintf(
constants.EmailNewUserRegistrationAdminMessage,
userInfo.Email,
userName,
userTypeName,
)
msg, err := proto.Marshal(&notification.SendEmailRequest{
SenderId: constants.AdminNotificationId,
To: constants.AdminNotificationEmails,
Subject: constants.RegistrationNewUserAdmin,
ContentType: constants.TextNotificationContentType,
Body: []byte(emailBody),
})
if err != nil {
return fmt.Errorf("marshaling admin notification: %w", err)
}
if err := broker.SendNotification(
broker.NotificationQueue,
constants.EmailNotificationMessageType,
msg,
h.logger,
); err != nil {
return fmt.Errorf("sending admin notification: %w", err)
}
return nil
}
// @Summary Статус подтверждения email
// @Description Получение статуса подтверждения email адреса пользователя
// @Tags auth
// @Accept json
// @Produce json
// @Param uid query string true "ID пользователя"
// @Success 200 {object} map[string]string "Статус подтверждения email"
// @Failure 400 {object} map[string]string "Неверные параметры запроса"
// @Failure 500 {object} map[string]string "Внутренняя ошибка сервера"
// @Router /api/v1/verify_email [get]
func (h *handler) getEmailVerificationStatusHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getEmailVerificationStatusHandler"
// Get user ID from query parameters
userID := r.URL.Query().Get("uid")
if userID == "" {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("missing parameter: uid",
slog.String("handler", handlerName))
return
}
// Check if email is verified
response, err := h.authManager.GetEmailVerificationStatus(r.Context(), userID)
if err != nil {
h.handleKeycloakError(w, err)
h.logger.Error("error getting email verification status",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
// Return response
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error encoding response",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
}
func (h *handler) validateEmail(email string) (bool, error) {
apiURL := fmt.Sprintf("%s?api_key=%s&email=%s",
constants.EmailVerificationServiceURL,
url.QueryEscape(h.emailVerificationServiceAPIKey),
url.QueryEscape(email),
)
client := &http.Client{
Timeout: 15 * time.Second,
}
req, err := http.NewRequestWithContext(
context.Background(),
http.MethodGet,
apiURL,
nil,
)
if err != nil {
return false, fmt.Errorf("failed to create request: %w", err)
}
//стоит что-то такое добавить req.Header.Set("User-Agent", "MyAppSecurity/1.0")
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("API request failed: %w", err)
}
defer resp.Body.Close()
var result struct {
Deliverability string `json:"deliverability"`
QualityScore string `json:"quality_score"`
IsDisposable struct {
Value bool `json:"value"`
} `json:"is_disposable_email"`
IsMxFound struct {
Value bool `json:"value"`
} `json:"is_mx_found"`
IsSmtpValid struct {
Value bool `json:"value"`
} `json:"is_smtp_valid"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, fmt.Errorf("failed to parse validation response: %w", err)
}
qualityScore, err := strconv.ParseFloat(result.QualityScore, 64)
if err != nil {
h.logger.Warn("could not parse quality score, ignoring in validation",
slog.String("quality_score", result.QualityScore))
qualityScore = 0
}
isValidEmail := result.Deliverability == "DELIVERABLE" && // Доставляемость
!result.IsDisposable.Value && // Не одноразовый адрес
result.IsMxFound.Value && // Есть MX-записи DNS
result.IsSmtpValid.Value && // Сервер отвечает на SMTP
qualityScore >= 0.7 // Высокий общий показатель качества
h.logger.Debug("email validation details",
slog.String("email", email),
slog.String("deliverability", result.Deliverability),
slog.String("quality_score", result.QualityScore),
slog.Bool("is_disposable", result.IsDisposable.Value),
slog.Bool("is_mx_found", result.IsMxFound.Value),
slog.Bool("is_smtp_valid", result.IsSmtpValid.Value),
slog.Bool("is_valid_result", isValidEmail))
return isValidEmail, nil
}
// @Summary Получить события пользователя
// @Description Получение ленты событий пользователя с возможностью фильтрации
// @Tags feed
// @Accept json
// @Produce json
// @Param uid path string true "ID пользователя"
// @Param user_type query string true "Тип пользователя (agent/distributor)"
// @Param event_type query string false "Тип события (через запятую)"
// @Param show_cancelled query bool false "Показывать отмененные события"
// @Param limit query int false "Лимит событий"
// @Param offset query int false "Смещение"
// @Success 200 {object} []feed.Event "Список событий"
// @Failure 400 {object} map[string]string "Неверные данные запроса"
// @Failure 401 {object} map[string]string "Неавторизованный доступ"
// @Failure 500 {object} map[string]string "Внутренняя ошибка сервера"
// @Security BearerAuth
// @Router /api/v1/feed/{uid}/events [get]
func (h *handler) GetUserEventsHandler(w http.ResponseWriter, r *http.Request) {
handlerName := "GetUserEventsHandler"
query := r.URL.Query()
vars := mux.Vars(r)
ownerID := vars["uid"]
ownerType := query.Get("user_type")
if ownerType == "" {
h.logger.Error("Missing required query parameter",
slog.String("parameter", "user_type"),
slog.String("handler", handlerName))
http.Error(w, "Missing required query parameter: type", http.StatusBadRequest)
return
}
h.logger.Debug("Request received",
slog.String("handler", handlerName),
slog.String("userID", ownerID),
slog.Any("queryParams", query))
eventTypesStr := query.Get("event_type")
eventTypesSlice := strings.Split(eventTypesStr, ",")
eventTypes := make([]feed.EventType, len(eventTypesStr))
for i, s := range eventTypesSlice {
eventTypes[i] = feed.EventType(s)
}
filter := feed.Filter{
OwnerID: ownerID,
EventTypes: eventTypes,
ShowCancelled: query.Get("show_cancelled") != "false",
}
if limitStr := query.Get("limit"); limitStr != "" {
limit, err := strconv.ParseUint(limitStr, 10, 64)
if err == nil && limit > 0 {
filter.Limit = limit
} else {
h.logger.Error("Invalid limit parameter",
slog.String("handler", handlerName),
slog.String("userID", ownerID),
slog.String("invalidValue", limitStr),
slog.String("error", "invalid integer format or value"))
http.Error(w, "Invalid limit parameter", http.StatusBadRequest)
return
}
} else {
filter.Limit = constants.DefaultFeedFilterLimit
}
if offsetStr := query.Get("offset"); offsetStr != "" {
offset, err := strconv.ParseUint(offsetStr, 10, 64)
if err == nil {
filter.Offset = offset
} else {
h.logger.Error("Invalid offset parameter",
slog.String("handler", handlerName),
slog.String("userID", ownerID),
slog.String("invalidValue", offsetStr),
slog.String("error", "invalid integer format or value"))
http.Error(w, "Invalid offset parameter", http.StatusBadRequest)
return
}
} else {
filter.Offset = 0
}
events, err := h.feed.Service.GetUserEvents(r.Context(), filter, ownerType)
if err != nil {
h.logger.Error("Error getting events",
slog.String("handler", handlerName),
slog.String("userID", ownerID),
slog.String("error", err.Error()))
http.Error(w, "Failed to retrieve events", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(events); err != nil {
h.logger.Error("Error encoding response",
slog.String("handler", handlerName),
slog.String("userID", ownerID),
slog.String("error", err.Error()))
}
}