Files
tp/main_dc/yalarba/api_yal/internal/domain/auth/handler.go
T
valitovgaziz 90a96b4125 Migrate easysite from api_es to api_yal
- Remove api_es service, Dockerfile, all Go source files
- Remove api_es from docker-compose.yml, nginx-ssl.conf, .env, Makefile
- Replace nginx /api/ proxy with /api/v1/ → api_yal:8787
- Add amenity/upload domains, AuthResponse, GET /auth/me, GET /objects/my to api_yal
- Rewrite easysite frontend: types, composables, and all 5 pages to use api_yal DTOs
- Wire nuxt.config public.apiBase, add useObjects CRUD composable
- Update docs references from api_es to api_yal
2026-06-12 10:14:38 +05:00

392 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
})
}