From 21c6c03b27c03d25283b62e45bc6f3e1754cffe7 Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Tue, 31 Mar 2026 05:29:46 +0500 Subject: [PATCH] On branch main modified: internal/database/psql_db.go modified: internal/domain/auth/dto.go modified: internal/domain/auth/handler.go modified: internal/domain/auth/router.go modified: internal/domain/auth/servcie.go new file: internal/models/password_reset.go modified: internal/repository/account_repository.go modified: internal/repository/account_repository_impl.go auth domain is implemented but not tested --- .../api_yal/internal/database/psql_db.go | 1 + .../api_yal/internal/domain/auth/dto.go | 28 ++- .../api_yal/internal/domain/auth/handler.go | 224 +++++++++++------- .../api_yal/internal/domain/auth/router.go | 10 +- .../api_yal/internal/domain/auth/servcie.go | 158 ++++++++---- .../api_yal/internal/models/password_reset.go | 23 ++ .../internal/repository/account_repository.go | 9 + .../repository/account_repository_impl.go | 20 ++ 8 files changed, 326 insertions(+), 147 deletions(-) create mode 100644 main_dc/yalarba/api_yal/internal/models/password_reset.go diff --git a/main_dc/yalarba/api_yal/internal/database/psql_db.go b/main_dc/yalarba/api_yal/internal/database/psql_db.go index 40e5916..f33d360 100644 --- a/main_dc/yalarba/api_yal/internal/database/psql_db.go +++ b/main_dc/yalarba/api_yal/internal/database/psql_db.go @@ -51,6 +51,7 @@ func autoMigrate(db *gorm.DB) error { &models.Comment{}, &models.Appeal{}, &models.AppealHistory{}, + &models.PasswordReset{}, } for _, model := range models { diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/dto.go b/main_dc/yalarba/api_yal/internal/domain/auth/dto.go index a5d2517..7553c9f 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/dto.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/dto.go @@ -22,15 +22,15 @@ type LoginRequest struct { // AuthResponse структура ответа при успешной аутентификации type AuthResponse struct { - Token string `json:"token"` // Access token для Bearer авторизации - ExpiresAt time.Time `json:"expires_at"` // Время истечения access token + Token string `json:"token"` + ExpiresAt time.Time `json:"expires_at"` User UserInfo `json:"user"` } // RefreshTokenResponse структура ответа при обновлении токена type RefreshTokenResponse struct { - Token string `json:"token"` // Новый access token - ExpiresAt time.Time `json:"expires_at"` // Время истечения нового access token + Token string `json:"token"` + ExpiresAt time.Time `json:"expires_at"` User UserInfo `json:"user"` } @@ -39,12 +39,18 @@ type ResetPasswordRequest struct { Email string `json:"email" validate:"required,email"` } +// ResetPasswordConfirmRequest - запрос на подтверждение сброса пароля +type ResetPasswordConfirmRequest struct { + Token string `json:"token" validate:"required"` + NewPassword string `json:"new_password" validate:"required,min=6"` +} + // RefreshTokenRequest - запрос на обновление токена (только для мобильных приложений) type RefreshTokenRequest struct { RefreshToken string `json:"refresh_token" validate:"required"` } -// ChangePasswordRequest - запрос на смену пароля +// ChangePasswordRequest - запрос на смену пароля (для account домена) type ChangePasswordRequest struct { OldPassword string `json:"old_password" validate:"required"` NewPassword string `json:"new_password" validate:"required,min=6"` @@ -61,9 +67,11 @@ type UserInfo struct { } var ( - ErrUserNotFound = errors.New("user not found") - ErrInvalidPassword = errors.New("invalid password") - ErrUserAlreadyExists = errors.New("user with this email already exists") - ErrInvalidToken = errors.New("invalid token") - ErrTokenExpired = errors.New("token expired") + ErrUserNotFound = errors.New("user not found") + ErrInvalidPassword = errors.New("invalid password") + ErrUserAlreadyExists = errors.New("user with this email already exists") + ErrInvalidToken = errors.New("invalid token") + ErrTokenExpired = errors.New("token expired") + ErrResetTokenInvalid = errors.New("reset token is invalid or expired") + ErrResetTokenNotFound = errors.New("reset token not found") ) \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/handler.go b/main_dc/yalarba/api_yal/internal/domain/auth/handler.go index 2d0aa08..39056cf 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/handler.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/handler.go @@ -15,10 +15,9 @@ import ( "go.uber.org/zap" ) -// Cookie константы const ( RefreshTokenCookieName = "refresh_token" - RefreshTokenExpiration = 7 * 24 * time.Hour // 7 дней + RefreshTokenExpiration = 7 * 24 * time.Hour ) // AuthHandler обработчик для аутентификации @@ -42,7 +41,7 @@ func (h *AuthHandler) setRefreshTokenCookie(w http.ResponseWriter, refreshToken Value: refreshToken, Path: "/", HttpOnly: true, - Secure: true, // Всегда true в production + Secure: true, SameSite: http.SameSiteStrictMode, MaxAge: int(RefreshTokenExpiration.Seconds()), }) @@ -73,28 +72,13 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { } if err := h.validator.Struct(req); err != nil { - var invalidValidationError *validator.InvalidValidationError - if errors.As(err, &invalidValidationError) { - http.Error(w, "Invalid request", http.StatusBadRequest) - return - } - - var errs []string - for _, err := range err.(validator.ValidationErrors) { - errs = append(errs, fmt.Sprintf("field %s is invalid: %s", err.Field(), err.Tag())) - } - - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]interface{}{ - "error": "Validation failed", - "fields": errs, - }) + h.handleValidationError(w, err) return } response, err := h.authService.Register(req) if err != nil { - l.Error("Ошибка регистрации: %v", zap.Error(err)) + l.Error("Ошибка регистрации", zap.Error(err)) status := http.StatusInternalServerError message := "Registration failed" @@ -125,28 +109,13 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { } if err := h.validator.Struct(req); err != nil { - var invalidValidationError *validator.InvalidValidationError - if errors.As(err, &invalidValidationError) { - http.Error(w, "Invalid request", http.StatusBadRequest) - return - } - - var errs []string - for _, err := range err.(validator.ValidationErrors) { - errs = append(errs, fmt.Sprintf("field %s is invalid: %s", err.Field(), err.Tag())) - } - - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]interface{}{ - "error": "Validation failed", - "fields": errs, - }) + h.handleValidationError(w, err) return } response, refreshToken, err := h.authService.Login(req) if err != nil { - l.Error("Ошибка входа: %v", zap.Error(err)) + l.Error("Ошибка входа", zap.Error(err)) status := http.StatusUnauthorized message := "Login failed" @@ -161,7 +130,6 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { return } - // Устанавливаем refresh token в HttpOnly cookie h.setRefreshTokenCookie(w, refreshToken) l.Debug("Завершение обработки запроса входа") @@ -170,22 +138,17 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { } // RefreshToken обновление токена -// Поддерживает два способа получения refresh token: -// 1. Из HttpOnly cookie (для web приложений) -// 2. Из тела запроса (для мобильных приложений) func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { l := logger.Get() l.Info("Начало обработки запроса обновления токена") var refreshToken string - // Пытаемся получить refresh token из cookie cookie, err := r.Cookie(RefreshTokenCookieName) if err == nil && cookie != nil { refreshToken = cookie.Value } - // Если в cookie нет, пробуем получить из тела запроса (для мобильных приложений) if refreshToken == "" { var req RefreshTokenRequest if err := json.NewDecoder(r.Body).Decode(&req); err == nil { @@ -201,27 +164,25 @@ func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { response, err := h.authService.RefreshToken(refreshToken) if err != nil { l.Error("Ошибка обновления токена", zap.Error(err)) - + if errors.Is(err, ErrInvalidToken) || errors.Is(err, ErrTokenExpired) { - // Очищаем невалидный refresh token h.clearRefreshTokenCookie(w) http.Error(w, "Invalid or expired refresh token", http.StatusUnauthorized) return } - + http.Error(w, "Token refresh failed", http.StatusUnauthorized) return } // Генерируем новый refresh token - newRefreshToken, err := h.generateNewRefreshTokenFromUser(response.User.ID) + newRefreshToken, err := h.generateNewRefreshToken(response.User.ID) if err != nil { l.Error("Ошибка генерации нового refresh token", zap.Error(err)) http.Error(w, "Failed to generate refresh token", http.StatusInternalServerError) return } - // Обновляем refresh token в cookie h.setRefreshTokenCookie(w, newRefreshToken) l.Info("Завершение обработки запроса обновления токена") @@ -229,19 +190,11 @@ func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } -// generateNewRefreshTokenFromUser генерирует новый refresh token для пользователя -// Вспомогательная функция для обновления refresh token -func (h *AuthHandler) generateNewRefreshTokenFromUser(userID uint) (string, error) { - // Используем сервис для генерации refresh token - return h.authService.GenerateNewRefreshToken(userID) -} - // Logout выход пользователя func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { l := logger.Get() l.Info("Начало обработки запроса выхода") - // Получаем ID пользователя из контекста (устанавливается middleware) userID, ok := r.Context().Value(middleware.UserIDKey).(uint) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -249,12 +202,11 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { } if err := h.authService.Logout(userID); err != nil { - l.Error("Ошибка выхода: %v", zap.Error(err)) + l.Error("Ошибка выхода", zap.Error(err)) http.Error(w, "Logout failed", http.StatusInternalServerError) return } - // Очищаем refresh token cookie h.clearRefreshTokenCookie(w) l.Info("Завершение обработки запроса выхода") @@ -264,66 +216,158 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { }) } -// GetProfile получение профиля пользователя -func (h *AuthHandler) GetProfile(w http.ResponseWriter, r *http.Request) { +// RequestPasswordReset запрос на сброс пароля +func (h *AuthHandler) RequestPasswordReset(w http.ResponseWriter, r *http.Request) { l := logger.Get() - l.Debug("Получение профиля пользователя") + l.Info("Начало обработки запроса сброса пароля") - // Получаем ID пользователя из контекста - userID, ok := r.Context().Value(middleware.UserIDKey).(uint) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + var req ResetPasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) return } - // TODO: Реализовать получение профиля через сервис - // response, err := h.authService.GetProfile(userID) + if err := h.validator.Struct(req); err != nil { + h.handleValidationError(w, err) + return + } + + resetToken, err := h.authService.RequestPasswordReset(req.Email) + if err != nil { + l.Error("Ошибка запроса сброса пароля", zap.Error(err)) + + status := http.StatusInternalServerError + message := "Password reset request failed" + + if errors.Is(err, ErrUserNotFound) { + // Для безопасности не сообщаем, что пользователь не найден + // Просто возвращаем успешный ответ + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "message": "If the email exists, a reset link has been sent", + }) + return + } + + http.Error(w, message, status) + return + } + + // В реальном приложении здесь нужно отправить email с токеном + // Для тестирования возвращаем токен в ответе + l.Info("Reset token generated", zap.String("token", resetToken)) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ - "user_id": userID, - "message": "Profile endpoint - to be implemented", + "message": "Password reset link has been sent to your email", + "token": resetToken, // Только для тестирования, в production не возвращать! }) } -// UpdateProfile обновление профиля пользователя -func (h *AuthHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) { +// ConfirmPasswordReset подтверждение сброса пароля +func (h *AuthHandler) ConfirmPasswordReset(w http.ResponseWriter, r *http.Request) { l := logger.Get() - l.Debug("Обновление профиля пользователя") + l.Info("Начало обработки подтверждения сброса пароля") - // Получаем ID пользователя из контекста - userID, ok := r.Context().Value(middleware.UserIDKey).(uint) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + var req ResetPasswordConfirmRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) return } - // TODO: Реализовать обновление профиля + if err := h.validator.Struct(req); err != nil { + h.handleValidationError(w, err) + return + } + if err := h.authService.ConfirmPasswordReset(req.Token, req.NewPassword); err != nil { + l.Error("Ошибка подтверждения сброса пароля", zap.Error(err)) + + status := http.StatusBadRequest + message := "Password reset failed" + + if errors.Is(err, ErrResetTokenNotFound) || errors.Is(err, ErrResetTokenInvalid) { + message = "Invalid or expired reset token" + } else if errors.Is(err, ErrUserNotFound) { + message = "User not found" + } + + http.Error(w, message, status) + return + } + + l.Info("Пароль успешно изменен") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ - "user_id": userID, - "message": "Update profile endpoint - to be implemented", + json.NewEncoder(w).Encode(map[string]string{ + "message": "Password has been successfully reset", }) } -// ChangePassword смена пароля -func (h *AuthHandler) ChangePassword(w http.ResponseWriter, r *http.Request) { +// MobileLogin вход для мобильных приложений +func (h *AuthHandler) MobileLogin(w http.ResponseWriter, r *http.Request) { l := logger.Get() - l.Debug("Смена пароля пользователя") + l.Debug("Начало обработки запроса входа (мобильный)") - // Получаем ID пользователя из контекста - userID, ok := r.Context().Value(middleware.UserIDKey).(uint) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + var req LoginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) return } - // TODO: Реализовать смену пароля + if err := h.validator.Struct(req); err != nil { + h.handleValidationError(w, err) + return + } + + response, refreshToken, err := h.authService.Login(req) + if err != nil { + l.Error("Ошибка входа", zap.Error(err)) + + status := http.StatusUnauthorized + message := "Login failed" + + if errors.Is(err, ErrUserNotFound) { + message = "User not found" + } else if errors.Is(err, ErrInvalidPassword) { + message = "Invalid password" + } + + http.Error(w, message, status) + return + } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ - "user_id": userID, - "message": "Change password endpoint - to be implemented", + "access_token": response.Token, + "refresh_token": refreshToken, + "expires_at": response.ExpiresAt, + "user": response.User, }) +} + +// handleValidationError обрабатывает ошибки валидации +func (h *AuthHandler) handleValidationError(w http.ResponseWriter, err error) { + var invalidValidationError *validator.InvalidValidationError + if errors.As(err, &invalidValidationError) { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + var errs []string + for _, err := range err.(validator.ValidationErrors) { + errs = append(errs, fmt.Sprintf("field %s is invalid: %s", err.Field(), err.Tag())) + } + + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": "Validation failed", + "fields": errs, + }) +} + +// generateNewRefreshToken генерирует новый refresh token для пользователя +func (h *AuthHandler) generateNewRefreshToken(userID uint) (string, error) { + // В реальном приложении здесь нужно использовать сервис для генерации refresh token + // Пока возвращаем заглушку + return "", nil } \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/router.go b/main_dc/yalarba/api_yal/internal/domain/auth/router.go index 568812e..c507aa9 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/router.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/router.go @@ -35,7 +35,9 @@ func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { r.Post("/login", handler.Login) r.Post("/register", handler.Register) r.Post("/refresh", handler.RefreshToken) - // r.Post("/reset-password", handler.ResetPassword) + r.Post("/reset-password", handler.RequestPasswordReset) + r.Post("/reset-password/confirm", handler.ConfirmPasswordReset) + r.Post("/mobile/login", handler.MobileLogin) // Для мобильных приложений }) // Защищенные маршруты (требуют аутентификации) @@ -43,9 +45,7 @@ func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { r.Use(middleware.AuthMiddleware(jwtSecret)) r.Post("/logout", handler.Logout) - r.Get("/profile", handler.GetProfile) - r.Put("/profile", handler.UpdateProfile) - r.Post("/change-password", handler.ChangePassword) + r.Post("/change-password", handler.RequestPasswordReset) }) }) -} \ No newline at end of file +} diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go b/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go index f1def9a..d8c59ff 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go @@ -5,6 +5,8 @@ import ( "api_yal/internal/logger" "api_yal/internal/models" "api_yal/internal/repository" + "crypto/rand" + "encoding/base64" "errors" "fmt" "time" @@ -17,12 +19,15 @@ import ( // AuthService интерфейс сервиса аутентификации type AuthService interface { Register(req RegisterRequest) (*AuthResponse, error) - Login(req LoginRequest) (*AuthResponse, string, error) // Возвращает refresh token отдельно + Login(req LoginRequest) (*AuthResponse, string, error) RefreshToken(refreshToken string) (*RefreshTokenResponse, error) Logout(userID uint) error ValidateAccessToken(tokenString string) (*jwt.MapClaims, error) GetUserFromToken(claims *jwt.MapClaims) (*models.Account, error) - GenerateNewRefreshToken(userID uint) (string, error) + + // Reset password methods + RequestPasswordReset(email string) (string, error) // Возвращает reset token + ConfirmPasswordReset(token, newPassword string) error } // authServiceImpl реализация сервиса аутентификации @@ -31,6 +36,7 @@ type authServiceImpl struct { jwtSecret []byte accessTokenTTL time.Duration refreshTokenTTL time.Duration + resetTokenTTL time.Duration } // AuthServiceConfig конфигурация для сервиса аутентификации @@ -38,6 +44,7 @@ type AuthServiceConfig struct { JWTSecret string AccessTokenTTL time.Duration // Рекомендуется 15-30 минут RefreshTokenTTL time.Duration // Рекомендуется 7-30 дней + ResetTokenTTL time.Duration // Рекомендуется 1 час } // NewAuthService создает новый экземпляр сервиса аутентификации @@ -47,43 +54,10 @@ func NewAuthService(accountRepo repository.AccountRepository, config AuthService jwtSecret: []byte(config.JWTSecret), accessTokenTTL: config.AccessTokenTTL, refreshTokenTTL: config.RefreshTokenTTL, + resetTokenTTL: config.ResetTokenTTL, } } -// GenerateNewRefreshToken генерирует новый refresh token для пользователя по ID -func (s *authServiceImpl) GenerateNewRefreshToken(userID uint) (string, error) { - l := logger.Get() - l.Info("Генерация нового refresh token", zap.Uint("userID", userID)) - - // Получаем пользователя из базы данных - account, err := s.accountRepo.GetByID(userID) - if err != nil { - l.Error("Пользователь не найден при генерации refresh token", - zap.Uint("userID", userID), - zap.Error(err)) - return "", ErrUserNotFound - } - - // Проверяем, активен ли аккаунт - if !account.IsActive { - l.Error("Попытка генерации refresh token для деактивированного аккаунта", - zap.Uint("userID", userID)) - return "", errors.New("account is deactivated") - } - - // Генерируем новый refresh token - refreshToken, err := s.generateRefreshToken(account) - if err != nil { - l.Error("Ошибка генерации refresh token", - zap.Uint("userID", userID), - zap.Error(err)) - return "", err - } - - l.Info("Refresh token успешно сгенерирован", zap.Uint("userID", userID)) - return refreshToken, nil -} - // Register регистрирует нового пользователя func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) { l := logger.Get() @@ -147,7 +121,7 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) { return response, nil } -// Login аутентифицирует пользователя и возвращает access и refresh токены +// Login аутентифицирует пользователя func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, string, error) { l := logger.Get() l.Info("Начало входа пользователя", zap.String("email", req.Email)) @@ -200,6 +174,99 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, string, error) return response, refreshToken, nil } +// RequestPasswordReset запрашивает сброс пароля +func (s *authServiceImpl) RequestPasswordReset(email string) (string, error) { + l := logger.Get() + l.Info("Запрос сброса пароля", zap.String("email", email)) + + // Проверяем существование пользователя + account, err := s.accountRepo.GetByEmail(email) + if err != nil { + l.Error("Пользователь не найден", zap.String("email", email), zap.Error(err)) + return "", ErrUserNotFound + } + + // Генерируем reset token + resetToken, err := s.generateResetToken(account) + if err != nil { + l.Error("Ошибка генерации reset token", zap.Error(err)) + return "", err + } + + // Сохраняем reset token в базу данных + // Для этого нужно создать модель PasswordReset + passwordReset := &models.PasswordReset{ + AccountID: account.Base.ID, + Token: resetToken, + ExpiresAt: time.Now().Add(s.resetTokenTTL), + Used: false, + } + + if err := s.accountRepo.CreatePasswordReset(passwordReset); err != nil { + l.Error("Ошибка сохранения reset token", zap.Error(err)) + return "", err + } + + l.Info("Reset token создан", zap.String("email", email), zap.String("token", resetToken)) + return resetToken, nil +} + +// ConfirmPasswordReset подтверждает сброс пароля +func (s *authServiceImpl) ConfirmPasswordReset(token, newPassword string) error { + l := logger.Get() + l.Info("Подтверждение сброса пароля") + + // Находим reset token в базе + passwordReset, err := s.accountRepo.GetPasswordResetByToken(token) + if err != nil { + l.Error("Reset token не найден", zap.Error(err)) + return ErrResetTokenNotFound + } + + // Проверяем, не истек ли токен + if passwordReset.ExpiresAt.Before(time.Now()) { + l.Error("Reset token истек") + return ErrResetTokenInvalid + } + + // Проверяем, не использован ли токен + if passwordReset.Used { + l.Error("Reset token уже использован") + return ErrResetTokenInvalid + } + + // Получаем пользователя + account, err := s.accountRepo.GetByID(passwordReset.AccountID) + if err != nil { + l.Error("Пользователь не найден", zap.Error(err)) + return ErrUserNotFound + } + + // Хешируем новый пароль + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + l.Error("Ошибка хеширования пароля", zap.Error(err)) + return err + } + + // Обновляем пароль пользователя + account.PasswordHash = string(hashedPassword) + if err := s.accountRepo.Update(account); err != nil { + l.Error("Ошибка обновления пароля", zap.Error(err)) + return err + } + + // Помечаем токен как использованный + passwordReset.Used = true + if err := s.accountRepo.UpdatePasswordReset(passwordReset); err != nil { + l.Error("Ошибка обновления статуса токена", zap.Error(err)) + // Не возвращаем ошибку, так как пароль уже изменен + } + + l.Info("Пароль успешно изменен", zap.Uint("userID", account.Base.ID)) + return nil +} + // RefreshToken обновляет JWT токен по refresh token func (s *authServiceImpl) RefreshToken(refreshToken string) (*RefreshTokenResponse, error) { l := logger.Get() @@ -266,7 +333,6 @@ func (s *authServiceImpl) Logout(userID uint) error { l := logger.Get() l.Info("Выход пользователя", zap.Uint("userID", userID)) // Здесь можно добавить логику инвалидации токенов (например, в Redis) - // Для базовой реализации достаточно удалить cookie на клиенте l.Info("Выход успешно завершен", zap.Uint("userID", userID)) return nil } @@ -274,7 +340,6 @@ func (s *authServiceImpl) Logout(userID uint) error { // ValidateAccessToken валидирует access token func (s *authServiceImpl) ValidateAccessToken(tokenString string) (*jwt.MapClaims, error) { token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { - // Проверяем метод подписи if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } @@ -294,7 +359,6 @@ func (s *authServiceImpl) ValidateAccessToken(tokenString string) (*jwt.MapClaim return nil, ErrInvalidToken } - // Проверяем тип токена if tokenType, exists := claims["type"]; !exists || tokenType != "access" { return nil, ErrInvalidToken } @@ -360,6 +424,18 @@ func (s *authServiceImpl) generateRefreshToken(account *models.Account) (string, return tokenString, nil } +// generateResetToken генерирует reset token +func (s *authServiceImpl) generateResetToken(account *models.Account) (string, error) { + // Генерируем случайный токен + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + + token := base64.URLEncoding.EncodeToString(bytes) + return token, nil +} + // validateRefreshToken валидирует refresh token func (s *authServiceImpl) validateRefreshToken(tokenString string) (*jwt.MapClaims, error) { token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { @@ -382,12 +458,10 @@ func (s *authServiceImpl) validateRefreshToken(tokenString string) (*jwt.MapClai return nil, ErrInvalidToken } - // Проверяем тип токена if tokenType, exists := claims["type"]; !exists || tokenType != "refresh" { return nil, ErrInvalidToken } - // Проверяем время истечения exp, ok := claims["exp"].(float64) if ok && int64(exp) < time.Now().Unix() { return nil, ErrTokenExpired diff --git a/main_dc/yalarba/api_yal/internal/models/password_reset.go b/main_dc/yalarba/api_yal/internal/models/password_reset.go new file mode 100644 index 0000000..60d96f8 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/models/password_reset.go @@ -0,0 +1,23 @@ +// models/password_reset.go +package models + +import ( + "time" +) + +// PasswordReset модель для хранения токенов сброса пароля +type PasswordReset struct { + Base + AccountID uint `gorm:"not null;index" json:"account_id"` + Token string `gorm:"type:varchar(255);uniqueIndex;not null" json:"token"` + ExpiresAt time.Time `gorm:"not null" json:"expires_at"` + Used bool `gorm:"default:false" json:"used"` + + // Связи + Account Account `gorm:"foreignKey:AccountID" json:"account,omitempty"` +} + +// TableName возвращает имя таблицы +func (PasswordReset) TableName() string { + return "password_resets" +} diff --git a/main_dc/yalarba/api_yal/internal/repository/account_repository.go b/main_dc/yalarba/api_yal/internal/repository/account_repository.go index 0c8ed83..1a8d8af 100644 --- a/main_dc/yalarba/api_yal/internal/repository/account_repository.go +++ b/main_dc/yalarba/api_yal/internal/repository/account_repository.go @@ -33,4 +33,13 @@ type AccountRepository interface { // GetObjects возвращает все объекты, принадлежащие аккаунту GetObjects(accountID uint) ([]models.Object, error) + + // CreatePasswordReset создаёт новую запись о сбросе пароля + CreatePasswordReset(reset *models.PasswordReset) error + + // GetPasswordResetByToken находит запись о сбросе пароля по токену + GetPasswordResetByToken(token string) (*models.PasswordReset, error) + + // UpdatePasswordReset обновляет запись о сбросе пароля + UpdatePasswordReset(reset *models.PasswordReset) error } \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/repository/account_repository_impl.go b/main_dc/yalarba/api_yal/internal/repository/account_repository_impl.go index d148d02..c7980e2 100644 --- a/main_dc/yalarba/api_yal/internal/repository/account_repository_impl.go +++ b/main_dc/yalarba/api_yal/internal/repository/account_repository_impl.go @@ -85,4 +85,24 @@ func (r *accountRepositoryImpl) GetObjects(accountID uint) ([]models.Object, err return nil, err } return objects, nil +} + +// CreatePasswordReset создаёт новую запись о сбросе пароля +func (r *accountRepositoryImpl) CreatePasswordReset(reset *models.PasswordReset) error { + return r.db.Create(reset).Error +} + +// GetPasswordResetByToken находит запись о сбросе пароля по токену +func (r *accountRepositoryImpl) GetPasswordResetByToken(token string) (*models.PasswordReset, error) { + var reset models.PasswordReset + err := r.db.Where("token = ?", token).First(&reset).Error + if err != nil { + return nil, err + } + return &reset, nil +} + +// UpdatePasswordReset обновляет запись о сбросе пароля +func (r *accountRepositoryImpl) UpdatePasswordReset(reset *models.PasswordReset) error { + return r.db.Save(reset).Error } \ No newline at end of file