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