// Package users_repo persists users. package users_repo import ( "context" "errors" "evening_detective_server/internal/repos" "evening_detective_server/internal/repos/user_agreements_repo" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" ) var ( // ErrUserNotFound indicates that a user was not found. ErrUserNotFound = errors.New("пользователь не найден") ) // UsersRepo persists users and their roles. type UsersRepo struct { pool *pgxpool.Pool } // NewUserRepo creates a users repository. func NewUserRepo(pool *pgxpool.Pool) *UsersRepo { return &UsersRepo{ pool: pool, } } func (s *UsersRepo) AddUserWithAgreements( ctx context.Context, username string, email string, passwordHash string, roles []string, agreements []user_agreements_repo.Agreement, ) (int, error) { if len(agreements) == 0 { return 0, errors.New("не переданы записи об акцепте соглашений") } tx, err := s.pool.Begin(ctx) if err != nil { return 0, err } defer tx.Rollback(ctx) //nolint:errcheck var id int err = tx.QueryRow( ctx, `INSERT INTO users (username, email, password_hash, roles) SELECT $1, $2, $3, $4 WHERE NOT EXISTS ( SELECT 1 FROM users WHERE username = $5 OR email = $6 ) RETURNING id`, username, email, passwordHash, roles, username, email, ).Scan(&id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return 0, errors.New("username или email уже используется") } var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23505" { return 0, errors.New("username или email уже используется") } return 0, err } for _, agreement := range agreements { if _, err = tx.Exec( ctx, `INSERT INTO user_agreements (user_id, agreement_type, version, ip, user_agent) VALUES ($1, $2, $3, $4, $5)`, id, agreement.AgreementType, agreement.Version, agreement.IP, agreement.UserAgent, ); err != nil { return 0, err } } if err = tx.Commit(ctx); err != nil { return 0, err } return id, nil } // UpdateUserPassword changes a user's password hash. func (s *UsersRepo) UpdateUserPassword( ctx context.Context, email string, passwordHash string, ) error { tag, err := s.pool.Exec( ctx, `UPDATE users SET password_hash = $1 WHERE email = $2`, passwordHash, email, ) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrUserNotFound } return nil } // GetUserByEmail returns a user by email. func (s *UsersRepo) GetUserByEmail( ctx context.Context, email string, ) (*repos.User, error) { user := &repos.User{} row := s.pool.QueryRow( ctx, `SELECT id, username, email, password_hash, roles, is_active FROM users WHERE email = $1`, email, ) err := row.Scan( &user.ID, &user.Username, &user.Email, &user.PasswordHash, &user.Roles, &user.IsActive, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrUserNotFound } return nil, err } return user, nil } // GetUserByID returns a user by ID. func (s *UsersRepo) GetUserByID( ctx context.Context, id int, ) (*repos.User, error) { user := &repos.User{} row := s.pool.QueryRow( ctx, `SELECT id, username, email, password_hash, roles, is_active FROM users WHERE id = $1`, id, ) err := row.Scan( &user.ID, &user.Username, &user.Email, &user.PasswordHash, &user.Roles, &user.IsActive, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrUserNotFound } return nil, err } return user, nil } // GetUsers returns users matching the requested filters. func (s *UsersRepo) GetUsers( ctx context.Context, ) ([]*repos.User, error) { rows, err := s.pool.Query( ctx, `SELECT id, username, email, roles, is_active, created_at FROM users ORDER BY created_at DESC`, ) if err != nil { return nil, err } defer rows.Close() var users []*repos.User for rows.Next() { user := &repos.User{} err := rows.Scan( &user.ID, &user.Username, &user.Email, &user.Roles, &user.IsActive, &user.CreatedAt, ) if err != nil { return nil, err } users = append(users, user) } if err := rows.Err(); err != nil { return nil, err } return users, nil } // AddUserRole assigns a role to a user. func (s *UsersRepo) AddUserRole( ctx context.Context, userID int, role string, ) error { _, err := s.pool.Exec( ctx, `UPDATE users SET roles = roles || $1::jsonb WHERE id = $2 AND NOT roles @> $1::jsonb`, fmt.Sprintf(`["%s"]`, role), userID, ) return err } // DeleteUserRole removes a role from a user. func (s *UsersRepo) DeleteUserRole( ctx context.Context, userID int, role string, ) error { _, err := s.pool.Exec( ctx, `UPDATE users SET roles = roles - $1::text WHERE id = $2`, role, userID, ) return err } func (s *UsersRepo) DeleteUser( ctx context.Context, userID int, ) error { tx, err := s.pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) //nolint:errcheck if _, err = tx.Exec( ctx, `DELETE FROM refresh_tokens WHERE user_id = $1`, userID, ); err != nil { return err } if _, err = tx.Exec( ctx, `UPDATE user_agreements SET user_id = NULL, ip = NULL, user_agent = NULL WHERE user_id = $1`, userID, ); err != nil { return err } tag, err := tx.Exec( ctx, `DELETE FROM users WHERE id = $1`, userID, ) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrUserNotFound } return tx.Commit(ctx) }