package http_router import ( "context" "encoding/json" "fmt" "html/template" "io" "log/slog" "net/http" "net/url" "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth" "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth/keycloak" "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/broker" "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants" dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types" "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/feed" notification "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/notifications/git-molva.ru/Molva/molva-notification-service" rmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model" authinfra "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/services/auth_infrastructure" "google.golang.org/protobuf/proto" ) // ------------------------------ // LOGIN USER // ------------------------------ func (h *handler) loginHandler(w http.ResponseWriter, r *http.Request) { const handlerName = "loginHandler" var request rmodel.LoginUserRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest) h.logger.Error("Invalid request body", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } defer func(body io.ReadCloser) { if err := body.Close(); err != nil { http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError) h.logger.Error("error closing body", slog.String("error", err.Error()), slog.String("handler", handlerName)) } }(r.Body) tokens, err := h.authManager.LoginUser(r.Context(), auth.LoginUserRequest{ Email: request.Email, Password: request.Password, }) if err != nil { h.handleKeycloakError(w, err) h.logger.Error("error while login user", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } userInfo, err := h.authManager.GetUserInfo(r.Context(), tokens.UserId) if err != nil { h.handleKeycloakError(w, err) h.logger.Error("error while getting user info", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } emailVerificationStatus, err := h.authManager.GetEmailVerificationStatus(r.Context(), tokens.UserId) if err != nil { h.handleKeycloakError(w, err) h.logger.Error("error while getting email verification status", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } w.Header().Set("Content-Type", "application/json") if err = json.NewEncoder(w).Encode(rmodel.LoginUserResponse{ Uid: tokens.UserId, AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, UserType: userInfo.UserType, Permissions: rmodel.Permissions{ Balance: userInfo.Permissions[keycloak.PermissionBalance], Company: userInfo.Permissions[keycloak.PermissionCompany], Employees: userInfo.Permissions[keycloak.PermissionEmployees], Profile: userInfo.Permissions[keycloak.PermissionProfile], Submissions: userInfo.Permissions[keycloak.PermissionSubmissions], Vacancies: userInfo.Permissions[keycloak.PermissionVacancies], }, EmailVerified: emailVerificationStatus.EmailVerified, }); err != nil { http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError) h.logger.Error("error while encoding response: ", slog.String("error", err.Error()), slog.String("handler", handlerName)) } } // ------------------------------ // REGISTER USER // ------------------------------ func (h *handler) registerHandler(w http.ResponseWriter, r *http.Request) { const handlerName = "registerHandler" var creds rmodel.UserCredentials if err := json.NewDecoder(r.Body).Decode(&creds); err != nil { http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest) h.logger.Error("error decoding request body", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } defer r.Body.Close() if h.env == "production" { isEmailValid, err := h.validateEmail(creds.Email) if err != nil { h.logger.Warn("EmailVerificationService API service error, proceeding with email validation via message", slog.String("error", err.Error()), slog.String("email", creds.Email), slog.String("handler", handlerName)) } else if !isEmailValid { h.logger.Warn("email validation failed - invalid email address", slog.String("email", creds.Email), slog.String("handler", handlerName)) http.Error(w, "Invalid email address", http.StatusBadRequest) return } } uid, err := h.authManager.RegisterUser(r.Context(), auth.RegisterUserRequest{ User: auth.User{ Email: creds.Email, Password: creds.Password, FirstName: creds.FirstName, SecondName: creds.LastName, Patronymic: creds.MiddleName, Number: creds.PhoneNumber, Permissions: creds.Permissions, }, UserType: creds.UserType, }) if err != nil { h.logger.Error("error while saving user to keycloak", slog.String("error", err.Error()), slog.String("handler", handlerName)) h.handleKeycloakError(w, err) return } if err := h.saveUser(r.Context(), uid.UserId, creds); err != nil { h.handleDBError(w, err) h.logger.Error("error saving user to DB", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } tokenResp, err := h.authManager.GetUserEmailVerificationToken(r.Context(), uid.UserId) if err != nil { h.logger.Error("error getting user token", slog.String("error", err.Error()), slog.String("handler", handlerName)) http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError) return } userName := formatName(creds.LastName, creds.FirstName, creds.MiddleName) if err := h.sendConfirmationEmail(r, creds.Email, userName, uid.UserId, tokenResp.AccessToken); err != nil { h.logger.Error("error sending confirmation email", slog.String("error", err.Error()), slog.String("handler", handlerName)) http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError) return } h.createWelcomeEvent(r.Context(), uid.UserId, creds, handlerName) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) if err := json.NewEncoder(w).Encode(&rmodel.RegisterResponse{UUID: uid.UserId}); err != nil { http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError) h.logger.Error("error while encoding response", slog.String("error", err.Error()), slog.String("handler", handlerName)) } } func (h *handler) createWelcomeEvent(ctx context.Context, userID string, creds rmodel.UserCredentials, handlerName string) { ownerType := feed.RoleAgent if creds.UserType == constants.DistributorClientType { ownerType = feed.RoleDistributor } event := feed.Event{ OwnerId: userID, OwnerType: ownerType, EventType: feed.EventWelcome, Message: "Добро пожаловать в Molva!", Visibility: feed.VisibilityPrivate, Payload: feed.EventPayload{}, } if err := h.feed.CreateEvent(ctx, &event); err != nil { h.logger.Error("failed to create feed event", slog.String("error", err.Error()), slog.String("handler", handlerName), slog.String("uid", userID)) } } func (h *handler) sendConfirmationEmail(r *http.Request, email, userName, userID, accessToken string) error { scheme := "https" if r.TLS == nil { scheme = "http" } confirmURL := fmt.Sprintf("%s://%s/api/v1/confirm_email?uid=%s&token=%s", scheme, r.Host, url.QueryEscape(userID), url.QueryEscape(accessToken), ) emailBody := fmt.Sprintf(constants.EmailVerificationTemplate, userName, confirmURL, confirmURL, ) msg, err := proto.Marshal(¬ification.SendEmailRequest{ To: []string{email}, Subject: constants.EmailVerificationMessageSubject, ContentType: constants.HTMLNotificationContentType, Body: []byte(emailBody), }) if err != nil { return fmt.Errorf("marshaling email message: %w", err) } if err := broker.SendNotification( broker.NotificationQueue, constants.EmailNotificationMessageType, msg, h.logger, ); err != nil { return fmt.Errorf("sending notification: %w", err) } return nil } // NOTE: lastName and firstName are required to be non-empty func formatName(lastName, firstName string, middleName *string) string { name := fmt.Sprintf("%s %s", lastName, firstName) if middleName != nil { name = fmt.Sprintf("%s %s", name, *middleName) } return name } func (h *handler) saveUser(ctx context.Context, uid string, creds rmodel.UserCredentials) error { userName := formatName(creds.LastName, creds.FirstName, creds.MiddleName) if _, err := h.dbClient.CreateUser(ctx, &dbtypes.UserSaveRequest{ Id: uid, FullName: userName, Phone: creds.PhoneNumber, Email: creds.Email, Type: dbtypes.UserType(creds.UserType), }); err != nil { return err } return nil } // ------------------------------ // LOGOUT USER // ------------------------------ func (h *handler) logoutHandler(w http.ResponseWriter, r *http.Request) { const handlerName = "logoutHandler" var request rmodel.LogoutUserRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { http.Error(w, constants.ErrInternalServerError.Error(), http.StatusBadRequest) h.logger.Error("error while decoding request body: ", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } if err := h.authManager.LogoutUser(r.Context(), auth.LogoutUserRequest{ RefreshToken: request.RefreshToken, }); err != nil { h.handleKeycloakError(w, err) return } } // ------------------------------ // REFRESH USER TOKEN // ------------------------------ func (h *handler) refreshTokenHandler(w http.ResponseWriter, r *http.Request) { const handlerName = "refreshTokenHandler" var request rmodel.RefreshTokenRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { http.Error(w, constants.ErrInternalServerError.Error(), http.StatusBadRequest) h.logger.Error("error while decoding request body: ", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } resp, err := h.authManager.GetNewAccessToken(r.Context(), auth.GetNewAccessTokenRequest{ RefreshToken: request.RefreshToken, }) if err != nil { h.handleKeycloakError(w, err) return } if err := json.NewEncoder(w).Encode(rmodel.RefreshTokenResponse{ AccessToken: resp.AccessToken, RefreshToken: resp.RefreshToken, }); err != nil { http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError) h.logger.Error("error while encoding response: ", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } } func (h *handler) confirmEmailPageHandler(w http.ResponseWriter, r *http.Request) { const handlerName = "confirmEmailPageHandler" if r.Method != http.MethodGet { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } uid := r.URL.Query().Get("uid") token := r.URL.Query().Get("token") if uid == "" || token == "" { http.Error(w, "Bad Request: отсутствует uid или token", http.StatusBadRequest) return } scheme := "http" if protocol := r.Header.Get("X-Forwarded-Proto"); protocol != "" { scheme = protocol } else if h.env == "production" || h.env == "development" { scheme = "https" } // Формируем URL для PATCH-запроса verificationLink := fmt.Sprintf("%s://%s/api/v1/verify_email?uid=%s&token=%s", scheme, r.Host, url.QueryEscape(uid), url.QueryEscape(token), ) tmpl, err := template.New("confirm").Parse(constants.EmailConfirmationPage) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := tmpl.Execute(w, struct { VerificationURL string }{ VerificationURL: verificationLink, }); err != nil { http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError) h.logger.Error("error while executing confirm email page template", slog.String("error", err.Error()), slog.String("handler", handlerName)) } } // ------------------------------ // RESET PASSWORD // ------------------------------ func (h *handler) forgotPasswordHandler(w http.ResponseWriter, r *http.Request) { const handlerName = "forgotPasswordHandler" var request rmodel.ForgotPasswordRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest) h.logger.Error("error while decoding request body: ", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } createOTPResp, err := h.authInfraService.CreatePasswordResetOTP(r.Context(), &authinfra.PasswordResetOTPCreateRequest{ Email: request.Email, }) if err != nil { h.handleAuthInfraError(w, err, handlerName) return } message, err := proto.Marshal(¬ification.SendEmailRequest{ To: []string{request.Email}, Subject: constants.ForgotPasswordNotificationMessageSubject, ContentType: constants.TextNotificationContentType, Body: fmt.Appendf(nil, constants.ForgotPasswordNotificationText, createOTPResp.OTP, ), }) if err != nil { http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError) h.logger.Error("error while marshaling forgot password notification message", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } if err := broker.SendNotification( broker.NotificationQueue, constants.EmailNotificationMessageType, message, h.logger, ); err != nil { http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError) h.logger.Error("error while sending forgot password notification", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } w.WriteHeader(http.StatusCreated) } func (h *handler) validateOTPHandler(w http.ResponseWriter, r *http.Request) { const handlerName = "validateOTPHandler" query := r.URL.Query() email := query.Get("email") if email == "" { http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest) h.logger.Debug("email is required, but missing", slog.String("handler", handlerName)) return } otp := query.Get("otp") if otp == "" { http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest) h.logger.Debug("OTP is required, but missing", slog.String("handler", handlerName)) return } checkOTPResp, err := h.authInfraService.ValidatePasswordResetOTP(r.Context(), &authinfra.ValidatePasswordResetOTPRequest{ Email: email, OTP: otp, }) if err != nil { h.handleAuthInfraError(w, err, handlerName) return } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(rmodel.ValidateOTPResponse{ Token: checkOTPResp.Token, }); 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) resetPasswordHandler(w http.ResponseWriter, r *http.Request) { const handlerName = "resetPasswordHandler" var request rmodel.ResetPasswordRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest) h.logger.Error("error decoding request body: ", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } if _, err := h.authInfraService.ValidatePasswordResetToken(r.Context(), &authinfra.ValidatePasswordResetTokenRequest{ Email: request.Email, Token: request.Token, }); err != nil { h.handleAuthInfraError(w, err, handlerName) return } if err := h.authManager.ResetPassword(r.Context(), auth.ResetPasswordRequest{ Email: request.Email, NewPassword: request.Password, }); err != nil { h.handleKeycloakError(w, err) h.logger.Error("error resetting password", slog.String("error", err.Error()), slog.String("handler", handlerName)) return } w.WriteHeader(http.StatusNoContent) }