small fixes

This commit is contained in:
2025-06-18 09:01:14 +03:00
parent 42f2b68848
commit 5e609017f0
15 changed files with 55 additions and 108 deletions

View File

@@ -24,34 +24,28 @@ type AuthService struct {
// Authenticate проверяет учетные данные и возвращает данные для ответа Yggdrasil.
func (s *AuthService) Authenticate(ctx context.Context, req models.AuthenticateRequest) (*models.AuthenticateResponse, error) {
// 1. Найти пользователя по имени
user, err := s.UserRepo.GetUserByUsername(ctx, req.Username)
if err != nil {
if errors.Is(err, database.ErrUserNotFound) {
return nil, ErrInvalidCredentials
}
return nil, err // Другая ошибка БД
return nil, err
}
// 2. Сравнить хеш пароля из БД с паролем из запроса
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
if err != nil {
// Если хеши не совпадают, bcrypt возвращает ошибку
return nil, ErrInvalidCredentials
}
// 3. Сгенерировать новый accessToken
accessToken := uuid.New().String()
// 4. Сохранить токен в БД
err = s.UserRepo.CreateAccessToken(ctx, user.ID, accessToken, req.ClientToken)
if err != nil {
return nil, err
}
// 5. Сформировать ответ согласно спецификации Yggdrasil
profile := models.ProfileInfo{
ID: strings.ReplaceAll(user.UUID.String(), "-", ""), // UUID без дефисов
ID: strings.ReplaceAll(user.UUID.String(), "-", ""),
Name: user.Username,
}
@@ -71,26 +65,21 @@ func (s *AuthService) Authenticate(ctx context.Context, req models.AuthenticateR
// 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 // Другая ошибка БД
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(),
@@ -98,17 +87,14 @@ func (s *AuthService) LoginUser(ctx context.Context, req models.LoginRequest) (s
"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

@@ -8,12 +8,12 @@ import (
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/hex" // Для преобразования хеша в строку
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"image/png" // Для валидации PNG
"image/png"
"io"
"mime/multipart"
"os"
@@ -45,7 +45,6 @@ func NewProfileService(repo *database.UserRepository, keyPath, domain string) (*
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
// Попробуем PKCS#8, если PKCS#1 не удался
key, errPkcs8 := x509.ParsePKCS8PrivateKey(block.Bytes)
if errPkcs8 != nil {
return nil, fmt.Errorf("failed to parse private key: %v / %v", err, errPkcs8)
@@ -71,7 +70,6 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U
return nil, err
}
// 1. Формируем структуру со свойствами текстур
textures := models.Textures{}
if profile.SkinHash != "" {
textures.SKIN = &models.TextureInfo{URL: fmt.Sprintf("http://%s/files/textures/%s", s.domain, profile.SkinHash)}
@@ -88,14 +86,12 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U
Textures: textures,
}
// 2. Маршализация в JSON и кодирование в Base64
valueJSON, err := json.Marshal(propValue)
if err != nil {
return nil, err
}
valueBase64 := base64.StdEncoding.EncodeToString(valueJSON)
// 3. Подпись
hasher := sha1.New()
hasher.Write([]byte(valueBase64))
hashed := hasher.Sum(nil)
@@ -106,7 +102,6 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U
}
signatureBase64 := base64.StdEncoding.EncodeToString(signature)
// 4. Формирование итогового ответа
response := &models.SessionProfileResponse{
ID: profileID,
Name: user.Username,
@@ -124,15 +119,12 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U
// UpdateUserSkin обрабатывает загрузку, валидацию и сохранение файла скина.
func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file multipart.File, header *multipart.FileHeader) error {
// 1. Читаем файл в память
fileBytes, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
// Возвращаем указатель файла в начало, чтобы его можно было прочитать снова
file.Seek(0, 0)
// 2. Валидация PNG 64x64
config, err := png.DecodeConfig(file)
if err != nil {
return errors.New("invalid PNG file")
@@ -141,20 +133,16 @@ func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file mu
return errors.New("skin must be 64x64 pixels")
}
// 3. Вычисляем SHA1 хеш
hasher := sha1.New()
hasher.Write(fileBytes)
hash := hex.EncodeToString(hasher.Sum(nil))
// 4. Сохраняем файл на диск
// Путь к хранилищу текстур должен быть конфигурируемым
storagePath := os.Getenv("TEXTURES_STORAGE_PATH")
if storagePath == "" {
return errors.New("textures storage path not configured")
}
filePath := fmt.Sprintf("%s/%s", storagePath, hash)
// Создаем файл только если его еще нет
if _, err := os.Stat(filePath); os.IsNotExist(err) {
outFile, err := os.Create(filePath)
if err != nil {
@@ -168,6 +156,5 @@ func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file mu
}
}
// 5. Сохраняем хеш в БД
return s.UserRepo.UpdateSkinHash(ctx, userID, hash)
}

View File

@@ -8,10 +8,22 @@ import (
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
"github.com/Tnze/go-mc/bot/basic"
"github.com/Tnze/go-mc/net"
"github.com/Tnze/go-mc/bot"
"github.com/Tnze/go-mc/chat"
)
type pingResponse struct {
Description chat.Message `json:"description"`
Players struct {
Max int `json:"max"`
Online int `json:"online"`
} `json:"players"`
Version struct {
Name string `json:"name"`
Protocol int `json:"protocol"`
} `json:"version"`
}
type ServerPoller struct {
Repo *database.ServerRepository
}
@@ -47,29 +59,19 @@ func (p *ServerPoller) pollAllServers(ctx context.Context) {
}
func (p *ServerPoller) pollServer(ctx context.Context, server *models.GameServer) {
resp, delay, err := net.PingAndListTimeout(server.Address, 5*time.Second)
resp, delay, err := bot.PingAndList(server.Address)
if err != nil {
log.Printf("Poller: failed to ping %s (%s): %v", server.Name, server.Address, err)
return
}
var status basic.ServerList
var status pingResponse
if err := json.Unmarshal(resp, &status); err != nil {
log.Printf("Poller: failed to unmarshal status for %s: %v", server.Name, err)
return
}
// MOTD может быть сложным объектом, извлекаем текст
var motdText string
if s, ok := status.Description.(string); ok {
motdText = s
} else {
if m, ok := status.Description.(map[string]interface{}); ok {
if t, ok := m["text"].(string); ok {
motdText = t
}
}
}
motdText := status.Description.String()
updateData := &models.ServerStatus{
StatusJSON: string(resp),

View File

@@ -19,10 +19,8 @@ var (
ErrPasswordTooShort = errors.New("password is too short (minimum 8 characters)")
)
// Регулярное выражение для валидации username
var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_]{3,16}$`)
// Регулярное выражение для валидации email (упрощенное, но эффективное)
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
type UserService struct {
@@ -31,7 +29,6 @@ type UserService struct {
// RegisterNewUser выполняет полный алгоритм регистрации
func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRequest) error {
// Шаг 2 из ТЗ: Валидация
if !usernameRegex.MatchString(req.Username) {
return ErrInvalidUsername
}
@@ -42,13 +39,11 @@ func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRe
return ErrPasswordTooShort
}
// Шаг 5 из ТЗ: Генерация хеша пароля
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12) // Стоимость 12, как в ТЗ
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12)
if err != nil {
return err
}
// Шаг 6 из ТЗ: Генерация UUID
userUUID, err := uuid.NewRandom()
if err != nil {
return err
@@ -59,16 +54,14 @@ func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRe
Username: req.Username,
Email: req.Email,
PasswordHash: string(passwordHash),
Role: "user", // По умолчанию
Role: "user",
}
// Вызываем метод репозитория для сохранения в БД
return s.Repo.CreateUserTx(ctx, user)
}
// ValidateJoinRequest проверяет запрос на присоединение к серверу.
func (s *AuthService) ValidateJoinRequest(ctx context.Context, req models.JoinRequest) error {
// Преобразуем UUID из строки без дефисов в стандартный формат
var uuidStr string
if len(req.SelectedProfile) == 32 {
uuidStr = fmt.Sprintf("%s-%s-%s-%s-%s",
@@ -87,18 +80,14 @@ func (s *AuthService) ValidateJoinRequest(ctx context.Context, req models.JoinRe
return fmt.Errorf("failed to parse profile UUID: %w", err)
}
// Проверяем токен в базе данных
err = s.UserRepo.ValidateAccessToken(ctx, req.AccessToken, userUUID)
if err != nil {
if errors.Is(err, database.ErrTokenNotFound) {
// Возвращаем ту же ошибку, что и при неверных кредах, чтобы не давать лишней информации
return ErrInvalidCredentials
}
return err
}
// В ТЗ указано "привязка serverId". В простой реализации это может быть просто логирование
// или запись в кеш (например, Redis). Для начала, просто прохождение валидации достаточно.
log.Printf("User %s successfully joined server with serverId %s", userUUID, req.ServerID)
return nil