package postgres import ( "context" "fmt" "strings" "telesrv/internal/domain" "telesrv/internal/store/postgres/sqlcgen" ) // ReservedUsernameStore is the operator username blocklist backed by the // reserved_usernames table. type ReservedUsernameStore struct { db sqlcgen.DBTX } // NewReservedUsernameStore builds the store on a pgx pool or transaction. func NewReservedUsernameStore(db sqlcgen.DBTX) *ReservedUsernameStore { return &ReservedUsernameStore{db: db} } func (s *ReservedUsernameStore) IsReserved(ctx context.Context, usernameLower string) (bool, error) { usernameLower = strings.ToLower(strings.TrimSpace(usernameLower)) if usernameLower == "" { return false, nil } var exists bool if err := s.db.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM reserved_usernames WHERE username_lower = $1)`, usernameLower).Scan(&exists); err != nil { return false, fmt.Errorf("check reserved username: %w", err) } return exists, nil } func (s *ReservedUsernameStore) ReserveUsername(ctx context.Context, username, reason, actor string) (bool, error) { username = strings.TrimSpace(username) lower := strings.ToLower(username) if lower == "" { return false, domain.ErrUsernameInvalid } tag, err := s.db.Exec(ctx, ` INSERT INTO reserved_usernames (username_lower, username, reason, actor) VALUES ($1, $2, $3, $4) ON CONFLICT (username_lower) DO NOTHING`, lower, username, reason, actor) if err != nil { return false, fmt.Errorf("reserve username: %w", err) } return tag.RowsAffected() > 0, nil } func (s *ReservedUsernameStore) UnreserveUsername(ctx context.Context, username string) (bool, error) { lower := strings.ToLower(strings.TrimSpace(username)) if lower == "" { return false, domain.ErrUsernameInvalid } tag, err := s.db.Exec(ctx, `DELETE FROM reserved_usernames WHERE username_lower = $1`, lower) if err != nil { return false, fmt.Errorf("unreserve username: %w", err) } return tag.RowsAffected() > 0, nil } func (s *ReservedUsernameStore) ReservedUsernames(ctx context.Context, filter domain.ReservedUsernameFilter) ([]domain.ReservedUsername, error) { limit := filter.Limit if limit <= 0 || limit > 500 { limit = 100 } offset := filter.Offset if offset < 0 { offset = 0 } args := []any{limit, offset} where := "" if q := strings.ToLower(strings.TrimSpace(filter.Query)); q != "" { args = append(args, q+"%") where = "WHERE username_lower LIKE $3" } rows, err := s.db.Query(ctx, ` SELECT username, reason, actor, created_at FROM reserved_usernames `+where+` ORDER BY created_at DESC, username_lower LIMIT $1 OFFSET $2`, args...) if err != nil { return nil, fmt.Errorf("list reserved usernames: %w", err) } defer rows.Close() out := make([]domain.ReservedUsername, 0, limit) for rows.Next() { var item domain.ReservedUsername if err := rows.Scan(&item.Username, &item.Reason, &item.Actor, &item.CreatedAt); err != nil { return nil, fmt.Errorf("scan reserved username: %w", err) } out = append(out, item) } return out, rows.Err() }