// File: backend/internal/database/user_repository.go package database import ( "context" "database/sql" "errors" "gitea.mrixs.me/minecraft-platform/backend/internal/models" "github.com/google/uuid" ) var ( ErrUserExists = errors.New("user with this username or email already exists") ) type UserRepository struct { DB *sql.DB } // CreateUserTx создает нового пользователя и его профиль в рамках одной транзакции func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) error { tx, err := r.DB.BeginTx(ctx, nil) if err != nil { return err } // Гарантируем откат транзакции в случае любой ошибки defer tx.Rollback() // Шаг 4 из ТЗ: Проверка уникальности var exists bool err = tx.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)", user.Username, user.Email).Scan(&exists) if err != nil { return err } if exists { return ErrUserExists } // Шаг 7 из ТЗ: INSERT в таблицу users. Получаем ID нового пользователя. var newUserID int err = tx.QueryRowContext(ctx, "INSERT INTO users (uuid, username, email, password_hash, role) VALUES ($1, $2, $3, $4, $5) RETURNING id", user.UUID, user.Username, user.Email, user.PasswordHash, user.Role, ).Scan(&newUserID) if err != nil { return err } // Шаг 9 из ТЗ: INSERT в таблицу profiles _, err = tx.ExecContext(ctx, "INSERT INTO profiles (user_id) VALUES ($1)", newUserID) if err != nil { return err } // Шаг 10 из ТЗ: Коммитим транзакцию return tx.Commit() } var ( ErrUserExists = errors.New("user with this username or email already exists") ErrUserNotFound = errors.New("user not found") // Новая ошибка ) // ... // GetUserByUsername находит пользователя по его имени. // Возвращает полную структуру User, включая хеш пароля для проверки. func (r *UserRepository) GetUserByUsername(ctx context.Context, username string) (*models.User, error) { user := &models.User{} var userUUID string query := "SELECT id, uuid, username, email, password_hash, role FROM users WHERE username = $1" err := r.DB.QueryRowContext(ctx, query, username).Scan( &user.ID, &userUUID, &user.Username, &user.Email, &user.PasswordHash, &user.Role, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrUserNotFound } return nil, err } user.UUID, _ = uuid.Parse(userUUID) return user, nil } // CreateAccessToken сохраняет новый токен доступа в базу данных. func (r *UserRepository) CreateAccessToken(ctx context.Context, userID int, accessToken, clientToken string) error { query := "INSERT INTO access_tokens (user_id, access_token, client_token) VALUES ($1, $2, $3)" _, err := r.DB.ExecContext(ctx, query, userID, accessToken, clientToken) return err } // GetProfileByUUID находит пользователя и его профиль по UUID. func (r *UserRepository) GetProfileByUUID(ctx context.Context, userUUID uuid.UUID) (*models.User, *models.Profile, error) { user := &models.User{UUID: userUUID} profile := &models.Profile{} var skinHash, capeHash sql.NullString // Используем NullString для полей, которые могут быть NULL query := ` SELECT u.id, u.username, p.skin_hash, p.cape_hash FROM users u JOIN profiles p ON u.id = p.user_id WHERE u.uuid = $1` err := r.DB.QueryRowContext(ctx, query, userUUID).Scan( &user.ID, &user.Username, &skinHash, &capeHash, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil, ErrUserNotFound } return nil, nil, err } if skinHash.Valid { profile.SkinHash = skinHash.String } if capeHash.Valid { profile.CapeHash = capeHash.String } return user, profile, nil } var ( // ... ErrTokenNotFound = errors.New("access token not found or invalid") // Новая ошибка ) // ValidateAccessToken проверяет, действителен ли токен для данного пользователя. // В нашей реализации мы просто проверяем его существование. // В более сложных системах здесь можно было бы проверять срок действия токена. func (r *UserRepository) ValidateAccessToken(ctx context.Context, token string, userUUID uuid.UUID) error { var exists bool query := ` SELECT EXISTS ( SELECT 1 FROM access_tokens at JOIN users u ON at.user_id = u.id WHERE at.access_token = $1 AND u.uuid = $2 )` err := r.DB.QueryRowContext(ctx, query, token, userUUID).Scan(&exists) if err != nil { return err } if !exists { return ErrTokenNotFound } return nil }