Skip to content

Commit 4b9e204

Browse files
committed
chore: adjust unscoped finds and coverage
code quality
1 parent 387c685 commit 4b9e204

12 files changed

Lines changed: 126 additions & 39 deletions

File tree

app/controllers/api/v1/scouting/players_controller.rb

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,20 @@ def index
2828
targets = targets.where('summoner_name ILIKE ? OR real_name ILIKE ?', search_term, search_term)
2929
end
3030

31-
# Sorting
32-
sort_by = params[:sort_by] || 'created_at'
33-
sort_order = params[:sort_order] || 'desc'
34-
35-
# Map 'rank' to actual column names
36-
if sort_by == 'rank'
37-
targets = targets.order("current_lp #{sort_order} NULLS LAST")
38-
elsif sort_by == 'winrate'
39-
targets = targets.order("performance_trend #{sort_order} NULLS LAST")
31+
# Whitelist for sort parameters to prevent SQL injection
32+
allowed_sort_fields = %w[created_at updated_at summoner_name current_tier priority status role region age]
33+
allowed_sort_orders = %w[asc desc]
34+
35+
sort_by = allowed_sort_fields.include?(params[:sort_by]) ? params[:sort_by] : 'created_at'
36+
sort_order = allowed_sort_orders.include?(params[:sort_order]&.downcase) ? params[:sort_order].downcase : 'desc'
37+
38+
# Handle special sort fields
39+
if params[:sort_by] == 'rank'
40+
targets = targets.order(Arel.sql("current_lp #{sort_order} NULLS LAST"))
41+
elsif params[:sort_by] == 'winrate'
42+
targets = targets.order(Arel.sql("performance_trend #{sort_order} NULLS LAST"))
4043
else
41-
targets = targets.order("#{sort_by} #{sort_order}")
44+
targets = targets.order(sort_by => sort_order)
4245
end
4346

4447
# Pagination

app/modules/authentication/controllers/auth_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def register
6464
end
6565
rescue ActiveRecord::RecordInvalid => e
6666
render_validation_errors(e)
67-
rescue => e
67+
rescue StandardError => _e
6868
render_error(message: 'Registration failed', code: 'REGISTRATION_ERROR')
6969
end
7070

app/modules/players/services/riot_sync_service.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,38 @@ class RiotSyncService
3333
require 'net/http'
3434
require 'json'
3535

36+
# Whitelist of valid Riot API regions to prevent host injection
37+
VALID_REGIONS = %w[
38+
br1 eun1 euw1 jp1 kr la1 la2 na1 oc1 tr1 ru ph2 sg2 th2 tw2 vn2
39+
].freeze
40+
3641
attr_reader :player, :region, :api_key
3742

3843
def initialize(player, region: nil, api_key: nil)
3944
@player = player
40-
@region = region || player.region.presence&.downcase || 'br1'
45+
@region = sanitize_region(region || player&.region || 'br1')
4146
@api_key = api_key || ENV['RIOT_API_KEY']
4247
end
4348

49+
private
50+
51+
# Sanitizes and validates region to prevent host injection
52+
#
53+
# @param region [String] Region code to sanitize
54+
# @return [String] Sanitized region code
55+
# @raise [ArgumentError] if region is invalid
56+
def sanitize_region(region)
57+
normalized = region.to_s.downcase.strip
58+
59+
unless VALID_REGIONS.include?(normalized)
60+
raise ArgumentError, "Invalid region: #{region}. Must be one of: #{VALID_REGIONS.join(', ')}"
61+
end
62+
63+
normalized
64+
end
65+
66+
public
67+
4468
def self.import(summoner_name:, role:, region:, organization:, api_key: nil)
4569
new(nil, region: region, api_key: api_key)
4670
.import_player(summoner_name, role, organization)

app/modules/schedules/controllers/schedules_controller.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ def index
2727
schedules = schedules.where(start_time: Time.current.beginning_of_week..Time.current.end_of_week)
2828
end
2929

30-
sort_order = params[:sort_order] || 'asc'
31-
schedules = schedules.order("start_time #{sort_order}")
30+
# Whitelist for sort parameters to prevent SQL injection
31+
allowed_sort_orders = %w[asc desc]
32+
sort_order = allowed_sort_orders.include?(params[:sort_order]&.downcase) ? params[:sort_order].downcase : 'asc'
33+
schedules = schedules.order(start_time: sort_order)
3234

3335
result = paginate(schedules)
3436

app/modules/scouting/controllers/players_controller.rb

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,26 @@ def index
2424
targets = targets.where('summoner_name ILIKE ? OR real_name ILIKE ?', search_term, search_term)
2525
end
2626

27-
sort_by = params[:sort_by] || 'created_at'
28-
sort_order = params[:sort_order] || 'desc'
29-
30-
# Map 'rank' to actual column names
31-
if sort_by == 'rank'
32-
targets = targets.order("current_lp #{sort_order} NULLS LAST")
33-
elsif sort_by == 'winrate'
34-
targets = targets.order("performance_trend #{sort_order} NULLS LAST")
27+
# Whitelist for sort parameters to prevent SQL injection
28+
allowed_sort_fields = %w[created_at updated_at summoner_name current_tier priority status role region age]
29+
allowed_sort_orders = %w[asc desc]
30+
31+
sort_by = allowed_sort_fields.include?(params[:sort_by]) ? params[:sort_by] : 'created_at'
32+
sort_order = allowed_sort_orders.include?(params[:sort_order]&.downcase) ? params[:sort_order].downcase : 'desc'
33+
34+
# Handle special sort fields with NULLS LAST
35+
if params[:sort_by] == 'rank'
36+
# Use Arel for safe SQL generation with NULLS LAST
37+
column = ScoutingTarget.arel_table[:current_lp]
38+
order_clause = sort_order == 'asc' ? column.asc.nulls_last : column.desc.nulls_last
39+
targets = targets.order(order_clause)
40+
elsif params[:sort_by] == 'winrate'
41+
# Use Arel for safe SQL generation with NULLS LAST
42+
column = ScoutingTarget.arel_table[:performance_trend]
43+
order_clause = sort_order == 'asc' ? column.asc.nulls_last : column.desc.nulls_last
44+
targets = targets.order(order_clause)
3545
else
36-
targets = targets.order("#{sort_by} #{sort_order}")
46+
targets = targets.order(sort_by => sort_order)
3747
end
3848

3949
result = paginate(targets)

app/modules/scrims/controllers/opponent_teams_controller.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
module Scrims
2+
# OpponentTeams Controller
3+
#
4+
# Manages opponent team records which are shared across organizations.
5+
# Security note: Update and delete operations are restricted to organizations
6+
# that have used this opponent team in scrims.
7+
#
28
class OpponentTeamsController < ApplicationController
39
include TierAuthorization
410

511
before_action :set_opponent_team, only: [:show, :update, :destroy, :scrim_history]
12+
before_action :verify_team_usage!, only: [:update, :destroy]
613

714
# GET /api/v1/scrims/opponent_teams
815
def index
@@ -75,18 +82,43 @@ def update
7582

7683
# DELETE /api/v1/scrims/opponent_teams/:id
7784
def destroy
85+
# Check if team has scrims from other organizations before deleting
86+
other_org_scrims = @opponent_team.scrims.where.not(organization_id: current_organization.id).exists?
87+
88+
if other_org_scrims
89+
return render json: {
90+
error: 'Cannot delete opponent team that is used by other organizations'
91+
}, status: :unprocessable_entity
92+
end
93+
7894
@opponent_team.destroy
7995
head :no_content
8096
end
8197

8298
private
8399

100+
# Finds opponent team by ID
101+
# Security Note: OpponentTeam is a shared resource across organizations.
102+
# Deletion is restricted to teams without cross-org usage (see destroy action).
103+
# Consider adding organization_id in future for proper multi-tenancy.
84104
def set_opponent_team
85105
@opponent_team = OpponentTeam.find(params[:id])
86106
rescue ActiveRecord::RecordNotFound
87107
render json: { error: 'Opponent team not found' }, status: :not_found
88108
end
89109

110+
# Verifies that current organization has used this opponent team
111+
# Prevents organizations from modifying/deleting teams they haven't interacted with
112+
def verify_team_usage!
113+
has_scrims = current_organization.scrims.exists?(opponent_team_id: @opponent_team.id)
114+
115+
unless has_scrims
116+
render json: {
117+
error: 'You cannot modify this opponent team. Your organization has not played against them.'
118+
}, status: :forbidden
119+
end
120+
end
121+
90122
def opponent_team_params
91123
params.require(:opponent_team).permit(
92124
:name,

app/modules/team_goals/controllers/team_goals_controller.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ def index
1616

1717
goals = goals.where(assigned_to_id: params[:assigned_to_id]) if params[:assigned_to_id].present?
1818

19-
sort_by = params[:sort_by] || 'created_at'
20-
sort_order = params[:sort_order] || 'desc'
21-
goals = goals.order("#{sort_by} #{sort_order}")
19+
# Whitelist for sort parameters to prevent SQL injection
20+
allowed_sort_fields = %w[created_at updated_at title status category start_date end_date progress]
21+
allowed_sort_orders = %w[asc desc]
22+
23+
sort_by = allowed_sort_fields.include?(params[:sort_by]) ? params[:sort_by] : 'created_at'
24+
sort_order = allowed_sort_orders.include?(params[:sort_order]&.downcase) ? params[:sort_order].downcase : 'desc'
25+
goals = goals.order(sort_by => sort_order)
2226

2327
result = paginate(goals)
2428

app/modules/vod_reviews/controllers/vod_reviews_controller.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ def index
2020
vod_reviews = vod_reviews.where('title ILIKE ?', search_term)
2121
end
2222

23-
sort_by = params[:sort_by] || 'created_at'
24-
sort_order = params[:sort_order] || 'desc'
25-
vod_reviews = vod_reviews.order("#{sort_by} #{sort_order}")
23+
# Whitelist for sort parameters to prevent SQL injection
24+
allowed_sort_fields = %w[created_at updated_at title status reviewed_at]
25+
allowed_sort_orders = %w[asc desc]
26+
27+
sort_by = allowed_sort_fields.include?(params[:sort_by]) ? params[:sort_by] : 'created_at'
28+
sort_order = allowed_sort_orders.include?(params[:sort_order]&.downcase) ? params[:sort_order].downcase : 'desc'
29+
vod_reviews = vod_reviews.order(sort_by => sort_order)
2630

2731
result = paginate(vod_reviews)
2832

app/modules/vod_reviews/controllers/vod_timestamps_controller.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,17 @@ def set_vod_review
9696
end
9797

9898
def set_vod_timestamp
99-
@timestamp = VodTimestamp.joins(:vod_review)
100-
.where(vod_reviews: { organization: current_organization })
101-
.find(params[:id])
99+
# Scope to organization through vod_review to prevent cross-org access
100+
@timestamp = VodTimestamp
101+
.joins(:vod_review)
102+
.where(vod_reviews: { organization_id: current_organization.id })
103+
.find_by!(id: params[:id])
104+
rescue ActiveRecord::RecordNotFound
105+
render_error(
106+
message: 'Timestamp not found',
107+
code: 'NOT_FOUND',
108+
status: :not_found
109+
)
102110
end
103111

104112
def vod_timestamp_params

spec/models/vod_timestamp_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@
4545

4646
describe '.chronological' do
4747
it 'orders by timestamp_seconds' do
48-
ts1 = create(:vod_timestamp, vod_review: vod_review, timestamp_seconds: 100)
49-
ts2 = create(:vod_timestamp, vod_review: vod_review, timestamp_seconds: 50)
50-
ts3 = create(:vod_timestamp, vod_review: vod_review, timestamp_seconds: 200)
48+
create(:vod_timestamp, vod_review: vod_review, timestamp_seconds: 100)
49+
create(:vod_timestamp, vod_review: vod_review, timestamp_seconds: 50)
50+
create(:vod_timestamp, vod_review: vod_review, timestamp_seconds: 200)
5151

5252
expect(VodTimestamp.chronological.pluck(:timestamp_seconds)).to eq([50, 100, 200])
5353
end

0 commit comments

Comments
 (0)