// handler.go package auth import ( "encoding/json" "errors" "fmt" "net/http" "time" "api_yal/internal/logger" "api_yal/internal/middleware" "github.com/go-playground/validator/v10" "go.uber.org/zap" ) // Cookie константы const ( RefreshTokenCookieName = "refresh_token" RefreshTokenExpiration = 7 * 24 * time.Hour // 7 дней ) // AuthHandler обработчик для аутентификации type AuthHandler struct { authService AuthService validator *validator.Validate } // NewAuthHandler создает новый экземпляр AuthHandler func NewAuthHandler(authService AuthService) *AuthHandler { return &AuthHandler{ authService: authService, validator: validator.New(), } } // setRefreshTokenCookie устанавливает HttpOnly cookie с refresh token func (h *AuthHandler) setRefreshTokenCookie(w http.ResponseWriter, refreshToken string) { http.SetCookie(w, &http.Cookie{ Name: RefreshTokenCookieName, Value: refreshToken, Path: "/", HttpOnly: true, Secure: true, // Всегда true в production SameSite: http.SameSiteStrictMode, MaxAge: int(RefreshTokenExpiration.Seconds()), }) } // clearRefreshTokenCookie удаляет refresh token cookie func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ Name: RefreshTokenCookieName, Value: "", Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode, MaxAge: -1, }) } // Register регистрация аккаунта пользователя func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { l := logger.Get() l.Info("Начало обработки запроса регистрации") var req RegisterRequest 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.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 } l.Info("Завершение обработки запроса регистрации") 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("Начало обработки запроса входа") 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, refreshToken, 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 } // Устанавливаем refresh token в HttpOnly cookie h.setRefreshTokenCookie(w, refreshToken) l.Debug("Завершение обработки запроса входа") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } // 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 { refreshToken = req.RefreshToken } } if refreshToken == "" { http.Error(w, "Refresh token required", http.StatusBadRequest) return } 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) 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("Завершение обработки запроса обновления токена") w.WriteHeader(http.StatusOK) 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) return } if err := h.authService.Logout(userID); err != nil { l.Error("Ошибка выхода: %v", zap.Error(err)) http.Error(w, "Logout failed", http.StatusInternalServerError) return } // Очищаем refresh token cookie h.clearRefreshTokenCookie(w) l.Info("Завершение обработки запроса выхода") 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", }) }