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
This commit is contained in:
@@ -51,6 +51,7 @@ func autoMigrate(db *gorm.DB) error {
|
||||
&models.Comment{},
|
||||
&models.Appeal{},
|
||||
&models.AppealHistory{},
|
||||
&models.PasswordReset{},
|
||||
}
|
||||
|
||||
for _, model := range models {
|
||||
|
||||
@@ -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"`
|
||||
@@ -66,4 +72,6 @@ var (
|
||||
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")
|
||||
)
|
||||
@@ -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 {
|
||||
@@ -203,7 +166,6 @@ func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
@@ -214,14 +176,13 @@ func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Генерируем новый 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)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"message": "Profile endpoint - to be implemented",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfile обновление профиля пользователя
|
||||
func (h *AuthHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
l := logger.Get()
|
||||
l.Debug("Обновление профиля пользователя")
|
||||
|
||||
// Получаем ID пользователя из контекста
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
if !ok {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
h.handleValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Реализовать обновление профиля
|
||||
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]interface{}{
|
||||
"user_id": userID,
|
||||
"message": "Update profile endpoint - to be implemented",
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "If the email exists, a reset link has been sent",
|
||||
})
|
||||
}
|
||||
|
||||
// ChangePassword смена пароля
|
||||
func (h *AuthHandler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
l := logger.Get()
|
||||
l.Debug("Смена пароля пользователя")
|
||||
|
||||
// Получаем ID пользователя из контекста
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
if !ok {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Реализовать смену пароля
|
||||
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": "Change password endpoint - to be implemented",
|
||||
"message": "Password reset link has been sent to your email",
|
||||
"token": resetToken, // Только для тестирования, в production не возвращать!
|
||||
})
|
||||
}
|
||||
|
||||
// ConfirmPasswordReset подтверждение сброса пароля
|
||||
func (h *AuthHandler) ConfirmPasswordReset(w http.ResponseWriter, r *http.Request) {
|
||||
l := logger.Get()
|
||||
l.Info("Начало обработки подтверждения сброса пароля")
|
||||
|
||||
var req ResetPasswordConfirmRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
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]string{
|
||||
"message": "Password has been successfully reset",
|
||||
})
|
||||
}
|
||||
|
||||
// MobileLogin вход для мобильных приложений
|
||||
func (h *AuthHandler) MobileLogin(w http.ResponseWriter, r *http.Request) {
|
||||
l := logger.Get()
|
||||
l.Debug("Начало обработки запроса входа (мобильный)")
|
||||
|
||||
var req LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
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{}{
|
||||
"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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -86,3 +86,23 @@ func (r *accountRepositoryImpl) GetObjects(accountID uint) ([]models.Object, 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
|
||||
}
|
||||
Reference in New Issue
Block a user