generated from VLADIMIR/template
301 lines
5.5 KiB
Go
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)
|
|
}
|