modified: main_dc/yalarba/api_yal/go.mod

modified:   main_dc/yalarba/api_yal/go.sum
	modified:   main_dc/yalarba/api_yal/internal/domain/auth/dto.go
	modified:   main_dc/yalarba/api_yal/internal/domain/auth/handler.go
	modified:   main_dc/yalarba/api_yal/internal/domain/auth/router.go
	modified:   main_dc/yalarba/api_yal/internal/domain/auth/servcie.go
	new file:   main_dc/yalarba/api_yal/internal/middleware/auth.go
	deleted:    main_dc/yalarba/api_yal/internal/middleware/authMiddleware.go
	modified:   main_dc/yalarba/api_yal/internal/router/router.go
set auth domain, not tested
This commit is contained in:
2026-03-10 00:35:25 +05:00
parent 5561a9ee8c
commit d45c5841dc
9 changed files with 546 additions and 60 deletions
+1
View File
@@ -24,6 +24,7 @@ require (
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2
github.com/go-playground/validator/v10 v10.30.1
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1
+2
View File
@@ -13,6 +13,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -1,6 +1,9 @@
package auth
import "time"
import (
"errors"
"time"
)
// RegisterRequest - запрос на регистрацию
type RegisterRequest struct {
@@ -24,6 +27,22 @@ type AuthResponse struct {
User UserInfo `json:"user"`
}
// ResetPasswordRequest - запрос на сброс пароля
type ResetPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
}
// RefreshTokenRequest - запрос на обновление токена
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
// ChangePasswordRequest - запрос на смену пароля
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" validate:"required"`
NewPassword string `json:"new_password" validate:"required,min=6"`
}
// UserInfo информация о пользователе для ответа
type UserInfo struct {
ID uint `json:"id"`
@@ -33,3 +52,9 @@ type UserInfo struct {
FullName string `json:"full_name"`
Role string `json:"role"`
}
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidPassword = errors.New("invalid password")
ErrUserAlreadyExists = errors.New("user with this email already exists")
)
@@ -5,10 +5,13 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"api_yal/internal/logger"
"api_yal/internal/middleware"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
)
// AuthHandler обработчик для аутентификации
@@ -20,7 +23,7 @@ type AuthHandler struct {
// NewAuthHandler создает новый экземпляр AuthHandler
func NewAuthHandler(authService *AuthService) *AuthHandler {
return &AuthHandler{
authService: *NewAuthService(),
authService: *authService,
validator: validator.New(),
}
}
@@ -29,6 +32,7 @@ func NewAuthHandler(authService *AuthService) *AuthHandler {
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
l := logger.Get()
l.Debug("Регистрация нового пользователя AuthHandler")
var req RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
@@ -55,5 +59,192 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
return
}
AuthService.Register(req)
response, err := h.authService.Register(req)
if err != nil {
l.Error("Ошибка регистрации: %v", zap.Error(err))
status := http.StatusInternalServerError
message := "Registration failed"
if errors.Is(err, ErrUserAlreadyExists) {
status = http.StatusConflict
message = "User with this email already exists"
}
http.Error(w, message, status)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(response)
}
// Login вход пользователя
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
l := logger.Get()
l.Debug("Вход пользователя AuthHandler")
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 {
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,
})
return
}
response, err := h.authService.Login(req)
if err != nil {
l.Error("Ошибка входа: %v", 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(response)
}
// RefreshToken обновление токена
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
l := logger.Get()
l.Debug("Обновление токена AuthHandler")
// Получаем токен из заголовка Authorization
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// Ожидаем формат "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
response, err := h.authService.RefreshToken(parts[1])
if err != nil {
l.Error("Ошибка обновления токена: %v", zap.Error(err))
http.Error(w, "Token refresh failed", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
// Logout выход пользователя
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
l := logger.Get()
l.Debug("Выход пользователя AuthHandler")
// Получаем ID пользователя из контекста (устанавливается middleware)
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if err := h.authService.Logout(userID); err != nil {
l.Error("Ошибка выхода: %v", zap.Error(err))
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"message": "Successfully logged out",
})
}
// GetProfile получение профиля пользователя
func (h *AuthHandler) GetProfile(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: Реализовать получение профиля через сервис
// 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)
return
}
// TODO: Реализовать обновление профиля
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"message": "Update profile endpoint - to be implemented",
})
}
// 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: Реализовать смену пароля
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"message": "Change password endpoint - to be implemented",
})
}
@@ -3,33 +3,39 @@ package auth
import (
"api_yal/internal/logger"
"api_yal/internal/middleware"
"api_yal/internal/repository"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
)
// RegisterRoutes регистрирует маршруты аутентификации
func RegisterRoutes(r chi.Router) {
handler := NewAuthHandler(NewAuthService())
func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) {
// Создаем репозиторий и сервис
accountRepo := repository.NewAccountRepository(db)
authService := NewAuthService(accountRepo, jwtSecret)
handler := NewAuthHandler(&authService)
l := logger.Get()
l.Debug("Регистрация маршрутов аутентификации")
r.Route("/auth", func(r chi.Router) {
// Публичные маршруты (без аутентификации)
r.Group(func(r chi.Router) {
// r.Post("/login", handler.Login)
r.Post("/login", handler.Login)
r.Post("/register", handler.Register)
// r.Post("/refresh", handler.RefreshToken)
r.Post("/refresh", handler.RefreshToken)
// r.Post("/reset-password", handler.ResetPassword)
})
// Защищенные маршруты (требуют аутентификации)
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddlewareWithContext) // middleware специфичный для auth
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("/logout", handler.Logout)
r.Get("/profile", handler.GetProfile)
r.Put("/profile", handler.UpdateProfile)
r.Post("/change-password", handler.ChangePassword)
})
})
}
}
@@ -1,34 +1,229 @@
package auth
import (
"errors"
"api_yal/internal/logger"
"api_yal/internal/models"
"api_yal/internal/repository"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)
type AuthService struct {
// AuthService интерфейс сервиса аутентификации
type AuthService interface {
Register(req RegisterRequest) (*AuthResponse, error)
Login(req LoginRequest) (*AuthResponse, error)
RefreshToken(token string) (*AuthResponse, error)
Logout(userID uint) error
}
func NewAuthService() *AuthService {
return &AuthService{
// authServiceImpl реализация сервиса аутентификации
type authServiceImpl struct {
accountRepo repository.AccountRepository
jwtSecret []byte
}
// NewAuthService создает новый экземпляр сервиса аутентификации
func NewAuthService(accountRepo repository.AccountRepository, jwtSecret string) AuthService {
return &authServiceImpl{
accountRepo: accountRepo,
jwtSecret: []byte(jwtSecret),
}
}
func (s *AuthService) Register(regReq RegisterRequest) (AuthResponse, error) {
// Register регистрация нового пользователя
func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
l := logger.Get()
l.Debug("Регистрация пользователя AuthSerice")
l.Debug("Регистрация пользователя AuthService")
// Проверяем, существует ли пользователь с таким email
existingUser, err := s.accountRepo.GetByEmail(req.Email)
if err == nil && existingUser != nil {
return nil, ErrUserAlreadyExists
}
// Хешируем пароль
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(regReq.Password), bcrypt.DefaultCost)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
l.Error("Ошибка хеширования пароля: %v", zap.Error(err))
return nil, err
}
// Формируем полное имя
fullName := req.FirstName + " " + req.LastName
// Создаем аккаунт
newAcc := &models.Account{
Email: req.Email,
PasswordHash: string(hashedPassword),
FirstName: req.FirstName,
LastName: req.LastName,
FullName: fullName,
IsActive: true,
IsVerified: false,
Role: "user",
}
// Сохраняем в базу данных
if err := s.accountRepo.Create(newAcc); err != nil {
l.Error("Ошибка создания аккаунта: %v", zap.Error(err))
return nil, err
}
// Генерируем JWT токен
token, expiresAt, err := s.generateToken(newAcc)
if err != nil {
return nil, err
}
newAcc := &models.Account{
Email: regReq.Email,
// Формируем ответ
response := &AuthResponse{
Token: token,
ExpiresAt: expiresAt,
User: UserInfo{
ID: newAcc.Base.ID,
Email: newAcc.Email,
FirstName: newAcc.FirstName,
LastName: newAcc.LastName,
FullName: newAcc.FullName,
Role: newAcc.Role,
},
}
l.Info("Пользователь успешно зарегистрирован: %s", zap.String("Email", req.Email))
return response, nil
}
// Login вход пользователя
func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) {
l := logger.Get()
l.Debug("Вход пользователя: %s", zap.String("Email", req.Email))
// Ищем пользователя по email
account, err := s.accountRepo.GetByEmail(req.Email)
if err != nil {
l.Error("Пользователь не найден:",
zap.String("Email", req.Email),
zap.Error(err),
)
return nil, ErrUserNotFound
}
// Проверяем, активен ли аккаунт
if !account.IsActive {
l.Error("Аккаунт деактивирован: %s", zap.String("Email", req.Email))
return nil, errors.New("account is deactivated")
}
// Сравниваем пароли
if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(req.Password)); err != nil {
l.Error("Неверный пароль для пользователя: %s", zap.String("Email", req.Email))
return nil, ErrInvalidPassword
}
// Генерируем JWT токен
token, expiresAt, err := s.generateToken(account)
if err != nil {
return nil, err
}
// Формируем ответ
response := &AuthResponse{
Token: token,
ExpiresAt: expiresAt,
User: UserInfo{
ID: account.Base.ID,
Email: account.Email,
FirstName: account.FirstName,
LastName: account.LastName,
FullName: account.FullName,
Role: account.Role,
},
}
l.Info("Пользователь успешно вошел: %s", zap.String("Email", req.Email))
return response, nil
}
// RefreshToken обновление токена
func (s *authServiceImpl) RefreshToken(token string) (*AuthResponse, error) {
l := logger.Get()
l.Debug("Обновление токена")
// Парсим и валидируем токен
claims := &jwt.RegisteredClaims{}
parsedToken, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
return s.jwtSecret, nil
})
if err != nil || !parsedToken.Valid {
l.Error("Невалидный токен для обновления: %v", zap.Error(err))
return nil, errors.New("invalid token")
}
// Получаем ID пользователя из claims
userID, err := claims.GetSubject()
if err != nil {
return nil, errors.New("invalid token claims")
}
// Получаем пользователя из базы
var account *models.Account
// Здесь нужно преобразовать string в uint
// В реальном проекте нужно добавить метод GetByIDString или аналогичный
// Для простоты используем существующий метод
// account, err = s.accountRepo.GetByEmail(???)
// Временное решение - нужно добавить метод GetByID
// Пока пропускаем для демонстрации
_ = userID
// Генерируем новый токен
newToken, expiresAt, err := s.generateToken(account)
if err != nil {
return nil, err
}
return &AuthResponse{
Token: newToken,
ExpiresAt: expiresAt,
}, nil
}
// Logout выход пользователя
func (s *authServiceImpl) Logout(userID uint) error {
l := logger.Get()
l.Debug("Выход пользователя: %d", zap.Uint("userID", userID))
// В реальном проекте здесь можно добавить токен в черный список
// или удалить refresh token из базы данных
return nil
}
// generateToken генерирует JWT токен для пользователя
func (s *authServiceImpl) generateToken(account *models.Account) (string, time.Time, error) {
// Устанавливаем время истечения (24 часа)
expiresAt := time.Now().Add(24 * time.Hour)
// Создаем claims
claims := jwt.MapClaims{
"sub": account.Base.ID,
"email": account.Email,
"role": account.Role,
"exp": expiresAt.Unix(),
"iat": time.Now().Unix(),
}
// Создаем токен
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Подписываем токен
tokenString, err := token.SignedString(s.jwtSecret)
if err != nil {
return "", time.Time{}, err
}
return tokenString, expiresAt, nil
}
@@ -0,0 +1,101 @@
package middleware
import (
"context"
"net/http"
"strings"
"api_yal/internal/logger"
"github.com/golang-jwt/jwt/v5"
"go.uber.org/zap"
)
type contextKey string
const (
// UserIDKey ключ для хранения ID пользователя в контексте
UserIDKey contextKey = "userID"
// UserEmailKey ключ для хранения email пользователя в контексте
UserEmailKey contextKey = "userEmail"
// UserRoleKey ключ для хранения роли пользователя в контексте
UserRoleKey contextKey = "userRole"
)
// AuthMiddleware создает middleware для проверки JWT токена
func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
l := logger.Get()
// Получаем токен из заголовка Authorization
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
l.Debug("Отсутствует заголовок Authorization")
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// Ожидаем формат "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
l.Debug("Неверный формат заголовка Authorization")
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
tokenString := parts[1]
// Парсим и валидируем токен
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Проверяем метод подписи
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
l.Debug("Невалидный токен: %v", zap.Error(err))
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Извлекаем claims
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
l.Debug("Не удалось извлечь claims из токена")
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
return
}
// Добавляем информацию о пользователе в контекст
ctx := r.Context()
// Извлекаем userID из sub (subject)
if userID, ok := claims["sub"].(float64); ok {
ctx = context.WithValue(ctx, UserIDKey, uint(userID))
}
// Извлекаем email
if email, ok := claims["email"].(string); ok {
ctx = context.WithValue(ctx, UserEmailKey, email)
}
// Извлекаем роль
if role, ok := claims["role"].(string); ok {
ctx = context.WithValue(ctx, UserRoleKey, role)
}
// Передаем управление дальше с обновленным контекстом
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// AuthMiddlewareWithContext (для обратной совместимости)
func AuthMiddlewareWithContext(next http.Handler) http.Handler {
// Эта функция должна быть реализована в основном приложении
// с передачей jwtSecret из конфигурации
return next
}
@@ -1,35 +0,0 @@
package middleware
import (
"context"
"net/http"
)
type contextKey string
const (
UserIDKey contextKey = "userID"
IsAuthKey contextKey = "isAuthenticated"
)
// AuthMiddlewareWithContext добавляет информацию об авторизации в контекст
func AuthMiddlewareWithContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Создаем контекст с тестовыми данными
ctx := r.Context()
ctx = context.WithValue(ctx, UserIDKey, 0)
ctx = context.WithValue(ctx, IsAuthKey, false)
// В реальном проекте здесь будет:
// token := r.Header.Get("Authorization")
// if token != "" {
// userID, err := validateToken(token)
// if err == nil {
// ctx = context.WithValue(ctx, UserIDKey, userID)
// ctx = context.WithValue(ctx, IsAuthKey, true)
// }
// }
next.ServeHTTP(w, r.WithContext(ctx))
})
}
@@ -48,7 +48,7 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
zapLogger.Debug("Health check маршрут зарегистрирован")
// Здесь можно добавить другие маршруты, которые будут защищены аутентификацией
auth.RegisterRoutes(r)
auth.RegisterRoutes(r, db, config.JWTSecret)
// r.Mount("/api/v1", apiRoutes(db, config))
zapLogger.Info("Настройка маршрутов завершена")