This commit is contained in:
Alex Shevchuk
2025-08-18 17:12:04 +03:00
commit d84487d238
157 changed files with 160686 additions and 0 deletions

732
internal/http/handlers.go Normal file
View File

@@ -0,0 +1,732 @@
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()))
}
}