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

557
internal/http/auth.go Normal file
View File

@@ -0,0 +1,557 @@
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(&notification.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(&notification.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)
}

769
internal/http/balance.go Normal file
View File

@@ -0,0 +1,769 @@
package http_router
import (
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
rmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model"
"github.com/gorilla/mux"
)
func (h *handler) getBalanceAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getBalanceAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
)
result, err := h.agentService.GetBalance(r.Context(), &rmodel.BalanceGetRequest{
OwnerId: agentId,
})
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error getting balance info",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getTransactionListAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getTransactionListAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
)
request, err := new(rmodel.TransactionListGetRequest).FromQuery(r.URL.Query())
if err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error parsing request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
request.OwnerId = agentId
result, err := h.agentService.GetTransactionList(r.Context(), request)
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error getting transactions",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) createTransactionAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "createTransactionAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
)
var request rmodel.TransactionCreateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error while unmarshalling request: ",
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)
request.OwnerId = agentId
result, err := h.agentService.CreateTransaction(r.Context(), &request)
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error creating transaction",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
// TODO: fix feed event
// go h.createTransactionFeedEvent(
// context.Background(),
// agentId,
// true,
// request.Amount,
// request.Currency,
// request.BankAccountId,
// handlerName,
// )
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getBankAccountListAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getBankAccountListAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
)
result, err := h.agentService.GetBankAccountList(r.Context(), &rmodel.BankAccountListGetRequest{
OwnerId: agentId,
})
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error getting bank accounts",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) createBankAccountAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "createBankAccountAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
)
var request rmodel.BankAccountCreateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error while unmarshalling request: ",
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)
request.OwnerId = agentId
result, err := h.agentService.CreateBankAccount(r.Context(), &request)
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error creating bank account",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
// FIXME: possible nil dereference
// h.createCreateBankAccountFeedEvent(r.Context(), agentId, true, resp, *request.BankName, handlerName)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) updateBankAccountAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "updateBankAccountAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
bankAccountId = vars["bank_account_id"]
)
var request rmodel.BankAccountUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error while unmarshalling request: ",
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)
request.Id = bankAccountId
request.OwnerId = agentId
if _, err := h.agentService.UpdateBankAccount(r.Context(), &request); err != nil {
h.handleAgentError(w, err)
h.logger.Error("error editing bank account",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go h.createEditBankAccountFeedEvent(
context.Background(),
agentId,
true,
bankAccountId,
handlerName,
)
w.WriteHeader(http.StatusNoContent)
}
// TODO: test when implemented
func (h *handler) deleteBankAccountAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "deleteBankAccountAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
bankAccountId = vars["bank_account_id"]
)
result, err := h.agentService.DeleteBankAccount(r.Context(), &rmodel.BankAccountDeleteRequest{
Id: bankAccountId,
OwnerId: agentId,
})
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error deleting bank account",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go func() {
if err := h.feed.CancelEvent(context.Background(), bankAccountId, "Банковский счёт удален"); err != nil {
h.logger.Error("error cancelling event: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
}
}()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getBalanceDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getBalanceDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
)
result, err := h.distributorService.GetBalance(r.Context(), &rmodel.BalanceGetRequest{
OwnerId: distId,
})
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting balance",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getCompanyBalanceDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getCompanyBalanceDistributorHandler"
var (
vars = mux.Vars(r)
companyId = vars["company_id"]
)
result, err := h.distributorService.GetBalance(r.Context(), &rmodel.BalanceGetRequest{
OwnerId: companyId,
})
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting balance",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getTransactionListDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getTransactionListDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
)
request, err := new(rmodel.TransactionListGetRequest).FromQuery(r.URL.Query())
if err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error parsing request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
request.OwnerId = distId
result, err := h.distributorService.GetTransactionList(r.Context(), request)
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting transactions",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getCompanyTransactionListDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getCompanyTransactionListDistributorHandler"
var (
vars = mux.Vars(r)
companyId = vars["company_id"]
)
request, err := new(rmodel.TransactionListGetRequest).FromQuery(r.URL.Query())
if err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error parsing request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
request.OwnerId = companyId
result, err := h.distributorService.GetTransactionList(r.Context(), request)
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting transactions",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) createTransactionDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "createTransactionDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
)
var request rmodel.TransactionCreateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error while unmarshalling request: ",
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)
request.OwnerId = distId
result, err := h.distributorService.CreateTransaction(r.Context(), &request)
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error creating transaction",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
// TODO: fix feed event
// go h.createTransactionFeedEvent(
// context.Background(),
// distId,
// false,
// request.Amount,
// request.Currency,
// request.BankAccountId,
// handlerName,
// )
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getBankAccountListDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getBankAccountListDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
)
result, err := h.distributorService.GetBankAccountList(r.Context(), &rmodel.BankAccountListGetRequest{
OwnerId: distId,
})
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting bank accounts",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getCompanyBankAccountListDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getCompanyBankAccountListDistributorHandler"
var (
vars = mux.Vars(r)
companyId = vars["company_id"]
)
result, err := h.distributorService.GetBankAccountList(r.Context(), &rmodel.BankAccountListGetRequest{
OwnerId: companyId,
})
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting bank accounts",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) createBankAccountDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "createBankAccountDistributorHandler"
var (
vars = mux.Vars(r)
companyId = vars["company_id"]
)
var request rmodel.BankAccountCreateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error while unmarshalling request: ",
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)
request.OwnerId = companyId
result, err := h.distributorService.CreateBankAccount(r.Context(), &request)
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error creating bank account",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
// FIXME: possible nil dereference
// go h.createCreateBankAccountFeedEvent(
// context.Background(),
// distId,
// false,
// resp,
// *request.BankName,
// handlerName,
// )
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) updateBankAccountDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "updateBankAccountDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
bankAccountId = vars["bank_account_id"]
)
var request rmodel.BankAccountUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error while unmarshalling request: ",
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)
request.OwnerId = distId
request.Id = bankAccountId
result, err := h.distributorService.UpdateBankAccount(r.Context(), &request)
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error editing bank account",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go h.createEditBankAccountFeedEvent(
context.Background(),
distId,
true,
bankAccountId,
handlerName,
)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) deleteBankAccountDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "deleteBankAccountDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
bankAccountId = vars["bank_account_id"]
)
result, err := h.distributorService.DeleteBankAccount(r.Context(), &rmodel.BankAccountDeleteRequest{
Id: bankAccountId,
OwnerId: distId,
})
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error deleting bank account",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go func() {
if err := h.feed.CancelEvent(context.Background(), bankAccountId, "Банковский счёт удален"); err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error while cancelling event: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
}
}()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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),
)
}
}

View File

@@ -0,0 +1,27 @@
package http_router
import (
"encoding/json"
"net/http"
)
func (h *handler) getBuildInfoHandler(w http.ResponseWriter, _ *http.Request) {
response := struct {
Version string `json:"version"`
Commit string `json:"commit"`
Date string `json:"date"`
}{
Version: h.buildConfig.Version,
Commit: h.buildConfig.Commit,
Date: h.buildConfig.Date,
}
responseJSON, err := json.Marshal(response)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(responseJSON)
}

182
internal/http/client.go Normal file
View File

@@ -0,0 +1,182 @@
package http_router
import (
"errors"
"log/slog"
"net/http"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database"
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/feed"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/integration"
objectStorage "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/object_storage"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/services/agent"
authinfra "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/services/auth_infrastructure"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/services/distributor"
"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/config"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
filemanager "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/file_manager"
urlShortener "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/url_shortener"
)
type handler struct {
env string
logger *slog.Logger
urlShortener urlShortener.Shortener
authManager auth.Manager
authInfraService authinfra.AuthInfrastructureService
fileManager filemanager.UserFileManager
dbClient database.Client
agentService agent.AgentService
distributorService distributor.DistributorService
cacheClient cache.Client
objectStorageClient objectStorage.Client
jwtManager keycloak.TokenManager
tempFileManager *TempFileManager
emailVerificationServiceAPIKey string
feed *feed.Handler
integrationClient integration.Client
buildConfig config.BuildInfo
}
type Config struct {
env string
logger *slog.Logger
secretConfig *config.SecretConfig
authManager auth.Manager
authInfraService authinfra.AuthInfrastructureService
fileManager filemanager.UserFileManager
dbClient database.Client
agentService agent.AgentService
distributorService distributor.DistributorService
emailVerificationServiceAPIKey string
cacheClient cache.Client
objectStorageClient objectStorage.Client
feed *feed.Handler
integrationClient integration.Client
buildConfig config.BuildInfo
}
func newHandler(c *Config) *handler {
tempManager, err := NewTempFileManager("")
if err != nil {
c.logger.Error("failed to create temp file manager", slog.String("error", err.Error()))
return nil
}
return &handler{
env: c.env,
logger: c.logger,
urlShortener: &urlShortener.LinkEncryption{Key: c.secretConfig.Key},
authManager: c.authManager,
authInfraService: c.authInfraService,
fileManager: c.fileManager,
dbClient: c.dbClient,
agentService: c.agentService,
distributorService: c.distributorService,
emailVerificationServiceAPIKey: c.emailVerificationServiceAPIKey,
cacheClient: c.cacheClient,
objectStorageClient: c.objectStorageClient,
jwtManager: keycloak.NewJWTManager(),
tempFileManager: tempManager,
feed: c.feed,
integrationClient: c.integrationClient,
buildConfig: c.buildConfig,
}
}
func (h *handler) handleKeycloakError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, keycloak.ErrUnknownUserType):
http.Error(w, keycloak.ErrUnknownUserType.Error(), http.StatusBadRequest)
case errors.Is(err, keycloak.ErrInvalidToken):
http.Error(w, keycloak.ErrInvalidToken.Error(), http.StatusUnauthorized)
case errors.Is(err, keycloak.ErrRealmClientUnauthorized):
http.Error(w, "internal server error", http.StatusInternalServerError)
case errors.Is(err, keycloak.ErrRealmClientNotFound):
http.Error(w, "internal server error", http.StatusInternalServerError)
case errors.Is(err, keycloak.ErrInternal):
http.Error(w, keycloak.ErrInternal.Error(), http.StatusInternalServerError)
case errors.Is(err, keycloak.ErrAlreadyExists):
http.Error(w, keycloak.ErrAlreadyExists.Error(), http.StatusBadRequest)
case errors.Is(err, keycloak.ErrBadRequest):
http.Error(w, keycloak.ErrBadRequest.Error(), http.StatusBadRequest)
case errors.Is(err, keycloak.ErrNotFound):
http.Error(w, keycloak.ErrNotFound.Error(), http.StatusNotFound)
default:
h.logger.Error("Unhandled keycloak error", slog.String("error", err.Error()))
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
}
}
func (h *handler) handleAuthInfraError(w http.ResponseWriter, err error, handlerName string) {
switch {
case errors.Is(err, authinfra.ErrBadRequest):
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
case errors.Is(err, authinfra.ErrUnauthorized):
http.Error(w, constants.ErrUnauthorized.Error(), http.StatusUnauthorized)
case errors.Is(err, authinfra.ErrNotFound):
http.Error(w, constants.ErrUnauthorized.Error(), http.StatusUnauthorized)
case errors.Is(err, authinfra.ErrInternal):
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("auth infrastructure error",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
default:
h.logger.Error("Unhandled auth infrastructure error", slog.String("error", err.Error()))
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
}
}
func (h *handler) handleDBError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, dberrors.ErrBadRequest):
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
case errors.Is(err, dberrors.ErrForbidden):
http.Error(w, constants.ErrForbidden.Error(), http.StatusForbidden)
case errors.Is(err, dberrors.ErrNotFound):
http.Error(w, constants.ErrNotFound.Error(), http.StatusNotFound)
case errors.Is(err, dberrors.ErrInternal):
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
default:
h.logger.Error("Unhandled database error", slog.String("error", err.Error()))
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
}
}
func (h *handler) handleAgentError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, agent.ErrBadRequest):
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
case errors.Is(err, agent.ErrForbidden):
http.Error(w, constants.ErrForbidden.Error(), http.StatusForbidden)
case errors.Is(err, agent.ErrNotFound):
http.Error(w, constants.ErrNotFound.Error(), http.StatusNotFound)
case errors.Is(err, agent.ErrInternal):
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
default:
h.logger.Error("Unhandled agent error", slog.String("error", err.Error()))
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
}
}
func (h *handler) handleDistributorError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, distributor.ErrBadRequest):
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
case errors.Is(err, distributor.ErrForbidden):
http.Error(w, constants.ErrForbidden.Error(), http.StatusForbidden)
case errors.Is(err, distributor.ErrNotFound):
http.Error(w, constants.ErrNotFound.Error(), http.StatusNotFound)
case errors.Is(err, distributor.ErrInternal):
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
default:
h.logger.Error("Unhandled distributor error", slog.String("error", err.Error()))
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
}
}

View File

@@ -0,0 +1,168 @@
package http_router
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
"github.com/gorilla/mux"
rmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model"
)
func (h *handler) getCompanyListAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getCompanyListAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
)
result, err := h.agentService.GetCompanyList(r.Context(), &rmodel.CompanyListGetRequest{
Id: agentId,
})
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error getting company list",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getCompanyByIdAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getCompanyByIdAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
companyId = vars["company_id"]
)
result, err := h.agentService.GetCompanyInfo(r.Context(), &rmodel.CompanyByIdGetRequest{
UserId: agentId,
CompanyId: companyId,
})
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error getting company info",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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),
)
}
}
// FIXME: foreign key violation review
func (h *handler) createCompanyAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "createAgentCompanyHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
)
var request rmodel.CompanyCreateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error while unmarshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
request.OwnerId = agentId
result, err := h.agentService.CreateCompany(r.Context(), &request)
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error creating company",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
// go h.createCreateCompanyFeedEvent(r.Context(), agentId, true, resp, handlerName)
if err := json.NewEncoder(w).Encode(result); 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) updateCompanyAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "updateCompanyAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
companyId = vars["company_id"]
)
var request rmodel.CompanyUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error while unmarshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
request.Id = companyId
if _, err := h.agentService.UpdateCompanyInfo(r.Context(), &request); err != nil {
h.handleAgentError(w, err)
h.logger.Error("error updating company info",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go h.createEditCompanyFeedEvent(
context.Background(),
agentId,
true,
companyId,
handlerName,
)
w.WriteHeader(http.StatusNoContent)
}

View File

@@ -0,0 +1,199 @@
package http_router
import (
"encoding/json"
"log/slog"
"net/http"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
rmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model"
"github.com/gorilla/mux"
)
func (h *handler) getCompanyListDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getCompanyListDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
)
result, err := h.distributorService.GetCompanyList(r.Context(), &rmodel.CompanyListGetRequest{
Id: distId,
})
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting company list",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getCompanyByIdDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getCompanyByIdDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
companyId = vars["company_id"]
)
result, err := h.distributorService.GetCompanyInfoById(r.Context(), &rmodel.CompanyByIdGetRequest{
UserId: distId,
CompanyId: companyId,
})
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting company list",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) createCompanyDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "createCompanyDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
)
var request rmodel.CompanyCreateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error unmarshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
request.OwnerId = distId
result, err := h.distributorService.CreateCompany(r.Context(), &request)
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error creating company",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
// TODO:
// h.createCreateCompanyFeedEvent(r.Context(), distId, false, resp, handlerName)
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) updateCompanyDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "updateCompanyDistributorHandler"
var (
vars = mux.Vars(r)
companyId = vars["company_id"]
)
var request rmodel.CompanyUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error unmarshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
request.Id = companyId
if _, err := h.distributorService.UpdateCompanyInfo(r.Context(), &request); err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error updating company info",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
// BUG: holds request for infinite time
// h.createEditCompanyFeedEvent(r.Context(), distId, false, comId, handlerName)
w.WriteHeader(http.StatusNoContent)
}
func (h *handler) addCompanyMemberDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "addCompanyMemberDistributorHandler"
var (
vars = mux.Vars(r)
companyId = vars["company_id"]
)
var request rmodel.AddDistributorCompanyMemberRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error while unmarshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
request.CompanyId = companyId
if _, err := h.distributorService.AddEmployee(r.Context(), &request); err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error adding new staff member",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
// TODO:
// h.createAddNewStaffMemberFeedEvent(r.Context(), distId, true, *req.NewStaffMember, companyId, handlerName)
w.WriteHeader(http.StatusNoContent)
}

75
internal/http/errors.go Normal file
View File

@@ -0,0 +1,75 @@
package http_router
import (
"errors"
"fmt"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
"log/slog"
"net/http"
)
// =============== CUSTOM LOGO ERRORS ===============
var (
ErrEmptyFile = errors.New("file is empty")
ErrMultipartParse = errors.New("failed to parse multipart form")
ErrFileNotFound = errors.New("failed to get file from form")
ErrTempFileCreate = errors.New("failed to create temporary file")
ErrFileOperation = errors.New("file operation failed")
)
// InvalidImageTypeError представляет ошибку неверного типа изображения
type InvalidImageTypeError struct {
ContentType string
}
func (e *InvalidImageTypeError) Error() string {
return fmt.Sprintf("invalid image type %s: only png, jpg, and webp are allowed", e.ContentType)
}
// FileTooLargeError представляет ошибку превышения размера файла
type FileTooLargeError struct {
Size int64
MaxSize int64
}
func (e *FileTooLargeError) Error() string {
return fmt.Sprintf("file too large: %d bytes, max %d bytes allowed", e.Size, e.MaxSize)
}
// =============== ERROR HANDLERS ===============
// handleAccessError обрабатывает ошибки прав или токенов
func (h *handler) handleAccessError(w http.ResponseWriter, err error, handlerName string) {
if errors.Is(err, constants.ErrUnauthorized) {
http.Error(w, constants.ErrUnauthorized.Error(), http.StatusUnauthorized)
} else if errors.Is(err, constants.ErrForbidden) {
http.Error(w, constants.ErrForbidden.Error(), http.StatusForbidden)
} else {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("some permission or authorization error",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
}
}
// handleUploadError обрабатывает ошибки логотипов
func (h *handler) handleUploadError(w http.ResponseWriter, err error, handlerName string) {
var invalidTypeErr *InvalidImageTypeError
var fileTooLargeErr *FileTooLargeError
// Проверяем типизированные ошибки
if errors.As(err, &invalidTypeErr) || errors.As(err, &fileTooLargeErr) ||
errors.Is(err, ErrEmptyFile) || errors.Is(err, ErrMultipartParse) ||
errors.Is(err, ErrFileNotFound) {
http.Error(w, err.Error(), http.StatusBadRequest)
h.logger.Error("client error uploading logo",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
} else {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error processing logo upload",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
}
}

474
internal/http/feed.go Normal file
View File

@@ -0,0 +1,474 @@
package http_router
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/feed"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/types"
pbfAgent "github.com/AlexOreL-272/ProtoMolva/go/gen/agent"
pbfDistr "github.com/AlexOreL-272/ProtoMolva/go/gen/distributor"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
//nolint:unused // TODO: review feed handlers
func (h *handler) createCreateCompanyFeedEvent(ctx context.Context, uid string, isAgent bool, resp []byte, handlerName string) {
var (
jsonResp interface{}
companyID string
companyName string
)
if isAgent {
jsonResp = new(pbfAgent.SetCompanyDataResponse)
} else {
jsonResp = new(pbfDistr.SetCompanyDataResponse)
}
ownerType := feed.RoleDistributor
if isAgent {
ownerType = feed.RoleAgent
}
if err := json.Unmarshal(resp, jsonResp); err != nil {
h.logger.Error(fmt.Sprintf("failed to unmarshal %s SetCompanyDataResponse", ownerType),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
switch resp := jsonResp.(type) {
case *pbfAgent.SetCompanyDataResponse:
company := resp.GetCompany()
companyID = company.GetCompanyId()
companyName = company.GetName()
case *pbfDistr.SetCompanyDataResponse:
company := resp.GetCompany()
companyID = company.GetCompanyId()
companyName = company.GetName()
default:
h.logger.Error("unexpected response type",
slog.String("handler", handlerName))
return
}
event := feed.Event{
OwnerId: uid,
OwnerType: ownerType,
EventType: feed.EventCompanyCreated,
Message: fmt.Sprintf("Компания %s создана", companyName),
Visibility: feed.VisibilityCompanyWide,
CompanyID: &companyID,
Payload: feed.EventPayload{
AttachmentId: companyID,
AttachmentType: feed.AttachmentTypeCompany,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("uid", uid),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("company_id", companyID),
slog.String("company_id", companyID),
)
}
}
//nolint:unused // TODO: review feed handlers
func (h *handler) createAddNewStaffMemberFeedEvent(ctx context.Context, uid string, isAgent bool, newStaffMember, companyId, handlerName string) {
ownerType := feed.RoleDistributor
if isAgent {
ownerType = feed.RoleAgent
}
event := feed.Event{
OwnerId: uid,
OwnerType: ownerType,
EventType: feed.EventNewCompanyMember,
Message: "Новый сотрудник добавлен в компанию",
Visibility: feed.VisibilityCompanyWide,
CompanyID: &companyId,
Payload: feed.EventPayload{
AttachmentId: newStaffMember,
AttachmentType: feed.AttachmentTypeProfile,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("uid", uid),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("company_id", companyId),
slog.String("new_staff_member", newStaffMember),
)
}
}
//nolint:unused // TODO: review feed handlers
func (h *handler) createSetSubmissionStatusFeedEvent(ctx context.Context, distributorId, submissionId string, submissionStatus types.Status, handlerName string) {
var message string
switch submissionStatus {
case types.StatusAccepted:
message = "Отклик на вакансию принят"
case types.StatusDeclined:
message = "Отклик на вакансию отклонен"
case types.StatusSuspended:
message = "Отклик на вакансию отложен"
default:
message = "Статус отклика на вакансию был изменен"
}
companyIds, err := h.feed.GetCompanyIdByUidTmp(ctx, distributorId)
if err != nil {
h.logger.Error("failed to get company Id",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("distributor_id", distributorId))
}
agentId, err := h.feed.GetAgentIdBySubmissionId(ctx, submissionId)
if err != nil {
//я хз стоит ли отдельно как-то обрабатывать ErrorNotFound(вроде как нет)
h.logger.Error("failed to get agent Id",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("submission_id", submissionId))
}
for _, companyId := range companyIds {
event := feed.Event{
OwnerId: distributorId,
OwnerType: feed.RoleDistributor,
EventType: feed.EventSubmissionStatusChanged,
Message: message,
Visibility: feed.VisibilityCompanyWide,
CompanyID: &companyId,
Payload: feed.EventPayload{
AttachmentId: submissionId,
AttachmentType: feed.AttachmentTypeCV,
AdditionalReceiver: agentId,
CustomData: submissionStatus.String(),
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("distributor_id", distributorId),
slog.String("agent_id", agentId),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("submission_id", submissionId),
slog.String("company_id", companyId),
)
}
}
}
//nolint:unused // TODO: review feed handlers
func (h *handler) createTransactionFeedEvent(ctx context.Context, uid string, isAgent bool, amount int64, currency, bankAccountId, handlerName string) {
var message string
if amount > 0 {
message = fmt.Sprintf("Вы получили %.2f %s", float64(amount)/100, currency)
} else {
message = fmt.Sprintf("С вашего аккаунта выведено %.2f %s", float64(-amount)/100, currency)
}
ownerType := feed.RoleDistributor
if isAgent {
ownerType = feed.RoleAgent
}
event := feed.Event{
OwnerId: uid,
OwnerType: ownerType,
EventType: feed.EventTransactionCreated,
Message: message,
Visibility: feed.VisibilityPrivate,
Payload: feed.EventPayload{
AttachmentId: bankAccountId,
AttachmentType: feed.AttachmentTypeBankAccount,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("error", err.Error()),
slog.String("uid", uid),
slog.String("handler", handlerName),
slog.String("bank_account_id", bankAccountId))
}
}
//nolint:unused // TODO: review feed handlers
func (h *handler) createCreateBankAccountFeedEvent(ctx context.Context, uid string, isAgent bool, resp []byte, bankName, handlerName string) {
var msg proto.Message
var ownerType feed.UserRole
if isAgent {
msg = &pbfAgent.CreateBankAccountResponse{}
ownerType = feed.RoleAgent
} else {
msg = &pbfDistr.CreateBankAccountResponse{}
ownerType = feed.RoleDistributor
}
if err := protojson.Unmarshal(resp, msg); err != nil {
h.logger.Error(fmt.Sprintf("failed to unmarshal %s CreateBankAccountResponse", ownerType),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
var bankAccountId string
switch v := msg.(type) {
case *pbfAgent.CreateBankAccountResponse:
bankAccountId = v.GetBankAccountId()
case *pbfDistr.CreateBankAccountResponse:
bankAccountId = v.GetBankAccountId()
default:
h.logger.Error("unexpected response type",
slog.String("handler", handlerName),
)
return
}
event := feed.Event{
OwnerId: uid,
OwnerType: ownerType,
EventType: feed.EventBankAccountCreated,
Message: fmt.Sprintf("Банковский счёт %s создан", bankName),
Visibility: feed.VisibilityPrivate,
Payload: feed.EventPayload{
AttachmentId: bankAccountId,
AttachmentType: feed.AttachmentTypeBankAccount,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("uid", uid),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("bank_account_id", bankAccountId),
)
}
}
func (h *handler) createEditBankAccountFeedEvent(ctx context.Context, uid string, isAgent bool, bankAccountID, handlerName string) {
ownerType := feed.RoleDistributor
if isAgent {
ownerType = feed.RoleAgent
}
event := feed.Event{
OwnerId: uid,
OwnerType: ownerType,
EventType: feed.EventBankAccountChanged,
Message: "Банковский счёт обновлён",
Visibility: feed.VisibilityPrivate,
Payload: feed.EventPayload{
AttachmentId: bankAccountID,
AttachmentType: feed.AttachmentTypeBankAccount,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("uid", uid),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("bank_account_id", bankAccountID))
}
}
func (h *handler) createEditCompanyFeedEvent(ctx context.Context, uid string, isAgent bool, companyID, handlerName string) {
ownerType := feed.RoleDistributor
if isAgent {
ownerType = feed.RoleAgent
}
event := feed.Event{
OwnerId: uid,
OwnerType: ownerType,
EventType: feed.EventCompanyChanged,
Message: "Данные компании обновлены",
Visibility: feed.VisibilityCompanyWide,
CompanyID: &companyID,
Payload: feed.EventPayload{
AttachmentId: companyID,
AttachmentType: feed.AttachmentTypeCompany,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("uid", uid),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("company_id", companyID))
}
}
func (h *handler) createSetProfileEvent(ctx context.Context, uid string, isAgent bool, handlerName string) {
ownerType := feed.RoleDistributor
if isAgent {
ownerType = feed.RoleAgent
}
event := feed.Event{
OwnerId: uid,
OwnerType: ownerType,
EventType: feed.EventProfileChanged,
Message: "Ваши данные профиля обновлены",
Visibility: feed.VisibilityPrivate,
Payload: feed.EventPayload{
AttachmentId: uid,
AttachmentType: feed.AttachmentTypeProfile,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("uid", uid),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
}
}
func (h *handler) createSetDistributorVacancyEvent(ctx context.Context, distributorId, vacancyId, vacancyName, handlerName string) {
event := feed.Event{
OwnerId: distributorId,
OwnerType: feed.RoleDistributor,
EventType: feed.EventVacancyCreated,
Message: fmt.Sprintf("Вакансия %s была создана", vacancyName),
Visibility: feed.VisibilityPublic,
Payload: feed.EventPayload{
AttachmentId: vacancyId,
AttachmentType: feed.AttachmentTypeVacancy,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("distributor_id", distributorId),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("vacancy_id", vacancyId),
)
}
}
func (h *handler) createEditDistributorVacancyEvent(ctx context.Context, distributorId, vacancyId, handlerName string) {
event := feed.Event{
OwnerId: distributorId,
OwnerType: feed.RoleDistributor,
EventType: feed.EventVacancyChanged,
Message: "Вакансия была изменена",
Visibility: feed.VisibilityPublic,
Payload: feed.EventPayload{
AttachmentId: vacancyId,
AttachmentType: feed.AttachmentTypeVacancy,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("distributor_id", distributorId),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("vacancy_id", vacancyId))
}
}
func (h *handler) createSendVacancyToModerationEvent(ctx context.Context, distributorId, vacancyId, handlerName string) {
companyIds, err := h.feed.GetCompanyIdByUidTmp(ctx, distributorId)
if err != nil {
h.logger.Error("failed to get company Id",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("distributor_id", distributorId))
}
for _, companyId := range companyIds {
event := feed.Event{
OwnerId: distributorId,
OwnerType: feed.RoleDistributor,
EventType: feed.EventVacancyModerationSent,
Message: "Вакансия была отправлена на модерацию",
Visibility: feed.VisibilityCompanyWide,
CompanyID: &companyId,
Payload: feed.EventPayload{
AttachmentId: vacancyId,
AttachmentType: feed.AttachmentTypeVacancy,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("distributor_id", distributorId),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("vacancy_id", vacancyId),
slog.String("company_id", companyId),
)
}
}
}
func (h *handler) createPostAnketaEvent(ctx context.Context, agentID, submissionId, handlerName string) {
companyIds, err := h.feed.GetCompanyIdByUidTmp(ctx, agentID)
if err != nil {
h.logger.Error("failed to get company Id",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("agent_id", agentID))
}
for _, companyId := range companyIds {
event := feed.Event{
OwnerId: agentID,
OwnerType: feed.RoleAgent,
EventType: feed.EventPostAnketa,
Message: "Анкета была заполнена",
Visibility: feed.VisibilityPrivate,
CompanyID: &companyId,
Payload: feed.EventPayload{
AttachmentId: submissionId,
AttachmentType: feed.AttachmentTypeSubmission,
},
}
if err := h.feed.CreateEvent(ctx, &event); err != nil {
h.logger.Error("failed to create feed event",
slog.String("agent_id", agentID),
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("submission_id", submissionId),
slog.String("company_id", companyId),
)
}
}
}

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()))
}
}

View File

@@ -0,0 +1,61 @@
package http_router
import (
"encoding/json"
"log/slog"
"net/http"
"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/integration"
intrmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model/integration"
)
func (h *handler) vkusvillIntegrationCallbackHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "vkusvillIntegrationCallbackHandler"
var request intrmodel.VkusvillSaveCandidateCallbackRequest
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
}
defer r.Body.Close()
h.logger.Debug("vkusvill integration callback request received",
slog.String("message", request.Message),
slog.String("handler", handlerName),
)
status, err := integration.VkusvillGetCandidateStatus(request.Params.Status)
if err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error getting vkusvill candidate status",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
if _, err := h.dbClient.UpdateSubmissionStatus(r.Context(), &dbtypes.SubmissionStatusUpdateRequest{
Id: request.Params.CandidateId,
Status: *dbtypes.NewSubmissionStatus(status.NullString()),
}); err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error while building and processing request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
// TODO: add event to feed
// go ...
w.WriteHeader(http.StatusAccepted)
}

612
internal/http/logo.go Normal file
View File

@@ -0,0 +1,612 @@
package http_router
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"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/cache"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/object_storage"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/types"
"github.com/aws/smithy-go/ptr"
"github.com/gorilla/mux"
)
// =============== FILE MANAGEMENT ===============
// TempFileManager управляет временными файлами с автоматической очисткой
type TempFileManager struct {
baseDir string
mu sync.Mutex
files map[string]*os.File
}
// Cleanup очищает все временные файлы при завершении работы
func (h *handler) Cleanup() {
h.tempFileManager.CleanupAll()
}
func NewTempFileManager(baseDir string) (*TempFileManager, error) {
if baseDir == "" {
baseDir = filepath.Join(os.TempDir(), "logo-uploads")
}
if err := os.MkdirAll(baseDir, constants.TempDirPermission); err != nil {
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
return &TempFileManager{
baseDir: baseDir,
files: make(map[string]*os.File),
}, nil
}
func (tm *TempFileManager) CreateTempFile(pattern string) (*os.File, error) {
tempFile, err := os.CreateTemp(tm.baseDir, pattern)
if err != nil {
return nil, err
}
tm.mu.Lock()
tm.files[tempFile.Name()] = tempFile
tm.mu.Unlock()
return tempFile, nil
}
func (tm *TempFileManager) CleanupFile(file *os.File) error {
if file == nil {
return nil
}
tm.mu.Lock()
delete(tm.files, file.Name())
tm.mu.Unlock()
name := file.Name()
if err := file.Close(); err != nil {
slog.Warn("failed to close temp file", slog.String("file", name), slog.String("error", err.Error()))
}
if err := os.Remove(name); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove temp file %s: %w", name, err)
}
return nil
}
func (tm *TempFileManager) CleanupAll() {
tm.mu.Lock()
defer tm.mu.Unlock()
for name, file := range tm.files {
_ = file.Close()
_ = os.Remove(name)
}
tm.files = make(map[string]*os.File)
}
type LogoFile struct {
File *os.File
ContentType string
Size int64
manager *TempFileManager
}
// Close закрывает файл и удаляет временный файл
func (lf *LogoFile) Close() error {
if lf.manager != nil {
return lf.manager.CleanupFile(lf.File)
}
if lf.File != nil {
name := lf.File.Name()
_ = lf.File.Close()
return os.Remove(name)
}
return nil
}
// Reset сбрасывает позицию чтения в начало файла
func (lf *LogoFile) Reset() error {
_, err := lf.File.Seek(0, io.SeekStart)
return err
}
// =============== LOGO HANDLERS ===============
func (h *handler) getCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getCompanyLogoHandler"
distributorID, companyID, token := h.extractLogoRequestData(r)
if err := h.validateUserAccess(token, distributorID); err != nil {
h.handleAccessError(w, err, handlerName)
return
}
logoLink, err := h.getLogoLinkWithCache(r.Context(), companyID)
if err != nil {
if errors.Is(err, object_storage.ErrObjectNotFound) {
response := types.GetLogoResponse{
Link: "",
ExpiresIn: time.Time{},
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
h.logger.Error("error encoding response",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
}
return
}
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error getting logo link",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
response := types.GetLogoResponse{
Link: logoLink,
ExpiresIn: time.Now().Add(constants.LogoLinkTTL),
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
h.logger.Error("error encoding response",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
}
}
func (h *handler) createCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "createCompanyLogoHandler"
distributorID, companyID, token := h.extractLogoRequestData(r)
if err := h.validateUserAccess(token, distributorID); err != nil {
h.handleAccessError(w, err, handlerName)
return
}
if err := h.validateUserPermissions(r.Context(), distributorID, keycloak.PermissionCompany, keycloak.PermissionLevelCanEdit); err != nil {
h.handleAccessError(w, err, handlerName)
return
}
logoFile, err := h.extractAndValidateLogoFile(r)
if err != nil {
h.handleUploadError(w, err, handlerName)
return
}
defer logoFile.Close()
// Сбрасываем позицию после валидации, чтобы не потерять данные
if err := logoFile.Reset(); err != nil {
h.logger.Error("failed to reset file position",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
return
}
// может стоит добавить таймаут записи в кэш
err = h.objectStorageClient.PutNewObject(
r.Context(),
companyID,
object_storage.LogoCategory,
logoFile.File,
object_storage.PutOptions{
ContentType: logoFile.ContentType,
},
)
if err != nil {
if errors.Is(err, object_storage.ErrObjectAlreadyExists) {
http.Error(w, "Logo already exists. Use PUT to update", http.StatusConflict)
return
}
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error uploading logo",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
h.refreshLogoCacheAsync(companyID)
w.WriteHeader(http.StatusCreated)
}
func (h *handler) updateCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "updateCompanyLogoHandler"
distributorID, companyID, token := h.extractLogoRequestData(r)
if err := h.validateUserAccess(token, distributorID); err != nil {
h.handleAccessError(w, err, handlerName)
return
}
if err := h.validateUserPermissions(r.Context(), distributorID, keycloak.PermissionCompany, keycloak.PermissionLevelCanEdit); err != nil {
h.handleAccessError(w, err, handlerName)
return
}
logoFile, err := h.extractAndValidateLogoFile(r)
if err != nil {
h.handleUploadError(w, err, handlerName)
return
}
defer logoFile.Close()
// Сбрасываем позицию после валидации, чтобы не потерять данные
if err := logoFile.Reset(); err != nil {
h.logger.Error("failed to reset file position",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
return
}
// Обновляем в хранилище
err = h.objectStorageClient.UpdateObject(
r.Context(),
companyID,
object_storage.LogoCategory,
logoFile.File,
object_storage.UpdateOptions{
ContentType: logoFile.ContentType,
},
)
if err != nil {
if errors.Is(err, object_storage.ErrObjectNotFound) {
http.Error(w, "Logo not found. Use POST to create", http.StatusNotFound)
return
}
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error updating logo",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
h.refreshLogoCacheAsync(companyID)
w.WriteHeader(http.StatusNoContent)
}
func (h *handler) deleteCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "deleteCompanyLogoHandler"
distributorID, companyID, token := h.extractLogoRequestData(r)
if err := h.validateUserAccess(token, distributorID); err != nil {
h.handleAccessError(w, err, handlerName)
return
}
if err := h.validateUserPermissions(r.Context(), distributorID, keycloak.PermissionCompany, keycloak.PermissionLevelCanEdit); err != nil {
h.handleAccessError(w, err, handlerName)
return
}
err := h.objectStorageClient.DeleteObject(r.Context(), companyID, object_storage.LogoCategory)
if err != nil {
if errors.Is(err, object_storage.ErrObjectNotFound) {
http.Error(w, constants.ErrNotFound.Error(), http.StatusNotFound)
return
}
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error deleting logo",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
// Очищаем кэш
if err := h.cacheClient.Del(r.Context(), companyID, cache.LogoValueType); err != nil {
h.logger.Error("error deleting logo from cache",
slog.String("error", err.Error()),
slog.String("company_id", companyID))
}
w.WriteHeader(http.StatusNoContent)
}
// =============== CORE LOGIC FUNCTIONS ===============
// extractAndValidateLogoFile извлекает файл из запроса и сохраняет во временный файл
func (h *handler) extractAndValidateLogoFile(r *http.Request) (*LogoFile, error) {
err := r.ParseMultipartForm(constants.ParseMultipartFormAllFile) //сохраняем на диск весь файл
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrMultipartParse, err)
}
defer func() { _ = r.MultipartForm.RemoveAll() }()
file, header, err := r.FormFile("logo")
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrFileNotFound, err)
}
defer file.Close()
// Проверяем размер файла
if header.Size == 0 {
return nil, ErrEmptyFile
}
if header.Size > constants.LogoMaxSize {
return nil, &FileTooLargeError{Size: header.Size, MaxSize: constants.LogoMaxSize}
}
contentType := header.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
// Создаем временный файл для копирования
tempFile, err := h.tempFileManager.CreateTempFile(constants.TempLogoFilePattern)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrTempFileCreate, err)
}
// Копируем содержимое во временный файл
copiedBytes, err := io.CopyN(tempFile, file, constants.LogoMaxSize+1)
if err != nil && err != io.EOF {
_ = h.tempFileManager.CleanupFile(tempFile)
return nil, fmt.Errorf("%w: failed to copy file: %v", ErrFileOperation, err)
}
logoFile := &LogoFile{
File: tempFile,
ContentType: contentType,
Size: copiedBytes,
manager: h.tempFileManager,
}
if err := h.validateLogoFile(logoFile); err != nil {
_ = logoFile.Close()
return nil, err
}
return logoFile, nil
}
// validateLogoFile валидирует файл изображения
func (h *handler) validateLogoFile(logoFile *LogoFile) error {
// Проверяем заявленный тип
if !isValidImageType(logoFile.ContentType) {
h.logger.Error("invalid image type",
slog.String("image_type", logoFile.ContentType),
slog.String("handler", "validateLogoFile"))
return &InvalidImageTypeError{ContentType: logoFile.ContentType}
}
if err := logoFile.Reset(); err != nil {
return fmt.Errorf("%w: failed to reset file position before validation: %v", ErrFileOperation, err)
}
// Читаем первые 512 байт для определения реального типа
buffer := make([]byte, constants.BytesToDetectContentType)
firstBytes, err := logoFile.File.Read(buffer)
if err != nil && err != io.EOF {
return fmt.Errorf("%w: failed to read file header: %v", ErrFileOperation, err)
}
detectedType := http.DetectContentType(buffer[:firstBytes])
if !isValidImageType(detectedType) {
h.logger.Error("invalid image type after detecting",
slog.String("image_type", detectedType),
slog.String("handler", "validateLogoFile"))
return &InvalidImageTypeError{ContentType: detectedType}
}
// Возвращаем позицию в начало
if err := logoFile.Reset(); err != nil {
return fmt.Errorf("%w: failed to reset file position: %v", ErrFileOperation, err)
}
return nil
}
// extractLogoRequestData извлекает данные из HTTP запроса
func (h *handler) extractLogoRequestData(r *http.Request) (distributorID, companyID, token string) {
vars := mux.Vars(r)
distributorID = vars["distributor_id"]
companyID = vars["company_id"]
token = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
return
}
// =============== VALIDATION FUNCTIONS ===============
// validateUserAccess проверяет авторизацию и соответствие пользователя
func (h *handler) validateUserAccess(token, expectedUserID string) error {
if token == "" {
return constants.ErrUnauthorized
}
userInfo, err := h.jwtManager.GetUserInfoFromToken(token)
if err != nil {
return fmt.Errorf("invalid token: %w", err)
}
if expectedUserID != userInfo.UserId {
return constants.ErrForbidden
}
return nil
}
// validateUserPermissions проверяет права доступа пользователя
func (h *handler) validateUserPermissions(ctx context.Context, userID, permission, level string) error {
ok, err := h.authManager.CheckPermissions(ctx, auth.CheckPermissionsRequest{
UserId: userID,
RequiredPermission: permission,
RequiredPermissionLevel: level,
})
if err != nil {
return fmt.Errorf("permission check failed: %w", err)
}
if !ok {
return constants.ErrForbidden
}
return nil
}
// isValidImageType проверяет допустимость типа изображения
func isValidImageType(contentType string) bool {
contentType = strings.ToLower(strings.TrimSpace(contentType))
if idx := strings.Index(contentType, ";"); idx != -1 {
contentType = contentType[:idx]
}
return constants.ValidImageTypes[contentType]
}
// =============== CACHE FUNCTIONS ===============
// refreshLogoCacheAsync асинхронно обновляет кэш логотипа
func (h *handler) refreshLogoCacheAsync(companyID string) {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), constants.RefreshLogoCacheAsyncTimeout)
defer cancel()
// Получаем новую ссылку на логотип
logoLink, err := h.objectStorageClient.GetPresignedLink(ctx,
companyID, object_storage.LogoCategory, object_storage.LinkOptions{
TTL: ptr.Duration(constants.LogoLinkTTL),
})
if err != nil {
h.logger.Warn("failed to get logo link for cache refresh",
slog.String("error", err.Error()),
slog.String("company_id", companyID))
return
}
if err = h.cacheClient.Del(ctx, companyID, cache.LogoValueType); err != nil {
h.logger.Warn("failed to delete logo from cache, it's ok if it first time creation",
slog.String("error", err.Error()),
slog.String("company_id", companyID))
}
response := types.GetLogoResponse{
Link: logoLink,
ExpiresIn: time.Now().Add(constants.LogoLinkTTL),
}
body, err := json.Marshal(response)
if err != nil {
h.logger.Warn("failed to marshal logo response for cache",
slog.String("error", err.Error()),
slog.String("company_id", companyID))
return
}
if err := h.cacheClient.Set(ctx, companyID, cache.LogoValueType, string(body), constants.LogoLinkTTL); err != nil {
h.logger.Warn("failed to update logo cache",
slog.String("error", err.Error()),
slog.String("company_id", companyID))
} else {
h.logger.Debug("logo cache refreshed successfully",
slog.String("company_id", companyID))
}
}()
}
// getLogoLinkWithCache получает ссылку на логотип из кэша или хранилища
func (h *handler) getLogoLinkWithCache(ctx context.Context, companyID string) (string, error) {
// Проверяем кэш
cachedData, err := h.cacheClient.Get(ctx, companyID, cache.LogoValueType)
if err == nil {
var response types.GetLogoResponse
if err := json.Unmarshal([]byte(cachedData), &response); err == nil {
return response.Link, nil
}
h.logger.Warn("failed to unmarshal logo response from cache",
slog.String("company_id", companyID))
}
if !errors.Is(err, cache.ErrKeyNotFound) {
h.logger.Warn("failed to get logo from cache",
slog.String("error", err.Error()),
slog.String("company_id", companyID))
}
// Получаем из хранилища ссылку на логотип
logoLink, err := h.objectStorageClient.GetPresignedLink(ctx,
companyID, object_storage.LogoCategory, object_storage.LinkOptions{
TTL: ptr.Duration(constants.LogoLinkTTL),
})
if err != nil {
return "", err
}
response := types.GetLogoResponse{
Link: logoLink,
ExpiresIn: time.Now().Add(constants.LogoLinkTTL),
}
body, err := json.Marshal(response)
if err != nil {
h.logger.Warn("failed to marshal logo response for cache",
slog.String("error", err.Error()),
slog.String("company_id", companyID))
} else {
if err := h.cacheClient.Set(ctx, companyID, cache.LogoValueType, string(body), constants.LogoLinkTTL); err != nil {
h.logger.Warn("failed to cache logo response",
slog.String("error", err.Error()),
slog.String("company_id", companyID))
}
}
return logoLink, nil
}
// =============== GRACEFUL SHUTDOWN ===============
// SetupGracefulShutdown делает graceful shutdown)
func (h *handler) SetupGracefulShutdown(ctx context.Context) {
go func() {
<-ctx.Done()
h.logger.Info("shutting down, cleaning up temporary files")
h.Cleanup()
}()
}

View File

@@ -0,0 +1,70 @@
package http_router
import (
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth/keycloak"
"log/slog"
"net/http"
"strings"
"time"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/config"
)
type Middleware struct {
logger *slog.Logger
authManager auth.Manager
}
type ConfigMiddleware struct {
logger *slog.Logger
keycloakCfg *config.Keycloak
}
func NewMiddleware(c *ConfigMiddleware) (*Middleware, error) {
authManager, err := keycloak.New(&keycloak.Config{
AuthServerAddr: c.keycloakCfg.BaseURL,
Realm: c.keycloakCfg.Realm,
ClientId: c.keycloakCfg.ClientId,
ClientSecret: c.keycloakCfg.ClientSecret,
})
if err != nil {
return nil, err
}
return &Middleware{
logger: c.logger,
authManager: authManager,
}, nil
}
// loggingMiddleware middleware для логирования запросов
func (m *Middleware) loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
m.logger.Info("Started request", slog.String("method", req.Method), slog.String("url", req.URL.Path))
next.ServeHTTP(w, req)
m.logger.Info("Completed request",
slog.String("path", req.URL.Path),
slog.Duration("duration", time.Since(start)))
})
}
// authMiddleware middleware для валидации и обновления токена при истечении
func (m *Middleware) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
token := strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ")
ok, err := m.authManager.CheckTokenIsValid(req.Context(), token)
if err != nil || !ok {
http.Error(w, "", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, req)
})
}

161
internal/http/profile.go Normal file
View File

@@ -0,0 +1,161 @@
package http_router
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
"github.com/gorilla/mux"
rmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model"
)
func (h *handler) getProfileAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getAgentProfileHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
)
result, err := h.agentService.GetProfile(r.Context(), &rmodel.ProfileGetRequest{
Id: agentId,
})
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error getting agent profile",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) updateProfileAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "setAgentProfileHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
)
var request rmodel.ProfileUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error unmarshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
request.Id = agentId
if _, err := h.agentService.UpdateProfile(r.Context(), &request); err != nil {
h.handleAgentError(w, err)
h.logger.Error("error updating agent profile",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go h.createSetProfileEvent(
context.Background(),
agentId,
true,
handlerName,
)
w.WriteHeader(http.StatusNoContent)
}
func (h *handler) getProfileDisributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getProfileDisributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
)
result, err := h.distributorService.GetProfile(r.Context(), &rmodel.ProfileGetRequest{
Id: distId,
})
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting distributor profile",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) updateProfileDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "updateProfileDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
)
var request rmodel.ProfileUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error while unmarshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
request.Id = distId
if _, err := h.distributorService.UpdateProfile(r.Context(), &request); err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error updating distributor profile",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go h.createSetProfileEvent(
context.Background(),
distId,
false,
handlerName,
)
w.WriteHeader(http.StatusNoContent)
}

335
internal/http/router.go Normal file
View File

@@ -0,0 +1,335 @@
package http_router
import (
"fmt"
"log/slog"
"net/http"
"net/http/pprof"
"strings"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/logger"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/feed"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/file_manager/s3_storage"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/integration"
objectStorage "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/object_storage"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/services/agent"
authinfra "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/services/auth_infrastructure"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/services/distributor"
pgdb "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/postgres"
formgenerator "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/form_generator"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/config"
"github.com/gorilla/mux"
)
type Router struct {
Mux *mux.Router
env string
logger *slog.Logger
middleware *Middleware
shortenerCfg *config.SecretConfig
abstractAPI string
cache cache.Client
objectStorage objectStorage.Client
feedHandler *feed.Handler
integrationClient integration.Client
databaseClient database.Client
agentService agent.AgentService
distributorService distributor.DistributorService
}
func New(logger *slog.Logger, cfg *config.Config) (*Router, error) {
middleware, err := NewMiddleware(&ConfigMiddleware{
logger: logger,
keycloakCfg: &cfg.Keycloak,
})
if err != nil {
return nil, err
}
cacheClient, err := cache.New(cache.ValKeyCacheConfig{
Addrs: strings.Split(cfg.ValkeyCache.Addrs, ","),
Password: cfg.ValkeyCache.Password,
ReadOnly: cfg.ValkeyCache.ReadOnly,
DialTimeout: cfg.ValkeyCache.DialTimeout,
PoolSize: cfg.ValkeyCache.PoolSize,
DefaultTTL: cfg.ValkeyCache.DefaultTTL,
RootCaFilePath: cfg.ValkeyCache.RootCaFilePath,
}, cache.ValkeyCacheType)
if err != nil {
return nil, err
}
databaseClient, err := database.New(database.PostgresClientType, pgdb.PostgresConfig{
Host: cfg.Database.Host,
Port: cfg.Database.Port,
Username: cfg.Database.Username,
Password: cfg.Database.Password,
Database: cfg.Database.DBName,
Schema: cfg.Database.Schema,
SSLMode: cfg.Database.SSLMode,
SSLRootCert: cfg.Database.RootCaFilePath,
})
if err != nil {
return nil, err
}
agentService, err := agent.New(agent.CrmAgentServiceType, agent.CrmAgentServiceConfig{
DbClient: databaseClient,
})
if err != nil {
return nil, err
}
distributorService, err := distributor.New(distributor.CrmDistributorServiceType, distributor.CrmDistributorServiceConfig{
DbClient: databaseClient,
})
if err != nil {
return nil, err
}
objectStorageClient, err := objectStorage.New(objectStorage.S3StorageConfig{
Bucket: cfg.S3Storage.Bucket,
DefaultLinkTTL: cfg.ValkeyCache.DefaultTTL,
}, objectStorage.ClientTypeS3)
if err != nil {
return nil, err
}
integrationClient, err := integration.New(integration.Config{
Cache: cacheClient,
Db: databaseClient,
Logger: logger,
Secrets: map[string]integration.CompanySecrets{
integration.VkusvillCompanyName: integration.VkusvillSecretsConfig{
ApiToken: cfg.Integration.Vkusvill.ApiToken,
},
},
})
if err != nil {
return nil, err
}
// TODO: вырезать ненужные аргументы в рамках https://tracker.yandex.ru/MOLVARAZRABOTKA-363
service, err := feed.NewService(fmt.Sprintf(
"postgres://%s:%s@%s:%d/%s?sslmode=%s",
cfg.Database.Username,
cfg.Database.Password,
cfg.Database.Host,
cfg.Database.Port,
cfg.Database.DBName,
cfg.Database.SSLMode,
), cfg.Database.Schema, logger, databaseClient)
if err != nil {
return nil, err
}
return &Router{
Mux: mux.NewRouter(),
env: cfg.Env,
logger: logger,
middleware: middleware,
shortenerCfg: &cfg.Secret,
abstractAPI: cfg.EmailVerificationService.APIKey, // may be nil dereference if no email verification service is configured
cache: cacheClient,
objectStorage: objectStorageClient,
feedHandler: feed.NewFeedHandler(logger, service),
databaseClient: databaseClient,
agentService: agentService,
distributorService: distributorService,
integrationClient: integrationClient,
}, nil
}
func SetupRouter(r *Router, buildCfg config.BuildInfo) {
authInfraService, err := authinfra.New(authinfra.CacheAuthInfrastructureServiceType, authinfra.CacheAuthInfraServiceConfig{
CacheClient: r.cache,
})
if err != nil {
panic(err)
}
s3FileManager := s3_storage.NewS3Storage(
r.objectStorage,
r.cache,
)
h := newHandler(&Config{
env: r.env,
logger: r.logger,
authManager: r.middleware.authManager,
authInfraService: authInfraService,
fileManager: s3FileManager,
dbClient: r.databaseClient,
agentService: r.agentService,
distributorService: r.distributorService,
secretConfig: r.shortenerCfg,
emailVerificationServiceAPIKey: r.abstractAPI,
cacheClient: r.cache,
objectStorageClient: r.objectStorage,
feed: r.feedHandler,
integrationClient: r.integrationClient,
buildConfig: buildCfg,
})
r.Mux.Use(r.middleware.loggingMiddleware)
setupAuthHandlers(r, h)
setupIntegrationHandlers(r, h)
setupClientHandlers(r, h)
setupValidationHandlers(r, h)
if r.env != logger.EnvProd {
setupSystemInfoHandlers(r, h)
}
subRouter := r.Mux.PathPrefix("/api/v1").Subrouter()
subRouter.Use(r.middleware.authMiddleware)
setupAgentHandlers(subRouter, h)
setupDistributorHandlers(subRouter, h)
setupEmployeesHandlers(subRouter, h)
setupLogoHandlers(subRouter, h)
// --------------- DOCUMENTS FOR FORM ------------
r.Mux.HandleFunc("/api/v1/docs/{file}", h.getFileHandler).Methods(http.MethodGet)
// --------------- FEED ------------
subRouter.HandleFunc("/feed/{uid}/events", h.GetUserEventsHandler).Methods(http.MethodGet)
}
func setupAuthHandlers(r *Router, h *handler) {
r.Mux.HandleFunc("/api/v1/login", h.loginHandler).Methods(http.MethodPost)
r.Mux.HandleFunc("/api/v1/register", h.registerHandler).Methods(http.MethodPost)
r.Mux.HandleFunc("/api/v1/logout", h.logoutHandler).Methods(http.MethodPost)
r.Mux.HandleFunc("/api/v1/refresh-token", h.refreshTokenHandler).Methods(http.MethodPost)
r.Mux.HandleFunc("/api/v1/verify_email", h.verifyEmailHandler).Methods(http.MethodPatch)
r.Mux.HandleFunc("/api/v1/verify_email", h.getEmailVerificationStatusHandler).Methods(http.MethodGet)
r.Mux.HandleFunc("/api/v1/confirm_email", h.confirmEmailPageHandler).Methods(http.MethodGet)
r.Mux.HandleFunc("/api/v1/forgot_password", h.forgotPasswordHandler).Methods(http.MethodPost)
r.Mux.HandleFunc("/api/v1/validate_otp", h.validateOTPHandler).Methods(http.MethodGet)
r.Mux.HandleFunc("/api/v1/reset_password", h.resetPasswordHandler).Methods(http.MethodPut)
}
func setupAgentHandlers(subRouter *mux.Router, h *handler) {
// // =============== COMPANY HANDLERS ===============
subRouter.HandleFunc("/agents/{agent_id}/companies", h.getCompanyListAgentHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/agents/{agent_id}/company/{company_id}", h.getCompanyByIdAgentHandler).Methods(http.MethodGet)
// TODO: review
// subRouter.HandleFunc("/agents/{agent_id}/company/{company_id}", h.addNewAgentCompanyMemberHandler).Methods(http.MethodPost)
subRouter.HandleFunc("/agents/{agent_id}/company", h.createCompanyAgentHandler).Methods(http.MethodPost)
subRouter.HandleFunc("/agents/{agent_id}/company/{company_id}", h.updateCompanyAgentHandler).Methods(http.MethodPatch)
// // =============== VACANCY HANDLERS ===============
subRouter.HandleFunc("/agents/{agent_id}/vacancies", h.getVacancyListAgentHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/agents/{agent_id}/vacancies/{vacancy_id}", h.getPersonalLinkHandler).Methods(http.MethodGet)
// // =============== SUBMISSION HANDLERS ===============
subRouter.HandleFunc("/agents/{agent_id}/submissions", h.getSubmissionListAgentHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/agents/{agent_id}/submissions/{submission_id}", h.deleteSubmissionAgentHandler).Methods(http.MethodDelete)
subRouter.HandleFunc("/agents/{agent_id}/submissions/{submission_id}/cv", h.getSubmissionCVHandler).Methods(http.MethodGet)
// // =============== PROFILE HANDLERS ===============
subRouter.HandleFunc("/agents/{agent_id}/profile", h.getProfileAgentHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/agents/{agent_id}/profile", h.updateProfileAgentHandler).Methods(http.MethodPatch)
// // =============== BALANCE HANDLERS ===============
subRouter.HandleFunc("/agents/{agent_id}/balance", h.getBalanceAgentHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/agents/{agent_id}/transactions", h.getTransactionListAgentHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/agents/{agent_id}/transactions", h.createTransactionAgentHandler).Methods(http.MethodPost)
subRouter.HandleFunc("/agents/{agent_id}/bank_accounts", h.getBankAccountListAgentHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/agents/{agent_id}/bank_accounts", h.createBankAccountAgentHandler).Methods(http.MethodPost)
subRouter.HandleFunc("/agents/{agent_id}/bank_accounts/{bank_account_id}", h.updateBankAccountAgentHandler).Methods(http.MethodPatch)
subRouter.HandleFunc("/agents/{agent_id}/bank_accounts/{bank_account_id}", h.deleteBankAccountAgentHandler).Methods(http.MethodDelete)
}
func setupDistributorHandlers(subRouter *mux.Router, h *handler) {
// // =============== COMPANY HANDLERS ===============
subRouter.HandleFunc("/distributor/{distributor_id}/companies", h.getCompanyListDistributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}", h.getCompanyByIdDistributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}", h.addCompanyMemberDistributorHandler).Methods(http.MethodPost)
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}", h.updateCompanyDistributorHandler).Methods(http.MethodPatch)
subRouter.HandleFunc("/distributor/{distributor_id}/company", h.createCompanyDistributorHandler).Methods(http.MethodPost)
// // =============== VACANCY HANDLERS ===============
subRouter.HandleFunc("/distributor/{distributor_id}/vacancies", h.getVacancyListDistributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/vacancies", h.createVacancyDistributorHandler).Methods(http.MethodPost)
subRouter.HandleFunc("/distributor/{distributor_id}/vacancies/{vacancy_id}", h.updateVacancyDistributorHandler).Methods(http.MethodPatch)
subRouter.HandleFunc("/distributor/{distributor_id}/vacancies/{vacancy_id}", h.deleteVacancyDistributorHandler).Methods(http.MethodDelete)
subRouter.HandleFunc("/distributor/{distributor_id}/vacancies/{vacancy_id}/moderation", h.sendVacancyToModerationHandler).Methods(http.MethodPost)
// // =============== SUBMISSION HANDLERS ===============
subRouter.HandleFunc("/distributor/{distributor_id}/vacancies/{vacancy_id}/submissions", h.getSubmissionListDistributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/vacancies/{vacancy_id}/submissions/{submission_id}/status", h.updateSubmissionStatusDistributorHandler).Methods(http.MethodPost)
subRouter.HandleFunc("/distributor/{distributor_id}/submissions/{submission_id}/cv", h.getSubmissionCVHandler).Methods(http.MethodGet)
// // =============== PROFILE HANDLERS ===============
subRouter.HandleFunc("/distributor/{distributor_id}/profile", h.getProfileDisributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/profile", h.updateProfileDistributorHandler).Methods(http.MethodPatch)
// // =============== BALANCE HANDLERS ===============
subRouter.HandleFunc("/distributor/{distributor_id}/balance", h.getBalanceDistributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}/balance", h.getCompanyBalanceDistributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/transactions", h.getTransactionListDistributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}/transactions", h.getCompanyTransactionListDistributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/transactions", h.createTransactionDistributorHandler).Methods(http.MethodPost)
subRouter.HandleFunc("/distributor/{distributor_id}/bank_accounts", h.getBankAccountListDistributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}/bank_accounts", h.getCompanyBankAccountListDistributorHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}/bank_accounts", h.createBankAccountDistributorHandler).Methods(http.MethodPost)
subRouter.HandleFunc("/distributor/{distributor_id}/bank_accounts/{bank_account_id}", h.updateBankAccountDistributorHandler).Methods(http.MethodPut)
subRouter.HandleFunc("/distributor/{distributor_id}/bank_accounts/{bank_account_id}", h.deleteBankAccountDistributorHandler).Methods(http.MethodDelete)
}
func setupEmployeesHandlers(subRouter *mux.Router, h *handler) {
// // =============== EMPLOYEES HANDLERS ===============
subRouter.HandleFunc("/companies/{company_id}/employees", h.getEmployeesHandler).Methods(http.MethodGet)
}
func setupClientHandlers(r *Router, h *handler) {
// // =============== CLIENT HANDLERS ===============
r.Mux.HandleFunc("/api/v1/anketa", h.getAnketaHandler).Methods(http.MethodGet)
r.Mux.HandleFunc("/api/v1/anketa", h.postAnketaHandler).Methods(http.MethodPost)
// --------------- FORM METADATA ---------------
//TODO FROM ALEXANDER: make err validation
formSys, _ := formgenerator.GetFileSystem()
r.Mux.PathPrefix("/api/v1/anketa/static/").Handler(http.StripPrefix("/api/v1/anketa/static/", http.FileServer(http.FS(formSys))))
}
func setupValidationHandlers(r *Router, h *handler) {
// // =============== VALIDATION HANDLERS ===============
r.Mux.HandleFunc("/api/v1/{uid}/validation", h.getUserValidationStatusHandler).Methods(http.MethodGet)
}
func setupLogoHandlers(subRouter *mux.Router, h *handler) {
// // =============== LOGO HANDLERS ===============
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}/logo", h.getCompanyLogoHandler).Methods(http.MethodGet)
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}/logo", h.createCompanyLogoHandler).Methods(http.MethodPost)
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}/logo", h.updateCompanyLogoHandler).Methods(http.MethodPut)
subRouter.HandleFunc("/distributor/{distributor_id}/company/{company_id}/logo", h.deleteCompanyLogoHandler).Methods(http.MethodDelete)
}
func setupIntegrationHandlers(r *Router, h *handler) {
r.Mux.HandleFunc("/api/v1/integration/vkusvill/callback", h.vkusvillIntegrationCallbackHandler).Methods(http.MethodPost)
}
func setupSystemInfoHandlers(r *Router, h *handler) {
r.Mux.HandleFunc("/api/v1/healthcheck", h.getBuildInfoHandler).Methods(http.MethodGet)
r.Mux.HandleFunc("/debug/pprof/", pprof.Index).Methods(http.MethodGet)
r.Mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline).Methods(http.MethodGet)
r.Mux.HandleFunc("/debug/pprof/profile", pprof.Profile).Methods(http.MethodGet)
r.Mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol).Methods(http.MethodGet)
r.Mux.HandleFunc("/debug/pprof/trace", pprof.Trace).Methods(http.MethodGet)
r.Mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")).Methods(http.MethodGet)
r.Mux.Handle("/debug/pprof/heap", pprof.Handler("heap")).Methods(http.MethodGet)
r.Mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate")).Methods(http.MethodGet)
r.Mux.Handle("/debug/pprof/block", pprof.Handler("block")).Methods(http.MethodGet)
}

375
internal/http/submission.go Normal file
View File

@@ -0,0 +1,375 @@
package http_router
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"strings"
"time"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/integration"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/types"
"github.com/gorilla/mux"
filemanager "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/file_manager"
rmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model"
)
func (h *handler) getSubmissionListAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getSubmissionListAgentHandler"
var (
vars = mux.Vars(r)
agentId = vars["agent_id"]
)
request, err := new(rmodel.SubmissionListGetRequest).FromQuery(r.URL.Query())
if err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error parsing request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
request.AgentId = agentId
result, err := h.agentService.GetSubmissionList(r.Context(), request)
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error getting submission list",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getSubmissionCVHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getSubmissionCVHandler"
var (
vars = mux.Vars(r)
submissionId = vars["submission_id"]
)
params := filemanager.ParameterTable{
filemanager.SubmissionIdParam: submissionId,
}
urls, err := h.fileManager.GetFilePaths(r.Context(), filemanager.CVFileType, params)
if err != nil {
if errors.Is(err, filemanager.ErrFileNotFound) {
http.Error(w, constants.ErrNotFound.Error(), http.StatusNotFound)
h.logger.Error("CV file not found",
slog.String("error", err.Error()),
slog.String("submissionId", submissionId),
slog.String("handler", handlerName))
return
}
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error while getting file URLs: ",
slog.String("error", err.Error()),
slog.String("submissionId", submissionId),
slog.String("handler", handlerName))
return
}
if len(urls) == 0 {
http.Error(w, constants.ErrNotFound.Error(), http.StatusNotFound)
h.logger.Error("No CV URLs found",
slog.String("submissionId", submissionId),
slog.String("handler", handlerName))
return
}
response := types.CVURLResponse{
URL: urls[0],
SubmissionID: submissionId,
ExpiresAt: time.Now().Add(constants.DefaultFileTTL - time.Hour),
}
h.logger.Debug("CV URL retrieved successfully",
slog.String("submissionId", submissionId),
slog.String("handler", handlerName))
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("submissionId", submissionId),
slog.String("handler", handlerName))
}
}
func (h *handler) deleteSubmissionAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "deleteSubmissionHandler"
var (
vars = mux.Vars(r)
submissionId = vars["submission_id"]
)
result, err := h.agentService.DeleteSubmission(r.Context(), &rmodel.SubmissionDeleteRequest{
Id: submissionId,
})
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error deleting submission",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go func() {
if err := h.feed.CancelEvent(r.Context(), submissionId, "Отклик был удален"); err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error cancelling event: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
}
}()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getSubmissionListDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getSubmissionsDistributorHandler"
var (
vars = mux.Vars(r)
vacancyId = vars["vacancy_id"]
)
request, err := new(rmodel.SubmissionListForVacancyGetRequest).FromQuery(r.URL.Query())
if err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error parsing request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
request.VacancyId = vacancyId
result, err := h.distributorService.GetSubmissionListForVacancy(r.Context(), request)
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting submissions for vacancy",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) updateSubmissionStatusDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "updateSubmissionStatusDistributorHandler"
var (
vars = mux.Vars(r)
submissionId = vars["submission_id"]
)
var request rmodel.SubmissionStatusUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error unmarshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
request.Id = submissionId
if _, err := h.distributorService.UpdateSubmissionStatus(r.Context(), &request); err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error updating submission status",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
// TODO:
// h.createSetSubmissionStatusFeedEvent(r.Context(), distId, submissionId, status.Status, handlerName)
w.WriteHeader(http.StatusNoContent)
}
//nolint:funlen // TODO: refactor
func (h *handler) postAnketaHandler(w http.ResponseWriter, r *http.Request) {
handlerName := "postAnketaHandler"
var (
agentID = r.FormValue("agent_id")
vacancyID = r.FormValue("vacancy_id")
cvLink = r.FormValue("cv")
name = r.FormValue("name")
phone = r.FormValue("phone_number")
email = r.FormValue("email")
birthday = r.FormValue("birthday")
info = r.FormValue("info")
firstName, lastName, middleName = splitName(name)
)
result, err := h.agentService.CreateSubmission(r.Context(), &rmodel.SubmissionCreateRequest{
AgentId: agentID,
VacancyId: vacancyID,
CandidateInfo: &rmodel.CandidateInfo{
FirstName: firstName,
LastName: lastName,
MiddleName: middleName,
PhoneNumber: phone,
Email: email,
Birthday: birthday,
CvLink: &cvLink,
Resume: &info,
},
})
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error creating submission",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
file, header, err := r.FormFile("cv_file")
if err == nil {
params := filemanager.ParameterTable{
filemanager.SubmissionIdParam: result.Id,
}
if err := h.fileManager.SaveFile(
r.Context(),
filemanager.CVFileType,
file,
header,
params,
); err != nil {
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error saving CV to file storage: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
} else if header != nil {
// CV is provided, but there are other errors
http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
h.logger.Error("error while extracting CV from request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
}
go h.createPostAnketaEvent(
context.Background(),
agentID,
result.Id,
handlerName,
)
go func() {
if err := h.integrationClient.HandleVacancyResponse(integration.HandleVacancy{
AgentId: agentID,
VacancyId: vacancyID,
SourceLid: r.URL.RawPath,
Candidate: integration.Candidate{
Id: result.Id,
Name: name,
Email: email,
Phone: phone,
Birthday: birthday,
Info: info,
CvLink: cvLink,
},
}); err != nil {
h.logger.Error("error while handling vacancy response integration",
slog.String("error", err.Error()),
slog.String("agent_id", agentID),
slog.String("vacancy_id", vacancyID),
slog.String("submission_id", result.Id),
slog.String("handler", handlerName),
)
}
}()
w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(result); 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 splitName(name string) (string, string, string) {
var (
parts = strings.SplitN(name, " ", 3)
lastName, firstName, middleName string
)
if len(parts) > 0 {
lastName = parts[0]
}
if len(parts) > 1 {
firstName = parts[1]
}
if len(parts) > 2 {
middleName = parts[2]
}
return lastName, firstName, middleName
}

267
internal/http/vacancy.go Normal file
View File

@@ -0,0 +1,267 @@
package http_router
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
rmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model"
"github.com/gorilla/mux"
)
// TODO: review
func (h *handler) setVacancyLogoLinks(
ctx context.Context,
vacancyList *rmodel.VacancyListGetResponse,
) {
const handlerName = "setVacancyLogoLinks"
for _, vacancy := range vacancyList.Vacancies {
logoLink, err := h.getLogoLinkWithCache(ctx, vacancy.Company.Id)
if err != nil {
h.logger.Warn("error getting logo link",
slog.String("error", err.Error()),
slog.String("vacancy_id", vacancy.Id),
slog.String("handler", handlerName),
)
continue
}
vacancy.Company.LogoLink = &logoLink
}
}
func (h *handler) getVacancyListAgentHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getVacancyListAgentHandler"
request, err := new(rmodel.VacancyListGetRequest).FromQuery(r.URL.Query())
if err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error parsing request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
result, err := h.agentService.GetVacancyList(r.Context(), request)
if err != nil {
h.handleAgentError(w, err)
h.logger.Error("error getting vacancy list",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
h.setVacancyLogoLinks(r.Context(), result)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) getVacancyListDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "getVacancyListDistributorHandler"
request, err := new(rmodel.VacancyListGetRequest).FromQuery(r.URL.Query())
if err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error parsing request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
result, err := h.distributorService.GetVacancyList(r.Context(), request)
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error getting vacancy list",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) createVacancyDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "createVacancyDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
)
var request rmodel.VacancyCreateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error unmarshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName))
return
}
defer r.Body.Close()
result, err := h.distributorService.CreateVacancy(r.Context(), &request)
if err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error creating vacancy",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go h.createSetDistributorVacancyEvent(
context.Background(),
distId,
result.Id,
request.Name,
handlerName,
)
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); 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) updateVacancyDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "updateVacancyDistributorHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
vacancyId = vars["vacancy_id"]
)
var request rmodel.VacancyUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, constants.ErrBadRequest.Error(), http.StatusBadRequest)
h.logger.Error("error unmarshalling request: ",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
request.Id = vacancyId
if _, err := h.distributorService.UpdateVacancy(r.Context(), &request); err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error updating vacancy",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go h.createEditDistributorVacancyEvent(
context.Background(),
distId,
vacancyId,
handlerName,
)
w.WriteHeader(http.StatusNoContent)
}
func (h *handler) deleteVacancyDistributorHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "deleteDistributorVacancyHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
vacancyId = vars["vacancy_id"]
)
if _, err := h.distributorService.DeleteVacancy(r.Context(), &rmodel.VacancyDeleteRequest{
Id: vacancyId,
}); err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error deleting vacancy",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go func(hdn *handler) {
if err := h.feed.CancelEvent(r.Context(), vacancyId, "Вакансия была удалена"); err != nil {
h.logger.Error("failed to cancel feed event",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
slog.String("vacancy_id", vacancyId),
slog.String("distributor_id", distId))
}
}(h)
w.WriteHeader(http.StatusNoContent)
}
func (h *handler) sendVacancyToModerationHandler(w http.ResponseWriter, r *http.Request) {
const handlerName = "sendVacancyToModerationHandler"
var (
vars = mux.Vars(r)
distId = vars["distributor_id"]
vacancyId = vars["vacancy_id"]
)
if _, err := h.distributorService.SendVacancyToModeration(r.Context(), &rmodel.SendVacancyToModerationRequest{
Id: vacancyId,
}); err != nil {
h.handleDistributorError(w, err)
h.logger.Error("error sending vacancy to moderation",
slog.String("error", err.Error()),
slog.String("handler", handlerName),
)
return
}
go h.createSendVacancyToModerationEvent(
context.Background(),
distId,
vacancyId,
handlerName,
)
w.WriteHeader(http.StatusNoContent)
}