d9e04cf865
modified: main_dc/yalarba/api_yal/internal/domain/auth/handler.go modified: main_dc/yalarba/api_yal/internal/domain/auth/servcie.go implement generate refresh token
365 lines
10 KiB
Go
365 lines
10 KiB
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"
|
||
)
|
||
|
||
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,
|
||
})
|
||
}
|
||
|
||
// 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,
|
||
})
|
||
} |