From 84e3424fcf8834026dc452748eecc4b98b3f4422 Mon Sep 17 00:00:00 2001 From: sto Date: Sun, 9 Aug 2026 10:09:04 +0200 Subject: [PATCH] Secure public contestant forms by asking the code first --- app/controllers/contestants_controller.rb | 69 ++++++++++++++++--- app/views/completions/_form.html.slim | 4 +- .../validate_organizer_code.html.slim | 20 ++++++ config/locales/en.yml | 1 + config/locales/fr.yml | 1 + config/routes.rb | 2 + 6 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 app/views/completions/validate_organizer_code.html.slim diff --git a/app/controllers/contestants_controller.rb b/app/controllers/contestants_controller.rb index 545c786..857e564 100644 --- a/app/controllers/contestants_controller.rb +++ b/app/controllers/contestants_controller.rb @@ -7,7 +7,7 @@ class ContestantsController < ApplicationController before_action :set_contest, only: %i[ index edit new create update destroy import upload_csv convert_csv finalize_import export generate_qrcodes generate_qrcodes_pdf generate_qrcodes_archive ] before_action :set_contestant, only: %i[ destroy edit update] before_action :set_completions, only: %i[edit update ] - skip_before_action :require_authentication, only: %i[ get_public_completion post_public_completion public_completion_updated ] + skip_before_action :require_authentication, only: %i[ get_public_completion get_public_completion_code post_public_completion_code post_public_completion public_completion_updated ] def index authorize @contest @@ -162,23 +162,74 @@ class ContestantsController < ApplicationController def get_public_completion skip_authorization + @public = true @contestant = Contestant.find(params[:contestant_id]) if !@contestant || !@contestant.contest.code.present? || !@contestant.contest.organizer_form not_found and return end + I18n.locale = @contestant.contest.lang + @completion = Completion.new + @title = @contestant.contest.name + + render "completions/validate_organizer_code" + end + + def post_public_completion_code + skip_authorization + @public = true + + @contestant = Contestant.find(params[:contestant_id]) + if !@contestant || !@contestant.contest.code.present? || !@contestant.contest.organizer_form + not_found and return + end + I18n.locale = @contestant.contest.lang + @title = @contestant.contest.name + + @completion = Completion.new(completion_params) + @completion.contest = @contestant.contest + @completion.contestant = @contestant + if !@completion.code.present? + to_modify = true + @completion.code = "incorrect-xZy" + end + @completion.save + if @completion.code != @contestant.contest.code + if to_modify + @completion.code = nil + end + render "completions/validate_organizer_code", status: :unprocessable_entity + else + redirect_to "/public/p/#{@contestant.id}/code?code=#{@completion.code}" + end + end + + def get_public_completion_code + skip_authorization + @public = true + + @contestant = Contestant.find(params[:contestant_id]) + if !@contestant || !@contestant.contest.code.present? || !@contestant.contest.organizer_form + not_found and return + end + I18n.locale = @contestant.contest.lang + + if !params.key?(:code) || params[:code] != @contestant.contest.code + redirect_to "/public/p/#{@contestant.id}" and return + end + @contest = @contestant.contest - I18n.locale = @contest.lang @puzzles = @contest.puzzles.where(hidden: false).or(@contest.puzzles.where(hidden: nil)).order(:id) @completion = Completion.new @completion.completed = true - @public = true + @completion.code = @contest.code render "completions/_form", locals: { completion: @completion, submit_text: t("helpers.buttons.create"), method: :post, url: "/public/p/#{params[:contestant_id]}" } end def post_public_completion skip_authorization + @public = true @contestant = Contestant.find(params[:contestant_id]) if !@contestant || !@contestant.contest.code.present? @@ -188,21 +239,17 @@ class ContestantsController < ApplicationController I18n.locale = @contest.lang @completion = Completion.new(completion_params) + if @completion.code != @contest.code + redirect_to "/public/p/#{@contestant.id}" and return + end + @completion.contest = @contest @completion.contestant = @contestant - if !@completion.code.present? - to_modify = true - @completion.code = "incorrect-xZy" - end if @completion.save extend_completions!(@completion.contestant) redirect_to "/public/p/#{params[:contestant_id]}/updated" else @puzzles = @contest.puzzles - @public = true - if to_modify - @completion.code = nil - end render "completions/_form", locals: { completion: @completion, submit_text: t("helpers.buttons.create"), method: :post, url: "/public/p/#{params[:contestant_id]}" }, status: :unprocessable_entity end end diff --git a/app/views/completions/_form.html.slim b/app/views/completions/_form.html.slim index 0352010..a73dfdc 100644 --- a/app/views/completions/_form.html.slim +++ b/app/views/completions/_form.html.slim @@ -126,9 +126,7 @@ .row.mb-3 .col .form-floating - = form.text_field :code, autocomplete: "off", class: "form-control" - = form.label :code - = t("completions.form.code") + = form.hidden_field :code, autocomplete: "off", class: "form-control" .row .col = form.submit submit_text, class: "btn btn-primary" \ No newline at end of file diff --git a/app/views/completions/validate_organizer_code.html.slim b/app/views/completions/validate_organizer_code.html.slim new file mode 100644 index 0000000..cc94366 --- /dev/null +++ b/app/views/completions/validate_organizer_code.html.slim @@ -0,0 +1,20 @@ +- if @public && @contestant.contest.puzzles.length == @contestant.completions.length + h4 + = t("completions.form.validate_name", name: @contestant.name) + .mt-3.alert.alert-warning + = t("completions.form.all_finished", name: @contestant.name) +- else + .row.mt-2 + .col + h4 + = t("completions.form.validate_name", name: @contestant.name) + = form_with model: @completion, url: "/public/p/#{@contestant.id}/code", method: "post" do |form| + .row.mt-3 + .col + .form-floating + = form.text_field :code, autocomplete: "off", class: "form-control" + = form.label :code + = t("completions.form.code") + .row.mt-3 + .col + = form.submit t("helpers.buttons.validate"), class: "btn btn-primary" \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index aa9bd3e..8d497fe 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -349,6 +349,7 @@ en: stopwatch_reset: Reset stopwatch_start: Start update: Save modifications + validate: Validate field: Field none: No field selected rank: Rank diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a576cd7..dee2c32 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -320,6 +320,7 @@ fr: stopwatch_reset: Ré-initialiser stopwatch_start: Démarrer update: Enregistrer les modifications + validate: Valider field: Champ none: Aucun champ sélectionné rank: Rang diff --git a/config/routes.rb b/config/routes.rb index 2b2b94d..89a863c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -63,6 +63,8 @@ Rails.application.routes.draw do get "public/:id/offline/:token/completed", to: "contests#offline_completed" get "public/p/:contestant_id", to: "contestants#get_public_completion" post "public/p/:contestant_id", to: "contestants#post_public_completion" + get "public/p/:contestant_id/code", to: "contestants#get_public_completion_code" + post "public/p/:contestant_id/code", to: "contestants#post_public_completion_code" get "public/p/:contestant_id/updated", to: "contestants#public_completion_updated" direct :public_scoreboard do |contest|