217 lines
6.2 KiB
Go
217 lines
6.2 KiB
Go
package database
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
|
||
"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 {
|
||
tx, err := r.DB.Begin(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer tx.Rollback(ctx)
|
||
|
||
var exists bool
|
||
err = tx.QueryRow(ctx,
|
||
"SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)").Scan(&exists)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if exists {
|
||
return ErrUserExists
|
||
}
|
||
|
||
var newUserID int
|
||
err = tx.QueryRow(ctx, "INSERT INTO users (uuid, username, email, password_hash, role) VALUES ($1, $2, $3, $4, $5) RETURNING id").Scan(&newUserID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
_, err = tx.Exec(ctx, "INSERT INTO profiles (user_id) VALUES ($1)")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
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
|
||
}
|