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.Route("/api", func(r chi.Router) {
|
||||||
r.Post("/register", userHandler.Register)
|
r.Post("/register", userHandler.Register)
|
||||||
|
r.Post("/login", authHandler.Login)
|
||||||
// Здесь будет публичный эндпоинт для логина в веб-интерфейс
|
// Здесь будет публичный эндпоинт для логина в веб-интерфейс
|
||||||
})
|
})
|
||||||
r.Route("/authserver", func(r chi.Router) {
|
r.Route("/authserver", func(r chi.Router) {
|
||||||
|
|||||||
@@ -78,3 +78,31 @@ func (h *AuthHandler) Join(w http.ResponseWriter, r *http.Request) {
|
|||||||
// В случае успеха возвращаем пустой ответ со статусом 204
|
// В случае успеха возвращаем пустой ответ со статусом 204
|
||||||
w.WriteHeader(http.StatusNoContent)
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
||||||
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -65,3 +68,48 @@ func (s *AuthService) Authenticate(ctx context.Context, req models.AuthenticateR
|
|||||||
|
|
||||||
return response, nil
|
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
|
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 пользователя без дефисов
|
SelectedProfile string `json:"selectedProfile"` // UUID пользователя без дефисов
|
||||||
ServerID string `json:"serverId"`
|
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