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" ) const ( RefreshTokenCookieName = "refresh_token" RefreshTokenExpiration = 7 * 24 * time.Hour ) // 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, 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 { h.handleValidationError(w, err) return } response, err := h.authService.Register(req) if err != nil { l.Error("Ошибка регистрации", 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 { 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 } h.setRefreshTokenCookie(w, refreshToken) l.Debug("Завершение обработки запроса входа") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } // RefreshToken обновление токена func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { l := logger.Get() l.Info("Начало обработки запроса обновления токена") var refreshToken string cookie, err := r.Cookie(RefreshTokenCookieName) if err == nil && cookie != nil { refreshToken = cookie.Value } 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) { 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.authService.GenerateRefreshTokenForUser(response.User.ID) if err != nil { l.Error("Ошибка генерации нового refresh token", zap.Error(err)) http.Error(w, "Failed to generate refresh token", http.StatusInternalServerError) return } h.setRefreshTokenCookie(w, newRefreshToken) l.Info("Завершение обработки запроса обновления токена") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } // Logout выход пользователя func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { l := logger.Get() l.Info("Начало обработки запроса выхода") 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("Ошибка выхода", zap.Error(err)) http.Error(w, "Logout failed", http.StatusInternalServerError) return } h.clearRefreshTokenCookie(w) l.Info("Завершение обработки запроса выхода") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "message": "Successfully logged out", }) } // RequestPasswordReset запрос на сброс пароля func (h *AuthHandler) RequestPasswordReset(w http.ResponseWriter, r *http.Request) { l := logger.Get() l.Info("Начало обработки запроса сброса пароля") var req ResetPasswordRequest 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 } 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{}{ "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, }) } // GetMe возвращает информацию о текущем пользователе func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) { userID, ok := r.Context().Value(middleware.UserIDKey).(uint) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } account, err := h.authService.GetUserFromID(userID) if err != nil { http.Error(w, "User not found", http.StatusNotFound) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "user": UserInfo{ ID: account.Base.ID, Email: account.Email, FirstName: account.FirstName, LastName: account.LastName, FullName: account.FullName, Role: account.Role, }, }) } // 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, }) }