290 lines
6.6 KiB
Go
290 lines
6.6 KiB
Go
// AngelaMos | 2026
|
|
// repository.go
|
|
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
|
|
"github.com/carterperez-dev/monitor-the-situation/backend/internal/core"
|
|
)
|
|
|
|
type Repository interface {
|
|
Create(ctx context.Context, user *User) error
|
|
GetByID(ctx context.Context, id string) (*User, error)
|
|
GetByEmail(ctx context.Context, email string) (*User, error)
|
|
Update(ctx context.Context, user *User) error
|
|
UpdatePassword(ctx context.Context, id, passwordHash string) error
|
|
IncrementTokenVersion(ctx context.Context, id string) error
|
|
SoftDelete(ctx context.Context, id string) error
|
|
List(ctx context.Context, params ListUsersParams) ([]User, int, error)
|
|
ExistsByEmail(ctx context.Context, email string) (bool, error)
|
|
}
|
|
|
|
type repository struct {
|
|
db core.DBTX
|
|
}
|
|
|
|
func NewRepository(db core.DBTX) Repository {
|
|
return &repository{db: db}
|
|
}
|
|
|
|
func (r *repository) Create(ctx context.Context, user *User) error {
|
|
query := `
|
|
INSERT INTO users (id, email, password_hash, name, role, tier)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING created_at, updated_at, token_version`
|
|
|
|
err := r.db.GetContext(ctx, user, query,
|
|
user.ID,
|
|
user.Email,
|
|
user.PasswordHash,
|
|
user.Name,
|
|
user.Role,
|
|
user.Tier,
|
|
)
|
|
if err != nil {
|
|
if isDuplicateKeyError(err) {
|
|
return fmt.Errorf("create user: %w", core.ErrDuplicateKey)
|
|
}
|
|
return fmt.Errorf("create user: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) GetByID(ctx context.Context, id string) (*User, error) {
|
|
query := `
|
|
SELECT id, email, password_hash, name, role, tier, token_version,
|
|
created_at, updated_at, deleted_at
|
|
FROM users
|
|
WHERE id = $1 AND deleted_at IS NULL`
|
|
|
|
var user User
|
|
err := r.db.GetContext(ctx, &user, query, id)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, fmt.Errorf("get user: %w", core.ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get user: %w", err)
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *repository) GetByEmail(
|
|
ctx context.Context,
|
|
email string,
|
|
) (*User, error) {
|
|
query := `
|
|
SELECT id, email, password_hash, name, role, tier, token_version,
|
|
created_at, updated_at, deleted_at
|
|
FROM users
|
|
WHERE email = $1 AND deleted_at IS NULL`
|
|
|
|
var user User
|
|
err := r.db.GetContext(ctx, &user, query, email)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, fmt.Errorf("get user by email: %w", core.ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get user by email: %w", err)
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *repository) Update(ctx context.Context, user *User) error {
|
|
query := `
|
|
UPDATE users
|
|
SET name = $2, role = $3, tier = $4, updated_at = NOW()
|
|
WHERE id = $1 AND deleted_at IS NULL
|
|
RETURNING updated_at`
|
|
|
|
err := r.db.GetContext(ctx, &user.UpdatedAt, query,
|
|
user.ID,
|
|
user.Name,
|
|
user.Role,
|
|
user.Tier,
|
|
)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return fmt.Errorf("update user: %w", core.ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("update user: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) UpdatePassword(
|
|
ctx context.Context,
|
|
id, passwordHash string,
|
|
) error {
|
|
query := `
|
|
UPDATE users
|
|
SET password_hash = $2, updated_at = NOW()
|
|
WHERE id = $1 AND deleted_at IS NULL`
|
|
|
|
result, err := r.db.ExecContext(ctx, query, id, passwordHash)
|
|
if err != nil {
|
|
return fmt.Errorf("update password: %w", err)
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("update password: %w", err)
|
|
}
|
|
|
|
if rows == 0 {
|
|
return fmt.Errorf("update password: %w", core.ErrNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) IncrementTokenVersion(
|
|
ctx context.Context,
|
|
id string,
|
|
) error {
|
|
query := `
|
|
UPDATE users
|
|
SET token_version = token_version + 1, updated_at = NOW()
|
|
WHERE id = $1 AND deleted_at IS NULL`
|
|
|
|
result, err := r.db.ExecContext(ctx, query, id)
|
|
if err != nil {
|
|
return fmt.Errorf("increment token version: %w", err)
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("increment token version: %w", err)
|
|
}
|
|
|
|
if rows == 0 {
|
|
return fmt.Errorf("increment token version: %w", core.ErrNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) SoftDelete(ctx context.Context, id string) error {
|
|
query := `
|
|
UPDATE users
|
|
SET deleted_at = NOW(), updated_at = NOW()
|
|
WHERE id = $1 AND deleted_at IS NULL`
|
|
|
|
result, err := r.db.ExecContext(ctx, query, id)
|
|
if err != nil {
|
|
return fmt.Errorf("delete user: %w", err)
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("delete user: %w", err)
|
|
}
|
|
|
|
if rows == 0 {
|
|
return fmt.Errorf("delete user: %w", core.ErrNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *repository) List(
|
|
ctx context.Context,
|
|
params ListUsersParams,
|
|
) ([]User, int, error) {
|
|
params.Normalize()
|
|
|
|
var conditions []string
|
|
var args []any
|
|
argIdx := 1
|
|
|
|
conditions = append(conditions, "deleted_at IS NULL")
|
|
|
|
if params.Search != "" {
|
|
conditions = append(conditions, fmt.Sprintf(
|
|
"(email ILIKE $%d OR name ILIKE $%d)", argIdx, argIdx))
|
|
args = append(args, "%"+escapeLike(params.Search)+"%")
|
|
argIdx++
|
|
}
|
|
|
|
if params.Role != "" {
|
|
conditions = append(conditions, fmt.Sprintf("role = $%d", argIdx))
|
|
args = append(args, params.Role)
|
|
argIdx++
|
|
}
|
|
|
|
if params.Tier != "" {
|
|
conditions = append(conditions, fmt.Sprintf("tier = $%d", argIdx))
|
|
args = append(args, params.Tier)
|
|
argIdx++
|
|
}
|
|
|
|
whereClause := strings.Join(conditions, " AND ")
|
|
|
|
countQuery := fmt.Sprintf(
|
|
"SELECT COUNT(*) FROM users WHERE %s",
|
|
whereClause,
|
|
)
|
|
var total int
|
|
if err := r.db.GetContext(ctx, &total, countQuery, args...); err != nil {
|
|
return nil, 0, fmt.Errorf("count users: %w", err)
|
|
}
|
|
|
|
query := fmt.Sprintf(`
|
|
SELECT id, email, name, role, tier, token_version,
|
|
created_at, updated_at, deleted_at
|
|
FROM users
|
|
WHERE %s
|
|
ORDER BY created_at DESC
|
|
LIMIT $%d OFFSET $%d`,
|
|
whereClause, argIdx, argIdx+1)
|
|
|
|
args = append(args, params.PageSize, params.Offset())
|
|
|
|
var users []User
|
|
if err := r.db.SelectContext(ctx, &users, query, args...); err != nil {
|
|
return nil, 0, fmt.Errorf("list users: %w", err)
|
|
}
|
|
|
|
return users, total, nil
|
|
}
|
|
|
|
func (r *repository) ExistsByEmail(
|
|
ctx context.Context,
|
|
email string,
|
|
) (bool, error) {
|
|
query := `SELECT EXISTS(SELECT 1 FROM users WHERE email = $1 AND deleted_at IS NULL)`
|
|
|
|
var exists bool
|
|
if err := r.db.GetContext(ctx, &exists, query, email); err != nil {
|
|
return false, fmt.Errorf("check email exists: %w", err)
|
|
}
|
|
|
|
return exists, nil
|
|
}
|
|
|
|
func isDuplicateKeyError(err error) bool {
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) {
|
|
return pgErr.Code == "23505"
|
|
}
|
|
return false
|
|
}
|
|
|
|
func escapeLike(s string) string {
|
|
s = strings.ReplaceAll(s, "\\", "\\\\")
|
|
s = strings.ReplaceAll(s, "%", "\\%")
|
|
s = strings.ReplaceAll(s, "_", "\\_")
|
|
return s
|
|
}
|