Files
test_deploy/internal/http/handlers.go
Alex Shevchuk d84487d238 1
2025-08-18 17:12:04 +03:00

733 lines
21 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"
)
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
}
}
//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
}
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))
}
}
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))
}
}
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))
}
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
}
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
}
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()))
}
}