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:
2026-03-31 05:29:46 +05:00
parent 8b40d1bfe5
commit 21c6c03b27
8 changed files with 326 additions and 147 deletions
@@ -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
}