Files
backend/internal/database/user_repository.go

201 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}
// UpdateSkinHash обновляет хеш скина для пользователя.
func (r *UserRepository) UpdateSkinHash(ctx context.Context, userID int, skinHash string) error {
query := "UPDATE profiles SET skin_hash = $1, updated_at = NOW() WHERE user_id = $2"
result, err := r.DB.ExecContext(ctx, query, skinHash, userID)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return ErrUserNotFound // Если профиль для user_id не найден
}
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
}