Files
evening_detective_server/internal/repos/users_repo/repo.go
T
2026-10-03 13:22:11 +07:00

301 lines
5.5 KiB
Go

// 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)
}