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