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(¬ification.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(¬ification.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())) } }