feat(auth): implement web login endpoint with JWT
This commit is contained in:
@@ -50,6 +50,7 @@ func main() {
|
||||
// --- Публичные роуты ---
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Post("/register", userHandler.Register)
|
||||
r.Post("/login", authHandler.Login)
|
||||
// Здесь будет публичный эндпоинт для логина в веб-интерфейс
|
||||
})
|
||||
r.Route("/authserver", func(r chi.Router) {
|
||||
|
||||
@@ -78,3 +78,31 @@ func (h *AuthHandler) Join(w http.ResponseWriter, r *http.Request) {
|
||||
// В случае успеха возвращаем пустой ответ со статусом 204
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
token, user, err := h.Service.LoginUser(r.Context(), req)
|
||||
if err != nil {
|
||||
if errors.Is(err, core.ErrInvalidCredentials) {
|
||||
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
log.Printf("internal server error during login: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := models.LoginResponse{
|
||||
Token: token,
|
||||
User: user,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package core
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
||||
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -65,3 +68,48 @@ func (s *AuthService) Authenticate(ctx context.Context, req models.AuthenticateR
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// LoginUser проверяет учетные данные и генерирует JWT для веб-сессии.
|
||||
func (s *AuthService) LoginUser(ctx context.Context, req models.LoginRequest) (string, *models.User, error) {
|
||||
// 1. Найти пользователя по логину (username или email)
|
||||
user, err := s.UserRepo.GetUserByLogin(ctx, req.Login)
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrUserNotFound) {
|
||||
return "", nil, ErrInvalidCredentials
|
||||
}
|
||||
return "", nil, err // Другая ошибка БД
|
||||
}
|
||||
|
||||
// 2. Сравнить хеш пароля
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
|
||||
if err != nil {
|
||||
return "", nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// 3. Создать JWT
|
||||
// Устанавливаем срок действия токена, например, 72 часа
|
||||
expirationTime := time.Now().Add(72 * time.Hour)
|
||||
|
||||
// Создаем claims (полезная нагрузка токена)
|
||||
claims := &jwt.MapClaims{
|
||||
"exp": expirationTime.Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"user_id": user.ID,
|
||||
"role": user.Role,
|
||||
}
|
||||
|
||||
// Создаем токен с указанием алгоритма подписи и claims
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// Подписываем токен нашим секретным ключом
|
||||
jwtSecret := os.Getenv("JWT_SECRET_KEY")
|
||||
tokenString, err := token.SignedString([]byte(jwtSecret))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Скрываем хеш пароля перед отправкой данных пользователя на клиент
|
||||
user.PasswordHash = ""
|
||||
|
||||
return tokenString, user, nil
|
||||
}
|
||||
|
||||
@@ -176,3 +176,25 @@ func (r *UserRepository) UpdateSkinHash(ctx context.Context, userID int, skinHas
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserByLogin находит пользователя по его имени или email.
|
||||
func (r *UserRepository) GetUserByLogin(ctx context.Context, login string) (*models.User, error) {
|
||||
user := &models.User{}
|
||||
var userUUID string
|
||||
|
||||
// Ищем по username ИЛИ по email
|
||||
query := "SELECT id, uuid, username, email, password_hash, role, created_at, updated_at FROM users WHERE username = $1 OR email = $1"
|
||||
err := r.DB.QueryRowContext(ctx, query, login).Scan(
|
||||
&user.ID, &userUUID, &user.Username, &user.Email, &user.PasswordHash, &user.Role, &user.CreatedAt, &user.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.UUID, _ = uuid.Parse(userUUID)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -74,3 +74,16 @@ type JoinRequest struct {
|
||||
SelectedProfile string `json:"selectedProfile"` // UUID пользователя без дефисов
|
||||
ServerID string `json:"serverId"`
|
||||
}
|
||||
|
||||
// LoginRequest - это тело запроса на /api/login
|
||||
type LoginRequest struct {
|
||||
// Позволяем логиниться как по username, так и по email
|
||||
Login string `json:"login"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// LoginResponse - это тело успешного ответа с JWT
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
User *User `json:"user"` // Отдаем информацию о пользователе
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user