feat(auth): implement web login endpoint with JWT

This commit is contained in:
2025-06-16 08:25:20 +03:00
parent 9c7940a70a
commit 45173c406c
5 changed files with 112 additions and 0 deletions

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"` // Отдаем информацию о пользователе
}