Files
backend/internal/database/user_repository.go

251 lines
7.5 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.
package database
import (
"context"
"database/sql"
"errors"
"log"
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrUserExists = errors.New("user with this username or email already exists")
)
type UserRepository struct {
DB *pgxpool.Pool
}
// CreateUserTx создает нового пользователя и его профиль в рамках одной транзакции
func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) error {
log.Println("[DB] Beginning CreateUserTx transaction")
tx, err := r.DB.Begin(ctx)
if err != nil {
log.Printf("[DB] ERROR: Failed to begin transaction: %v", err)
return err
}
defer tx.Rollback(ctx)
log.Println("[DB] Checking if user exists...")
var exists bool
err = tx.QueryRow(ctx,
"SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)", user.Username, user.Email).Scan(&exists)
if err != nil {
log.Printf("[DB] ERROR: User existence check failed: %v", err)
return err
}
if exists {
log.Printf("[DB] User with username '%s' or email '%s' already exists.", user.Username, user.Email)
return ErrUserExists
}
log.Println("[DB] User does not exist, proceeding.")
log.Println("[DB] Inserting into 'users' table...")
var newUserID int
err = tx.QueryRow(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 {
log.Printf("[DB] ERROR: Insert into 'users' failed: %v", err)
return err
}
log.Printf("[DB] Inserted into 'users' table successfully. New user ID: %d", newUserID)
log.Println("[DB] Inserting into 'profiles' table...")
_, err = tx.Exec(ctx, "INSERT INTO profiles (user_id) VALUES ($1)", newUserID)
if err != nil {
log.Printf("[DB] ERROR: Insert into 'profiles' failed: %v", err)
return err
}
log.Println("[DB] Inserted into 'profiles' table successfully.")
log.Println("[DB] Committing transaction...")
return tx.Commit(ctx)
}
var (
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.QueryRow(ctx, query, username).Scan(
&user.ID, &userUUID, &user.Username, &user.Email, &user.PasswordHash, &user.Role,
)
if err != nil {
if errors.Is(err, pgx.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.Exec(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
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.QueryRow(ctx, query, userUUID).Scan(
&user.ID, &user.Username, &skinHash, &capeHash,
)
if err != nil {
if errors.Is(err, pgx.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.QueryRow(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.Exec(ctx, query, skinHash, userID)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return ErrUserNotFound
}
return nil
}
// GetUserByLogin находит пользователя по его имени или email.
func (r *UserRepository) GetUserByLogin(ctx context.Context, login string) (*models.User, error) {
user := &models.User{}
var userUUID string
query := "SELECT id, uuid, username, email, password_hash, role, created_at, updated_at FROM users WHERE username = $1 OR email = $1"
err := r.DB.QueryRow(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, pgx.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, err
}
user.UUID, _ = uuid.Parse(userUUID)
return user, nil
}
// GetAllUsers возвращает список всех пользователей.
func (r *UserRepository) GetAllUsers(ctx context.Context) ([]models.User, error) {
rows, err := r.DB.Query(ctx, "SELECT id, uuid, username, email, role, created_at, updated_at FROM users ORDER BY id")
if err != nil {
return nil, err
}
defer rows.Close()
var users []models.User
for rows.Next() {
var u models.User
var userUUID string
if err := rows.Scan(&u.ID, &userUUID, &u.Username, &u.Email, &u.Role, &u.CreatedAt, &u.UpdatedAt); err != nil {
return nil, err
}
u.UUID, _ = uuid.Parse(userUUID)
users = append(users, u)
}
return users, nil
}
// UpdateUserRole обновляет роль пользователя по его ID.
func (r *UserRepository) UpdateUserRole(ctx context.Context, userID int, newRole string) error {
res, err := r.DB.Exec(ctx, "UPDATE users SET role = $1 WHERE id = $2", newRole, userID)
if err != nil {
return err
}
if res.RowsAffected() == 0 {
return ErrUserNotFound
}
return nil
}
// GetUserByID находит пользователя по его ID.
func (r *UserRepository) GetUserByID(ctx context.Context, userID int) (*models.User, error) {
user := &models.User{}
var userUUID string
query := "SELECT id, uuid, username, email, password_hash, role, created_at, updated_at FROM users WHERE id = $1"
err := r.DB.QueryRow(ctx, query, userID).Scan(
&user.ID, &userUUID, &user.Username, &user.Email, &user.PasswordHash, &user.Role, &user.CreatedAt, &user.UpdatedAt,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, err
}
user.UUID, _ = uuid.Parse(userUUID)
return user, nil
}