Skip to content

Commit 7001449

Browse files
committed
feat: implement arenaBR free agents register
1 parent 2ab661e commit 7001449

16 files changed

Lines changed: 272 additions & 43 deletions

File tree

.brakeman.ignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
{
22
"ignored_warnings": [
3+
{
4+
"warning_type": "Mass Assignment",
5+
"warning_code": 105,
6+
"fingerprint": "f2fd7351c85e531b66f6444ab8a89071e039b96befcdd5a6f897d3f55bb2d9dd",
7+
"check_name": "PermitAttributes",
8+
"message": "Potentially dangerous key allowed for mass assignment",
9+
"file": "app/modules/players/controllers/players_controller.rb",
10+
"line": 368,
11+
"note": "':role' is a player in-game position (top/jungle/mid/adc/support), not a user access role. riot_puuid and riot_summoner_id were intentionally removed from this permit list."
12+
},
313
{
414
"warning_type": "Mass Assignment",
515
"warning_code": 105,

app/controllers/api/v1/organizations_controller.rb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,13 @@ def set_organization
6363
end
6464

6565
def require_admin_or_owner
66-
allowed_roles = %w[admin owner]
67-
unless allowed_roles.include?(@current_user.role)
68-
return render_error(
69-
message: 'Only admins and owners can update organization settings',
70-
code: 'FORBIDDEN',
71-
status: :forbidden
72-
)
73-
end
66+
return if %w[admin owner].include?(@current_user.role)
67+
68+
render_error(
69+
message: 'Only admins and owners can update organization settings',
70+
code: 'FORBIDDEN',
71+
status: :forbidden
72+
)
7473
end
7574

7675
def org_params

app/controllers/concerns/authenticatable.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@ def authenticate_request! # rubocop:disable Metrics/AbcSize, Metrics/MethodLengt
2727

2828
if @jwt_payload[:entity_type] == 'player'
2929
# ── Player token ──────────────────────────────────────────────────────
30-
@current_player = Player.unscoped.find(@jwt_payload[:player_id])
31-
@current_organization = Organization.find(@jwt_payload[:organization_id])
30+
# Free agents (auto-cadastro via ArenaBR) têm organization_id: nil
31+
@current_player = Player.unscoped.find(@jwt_payload[:player_id])
3232

33-
Current.organization_id = @current_organization.id
34-
Rails.logger.info("[AUTH] Player token: player_id=#{@current_player.id} org=#{@current_organization.id}")
33+
org_id = @jwt_payload[:organization_id]
34+
@current_organization = org_id.present? ? Organization.find(org_id) : nil
35+
36+
Current.organization_id = @current_organization&.id
37+
org_label = @current_organization&.id || 'free_agent'
38+
Rails.logger.info("[AUTH] Player token: player_id=#{@current_player.id} org=#{org_label}")
3539
return
3640
end
3741

app/modules/admin/controllers/status_incidents_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ def require_admin_access
111111
end
112112

113113
def set_incident
114+
# StatusIncidents are platform-wide (not org-scoped) — intentionally unscoped.
115+
# This endpoint requires admin or owner role (see require_admin_access before_action).
116+
# nosemgrep: ruby.rails.security.brakeman.check-unscoped-find
114117
@incident = StatusIncident.find(params[:id])
115118
end
116119

app/modules/authentication/controllers/auth_controller.rb

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module Controllers
2626
#
2727
class AuthController < Api::V1::BaseController
2828
skip_before_action :authenticate_request!,
29-
only: %i[register login player_login forgot_password reset_password refresh]
29+
only: %i[register login player_login player_register forgot_password reset_password refresh]
3030

3131
# Registers a new user and organization
3232
#
@@ -170,6 +170,11 @@ def player_login # rubocop:disable Metrics/CyclomaticComplexity, Metrics/Perceiv
170170
tokens = JwtService.generate_player_tokens(player)
171171
player.update_last_login!
172172

173+
Rails.logger.info(
174+
"[AUTH] player_login: id=#{player.id} email=#{player_email} " \
175+
"org=#{player.organization_id || 'free_agent'} ip=#{request.remote_ip}"
176+
)
177+
173178
render_success(
174179
{
175180
player: {
@@ -212,6 +217,124 @@ def player_login # rubocop:disable Metrics/CyclomaticComplexity, Metrics/Perceiv
212217
render_error(message: 'Credenciais inválidas', code: 'INVALID_CREDENTIALS', status: :unauthorized)
213218
end
214219

220+
# Registers a new player (ArenaBR self-registration)
221+
#
222+
# Creates a Player without an organization — the player enters as a Free Agent.
223+
# Uses the separate player_password auth path, completely isolated from User auth.
224+
#
225+
# Security:
226+
# - organization_id is NEVER accepted from params (prevents privilege escalation)
227+
# - player_access_enabled is always set server-side
228+
# - password minimum 8 chars enforced at model level
229+
# - summoner_name is the only game identity accepted (no riot_puuid injection)
230+
# - rate-limited by rack-attack (player-register/ip: 5/hour)
231+
#
232+
# POST /api/v1/auth/player-register
233+
#
234+
# @param player_email [String] Email for player login
235+
# @param password [String] Password (min 8 chars)
236+
# @param password_confirmation [String] Must match password
237+
# @param summoner_name [String] Riot summoner name (e.g. "GameName#TAG")
238+
# @param discord_user_id [String] Discord username (optional)
239+
#
240+
def player_register # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
241+
player_email = params[:player_email]&.downcase&.strip
242+
summoner_name = params[:summoner_name]&.strip
243+
password = params[:password]
244+
password_conf = params[:password_confirmation]
245+
discord = params[:discord_user_id]&.strip
246+
247+
# ── Validate required fields ─────────────────────────────────────────
248+
missing = []
249+
missing << 'player_email' if player_email.blank?
250+
missing << 'password' if password.blank?
251+
missing << 'summoner_name' if summoner_name.blank?
252+
253+
if missing.any?
254+
return render_error(
255+
message: "Campos obrigatórios faltando: #{missing.join(', ')}",
256+
code: 'MISSING_FIELDS',
257+
status: :bad_request
258+
)
259+
end
260+
261+
# ── Password confirmation ─────────────────────────────────────────────
262+
if password != password_conf
263+
return render_error(
264+
message: 'Senhas não coincidem',
265+
code: 'PASSWORD_MISMATCH',
266+
status: :unprocessable_entity
267+
)
268+
end
269+
270+
# ── Duplicate email check ─────────────────────────────────────────────
271+
if Player.exists?(player_email: player_email)
272+
return render_error(
273+
message: 'Já existe uma conta de jogador com este email',
274+
code: 'DUPLICATE_EMAIL',
275+
status: :unprocessable_entity
276+
)
277+
end
278+
279+
# ── Duplicate summoner name check ──────────────────────────────────────
280+
if Player.exists?(['LOWER(summoner_name) = ?', summoner_name.downcase])
281+
return render_error(
282+
message: 'Summoner name já cadastrado na plataforma',
283+
code: 'DUPLICATE_SUMMONER',
284+
status: :unprocessable_entity
285+
)
286+
end
287+
288+
# ── Create player — SECURITY: organization_id always nil (free agent) ──
289+
player = Player.new(
290+
player_email: player_email,
291+
player_password: password,
292+
summoner_name: summoner_name,
293+
discord_user_id: discord.presence,
294+
player_access_enabled: true,
295+
status: 'active',
296+
role: 'top' # placeholder — player updates via profile
297+
# organization_id intentionally omitted (nil) — free agent
298+
)
299+
300+
unless player.save
301+
return render_error(
302+
message: 'Erro ao criar conta',
303+
code: 'VALIDATION_ERROR',
304+
status: :unprocessable_entity,
305+
details: player.errors.as_json
306+
)
307+
end
308+
309+
Rails.logger.info("[AUTH] Player registered: id=#{player.id} email=#{player_email} summoner=#{summoner_name}")
310+
311+
tokens = JwtService.generate_player_tokens(player)
312+
313+
render_created(
314+
{
315+
player: {
316+
id: player.id,
317+
summoner_name: player.summoner_name,
318+
player_email: player.player_email,
319+
discord_user_id: player.discord_user_id,
320+
role: player.role,
321+
status: player.status,
322+
organization_id: nil,
323+
organization_name: nil,
324+
is_free_agent: true,
325+
solo_queue_tier: nil,
326+
solo_queue_rank: nil,
327+
solo_queue_lp: nil,
328+
current_rank: nil
329+
}
330+
}.merge(tokens),
331+
message: 'Conta criada! Você está no pool de Free Agents do ArenaBR Season 1.'
332+
)
333+
rescue StandardError => e
334+
Rails.logger.error("Player register error: #{e.class} - #{e.message}")
335+
render_error(message: 'Erro interno ao criar conta', code: 'INTERNAL_ERROR', status: :internal_server_error)
336+
end
337+
215338
# Refreshes an access token using a refresh token
216339
#
217340
# Validates the refresh token and generates a new access token.

app/modules/matchmaking/services/match_suggestion_service.rb

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ def available_now
3232
.map { |w| build_suggestion(w, score_window(w)[:score]) }
3333
end
3434

35+
TIER_SCORE = {
36+
'CHALLENGER' => 9, 'GRANDMASTER' => 8, 'MASTER' => 7,
37+
'DIAMOND' => 6, 'EMERALD' => 5, 'PLATINUM' => 4,
38+
'GOLD' => 3, 'SILVER' => 2, 'BRONZE' => 1, 'IRON' => 0
39+
}.freeze
40+
41+
TIER_LABEL = {
42+
9 => 'Challenger', 8 => 'Grandmaster', 7 => 'Master',
43+
6 => 'Diamond', 5 => 'Emerald', 4 => 'Platinum',
44+
3 => 'Gold', 2 => 'Silver', 1 => 'Bronze', 0 => 'Iron'
45+
}.freeze
46+
3547
private
3648

3749
def find_candidate_windows
@@ -95,18 +107,6 @@ def build_suggestion(window, score)
95107
}
96108
end
97109

98-
TIER_SCORE = {
99-
'CHALLENGER' => 9, 'GRANDMASTER' => 8, 'MASTER' => 7,
100-
'DIAMOND' => 6, 'EMERALD' => 5, 'PLATINUM' => 4,
101-
'GOLD' => 3, 'SILVER' => 2, 'BRONZE' => 1, 'IRON' => 0
102-
}.freeze
103-
104-
TIER_LABEL = {
105-
9 => 'Challenger', 8 => 'Grandmaster', 7 => 'Master',
106-
6 => 'Diamond', 5 => 'Emerald', 4 => 'Platinum',
107-
3 => 'Gold', 2 => 'Silver', 1 => 'Bronze', 0 => 'Iron'
108-
}.freeze
109-
110110
# Returns confirmed W/L from cross-org validated reports only
111111
def compute_record(org)
112112
confirmed = ScrimResultReport.confirmed.where(organization: org)

app/modules/players/controllers/players_controller.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,8 @@ def player_params
372372
:solo_queue_wins, :solo_queue_losses,
373373
:flex_queue_tier, :flex_queue_rank, :flex_queue_lp,
374374
:peak_tier, :peak_rank, :peak_season,
375-
:riot_puuid, :riot_summoner_id,
375+
# riot_puuid and riot_summoner_id are intentionally excluded —
376+
# these fields must only be updated via the Riot sync service, never by user input.
376377
:twitter_handle, :twitch_channel, :instagram_handle,
377378
:notes
378379
)

app/modules/players/models/player.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ class Player < ApplicationRecord
3838
include Searchable
3939

4040
# Associations
41-
belongs_to :organization
41+
# optional: true — self-registered free agents (ArenaBR) can exist without an org
42+
belongs_to :organization, optional: true
4243
has_many :player_match_stats, dependent: :destroy
4344
has_many :matches, through: :player_match_stats
4445
has_many :champion_pools, dependent: :destroy

app/modules/scrims/services/scrim_result_validation_service.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ def call
3737
outcome = compare_with_opponent(report)
3838
{ status: outcome, report: report }
3939
end
40-
rescue ArgumentError => e
41-
{ status: :error, message: e.message }
42-
rescue ActiveRecord::RecordInvalid => e
40+
rescue ArgumentError, ActiveRecord::RecordInvalid => e
4341
{ status: :error, message: e.message }
4442
end
4543

config/brakeman.ignore

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
{
22
"ignored_warnings": [
33
{
4-
"fingerprint": "2df0aabf76fb1d7111cc2955d44606976db16719d3b2350db8c3b09fb5cbe613",
5-
"note": "False positive — :role is an in-game position (top/jungle/mid/adc/support), NOT a user authorization/admin role. The Player model has no privileged role attribute. Reviewed 2026-02-28."
4+
"fingerprint": "f2fd7351c85e531b66f6444ab8a89071e039b96befcdd5a6f897d3f55bb2d9dd",
5+
"note": "False positive — :role is a League of Legends in-game position (top/jungle/mid/adc/support), NOT a user authorization role. riot_puuid and riot_summoner_id were intentionally removed from this permit list. Reviewed 2026-04-09."
66
},
77
{
88
"fingerprint": "4e9eb66fae6365a1347b7ceb5de9c3834f80bfac2416c80526b09fc6a66eb4fa",
99
"note": "False positive — the SQL interpolation only inserts PostgreSQL numbered bind-parameter placeholders ($1, $2, ...). The actual type_names values are passed separately as exec_query bind parameters, never concatenated into the SQL string. No user input reaches this code path. Reviewed 2026-02-28."
1010
},
11-
{
12-
"fingerprint": "7bdf978768b5058d8c79ec541e68cb8643bbf19ab5ab52d60e6bd420a91bc416",
13-
"note": "False positive — safe_ids values come from Meilisearch hit IDs (internal database PKs) and are individually escaped via ActiveRecord::Base.connection.quote() before interpolation. User search query is sent only to Meilisearch, never interpolated into SQL. Reviewed 2026-02-28."
14-
},
1511
{
1612
"fingerprint": "82553a8da70acefb77b22bab7fb95616b808a9604a23dff455508e0ad77e3107",
1713
"note": "False positive — the SQL interpolation only inserts PostgreSQL numbered bind-parameter placeholders ($1, $2, ...). The actual type_names values are passed separately as exec_query bind parameters. No user input reaches this code path. Reviewed 2026-04-05."
@@ -31,10 +27,6 @@
3127
{
3228
"fingerprint": "a53e36aea1309fb0af3b08b9d5403838087ed98264a2a158a98adde5f6d496d3",
3329
"note": "False positive — :role is a League of Legends champion role (adc/jungle/mid/support/top), NOT a user authorization role. SavedBuild model has no admin/banned/account_id or privilege-escalation fields. Reviewed 2026-02-28."
34-
},
35-
{
36-
"fingerprint": "c98c465cbb4e6f99aae70dcf4cef99d859236873e06d64cc46a02a1f51f508a5",
37-
"note": "False positive — identical pattern to pg_type_cache. Only PostgreSQL numbered placeholders ($1, $2, ...) are interpolated; actual type name values are bound separately via exec_query params. type_names defaults to a hard-coded whitelist of pg type strings, never accepts raw user input. Reviewed 2026-02-28."
3830
}
3931
]
4032
}

0 commit comments

Comments
 (0)