From fb9da1d1bd854f54890c426ae6783b0754d65d6b Mon Sep 17 00:00:00 2001 From: ewez Date: Tue, 15 Sep 2026 21:30:59 +0200 Subject: [PATCH 01/10] Added basic Organization model --- app/models/organization.rb | 13 +++++++++++++ app/models/story_group.rb | 2 ++ app/models/story_group_invite.rb | 2 +- app/models/user.rb | 2 ++ db/migrate/20260915175517_create_organizations.rb | 10 ++++++++++ db/schema.rb | 4 +++- test/factories/organizations.rb | 6 ++++++ test/models/organization_test.rb | 7 +++++++ 8 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 app/models/organization.rb create mode 100644 db/migrate/20260915175517_create_organizations.rb create mode 100644 test/factories/organizations.rb create mode 100644 test/models/organization_test.rb diff --git a/app/models/organization.rb b/app/models/organization.rb new file mode 100644 index 00000000..206d6969 --- /dev/null +++ b/app/models/organization.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Organization < ApplicationRecord + + has_many :users, foreign_key: 'university_id', dependent: :destroy + + validates :name, length: { maximum: 100 } + + def join_condition + users.size < max_members + end + +end diff --git a/app/models/story_group.rb b/app/models/story_group.rb index b8ea594c..c51654b9 100644 --- a/app/models/story_group.rb +++ b/app/models/story_group.rb @@ -22,6 +22,8 @@ class StoryGroup < ApplicationRecord validate :acceptable_currency_icon validates :default_lives, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + delegate :organization, to: :owner + def acceptable_icon return unless icon.attached? diff --git a/app/models/story_group_invite.rb b/app/models/story_group_invite.rb index 462c5541..1bbf1744 100644 --- a/app/models/story_group_invite.rb +++ b/app/models/story_group_invite.rb @@ -21,7 +21,7 @@ def expire_time_condition end def usable? - use_count_condition && expire_time_condition + use_count_condition && expire_time_condition && story_group.organization.join_condition end def use! diff --git a/app/models/user.rb b/app/models/user.rb index fbfe3f90..c2b7466a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -16,6 +16,8 @@ class User < ApplicationRecord has_many :notifications, dependent: :destroy has_one :login_token, dependent: :destroy + belongs_to :organization, optional: true + normalizes :email, with: ->(email) { email.strip.downcase } validates :email, length: { maximum: 255 }, uniqueness: true, allow_nil: true diff --git a/db/migrate/20260915175517_create_organizations.rb b/db/migrate/20260915175517_create_organizations.rb new file mode 100644 index 00000000..b878256e --- /dev/null +++ b/db/migrate/20260915175517_create_organizations.rb @@ -0,0 +1,10 @@ +class CreateOrganizations < ActiveRecord::Migration[8.1] + def change + create_table :organizations do |t| + t.string :name + t.integer :max_members + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 5b44a1fc..79165654 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_05_27_203608) do +ActiveRecord::Schema[8.1].define(version: 2026_09_13_120000) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -190,10 +190,12 @@ t.datetime "created_at", null: false t.integer "current_currency", default: 0 t.integer "lives" + t.string "nickname" t.integer "story_group_id" t.integer "total_currency", default: 0 t.datetime "updated_at", null: false t.integer "user_id" + t.index "story_group_id, lower((nickname)::text)", name: "index_story_group_students_on_group_and_lower_nickname", unique: true, where: "(nickname IS NOT NULL)" t.index ["user_id", "story_group_id"], name: "index_story_group_students_on_user_id_and_story_group_id", unique: true end diff --git a/test/factories/organizations.rb b/test/factories/organizations.rb new file mode 100644 index 00000000..fb059d0a --- /dev/null +++ b/test/factories/organizations.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :organization do + name { "MyString" } + max_members { 1 } + end +end diff --git a/test/models/organization_test.rb b/test/models/organization_test.rb new file mode 100644 index 00000000..3f86f1cd --- /dev/null +++ b/test/models/organization_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class OrganizationTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end From bc8a8f1f34d099b9753a402d1388bb4ec26ea5df Mon Sep 17 00:00:00 2001 From: ewez43 Date: Wed, 23 Sep 2026 20:28:23 +0200 Subject: [PATCH 02/10] added basic organizations CRUD controller for global admin --- .../shared/main_sidebar_component.rb | 7 +- app/controllers/organizations_controller.rb | 64 +++++++++++++++++++ app/helpers/organizations_helper.rb | 2 + app/models/organization.rb | 2 +- app/models/story_group_invite.rb | 2 +- app/policies/organization_policy.rb | 39 +++++++++++ app/services/session_user_builder.rb | 11 ++++ app/views/organizations/edit.html.haml | 2 + app/views/organizations/index.html.haml | 19 ++++++ app/views/organizations/new.html.haml | 2 + app/views/organizations/show.html.haml | 2 + config/routes.rb | 2 + ..._add_university_number_to_organizations.rb | 6 ++ ...ve_university_number_from_organizations.rb | 5 ++ db/queue_schema.rb | 2 +- db/schema.rb | 11 +++- .../organizations_controller_test.rb | 31 +++++++++ test/factories/organizations.rb | 2 +- 18 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 app/controllers/organizations_controller.rb create mode 100644 app/helpers/organizations_helper.rb create mode 100644 app/policies/organization_policy.rb create mode 100644 app/views/organizations/edit.html.haml create mode 100644 app/views/organizations/index.html.haml create mode 100644 app/views/organizations/new.html.haml create mode 100644 app/views/organizations/show.html.haml create mode 100644 db/migrate/20260922155430_add_university_number_to_organizations.rb create mode 100644 db/migrate/20260923171241_remove_university_number_from_organizations.rb create mode 100644 test/controllers/organizations_controller_test.rb diff --git a/app/components/shared/main_sidebar_component.rb b/app/components/shared/main_sidebar_component.rb index 8bbfc90f..97e145b3 100644 --- a/app/components/shared/main_sidebar_component.rb +++ b/app/components/shared/main_sidebar_component.rb @@ -44,7 +44,12 @@ def primary_buttons [ { text: 'Strona główna', path: helpers.home_path, icon: 'fa-house' }, { text: 'Grupy Fabularne', path: helpers.story_groups_path, icon: 'fa-book' }, - ] + (if user.global_admin? + { + text: 'Organizacje', path: helpers.organizations_path, icon: 'fa-briefcase', + } + end), + ].compact end def story_group_buttons diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb new file mode 100644 index 00000000..648593c8 --- /dev/null +++ b/app/controllers/organizations_controller.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +class OrganizationsController < ApplicationController + before_action :set_organization, only: %i[show edit update destroy] + + def index + @organizations = policy_scope(Organization) + end + + def show + authorize @organization + end + + def new + @organization = Organization.new + authorize @organization + end + + def create + @organization = Organization.new(organization_params) + + authorize @organization + + if @organization.save + redirect_outside_turbo_frame organization_path(@organization), + notice: 'Pomyślnie utworzono organizację.' + else + render :new, status: :unprocessable_content + end + end + + def edit + authorize @organization + end + + def update + if @organization.update(organization_params) + redirect_outside_turbo_frame organization_path(@organization), + notice: 'Pomyślnie zaktualizowano organizację.' + else + render :edit, status: :unprocessable_content + end + end + + def destroy + authorize @organization + + @organization.destroy! + redirect_to organizations_path, notice: 'Pomyślnie usunięto organizację.', status: :see_other + end + + def set_organization + @organization = Organization.find(params.expect(:id)) + end + + def organization_params + params.expect( + organization: %i[ + name + max_members + ], + ) + end +end diff --git a/app/helpers/organizations_helper.rb b/app/helpers/organizations_helper.rb new file mode 100644 index 00000000..24cc9a80 --- /dev/null +++ b/app/helpers/organizations_helper.rb @@ -0,0 +1,2 @@ +module OrganizationsHelper +end diff --git a/app/models/organization.rb b/app/models/organization.rb index 206d6969..0de69d2b 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -2,7 +2,7 @@ class Organization < ApplicationRecord - has_many :users, foreign_key: 'university_id', dependent: :destroy + has_many :users, dependent: :destroy validates :name, length: { maximum: 100 } diff --git a/app/models/story_group_invite.rb b/app/models/story_group_invite.rb index 1bbf1744..462c5541 100644 --- a/app/models/story_group_invite.rb +++ b/app/models/story_group_invite.rb @@ -21,7 +21,7 @@ def expire_time_condition end def usable? - use_count_condition && expire_time_condition && story_group.organization.join_condition + use_count_condition && expire_time_condition end def use! diff --git a/app/policies/organization_policy.rb b/app/policies/organization_policy.rb new file mode 100644 index 00000000..bc86c7b2 --- /dev/null +++ b/app/policies/organization_policy.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class OrganizationPolicy < ApplicationPolicy + class Scope < ApplicationPolicy::Scope + def resolve + if user.global_admin? + scope.all + elsif user.organization_admin? + scope.where(id: user.organization.id) + else + scope.none + end + end + end + + def show? + user.global_admin? || user.organization_admin? + end + + def new? + user.global_admin? + end + + def create? + user.global_admin? + end + + def edit? + user.global_admin? + end + + def update? + user.global_admin? + end + + def destroy? + user.global_admin? + end +end diff --git a/app/services/session_user_builder.rb b/app/services/session_user_builder.rb index ea607985..24957108 100644 --- a/app/services/session_user_builder.rb +++ b/app/services/session_user_builder.rb @@ -12,6 +12,17 @@ def build user = find_or_initialize_user if user.new_record? || user.first_login? + # organization = Organization.find_by( + # university_name: @provider.university_name, + # ) + # organization.with_lock do + # if organization.join_condition + # user.assign_attributes(user_params) + # user.organization = organization + # user.first_login = false + # user.save!(context: :account_setup) + # end + # end user.assign_attributes(user_params) user.first_login = false user.save!(context: :account_setup) diff --git a/app/views/organizations/edit.html.haml b/app/views/organizations/edit.html.haml new file mode 100644 index 00000000..6d31ac70 --- /dev/null +++ b/app/views/organizations/edit.html.haml @@ -0,0 +1,2 @@ +%h1 Organizations#edit +%p Find me in app/views/organizations/edit.html.haml diff --git a/app/views/organizations/index.html.haml b/app/views/organizations/index.html.haml new file mode 100644 index 00000000..6e3d37ef --- /dev/null +++ b/app/views/organizations/index.html.haml @@ -0,0 +1,19 @@ +- content_for :title, "Organizacje" + +.dark-page + - if @organizations.empty? + %p.text-dimmed Brak organizacji. + - else + .row.g-3 + - @organizations.each do |organization| + .col-12.col-sm-6.col-md-4.col-xl-3 + = link_to organization_path(organization), class: "text-decoration-none d-block h-100" do + .dark-card.cursor-pointer + + .dark-card-body + .dark-card-title= organization.name + + - if policy(:organization).create? + = link_to new_organization_path, class: "btn btn-primary position-fixed bottom-0 end-0 mb-4 me-4", data: { turbo_frame: "modal" } do + %i.fa-solid.fa-plus.me-2 + Utwórz organizację diff --git a/app/views/organizations/new.html.haml b/app/views/organizations/new.html.haml new file mode 100644 index 00000000..8a5132e1 --- /dev/null +++ b/app/views/organizations/new.html.haml @@ -0,0 +1,2 @@ +%h1 Organizations#new +%p Find me in app/views/organizations/new.html.haml diff --git a/app/views/organizations/show.html.haml b/app/views/organizations/show.html.haml new file mode 100644 index 00000000..ab3e63bd --- /dev/null +++ b/app/views/organizations/show.html.haml @@ -0,0 +1,2 @@ +%h1 Organizations#show +%p Find me in app/views/organizations/show.html.haml diff --git a/config/routes.rb b/config/routes.rb index 675a646c..d9aac340 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,6 +41,8 @@ post :mark_as_read, on: :collection end + resources :organizations + namespace 'auth' do get '/:provider/callback', to: 'usos#create', as: :callback resource :passwordless, only: %i[new create], controller: 'passwordless' diff --git a/db/migrate/20260922155430_add_university_number_to_organizations.rb b/db/migrate/20260922155430_add_university_number_to_organizations.rb new file mode 100644 index 00000000..450427c4 --- /dev/null +++ b/db/migrate/20260922155430_add_university_number_to_organizations.rb @@ -0,0 +1,6 @@ +class AddUniversityNumberToOrganizations < ActiveRecord::Migration[8.1] + def change + add_column :organizations, :university_number, :string + add_index :organizations, :university_number, unique: true + end +end diff --git a/db/migrate/20260923171241_remove_university_number_from_organizations.rb b/db/migrate/20260923171241_remove_university_number_from_organizations.rb new file mode 100644 index 00000000..ea40e8aa --- /dev/null +++ b/db/migrate/20260923171241_remove_university_number_from_organizations.rb @@ -0,0 +1,5 @@ +class RemoveUniversityNumberFromOrganizations < ActiveRecord::Migration[8.1] + def change + remove_column :organizations, :university_number, :string + end +end diff --git a/db/queue_schema.rb b/db/queue_schema.rb index 2181653f..3262920a 100644 --- a/db/queue_schema.rb +++ b/db/queue_schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 1) do +ActiveRecord::Schema[8.1].define(version: 0) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" diff --git a/db/schema.rb b/db/schema.rb index 79165654..97f9f40d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_09_13_120000) do +ActiveRecord::Schema[8.1].define(version: 2026_09_23_171241) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -165,6 +165,13 @@ t.index ["user_id"], name: "index_notifications_on_user_id" end + create_table "organizations", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "max_members" + t.string "name" + t.datetime "updated_at", null: false + end + create_table "ranks", force: :cascade do |t| t.datetime "created_at", null: false t.integer "discount" @@ -190,12 +197,10 @@ t.datetime "created_at", null: false t.integer "current_currency", default: 0 t.integer "lives" - t.string "nickname" t.integer "story_group_id" t.integer "total_currency", default: 0 t.datetime "updated_at", null: false t.integer "user_id" - t.index "story_group_id, lower((nickname)::text)", name: "index_story_group_students_on_group_and_lower_nickname", unique: true, where: "(nickname IS NOT NULL)" t.index ["user_id", "story_group_id"], name: "index_story_group_students_on_user_id_and_story_group_id", unique: true end diff --git a/test/controllers/organizations_controller_test.rb b/test/controllers/organizations_controller_test.rb new file mode 100644 index 00000000..1965ef36 --- /dev/null +++ b/test/controllers/organizations_controller_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'test_helper' + +class OrganizationsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = FactoryBot.create(:user, role: :global_admin) + sign_in @user + @organization = FactoryBot.create(:organization) + end + + test 'should get index' do + get organizations_url + assert_response :success + end + + test 'should get show' do + get organization_url(@organization) + assert_response :success + end + + test 'should get new' do + get new_organization_url + assert_response :success + end + + test 'should get edit' do + get edit_organization_url(@organization) + assert_response :success + end +end diff --git a/test/factories/organizations.rb b/test/factories/organizations.rb index fb059d0a..4a4d3509 100644 --- a/test/factories/organizations.rb +++ b/test/factories/organizations.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :organization do - name { "MyString" } + name { 'MyString' } max_members { 1 } end end From eb0acfea7a73337dbd01bd42bbd83f6a8a38b24f Mon Sep 17 00:00:00 2001 From: ewez43 Date: Sun, 27 Sep 2026 18:08:52 +0200 Subject: [PATCH 03/10] Added ability for global admin to add org admins --- app/controllers/organizations_controller.rb | 31 ++++++++++++++++++- app/models/organization.rb | 1 + app/policies/organization_policy.rb | 8 +++++ app/views/organizations/_form.html.haml | 25 +++++++++++++++ .../_global_admin_show.html.haml | 25 +++++++++++++++ .../organizations/_organization.html.haml | 15 +++++++++ app/views/organizations/_user_form.html.haml | 25 +++++++++++++++ app/views/organizations/edit.html.haml | 9 ++++-- app/views/organizations/new.html.haml | 9 ++++-- app/views/organizations/new_admin.html.haml | 7 +++++ app/views/organizations/show.html.haml | 5 +-- config/routes.rb | 7 ++++- ...0925121911_add_organization_id_to_users.rb | 5 +++ db/schema.rb | 5 ++- 14 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 app/views/organizations/_form.html.haml create mode 100644 app/views/organizations/_global_admin_show.html.haml create mode 100644 app/views/organizations/_organization.html.haml create mode 100644 app/views/organizations/_user_form.html.haml create mode 100644 app/views/organizations/new_admin.html.haml create mode 100644 db/migrate/20260925121911_add_organization_id_to_users.rb diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 648593c8..d20fc18a 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class OrganizationsController < ApplicationController - before_action :set_organization, only: %i[show edit update destroy] + before_action :set_organization, only: %i[show edit update destroy new_admin add_admin] def index @organizations = policy_scope(Organization) @@ -9,6 +9,8 @@ def index def show authorize @organization + + @admins = @organization.users.organization_admin end def new @@ -49,6 +51,24 @@ def destroy redirect_to organizations_path, notice: 'Pomyślnie usunięto organizację.', status: :see_other end + def new_admin + @org_admin = User.new + end + + def add_admin + @org_admin = User.new(user_params) + @org_admin.organization = @organization + @org_admin.university_name = @organization.name + @org_admin.role = 3 + + if @org_admin.save + redirect_outside_turbo_frame organization_path(@organizaton), + notice: 'Pomyślnie dodano administratora organizacji.' + else + render :new_admin, status: :unprocessable_content + end + end + def set_organization @organization = Organization.find(params.expect(:id)) end @@ -61,4 +81,13 @@ def organization_params ], ) end + + def user_params + params.expect( + user: %i[ + email + full_name + ], + ) + end end diff --git a/app/models/organization.rb b/app/models/organization.rb index 0de69d2b..74a08ae5 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -5,6 +5,7 @@ class Organization < ApplicationRecord has_many :users, dependent: :destroy validates :name, length: { maximum: 100 } + validate :join_condition def join_condition users.size < max_members diff --git a/app/policies/organization_policy.rb b/app/policies/organization_policy.rb index bc86c7b2..552685a2 100644 --- a/app/policies/organization_policy.rb +++ b/app/policies/organization_policy.rb @@ -36,4 +36,12 @@ def update? def destroy? user.global_admin? end + + def new_admin? + user.global_admin? + end + + def add_admin? + user.global_admin? + end end diff --git a/app/views/organizations/_form.html.haml b/app/views/organizations/_form.html.haml new file mode 100644 index 00000000..929634c1 --- /dev/null +++ b/app/views/organizations/_form.html.haml @@ -0,0 +1,25 @@ += form_with model: organization do |form| + - if organization.errors.any? + .form-errors.p-3.mb-3 + %h2= "#{pluralize(organization.errors.count, "błąd", "błędy")} uniemożliwił zapisanie organizacji:" + %ul + - organization.errors.each do |error| + %li= error.full_message + + .form-section.p-4.mb-3 + %h2.section-title.mb-3 Nazwa + + .field-group.mb-3 + = form.label :name, "Nazwa" + = form.text_field :name, class: "form-control" + + .form-section.p-4.mb-3 + %h2.section-title.mb-3 Maksymalna ilość członków + + .field-group.mb-3 + = form.label :max_members, "Maksymalna ilość członków" + = form.number_field :max_members, min: 1, step: 1, class: "form-control" + + .form-actions.gap-3.mt-4 + = link_to "Anuluj", cancel_path, class: "btn-cancel py-2 px-3", data: { turbo_frame: "_top" } + = form.submit 'Zapisz', class: "btn-submit py-2 px-4" diff --git a/app/views/organizations/_global_admin_show.html.haml b/app/views/organizations/_global_admin_show.html.haml new file mode 100644 index 00000000..1fc52240 --- /dev/null +++ b/app/views/organizations/_global_admin_show.html.haml @@ -0,0 +1,25 @@ +.dark-page + .d-flex.align-items-center.justify-content-between.mb-4 + %h1.h3.mb-0= @organization.name + .d-flex.align-items-center.gap-2 + - if policy(@organization).update? || policy(@organization).destroy? + .dropdown + %button.btn.btn-secondary.border-0{ type: "button", data: { bs_toggle: "dropdown" }, "aria-expanded": false } + %i.fa-solid.fa-ellipsis-vertical + .dropdown-menu.dropdown-menu-end + - if policy(@organization).update? + = link_to edit_organization_path(@organization), class: "btn btn-flat border-0 text-start w-100 rounded", data: { turbo_frame: "modal" } do + %i.fa-solid.fa-pen.me-2 + Edytuj organizację + - if policy(@organization).add_admin? + = link_to new_admin_organization_path(@organization), class: "btn btn-flat border-0 text-start w-100 rounded", data: { turbo_frame: "modal" } do + %i.fa-solid.fa-user-shield.me-2 + Dodaj administatora + - if policy(@organization).destroy? + = button_to @organization, method: :delete, + form: { data: { turbo_confirm: "Czy na pewno chcesz usunąć organizację \"#{@organization.name}\"?" } }, + class: "btn btn-flat text-danger border-0 text-start w-100 rounded" do + %i.fa-solid.fa-trash.me-1 + Usuń organizację + + = render 'organization' diff --git a/app/views/organizations/_organization.html.haml b/app/views/organizations/_organization.html.haml new file mode 100644 index 00000000..1355f9d8 --- /dev/null +++ b/app/views/organizations/_organization.html.haml @@ -0,0 +1,15 @@ +%div{id: dom_id(@organization)} + %p + %strong Name: + = @organization.name + + %p + %strong Max members: + = @organization.max_members + + %p + %strong Organization admins: + - for admin in @admins + = admin.email + = admin.full_name + diff --git a/app/views/organizations/_user_form.html.haml b/app/views/organizations/_user_form.html.haml new file mode 100644 index 00000000..269b8ff1 --- /dev/null +++ b/app/views/organizations/_user_form.html.haml @@ -0,0 +1,25 @@ += form_with model: user, url: add_admin_organization_path(@organization) do |form| + - if user.errors.any? + .form-errors.p-3.mb-3 + %h2= "#{pluralize(user.errors.count, "błąd", "błędy")} uniemożliwił dodanie administratora:" + %ul + - user.errors.each do |error| + %li= error.full_message + + .form-section.p-4.mb-3 + %h2.section-title.mb-3 Email + + .field-group.mb-3 + = form.label :email, "Email" + = form.text_field :email, class: "form-control" + + .form-section.p-4.mb-3 + %h2.section-title.mb-3 Imię i nazwisko + + .field-group.mb-3 + = form.label :full_name, "Imię i nazwisko" + = form.text_field :full_name, class: "form-control" + + .form-actions.gap-3.mt-4 + = link_to "Anuluj", cancel_path, class: "btn-cancel py-2 px-3", data: { turbo_frame: "_top" } + = form.submit 'Zapisz', class: "btn-submit py-2 px-4" diff --git a/app/views/organizations/edit.html.haml b/app/views/organizations/edit.html.haml index 6d31ac70..e7306e46 100644 --- a/app/views/organizations/edit.html.haml +++ b/app/views/organizations/edit.html.haml @@ -1,2 +1,7 @@ -%h1 Organizations#edit -%p Find me in app/views/organizations/edit.html.haml +- content_for :title, "Edytuj organizację — #{@organization.name}" + += turbo_frame_tag 'modal' do + .modal-overlay.py-5.px-3 + .form-card.p-4 + %h1.page-title.mb-4 Edytuj organizację + = render "form", organization: @organization, cancel_path: organization_path(@organization), submit_label: "Save" diff --git a/app/views/organizations/new.html.haml b/app/views/organizations/new.html.haml index 8a5132e1..e2fb3a9f 100644 --- a/app/views/organizations/new.html.haml +++ b/app/views/organizations/new.html.haml @@ -1,2 +1,7 @@ -%h1 Organizations#new -%p Find me in app/views/organizations/new.html.haml +- content_for :title, "Nowa organizacja" + += turbo_frame_tag 'modal' do + .modal-overlay.py-5.px-3 + .form-card.p-4 + %h1.page-title.mb-4 Nowa organizacja + = render "form", organization: @organization, cancel_path: organizations_path, submit_label: "Create" diff --git a/app/views/organizations/new_admin.html.haml b/app/views/organizations/new_admin.html.haml new file mode 100644 index 00000000..a41ee34b --- /dev/null +++ b/app/views/organizations/new_admin.html.haml @@ -0,0 +1,7 @@ +- content_for :title, "Dodaj administratora organizacji" + += turbo_frame_tag 'modal' do + .modal-overlay.py-5.px-3 + .form-card.p-4 + %h1.page-title.mb-4 Dodaj administratora organizacji + = render "user_form", user: @org_admin, cancel_path: organization_path(@organization), submit_label: "Add" diff --git a/app/views/organizations/show.html.haml b/app/views/organizations/show.html.haml index ab3e63bd..671142fc 100644 --- a/app/views/organizations/show.html.haml +++ b/app/views/organizations/show.html.haml @@ -1,2 +1,3 @@ -%h1 Organizations#show -%p Find me in app/views/organizations/show.html.haml +- content_for :title, @organization.name + += render 'global_admin_show' diff --git a/config/routes.rb b/config/routes.rb index d9aac340..1b1e7d30 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,7 +41,12 @@ post :mark_as_read, on: :collection end - resources :organizations + resources :organizations do + member do + get :new_admin + post :add_admin + end + end namespace 'auth' do get '/:provider/callback', to: 'usos#create', as: :callback diff --git a/db/migrate/20260925121911_add_organization_id_to_users.rb b/db/migrate/20260925121911_add_organization_id_to_users.rb new file mode 100644 index 00000000..4b265de0 --- /dev/null +++ b/db/migrate/20260925121911_add_organization_id_to_users.rb @@ -0,0 +1,5 @@ +class AddOrganizationIdToUsers < ActiveRecord::Migration[8.1] + def change + add_reference :users, :organization, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 97f9f40d..6e7409eb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_09_23_171241) do +ActiveRecord::Schema[8.1].define(version: 2026_09_25_121911) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -258,12 +258,14 @@ t.string "email" t.boolean "first_login", default: true t.string "full_name" + t.bigint "organization_id" t.integer "role", default: 1 t.string "university_name" t.string "university_number" t.datetime "updated_at", null: false t.string "usos_id" t.index ["email"], name: "index_users_on_email", unique: true + t.index ["organization_id"], name: "index_users_on_organization_id" t.index ["usos_id", "university_name"], name: "index_users_on_usos_id_and_university_name", unique: true end @@ -297,4 +299,5 @@ add_foreign_key "students_badges", "story_group_students" add_foreign_key "students_items", "items" add_foreign_key "students_items", "story_group_students" + add_foreign_key "users", "organizations" end From feac7df0c34453100f720475516f3577a701486b Mon Sep 17 00:00:00 2001 From: ewez Date: Sat, 10 Oct 2026 21:11:24 +0200 Subject: [PATCH 04/10] Added global and organization admin modules --- .../shared/main_sidebar_component.rb | 5 ++ app/controllers/account_setups_controller.rb | 36 ++++++++ app/controllers/application_controller.rb | 5 ++ app/controllers/auth/usos_controller.rb | 4 + .../organizations/users_controller.rb | 50 +++++++++++ app/controllers/organizations_controller.rb | 87 +++++++++++++++++-- app/controllers/sessions_controller.rb | 1 + app/helpers/organizations_helper.rb | 14 +++ .../controllers/table_search_controller.ts | 16 +++- app/mailers/new_user_mailer.rb | 11 +++ app/mailers/organization_admin_mailer.rb | 11 +++ app/models/login_token.rb | 3 +- app/models/organization.rb | 20 +++-- app/models/user.rb | 20 ++++- app/policies/organization_policy.rb | 20 ++++- app/policies/story_group_policy.rb | 4 +- app/services/session_user_builder.rb | 45 ++++++---- app/views/account_setups/edit.html.haml | 23 +++++ .../invitation_email.html.haml | 7 ++ .../invitation_email.text.haml | 3 + .../invitation_email.html.haml | 6 ++ .../invitation_email.text.haml | 3 + app/views/organizations/_admin_form.html.haml | 18 ++++ .../_organization_admin_show.html.haml | 51 +++++++++++ app/views/organizations/_user_form.html.haml | 30 ++++++- app/views/organizations/new_admin.html.haml | 2 +- app/views/organizations/new_user.html.haml | 8 ++ app/views/organizations/show.html.haml | 5 +- app/views/organizations/users/edit.html.haml | 8 ++ config/routes.rb | 6 ++ ..._add_unique_index_to_organizations_name.rb | 7 ++ db/queue_schema.rb | 2 +- db/schema.rb | 5 +- db/seeds.rb | 1 + db/seeds/organizations.rb | 53 +++++++++++ package-lock.json | 14 ++- .../account_setups_controller_test.rb | 55 ++++++++++++ .../organizations/users_controller_test.rb | 82 +++++++++++++++++ .../organizations_controller_test.rb | 72 +++++++++++++++ test/factories/organizations.rb | 4 +- .../mailers/organization_admin_mailer_test.rb | 18 ++++ .../organization_admin_mailer_preview.rb | 13 +++ test/models/organization_test.rb | 44 +++++++++- test/models/user_test.rb | 25 ++++++ test/services/session_user_builder_test.rb | 24 +++++ 45 files changed, 890 insertions(+), 51 deletions(-) create mode 100644 app/controllers/account_setups_controller.rb create mode 100644 app/controllers/organizations/users_controller.rb create mode 100644 app/mailers/new_user_mailer.rb create mode 100644 app/mailers/organization_admin_mailer.rb create mode 100644 app/views/account_setups/edit.html.haml create mode 100644 app/views/new_user_mailer/invitation_email.html.haml create mode 100644 app/views/new_user_mailer/invitation_email.text.haml create mode 100644 app/views/organization_admin_mailer/invitation_email.html.haml create mode 100644 app/views/organization_admin_mailer/invitation_email.text.haml create mode 100644 app/views/organizations/_admin_form.html.haml create mode 100644 app/views/organizations/_organization_admin_show.html.haml create mode 100644 app/views/organizations/new_user.html.haml create mode 100644 app/views/organizations/users/edit.html.haml create mode 100644 db/migrate/20261010120000_add_unique_index_to_organizations_name.rb create mode 100644 db/seeds/organizations.rb create mode 100644 test/controllers/account_setups_controller_test.rb create mode 100644 test/controllers/organizations/users_controller_test.rb create mode 100644 test/mailers/organization_admin_mailer_test.rb create mode 100644 test/mailers/previews/organization_admin_mailer_preview.rb diff --git a/app/components/shared/main_sidebar_component.rb b/app/components/shared/main_sidebar_component.rb index 97e145b3..d575c7ad 100644 --- a/app/components/shared/main_sidebar_component.rb +++ b/app/components/shared/main_sidebar_component.rb @@ -49,6 +49,11 @@ def primary_buttons text: 'Organizacje', path: helpers.organizations_path, icon: 'fa-briefcase', } end), + (if user.organization_admin? + { + text: @user.organization.name, path: helpers.organization_path(@user.organization), icon: 'fa-briefcase', + } + end), ].compact end diff --git a/app/controllers/account_setups_controller.rb b/app/controllers/account_setups_controller.rb new file mode 100644 index 00000000..82b39994 --- /dev/null +++ b/app/controllers/account_setups_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class AccountSetupsController < ApplicationController + layout 'public' + + skip_before_action :require_account_setup! + before_action :redirect_configured_user + + def edit; end + + def update + @current_user.assign_attributes(account_setup_params) + @current_user.first_login = false + + if @current_user.save(context: :account_setup) + redirect_to home_path, notice: 'Pomyślnie skonfigurowano konto.' + else + @current_user.first_login = true + render :edit, status: :unprocessable_content + end + end + + private + + def redirect_configured_user + redirect_to home_path unless @current_user.needs_account_setup? + end + + def account_setup_params + params.expect( + user: %i[ + full_name + ], + ) + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0b00d57c..a03b826d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base before_action :set_current_user before_action :authenticate! + before_action :require_account_setup! before_action :set_sidebar_state rescue_from ActiveRecord::RecordNotFound, with: :record_not_found @@ -36,6 +37,10 @@ def authenticate! redirect_to login_path, alert: 'Zaloguj się, aby kontynuować.' unless @current_user end + def require_account_setup! + redirect_to edit_account_setup_path if @current_user&.needs_account_setup? + end + def record_not_found redirect_to root_path, alert: 'Nie znaleziono.' end diff --git a/app/controllers/auth/usos_controller.rb b/app/controllers/auth/usos_controller.rb index 7d64c233..5d0db411 100644 --- a/app/controllers/auth/usos_controller.rb +++ b/app/controllers/auth/usos_controller.rb @@ -20,6 +20,10 @@ def create redirect_to root_path, alert: 'Nieprawidłowy dostawca logowania.' rescue Providers::InvalidAuthError redirect_to root_path, alert: 'Nieprawidłowa próba autoryzacji.' + rescue SessionUserBuilder::OrganizationNotFoundError + redirect_to root_path, alert: 'Twoja uczelnia nie jest zarejestrowana jako organizacja.' + rescue SessionUserBuilder::OrganizationFullError + redirect_to root_path, alert: 'Organizacja Twojej uczelni osiągnęła limit członków.' end private diff --git a/app/controllers/organizations/users_controller.rb b/app/controllers/organizations/users_controller.rb new file mode 100644 index 00000000..0b3055c4 --- /dev/null +++ b/app/controllers/organizations/users_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class Organizations::UsersController < ApplicationController + helper OrganizationsHelper + + before_action :set_organization + before_action :set_user + + def edit; end + + def update + @user.assign_attributes(user_params) + + if @user.save(context: :user_invitation) + redirect_outside_turbo_frame organization_path(@organization), + notice: 'Pomyślnie zaktualizowano użytkownika.' + else + render :edit, status: :unprocessable_content + end + end + + def destroy + @user.destroy! + redirect_to organization_path(@organization), notice: 'Pomyślnie usunięto użytkownika.', status: :see_other + end + + private + + def set_organization + @organization = Organization.find(params.expect(:organization_id)) + authorize @organization, :manage_users? + end + + # Admins are managed separately, so only students and teachers are reachable here. + def set_user + @user = @organization.users.where(role: User::INVITABLE_ROLES).find(params.expect(:id)) + end + + def user_params + params.expect( + user: %i[ + email + full_name + university_number + usos_id + role + ], + ) + end +end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index d20fc18a..177c02cc 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true class OrganizationsController < ApplicationController - before_action :set_organization, only: %i[show edit update destroy new_admin add_admin] + ADMIN_INVITATION_EXPIRES_IN = 7.days + USER_INVITATION_EXPIRES_IN = 7.days + + before_action :set_organization, only: %i[show edit update destroy new_admin add_admin new_user add_user] def index @organizations = policy_scope(Organization) @@ -11,6 +14,10 @@ def show authorize @organization @admins = @organization.users.organization_admin + + return unless @current_user.organization_admin? + + @users = @organization.users.where(role: User::INVITABLE_ROLES).order(:created_at) end def new @@ -36,6 +43,8 @@ def edit end def update + authorize @organization + if @organization.update(organization_params) redirect_outside_turbo_frame organization_path(@organization), notice: 'Pomyślnie zaktualizowano organizację.' @@ -52,23 +61,64 @@ def destroy end def new_admin + authorize @organization + @org_admin = User.new end def add_admin + authorize @organization + @org_admin = User.new(user_params) @org_admin.organization = @organization @org_admin.university_name = @organization.name - @org_admin.role = 3 + @org_admin.role = :organization_admin + @org_admin.first_login = true - if @org_admin.save - redirect_outside_turbo_frame organization_path(@organizaton), - notice: 'Pomyślnie dodano administratora organizacji.' + token = User.transaction do + @org_admin.save(context: :admin_invitation) && + @org_admin.create_login_token!(expires_in: ADMIN_INVITATION_EXPIRES_IN) + end + + if token + send_admin_invitation(token) + redirect_outside_turbo_frame organization_path(@organization), + notice: 'Pomyślnie dodano administratora organizacji. Wysłano zaproszenie na email.' else render :new_admin, status: :unprocessable_content end end + def new_user + authorize @organization + + @new_user = User.new + end + + def add_user + authorize @organization + + @new_user = User.new(user_params) + @new_user.organization = @organization + @new_user.university_name = @organization.name + @new_user.first_login = true + + token = User.transaction do + @new_user.save(context: :user_invitation) && + @new_user.create_login_token!(expires_in: USER_INVITATION_EXPIRES_IN) + end + + if token + send_user_invitation(token) + redirect_outside_turbo_frame organization_path(@organization), + notice: 'Pomyślnie dodano użytkownika do organizacji. Wysłano zaproszenie na email.' + else + render :new_user, status: :unprocessable_content + end + end + + private + def set_organization @organization = Organization.find(params.expect(:id)) end @@ -82,11 +132,38 @@ def organization_params ) end + def send_admin_invitation(token) + OrganizationAdminMailer.with( + token_link: auth_passwordless_verify_url(token: token.raw_token), + email: @org_admin.email, + organization_name: @organization.name, + ).invitation_email.deliver_later + end + + def send_user_invitation(token) + NewUserMailer.with( + token_link: auth_passwordless_verify_url(token: token.raw_token), + email: @new_user.email, + organization_name: @organization.name, + ).invitation_email.deliver_later + end + + def admin_params + params.expect( + user: %i[ + email + ], + ) + end + def user_params params.expect( user: %i[ email full_name + university_number + usos_id + role ], ) end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 9c52b0de..ecba2a92 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -6,6 +6,7 @@ class SessionsController < ApplicationController layout 'public' skip_before_action :authenticate!, only: :new + skip_before_action :require_account_setup!, only: :destroy before_action :redirect_logged_user, only: :new def new; end diff --git a/app/helpers/organizations_helper.rb b/app/helpers/organizations_helper.rb index 24cc9a80..1a3145b5 100644 --- a/app/helpers/organizations_helper.rb +++ b/app/helpers/organizations_helper.rb @@ -1,2 +1,16 @@ +# frozen_string_literal: true + module OrganizationsHelper + USER_ROLE_LABELS = { + 'student' => 'Student', + 'teacher' => 'Nauczyciel', + }.freeze + + def user_role_label(role) + USER_ROLE_LABELS.fetch(role.to_s, role.to_s.humanize) + end + + def user_role_options + User::INVITABLE_ROLES.map { |role| [user_role_label(role), role] } + end end diff --git a/app/javascript/controllers/table_search_controller.ts b/app/javascript/controllers/table_search_controller.ts index cde0bdd5..56417306 100644 --- a/app/javascript/controllers/table_search_controller.ts +++ b/app/javascript/controllers/table_search_controller.ts @@ -2,16 +2,24 @@ import { application } from "@controllers/application" import { Controller } from "@hotwired/stimulus" class TableSearchController extends Controller { - static targets = ["input", "table"] + static targets = ["input", "table", "select"] declare readonly inputTarget: HTMLInputElement declare readonly tableTarget: HTMLElement + declare readonly selectTarget: HTMLSelectElement + declare readonly hasSelectTarget: boolean + // Rows match on data-search-text when present, falling back to the sticky name column. + // An optional select target narrows rows further by their data-filter-value. filter() { const query = this.inputTarget.value.toLowerCase().trim() - this.tableTarget.querySelectorAll("tbody tr").forEach(row => { - const name = row.querySelector("td.sticky-col")?.textContent?.toLowerCase() ?? "" - row.hidden = query.length > 0 && !name.includes(query) + const selected = this.hasSelectTarget ? this.selectTarget.value : "" + + this.tableTarget.querySelectorAll("tbody tr").forEach((row) => { + const text = (row.dataset.searchText ?? row.querySelector("td.sticky-col")?.textContent ?? "").toLowerCase() + const matchesQuery = query.length === 0 || text.includes(query) + const matchesSelect = selected === "" || row.dataset.filterValue === selected + row.hidden = !(matchesQuery && matchesSelect) }) } } diff --git a/app/mailers/new_user_mailer.rb b/app/mailers/new_user_mailer.rb new file mode 100644 index 00000000..06e4c639 --- /dev/null +++ b/app/mailers/new_user_mailer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class NewUserMailer < ApplicationMailer + def invitation_email + @token_link = params[:token_link] + @email = params[:email] + @organization_name = params[:organization_name] + + mail to: @email, subject: 'Zaproszenie do systemu GamificationHub' + end +end diff --git a/app/mailers/organization_admin_mailer.rb b/app/mailers/organization_admin_mailer.rb new file mode 100644 index 00000000..6e981c0f --- /dev/null +++ b/app/mailers/organization_admin_mailer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class OrganizationAdminMailer < ApplicationMailer + def invitation_email + @token_link = params[:token_link] + @email = params[:email] + @organization_name = params[:organization_name] + + mail to: @email, subject: 'Zaproszenie do systemu GamificationHub' + end +end diff --git a/app/models/login_token.rb b/app/models/login_token.rb index 1cd7d065..9070a2ab 100644 --- a/app/models/login_token.rb +++ b/app/models/login_token.rb @@ -2,6 +2,7 @@ class LoginToken < ApplicationRecord TOKEN_BYTES = 24 + DEFAULT_EXPIRES_IN = 5.minutes belongs_to :user @@ -32,6 +33,6 @@ def generate_token! @raw_token = raw_token self.token_digest = self.class.digest(raw_token) - self.expires_at = 5.minutes.from_now + self.expires_at ||= DEFAULT_EXPIRES_IN.from_now end end diff --git a/app/models/organization.rb b/app/models/organization.rb index 74a08ae5..7a36a8bf 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -1,14 +1,24 @@ # frozen_string_literal: true class Organization < ApplicationRecord - has_many :users, dependent: :destroy - validates :name, length: { maximum: 100 } - validate :join_condition + validates :name, length: { maximum: 100 }, uniqueness: true + validates :max_members, presence: true, numericality: { only_integer: true, greater_than: 0 } + validate :max_members_not_below_members_count - def join_condition - users.size < max_members + def full? + max_members.present? && users.count >= max_members end + private + + def max_members_not_below_members_count + return if new_record? || max_members.blank? + + members_count = users.count + return if max_members >= members_count + + errors.add(:max_members, "can't be lower than the current number of members (#{members_count})") + end end diff --git a/app/models/user.rb b/app/models/user.rb index c2b7466a..af6a6c0f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,6 +8,8 @@ class User < ApplicationRecord global_admin: 4, } + INVITABLE_ROLES = %w[student teacher].freeze + has_many :student_memberships, class_name: 'StoryGroupStudent', foreign_key: 'user_id', dependent: :destroy has_many :teacher_memberships, class_name: 'StoryGroupTeacher', foreign_key: 'user_id', dependent: :destroy has_many :owner_story_groups, class_name: 'StoryGroup', foreign_key: 'owner_id' @@ -26,6 +28,10 @@ class User < ApplicationRecord validates :full_name, length: { maximum: 80 } validates :university_name, length: { maximum: 100 } validates_presence_of :email, :full_name, on: :account_setup + validates_presence_of :email, on: :admin_invitation + validates_presence_of :email, :full_name, on: :user_invitation + validates :role, inclusion: { in: INVITABLE_ROLES }, on: :user_invitation + validate :organization_has_free_slot, if: :will_save_change_to_organization_id? encrypts :email, deterministic: true encrypts :university_number, deterministic: true @@ -35,12 +41,22 @@ def all_story_groups (owner_story_groups + student_story_groups + teacher_story_groups).uniq end - def create_login_token! + def needs_account_setup? + organization_admin? && first_login? + end + + def create_login_token!(expires_in: LoginToken::DEFAULT_EXPIRES_IN) consume_login_token! - LoginToken.create!(user: self) + LoginToken.create!(user: self, expires_at: expires_in.from_now) end def consume_login_token! login_token&.destroy! end + + private + + def organization_has_free_slot + errors.add(:organization, 'has reached its member limit') if organization&.full? + end end diff --git a/app/policies/organization_policy.rb b/app/policies/organization_policy.rb index 552685a2..0592406d 100644 --- a/app/policies/organization_policy.rb +++ b/app/policies/organization_policy.rb @@ -14,7 +14,7 @@ def resolve end def show? - user.global_admin? || user.organization_admin? + user.global_admin? || own_organization_admin? end def new? @@ -44,4 +44,22 @@ def new_admin? def add_admin? user.global_admin? end + + def new_user? + manage_users? + end + + def add_user? + manage_users? + end + + def manage_users? + user.global_admin? || own_organization_admin? + end + + private + + def own_organization_admin? + user.organization_admin? && user.organization_id == record.id + end end diff --git a/app/policies/story_group_policy.rb b/app/policies/story_group_policy.rb index a58b891b..a8eabc53 100644 --- a/app/policies/story_group_policy.rb +++ b/app/policies/story_group_policy.rb @@ -3,8 +3,10 @@ class StoryGroupPolicy < ApplicationPolicy class Scope < ApplicationPolicy::Scope def resolve - if user.organization_admin? || user.global_admin? + if user.global_admin? scope.all.includes(:owner) + elsif user.organization_admin? + scope.where(owner_id: user.organization.users.select(:id)) elsif user.teacher? owner = scope.where(owner_id: user.id) # and also those to which they belong as teacher and student teaching = scope.where(id: user.teacher_story_groups.select(:id)) diff --git a/app/services/session_user_builder.rb b/app/services/session_user_builder.rb index 24957108..b8677400 100644 --- a/app/services/session_user_builder.rb +++ b/app/services/session_user_builder.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class SessionUserBuilder + class OrganizationNotFoundError < StandardError; end + class OrganizationFullError < StandardError; end + attr_reader :provider def initialize(provider) @@ -8,24 +11,14 @@ def initialize(provider) end def build - user_params = @provider.user_params + # Also populates the provider's attributes (email, usos_id, university_name) used below. + @user_params = @provider.user_params user = find_or_initialize_user - if user.new_record? || user.first_login? - # organization = Organization.find_by( - # university_name: @provider.university_name, - # ) - # organization.with_lock do - # if organization.join_condition - # user.assign_attributes(user_params) - # user.organization = organization - # user.first_login = false - # user.save!(context: :account_setup) - # end - # end - user.assign_attributes(user_params) - user.first_login = false - user.save!(context: :account_setup) + if user.new_record? + create_in_organization(user) + elsif user.first_login? + complete_setup(user) end user @@ -33,6 +26,26 @@ def build private + # New accounts join the organization named after the provider's university. + # The lock keeps concurrent sign-ups from overfilling it. + def create_in_organization(user) + organization = Organization.find_by(name: @provider.university_name) + raise OrganizationNotFoundError unless organization + + organization.with_lock do + raise OrganizationFullError if organization.full? + + user.organization = organization + complete_setup(user) + end + end + + def complete_setup(user) + user.assign_attributes(@user_params) + user.first_login = false + user.save!(context: :account_setup) + end + def find_or_initialize_user if @provider.usos_id user = User.find_by( diff --git a/app/views/account_setups/edit.html.haml b/app/views/account_setups/edit.html.haml new file mode 100644 index 00000000..4295cca7 --- /dev/null +++ b/app/views/account_setups/edit.html.haml @@ -0,0 +1,23 @@ +- content_for :title, "Konfiguracja konta" + +%h1.fs-2 Uzupełnij swoje dane + += form_with model: @current_user, url: account_setup_path, method: :patch, class: 'w-75' do |f| + - if @current_user.errors.any? + .form-errors.p-3.mb-3 + %h2 Popraw poniższe błędy: + %ul + - @current_user.errors.full_messages.each do |message| + %li= message + + .d-block.field-group.mb-2 + = f.label :email, "Email" + = f.email_field :email, class: 'form-control', disabled: true + + .d-block.field-group.mb-2 + = f.label :full_name, "Imię i nazwisko" + = f.text_field :full_name, class: 'form-control', required: true + + = f.submit 'Zapisz', class: 'btn btn-primary w-100 fw-medium' + += button_to 'Wyloguj', logout_path, method: :delete, class: 'btn-link text-dimmed border-0 bg-transparent' diff --git a/app/views/new_user_mailer/invitation_email.html.haml b/app/views/new_user_mailer/invitation_email.html.haml new file mode 100644 index 00000000..2c02f1fa --- /dev/null +++ b/app/views/new_user_mailer/invitation_email.html.haml @@ -0,0 +1,7 @@ + +%h1 Zaproszenie + +%p= "Zostałeś dodany jako członek organizacji #{@organization_name} w GamificationHub." + +%p Aby dokończyć zakładanie konta i uzupełnić swoje dane, kliknij w link: +%a{href: @token_link}= @token_link diff --git a/app/views/new_user_mailer/invitation_email.text.haml b/app/views/new_user_mailer/invitation_email.text.haml new file mode 100644 index 00000000..4c42cc29 --- /dev/null +++ b/app/views/new_user_mailer/invitation_email.text.haml @@ -0,0 +1,3 @@ +Zaproszenie += "Zostałeś dodany jako członek organizacji #{@organization_name} w GamificationHub." += "Aby dokończyć zakładanie konta i uzupełnić swoje dane, kliknij w link: #{@token_link}" diff --git a/app/views/organization_admin_mailer/invitation_email.html.haml b/app/views/organization_admin_mailer/invitation_email.html.haml new file mode 100644 index 00000000..57fe2542 --- /dev/null +++ b/app/views/organization_admin_mailer/invitation_email.html.haml @@ -0,0 +1,6 @@ +%h1 Zaproszenie + +%p= "Zostałeś dodany jako administrator organizacji #{@organization_name} w GamificationHub." + +%p Aby dokończyć zakładanie konta i uzupełnić swoje dane, kliknij w link: +%a{href: @token_link}= @token_link diff --git a/app/views/organization_admin_mailer/invitation_email.text.haml b/app/views/organization_admin_mailer/invitation_email.text.haml new file mode 100644 index 00000000..4d80dcdb --- /dev/null +++ b/app/views/organization_admin_mailer/invitation_email.text.haml @@ -0,0 +1,3 @@ +Zaproszenie += "Zostałeś dodany jako administrator organizacji #{@organization_name} w GamificationHub." += "Aby dokończyć zakładanie konta i uzupełnić swoje dane, kliknij w link: #{@token_link}" diff --git a/app/views/organizations/_admin_form.html.haml b/app/views/organizations/_admin_form.html.haml new file mode 100644 index 00000000..47b85f34 --- /dev/null +++ b/app/views/organizations/_admin_form.html.haml @@ -0,0 +1,18 @@ += form_with model: user, url: add_admin_organization_path(@organization) do |form| + - if user.errors.any? + .form-errors.p-3.mb-3 + %h2= "#{pluralize(user.errors.count, "błąd", "błędy")} uniemożliwił dodanie administratora:" + %ul + - user.errors.each do |error| + %li= error.full_message + + .form-section.p-4.mb-3 + %h2.section-title.mb-3 Email + + .field-group.mb-3 + = form.label :email, "Email" + = form.text_field :email, class: "form-control" + + .form-actions.gap-3.mt-4 + = link_to "Anuluj", cancel_path, class: "btn-cancel py-2 px-3", data: { turbo_frame: "_top" } + = form.submit 'Zapisz', class: "btn-submit py-2 px-4" diff --git a/app/views/organizations/_organization_admin_show.html.haml b/app/views/organizations/_organization_admin_show.html.haml new file mode 100644 index 00000000..beb6e6a9 --- /dev/null +++ b/app/views/organizations/_organization_admin_show.html.haml @@ -0,0 +1,51 @@ +.dark-page{ data: { controller: 'table-search' } } + .d-flex.align-items-center.justify-content-between.mb-4 + %h1.h3.mb-0= @organization.name + - if policy(@organization).add_user? + = link_to new_user_organization_path(@organization), class: "btn btn-primary", data: { turbo_frame: "modal" } do + %i.fa-solid.fa-plus.me-1 + Dodaj użytkownika + + - if @users.empty? + %p.text-dimmed Brak użytkowników w organizacji. + - else + .d-flex.gap-2.mb-3 + .field-group.mb-0.flex-grow-1 + .input-group + = text_field_tag :search, nil, placeholder: 'Wyszukaj po imieniu, nazwisku lub emailu', class: 'form-control', + data: { action: 'input->table-search#filter', table_search_target: 'input' } + %span.input-group-text + %i.fa-solid.fa-magnifying-glass + .field-group.mb-0 + = select_tag :role, options_for_select(user_role_options), include_blank: 'Wszystkie role', + class: 'form-control', + data: { action: 'change->table-search#filter', table_search_target: 'select' } + + %table.table.table-hover.align-middle.mb-0{ data: { table_search_target: 'table' } } + %thead + %tr + %th.ps-4 Imię i nazwisko + %th Email + %th Rola + %th Numer uniwersytecki + %th.text-end.pe-4 Akcje + %tbody + - @users.each do |user| + %tr{ id: dom_id(user), data: { search_text: [user.full_name, user.email].compact.join(' '), filter_value: user.role } } + %td.ps-4= user.full_name + %td= user.email + %td= user_role_label(user.role) + %td= user.university_number + %td.text-end.pe-4 + .d-flex.justify-content-end.gap-2 + = link_to edit_organization_user_path(@organization, user), + class: "btn btn-sm btn-secondary border-0", + title: "Edytuj", + data: { turbo_frame: "modal" } do + %i.fa-solid.fa-pen + = button_to organization_user_path(@organization, user), + method: :delete, + form: { data: { turbo_confirm: "Czy na pewno chcesz usunąć użytkownika #{user.full_name}?" } }, + class: "btn btn-sm btn-danger border-0", + title: "Usuń" do + %i.fa-solid.fa-trash diff --git a/app/views/organizations/_user_form.html.haml b/app/views/organizations/_user_form.html.haml index 269b8ff1..d67d0b11 100644 --- a/app/views/organizations/_user_form.html.haml +++ b/app/views/organizations/_user_form.html.haml @@ -1,7 +1,8 @@ -= form_with model: user, url: add_admin_organization_path(@organization) do |form| + += form_with model: user, url: url do |form| - if user.errors.any? .form-errors.p-3.mb-3 - %h2= "#{pluralize(user.errors.count, "błąd", "błędy")} uniemożliwił dodanie administratora:" + %h2= "#{pluralize(user.errors.count, "błąd", "błędy")} uniemożliwił zapisanie użytkownika:" %ul - user.errors.each do |error| %li= error.full_message @@ -14,12 +15,33 @@ = form.text_field :email, class: "form-control" .form-section.p-4.mb-3 - %h2.section-title.mb-3 Imię i nazwisko + %h2.section-title.mb-3 Imię i Nazwisko .field-group.mb-3 - = form.label :full_name, "Imię i nazwisko" + = form.label :full_name, "Imię i Nazwisko" = form.text_field :full_name, class: "form-control" + .form-section.p-4.mb-3 + %h2.section-title.mb-3 Numer uniwersytecki (opcjonalnie) + + .field-group.mb-3 + = form.label :university_number, "Numer uniwersytecki (opcjonalnie)" + = form.text_field :university_number, class: "form-control" + + .form-section.p-4.mb-3 + %h2.section-title.mb-3 ID USOS (opcjonalnie) + + .field-group.mb-3 + = form.label :usos_id, "ID USOS (opcjonalnie)" + = form.text_field :usos_id, class: "form-control" + + .form-section.p-4.mb-3 + %h2.section-title.mb-3 Rola + + .field-group.mb-3 + = form.label :role, "Rola" + = form.select :role, user_role_options, {}, class: "form-control" + .form-actions.gap-3.mt-4 = link_to "Anuluj", cancel_path, class: "btn-cancel py-2 px-3", data: { turbo_frame: "_top" } = form.submit 'Zapisz', class: "btn-submit py-2 px-4" diff --git a/app/views/organizations/new_admin.html.haml b/app/views/organizations/new_admin.html.haml index a41ee34b..3fed7ef2 100644 --- a/app/views/organizations/new_admin.html.haml +++ b/app/views/organizations/new_admin.html.haml @@ -4,4 +4,4 @@ .modal-overlay.py-5.px-3 .form-card.p-4 %h1.page-title.mb-4 Dodaj administratora organizacji - = render "user_form", user: @org_admin, cancel_path: organization_path(@organization), submit_label: "Add" + = render "admin_form", user: @org_admin, cancel_path: organization_path(@organization), submit_label: "Add" diff --git a/app/views/organizations/new_user.html.haml b/app/views/organizations/new_user.html.haml new file mode 100644 index 00000000..11ad8ff2 --- /dev/null +++ b/app/views/organizations/new_user.html.haml @@ -0,0 +1,8 @@ +- content_for :title, "Dodaj użytkownika organizacji" + += turbo_frame_tag 'modal' do + .modal-overlay.py-5.px-3 + .form-card.p-4 + %h1.page-title.mb-4 Dodaj użytkownika organizacji + = render "user_form", user: @new_user, url: add_user_organization_path(@organization), + cancel_path: organization_path(@organization) diff --git a/app/views/organizations/show.html.haml b/app/views/organizations/show.html.haml index 671142fc..9b43cdc6 100644 --- a/app/views/organizations/show.html.haml +++ b/app/views/organizations/show.html.haml @@ -1,3 +1,6 @@ - content_for :title, @organization.name -= render 'global_admin_show' +- if @current_user.global_admin? + = render 'global_admin_show' +- if @current_user.organization_admin? + = render 'organization_admin_show' diff --git a/app/views/organizations/users/edit.html.haml b/app/views/organizations/users/edit.html.haml new file mode 100644 index 00000000..8d46bca0 --- /dev/null +++ b/app/views/organizations/users/edit.html.haml @@ -0,0 +1,8 @@ +- content_for :title, "Edytuj użytkownika — #{@user.full_name}" + += turbo_frame_tag 'modal' do + .modal-overlay.py-5.px-3 + .form-card.p-4 + %h1.page-title.mb-4 Edytuj użytkownika + = render "organizations/user_form", user: @user, url: organization_user_path(@organization, @user), + cancel_path: organization_path(@organization) diff --git a/config/routes.rb b/config/routes.rb index 1b1e7d30..5b086f1d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,9 +45,15 @@ member do get :new_admin post :add_admin + get :new_user + post :add_user end + + resources :users, only: %i[edit update destroy], module: :organizations end + resource :account_setup, only: %i[edit update] + namespace 'auth' do get '/:provider/callback', to: 'usos#create', as: :callback resource :passwordless, only: %i[new create], controller: 'passwordless' diff --git a/db/migrate/20261010120000_add_unique_index_to_organizations_name.rb b/db/migrate/20261010120000_add_unique_index_to_organizations_name.rb new file mode 100644 index 00000000..2f1a2b5a --- /dev/null +++ b/db/migrate/20261010120000_add_unique_index_to_organizations_name.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddUniqueIndexToOrganizationsName < ActiveRecord::Migration[8.1] + def change + add_index :organizations, :name, unique: true + end +end diff --git a/db/queue_schema.rb b/db/queue_schema.rb index 3262920a..2181653f 100644 --- a/db/queue_schema.rb +++ b/db/queue_schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 0) do +ActiveRecord::Schema[8.1].define(version: 1) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" diff --git a/db/schema.rb b/db/schema.rb index 6e7409eb..4e1a59c0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_09_25_121911) do +ActiveRecord::Schema[8.1].define(version: 2026_10_10_120000) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -170,6 +170,7 @@ t.integer "max_members" t.string "name" t.datetime "updated_at", null: false + t.index ["name"], name: "index_organizations_on_name", unique: true end create_table "ranks", force: :cascade do |t| @@ -197,10 +198,12 @@ t.datetime "created_at", null: false t.integer "current_currency", default: 0 t.integer "lives" + t.string "nickname" t.integer "story_group_id" t.integer "total_currency", default: 0 t.datetime "updated_at", null: false t.integer "user_id" + t.index "story_group_id, lower((nickname)::text)", name: "index_story_group_students_on_group_and_lower_nickname", unique: true, where: "(nickname IS NOT NULL)" t.index ["user_id", "story_group_id"], name: "index_story_group_students_on_user_id_and_story_group_id", unique: true end diff --git a/db/seeds.rb b/db/seeds.rb index 1cfe449a..7a3dfae2 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -6,6 +6,7 @@ Dir[Rails.root.join('db/seeds/*.rb')].each { |file| require file } Seeds::Users.call +Seeds::Organizations.call Seeds::StoryGroups.call Seeds::Ranks.call Seeds::Badges.call diff --git a/db/seeds/organizations.rb b/db/seeds/organizations.rb new file mode 100644 index 00000000..173496fb --- /dev/null +++ b/db/seeds/organizations.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Seeds + class Organizations < Base + ORGANIZATIONS = [ + # Same name as the seeded users' university_name - all of them (except global admins) become its members. + { name: 'Example university', max_members: 50 }, + { name: 'Politechnika Przykładowa', max_members: 20 }, + # Matches Providers::UsosAdapter#university_name, so new USOS sign-ups have an organization to join. + { name: 'Uniwersytet im. Adama Mickiewicza', max_members: 1000 }, + ].freeze + + ADMINS = { + 'Example university' => [ + { email: 'pawel.wisniewski@example.com' }, + # Invited but not yet set up - logging in leads to the account setup form. + { email: 'zofia.nowicka@example.com', full_name: nil, first_login: true }, + ], + }.freeze + + def call + log_start 'organizations' + + ORGANIZATIONS.each do |organization_data| + organization = Organization.find_or_create_by!(name: organization_data[:name]) do |org| + org.max_members = organization_data[:max_members] + end + + build_admins(organization) + assign_members(organization) + end + + log_finish 'organizations' + end + + private + + def assign_members(organization) + User.where(university_name: organization.name, organization_id: nil) + .where.not(role: :global_admin) + .find_each { |user| user.update!(organization: organization) } + end + + def build_admins(organization) + ADMINS.fetch(organization.name, []).each do |admin_data| + admin = User.find_by(email: admin_data[:email]) || + FactoryBot.build(:user, :organization_admin, **admin_data, usos_id: nil, university_number: nil) + + admin.update!(organization: organization, university_name: organization.name) + end + end + end +end diff --git a/package-lock.json b/package-lock.json index e74cf730..a1fb6dd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -618,14 +618,14 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz", "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@hotwired/stimulus-webpack-helpers": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@hotwired/stimulus-webpack-helpers/-/stimulus-webpack-helpers-1.0.1.tgz", "integrity": "sha512-wa/zupVG0eWxRYJjC1IiPBdt3Lruv0RqGN+/DTMmUWUyMAEB27KXmVY6a8YpUVTM7QwVuaLNGW4EqDgrS2upXQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@hotwired/stimulus": ">= 3.0" } @@ -1031,6 +1031,7 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -1170,6 +1171,7 @@ "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", @@ -1374,6 +1376,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1615,6 +1618,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1856,6 +1860,7 @@ "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2146,7 +2151,8 @@ "version": "4.6.13", "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/flatted": { "version": "3.4.2", @@ -2659,6 +2665,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3141,6 +3148,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/test/controllers/account_setups_controller_test.rb b/test/controllers/account_setups_controller_test.rb new file mode 100644 index 00000000..395b7be2 --- /dev/null +++ b/test/controllers/account_setups_controller_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'test_helper' + +class AccountSetupsControllerTest < ActionDispatch::IntegrationTest + setup do + @admin = FactoryBot.create(:user, :organization_admin, first_login: true, full_name: nil) + end + + test 'org admin with first login is redirected to account setup' do + sign_in @admin + follow_redirect! + + assert_redirected_to edit_account_setup_path + end + + test 'non admin user with first login is not redirected to account setup' do + sign_in FactoryBot.create(:user, :student, first_login: true) + get home_path + + assert_response :success + end + + test '#edit - renders form' do + sign_in @admin + get edit_account_setup_path + + assert_response :success + end + + test '#edit - user not needing setup is redirected home' do + sign_in FactoryBot.create(:user, :student, first_login: true) + get edit_account_setup_path + + assert_redirected_to home_path + end + + test '#update - saves data and finishes setup' do + sign_in @admin + patch account_setup_path, params: { user: { full_name: 'Anna Kowalska' } } + + assert_redirected_to home_path + @admin.reload + assert_equal 'Anna Kowalska', @admin.full_name + assert_not @admin.first_login? + end + + test '#update - requires full name' do + sign_in @admin + patch account_setup_path, params: { user: { full_name: '' } } + + assert_response :unprocessable_content + assert @admin.reload.first_login? + end +end diff --git a/test/controllers/organizations/users_controller_test.rb b/test/controllers/organizations/users_controller_test.rb new file mode 100644 index 00000000..0a606b50 --- /dev/null +++ b/test/controllers/organizations/users_controller_test.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'test_helper' + +class Organizations::UsersControllerTest < ActionDispatch::IntegrationTest + setup do + @organization = FactoryBot.create(:organization, max_members: 10) + @admin = FactoryBot.create(:user, :organization_admin, organization: @organization) + @student = FactoryBot.create(:user, :student, organization: @organization, full_name: 'Jan Nowak') + sign_in @admin + end + + test 'org admin sees users table on organization page' do + get organization_url(@organization) + + assert_response :success + assert_select "tr#user_#{@student.id}" + assert_select "tr#user_#{@admin.id}", count: 0 + end + + test '#edit - renders form' do + get edit_organization_user_url(@organization, @student) + assert_response :success + end + + test '#update - updates user' do + patch organization_user_url(@organization, @student), + params: { user: { full_name: 'Jan Kowalski', role: 'teacher' } } + + assert_turbo_redirected_to organization_url(@organization) + @student.reload + assert_equal 'Jan Kowalski', @student.full_name + assert @student.teacher? + end + + test '#update - cannot promote user to admin' do + patch organization_user_url(@organization, @student), params: { user: { role: 'global_admin' } } + + assert_response :unprocessable_content + assert @student.reload.student? + end + + test '#destroy - removes user' do + assert_difference 'User.count', -1 do + delete organization_user_url(@organization, @student) + end + + assert_redirected_to organization_url(@organization) + end + + test 'cannot edit or destroy an admin' do + other_admin = FactoryBot.create(:user, :organization_admin, organization: @organization) + + assert_no_difference 'User.count' do + delete organization_user_url(@organization, other_admin) + end + assert_redirected_to root_path + + patch organization_user_url(@organization, other_admin), params: { user: { role: 'student' } } + assert other_admin.reload.organization_admin? + end + + test 'admin of another organization cannot manage users' do + other_organization = FactoryBot.create(:organization, max_members: 10) + sign_out + sign_in FactoryBot.create(:user, :organization_admin, organization: other_organization) + + assert_no_difference 'User.count' do + delete organization_user_url(@organization, @student) + end + assert_redirected_to root_path + end + + test 'cannot reach a user from another organization through this one' do + outsider = FactoryBot.create(:user, :student, organization: FactoryBot.create(:organization)) + + assert_no_difference 'User.count' do + delete organization_user_url(@organization, outsider) + end + assert_redirected_to root_path + end +end diff --git a/test/controllers/organizations_controller_test.rb b/test/controllers/organizations_controller_test.rb index 1965ef36..aa1806f9 100644 --- a/test/controllers/organizations_controller_test.rb +++ b/test/controllers/organizations_controller_test.rb @@ -28,4 +28,76 @@ class OrganizationsControllerTest < ActionDispatch::IntegrationTest get edit_organization_url(@organization) assert_response :success end + + test '#add_admin - creates org admin and sends invitation email' do + assert_difference 'User.count', 1 do + assert_enqueued_emails 1 do + post add_admin_organization_url(@organization), + params: { user: { email: 'admin@example.com', full_name: 'Anna Kowalska' } } + end + end + + assert_turbo_redirected_to organization_url(@organization) + + admin = User.find_by(email: 'admin@example.com') + assert admin.organization_admin? + assert admin.first_login? + assert_equal @organization, admin.organization + assert admin.login_token.expires_at > 6.days.from_now + end + + test '#add_admin - does not create admin without email' do + assert_no_difference 'User.count' do + assert_no_enqueued_emails do + post add_admin_organization_url(@organization), params: { user: { email: '', full_name: 'Anna' } } + end + end + + assert_response :unprocessable_content + end + + test '#add_admin - does not create admin when organization is full' do + FactoryBot.create(:user, organization: @organization) + + assert_no_difference 'User.count' do + assert_no_enqueued_emails do + post add_admin_organization_url(@organization), params: { user: { email: 'admin@example.com' } } + end + end + + assert_response :unprocessable_content + end + + test '#add_admin - non global admin cannot add admin' do + sign_out + sign_in FactoryBot.create(:user, :organization_admin, organization: @organization) + + assert_no_difference 'User.count' do + post add_admin_organization_url(@organization), params: { user: { email: 'admin@example.com' } } + end + + assert_redirected_to root_path + end + + test '#add_user - creates user with selected role' do + assert_difference 'User.count', 1 do + post add_user_organization_url(@organization), + params: { user: { email: 'teacher@example.com', full_name: 'Jan Nowak', role: 'teacher' } } + end + + assert User.find_by(email: 'teacher@example.com').teacher? + end + + test '#add_user - does not create user with admin role' do + %w[organization_admin global_admin].each do |role| + assert_no_difference 'User.count' do + assert_no_enqueued_emails do + post add_user_organization_url(@organization), + params: { user: { email: 'evil@example.com', full_name: 'Jan Nowak', role: role } } + end + end + + assert_response :unprocessable_content + end + end end diff --git a/test/factories/organizations.rb b/test/factories/organizations.rb index 4a4d3509..1189a519 100644 --- a/test/factories/organizations.rb +++ b/test/factories/organizations.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + FactoryBot.define do factory :organization do - name { 'MyString' } + sequence(:name) { |n| "Organization #{n}" } max_members { 1 } end end diff --git a/test/mailers/organization_admin_mailer_test.rb b/test/mailers/organization_admin_mailer_test.rb new file mode 100644 index 00000000..885aa03b --- /dev/null +++ b/test/mailers/organization_admin_mailer_test.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'test_helper' + +class OrganizationAdminMailerTest < ActionMailer::TestCase + test 'invitation_email' do + mail = OrganizationAdminMailer.with( + token_link: 'http://example.com/test', + email: 'jan.nowak@example.com', + organization_name: 'Uniwersytet', + ).invitation_email + assert_equal 'Zaproszenie do systemu GamificationHub', mail.subject + assert_equal ['jan.nowak@example.com'], mail.to + assert_equal ['from@example.com'], mail.from + assert_match 'administrator organizacji Uniwersytet', mail.html_part.body.decoded + assert_match 'http://example.com/test', mail.html_part.body.decoded + end +end diff --git a/test/mailers/previews/organization_admin_mailer_preview.rb b/test/mailers/previews/organization_admin_mailer_preview.rb new file mode 100644 index 00000000..d2a4e336 --- /dev/null +++ b/test/mailers/previews/organization_admin_mailer_preview.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Preview all emails at http://localhost:3000/rails/mailers/organization_admin_mailer +class OrganizationAdminMailerPreview < ActionMailer::Preview + # Preview this email at http://localhost:3000/rails/mailers/organization_admin_mailer/invitation_email + def invitation_email + OrganizationAdminMailer.with( + token_link: 'http://example.com/test', + email: 'jan.nowak@example.com', + organization_name: 'Uniwersytet', + ).invitation_email + end +end diff --git a/test/models/organization_test.rb b/test/models/organization_test.rb index 3f86f1cd..309d7386 100644 --- a/test/models/organization_test.rb +++ b/test/models/organization_test.rb @@ -1,7 +1,43 @@ -require "test_helper" +# frozen_string_literal: true + +require 'test_helper' class OrganizationTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end + test 'requires max_members' do + organization = FactoryBot.build(:organization, max_members: nil) + + assert_not organization.valid? + assert organization.errors.added?(:max_members, :blank) + end + + test 'requires positive max_members' do + organization = FactoryBot.build(:organization, max_members: 0) + + assert_not organization.valid? + end + + test 'full? when members count reaches max_members' do + organization = FactoryBot.create(:organization, max_members: 1) + assert_not organization.full? + + FactoryBot.create(:user, organization: organization) + assert organization.full? + end + + test 'cannot lower max_members below current members count' do + organization = FactoryBot.create(:organization, max_members: 2) + FactoryBot.create_list(:user, 2, organization: organization) + + organization.max_members = 1 + assert_not organization.valid? + assert_includes organization.errors[:max_members], "can't be lower than the current number of members (2)" + end + + test 'requires unique name' do + FactoryBot.create(:organization, name: 'Example university') + organization = FactoryBot.build(:organization, name: 'Example university') + + assert_not organization.valid? + assert organization.errors[:name].any? + end end diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 1d199a89..a39e905d 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -9,4 +9,29 @@ class UserTest < ActiveSupport::TestCase assert_equal 'siema@gmail.com', user.email end + + test 'cannot join a full organization' do + organization = FactoryBot.create(:organization, max_members: 1) + FactoryBot.create(:user, organization: organization) + + user = FactoryBot.build(:user, organization: organization) + assert_not user.valid? + assert_includes user.errors[:organization], 'has reached its member limit' + end + + test 'existing member can be updated in a full organization' do + organization = FactoryBot.create(:organization, max_members: 1) + user = FactoryBot.create(:user, organization: organization) + + assert user.update(full_name: 'Nowe Imię') + end + + test 'invited user can only be a student or teacher' do + assert FactoryBot.build(:user, role: :student).valid?(:user_invitation) + assert FactoryBot.build(:user, role: :teacher).valid?(:user_invitation) + + user = FactoryBot.build(:user, role: :global_admin) + assert_not user.valid?(:user_invitation) + assert user.errors[:role].any? + end end diff --git a/test/services/session_user_builder_test.rb b/test/services/session_user_builder_test.rb index e6b80b1c..674819cb 100644 --- a/test/services/session_user_builder_test.rb +++ b/test/services/session_user_builder_test.rb @@ -30,10 +30,34 @@ def build_params end test 'create new user if no user in database' do + organization = FactoryBot.create(:organization, name: 'Example university') + assert_difference 'User.count', 1 do user = SessionUserBuilder.new(@provider).build assert_equal 'jan.nowak@gmail.com', user.email + assert_equal organization, user.organization + end + end + + test 'do not create new user if no organization matches university name' do + FactoryBot.create(:organization, name: 'Other university') + + assert_no_difference 'User.count' do + assert_raises SessionUserBuilder::OrganizationNotFoundError do + SessionUserBuilder.new(@provider).build + end + end + end + + test 'do not create new user if organization is full' do + organization = FactoryBot.create(:organization, name: 'Example university', max_members: 1) + FactoryBot.create(:user, organization: organization) + + assert_no_difference 'User.count' do + assert_raises SessionUserBuilder::OrganizationFullError do + SessionUserBuilder.new(@provider).build + end end end From b0bd2ad24ac7efd2b4b96990c5d2e077d95bdd27 Mon Sep 17 00:00:00 2001 From: ewez Date: Sat, 10 Oct 2026 22:12:48 +0200 Subject: [PATCH 05/10] added missing comments for linter --- db/migrate/20260915175517_create_organizations.rb | 2 ++ .../20260922155430_add_university_number_to_organizations.rb | 2 ++ ...0260923171241_remove_university_number_from_organizations.rb | 2 ++ db/migrate/20260925121911_add_organization_id_to_users.rb | 2 ++ 4 files changed, 8 insertions(+) diff --git a/db/migrate/20260915175517_create_organizations.rb b/db/migrate/20260915175517_create_organizations.rb index b878256e..a58dccf5 100644 --- a/db/migrate/20260915175517_create_organizations.rb +++ b/db/migrate/20260915175517_create_organizations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateOrganizations < ActiveRecord::Migration[8.1] def change create_table :organizations do |t| diff --git a/db/migrate/20260922155430_add_university_number_to_organizations.rb b/db/migrate/20260922155430_add_university_number_to_organizations.rb index 450427c4..9edb9317 100644 --- a/db/migrate/20260922155430_add_university_number_to_organizations.rb +++ b/db/migrate/20260922155430_add_university_number_to_organizations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddUniversityNumberToOrganizations < ActiveRecord::Migration[8.1] def change add_column :organizations, :university_number, :string diff --git a/db/migrate/20260923171241_remove_university_number_from_organizations.rb b/db/migrate/20260923171241_remove_university_number_from_organizations.rb index ea40e8aa..d08e8098 100644 --- a/db/migrate/20260923171241_remove_university_number_from_organizations.rb +++ b/db/migrate/20260923171241_remove_university_number_from_organizations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RemoveUniversityNumberFromOrganizations < ActiveRecord::Migration[8.1] def change remove_column :organizations, :university_number, :string diff --git a/db/migrate/20260925121911_add_organization_id_to_users.rb b/db/migrate/20260925121911_add_organization_id_to_users.rb index 4b265de0..2604b9ee 100644 --- a/db/migrate/20260925121911_add_organization_id_to_users.rb +++ b/db/migrate/20260925121911_add_organization_id_to_users.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddOrganizationIdToUsers < ActiveRecord::Migration[8.1] def change add_reference :users, :organization, foreign_key: true From d942661034654959cc55c9777a084a6deb203ca6 Mon Sep 17 00:00:00 2001 From: cub8 Date: Thu, 8 Oct 2026 16:14:34 +0200 Subject: [PATCH 06/10] Refactor + fix bug with qrcodes --- app/controllers/application_controller.rb | 38 ++----------------- .../auth/passwordless_controller.rb | 35 +++++------------ app/controllers/auth/usos_controller.rb | 8 ++-- app/controllers/concerns/auth/sign_in.rb | 25 ++++++++++++ app/controllers/concerns/authentication.rb | 38 +++++++++++++++++++ app/controllers/concerns/sidebar.rb | 13 +++++++ app/controllers/concerns/status_redirects.rb | 11 ++++++ 7 files changed, 103 insertions(+), 65 deletions(-) create mode 100644 app/controllers/concerns/auth/sign_in.rb create mode 100644 app/controllers/concerns/authentication.rb create mode 100644 app/controllers/concerns/sidebar.rb create mode 100644 app/controllers/concerns/status_redirects.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a49c5d88..0e24dbc7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,22 +2,14 @@ class ApplicationController < ActionController::Base include Pundit::Authorization + include Authentication + include Sidebar + include StatusRedirects - attr_reader :current_user - - # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. allow_browser versions: :modern - # The whole app renders through one layout. `@in_modal` is set by each - # controller's own before_action (the turbo-frame id it dialogs answer into - # differs per screen), so this only decides page-or-frame. layout -> { @in_modal ? false : 'application' } - before_action :set_current_user - before_action :authenticate! - before_action :require_account_setup! - before_action :set_sidebar_state - rescue_from ActiveRecord::RecordNotFound, with: :record_not_found rescue_from Pundit::NotAuthorizedError, with: :not_authorized @@ -29,28 +21,4 @@ def redirect_outside_turbo_frame(path, notice: nil, alert: nil) render turbo_stream: turbo_stream.action(:redirect, path) end - - def set_sidebar_state - @sidebar_collapsed = cookies[:sidebar_collapsed] == 'true' - end - - def set_current_user - @current_user = User.find_by(id: session[:user_id]) - end - - def authenticate! - redirect_to login_path, alert: 'Zaloguj się, aby kontynuować.' unless @current_user - end - - def require_account_setup! - redirect_to edit_account_setup_path if @current_user&.needs_account_setup? - end - - def record_not_found - redirect_to root_path, alert: 'Nie znaleziono.' - end - - def not_authorized - redirect_to root_path, alert: 'Nie znaleziono.' - end end diff --git a/app/controllers/auth/passwordless_controller.rb b/app/controllers/auth/passwordless_controller.rb index 8c47f317..60433724 100644 --- a/app/controllers/auth/passwordless_controller.rb +++ b/app/controllers/auth/passwordless_controller.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true class Auth::PasswordlessController < ApplicationController - include LoggedUserRedirector + include Authentication + include Auth::SignIn - # How long the "Wyślij ponownie" button stays disabled on the inbox screen. - # Presentational only — the real limit is the Rack::Attack throttle on - # POST /auth/passwordless (config/initializers/rack_attack.rb). RESEND_COOLDOWN = 60 skip_before_action :authenticate! @@ -18,12 +16,14 @@ def new; end def inbox @email = session[:pending_login_email] sent_at = session[:pending_login_sent_at].to_i + current_time = Time.current.to_i - # No pending send, or one old enough that its link has already expired. return redirect_to new_auth_passwordless_path if @email.blank? || sent_at.zero? - return redirect_to new_auth_passwordless_path if Time.current.to_i - sent_at > LoginToken::EXPIRES_IN + return redirect_to new_auth_passwordless_path if current_time - sent_at > LoginToken::EXPIRES_IN - @cooldown = [RESEND_COOLDOWN - (Time.current.to_i - sent_at), 0].max + delta = current_time - sent_at + + @cooldown = [RESEND_COOLDOWN - delta, 0].max end def verify @@ -35,10 +35,7 @@ def verify user = login_token.user user.consume_login_token! - reset_session - session[:user_id] = user.id - - redirect_to home_path, notice: 'Zalogowano pomyślnie.' + sign_in_and_redirect(user) end def create @@ -46,12 +43,10 @@ def create user = User.find_by(email: email) service = BypassLoginService.new(email: email) - # Whether this is a resend depends only on prior session state, never on - # whether the address exists — so it cannot be used to enumerate accounts. resend = session[:pending_login_email].present? if user - return bypass_login(user) if service.can_bypass_login? + return sign_in_and_redirect(user) if service.can_bypass_login? token = user.create_login_token! token_link = auth_passwordless_verify_url(token: token.raw_token) @@ -63,14 +58,10 @@ def create ).token_email.deliver_later end - # Everything below MUST stay outside the `if user` block: a known and an - # unknown address have to produce byte-identical responses. Do not move - # these next to the mailer call. session[:pending_login_email] = email session[:pending_login_sent_at] = Time.current.to_i - # No notice on a first send — the inbox screen is itself the confirmation. - notice = resend ? 'Wysłaliśmy nowy link. Poprzedni już nie działa.' : nil + notice = 'Wysłaliśmy nowy link. Poprzedni już nie działa.' if resend redirect_to auth_passwordless_inbox_path, notice: notice end @@ -80,10 +71,4 @@ def clear_pending_login session.delete(:pending_login_email) session.delete(:pending_login_sent_at) end - - def bypass_login(user) - reset_session - session[:user_id] = user.id - redirect_to home_path, notice: 'Zalogowano pomyślnie.' - end end diff --git a/app/controllers/auth/usos_controller.rb b/app/controllers/auth/usos_controller.rb index 68da2fae..02343863 100644 --- a/app/controllers/auth/usos_controller.rb +++ b/app/controllers/auth/usos_controller.rb @@ -3,7 +3,8 @@ class Auth::UsosController < ApplicationController class InvalidProviderError < StandardError; end - include LoggedUserRedirector + include Authentication + include Auth::SignIn skip_before_action :authenticate! before_action :redirect_logged_user @@ -13,10 +14,7 @@ def create builder = SessionUserBuilder.new(provider) user = builder.build - reset_session - session[:user_id] = user.id - - redirect_to home_path + sign_in_and_redirect(user) rescue InvalidProviderError redirect_to root_path, alert: 'Nieprawidłowy dostawca logowania.' rescue Providers::InvalidAuthError diff --git a/app/controllers/concerns/auth/sign_in.rb b/app/controllers/concerns/auth/sign_in.rb new file mode 100644 index 00000000..907f2b0e --- /dev/null +++ b/app/controllers/concerns/auth/sign_in.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Auth::SignIn + extend ActiveSupport::Concern + + private + + def sign_in_and_redirect(user) + return_to = session[:return_to] + reset_session + session[:user_id] = user.id + + redirect_to after_login_path(return_to), notice: 'Zalogowano pomyślnie.' + end + + def after_login_path(path) + return path if local_path?(path) + + home_path + end + + def local_path?(path) + path&.start_with?('/') && !path.start_with?('//') + end +end diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb new file mode 100644 index 00000000..d41faabe --- /dev/null +++ b/app/controllers/concerns/authentication.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module ::Authentication + extend ::ActiveSupport::Concern + + included do + before_action :set_current_user + before_action :authenticate! + before_action :require_account_setup! + end + + protected + + attr_reader :current_user + + def set_current_user + @current_user = User.find_by(id: session[:user_id]) + end + + def authenticate! + return if @current_user + + store_return_path + redirect_to login_path, alert: 'Zaloguj się, aby kontynuować.' + end + + def store_return_path + session[:return_to] = request.fullpath if full_page_navigation? + end + + def full_page_navigation? + request.get? && !request.xhr? && !turbo_frame_request? + end + + def require_account_setup! + redirect_to edit_account_setup_path if @current_user&.needs_account_setup? + end +end diff --git a/app/controllers/concerns/sidebar.rb b/app/controllers/concerns/sidebar.rb new file mode 100644 index 00000000..ba78a096 --- /dev/null +++ b/app/controllers/concerns/sidebar.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Sidebar + included do + before_action :set_sidebar_state + end + + protected + + def set_sidebar_state + @sidebar_collapsed = cookies[:sidebar_collapsed] == 'true' + end +end diff --git a/app/controllers/concerns/status_redirects.rb b/app/controllers/concerns/status_redirects.rb new file mode 100644 index 00000000..1edb3569 --- /dev/null +++ b/app/controllers/concerns/status_redirects.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module StatusRedirects + def record_not_found + redirect_to root_path, alert: 'Nie znaleziono.' + end + + def not_authorized + redirect_to root_path, alert: 'Nie znaleziono.' + end +end From ffd3e98e60fe1ef1b4dfce09ef91cc9d29b1c54b Mon Sep 17 00:00:00 2001 From: cub8 Date: Thu, 8 Oct 2026 16:22:25 +0200 Subject: [PATCH 07/10] Fix errors --- .../auth/passwordless_controller.rb | 2 +- app/controllers/auth/usos_controller.rb | 2 +- app/controllers/concerns/sidebar.rb | 2 + app/controllers/concerns/status_redirects.rb | 2 + app/controllers/ranking_controller.rb | 1 - package-lock.json | 128 +- yarn.lock | 1422 ----------------- 7 files changed, 70 insertions(+), 1489 deletions(-) delete mode 100644 yarn.lock diff --git a/app/controllers/auth/passwordless_controller.rb b/app/controllers/auth/passwordless_controller.rb index 60433724..e29ed773 100644 --- a/app/controllers/auth/passwordless_controller.rb +++ b/app/controllers/auth/passwordless_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Auth::PasswordlessController < ApplicationController - include Authentication include Auth::SignIn + include LoggedUserRedirector RESEND_COOLDOWN = 60 diff --git a/app/controllers/auth/usos_controller.rb b/app/controllers/auth/usos_controller.rb index 02343863..a41c2169 100644 --- a/app/controllers/auth/usos_controller.rb +++ b/app/controllers/auth/usos_controller.rb @@ -3,8 +3,8 @@ class Auth::UsosController < ApplicationController class InvalidProviderError < StandardError; end - include Authentication include Auth::SignIn + include LoggedUserRedirector skip_before_action :authenticate! before_action :redirect_logged_user diff --git a/app/controllers/concerns/sidebar.rb b/app/controllers/concerns/sidebar.rb index ba78a096..2d68c73b 100644 --- a/app/controllers/concerns/sidebar.rb +++ b/app/controllers/concerns/sidebar.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Sidebar + extend ActiveSupport::Concern + included do before_action :set_sidebar_state end diff --git a/app/controllers/concerns/status_redirects.rb b/app/controllers/concerns/status_redirects.rb index 1edb3569..69180f2c 100644 --- a/app/controllers/concerns/status_redirects.rb +++ b/app/controllers/concerns/status_redirects.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module StatusRedirects + protected + def record_not_found redirect_to root_path, alert: 'Nie znaleziono.' end diff --git a/app/controllers/ranking_controller.rb b/app/controllers/ranking_controller.rb index 99b84eea..cc43c075 100644 --- a/app/controllers/ranking_controller.rb +++ b/app/controllers/ranking_controller.rb @@ -3,7 +3,6 @@ class RankingController < ApplicationController include StoryGroupAuthorization - before_action :set_story_group before_action :set_presentation, only: %i[confirm_visibility confirm_mode] before_action :authorize_story_group_manage!, except: :show diff --git a/package-lock.json b/package-lock.json index edfd0009..4f2f2331 100644 --- a/package-lock.json +++ b/package-lock.json @@ -866,53 +866,7 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@rails/actioncable": { - "version": "8.1.200", - "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-8.1.200.tgz", - "integrity": "sha512-on0DSb7AFUkq1ocxivDNQhhGW/RQpY91zvRVyyaEWP4gOOZWy33P/UyxjQk74IENWNrTqs8+zOGHwTjiiFruRw==", - "license": "MIT" - }, - "node_modules/@stylistic/eslint-plugin": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", - "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/types": "^8.56.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "estraverse": "^5.3.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^9.0.0 || ^10.0.0" - } - }, - "node_modules/@tailwindcss/cli": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.3.tgz", - "integrity": "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@parcel/watcher": "2.5.1", - "@tailwindcss/node": "4.3.3", - "@tailwindcss/oxide": "4.3.3", - "enhanced-resolve": "^5.24.1", - "mri": "^1.2.0", - "picocolors": "^1.1.1", - "tailwindcss": "4.3.3" - }, - "bin": { - "tailwindcss": "dist/index.mjs" - } - }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher": { + "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", @@ -948,7 +902,7 @@ "@parcel/watcher-win32-x64": "2.5.1" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-android-arm64": { + "node_modules/@parcel/watcher-android-arm64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", @@ -969,7 +923,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-darwin-arm64": { + "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", @@ -990,7 +944,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-darwin-x64": { + "node_modules/@parcel/watcher-darwin-x64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", @@ -1011,7 +965,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-freebsd-x64": { + "node_modules/@parcel/watcher-freebsd-x64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", @@ -1032,7 +986,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm-glibc": { + "node_modules/@parcel/watcher-linux-arm-glibc": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", @@ -1053,7 +1007,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm-musl": { + "node_modules/@parcel/watcher-linux-arm-musl": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", @@ -1074,7 +1028,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm64-glibc": { + "node_modules/@parcel/watcher-linux-arm64-glibc": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", @@ -1095,7 +1049,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-arm64-musl": { + "node_modules/@parcel/watcher-linux-arm64-musl": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", @@ -1116,7 +1070,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-x64-glibc": { + "node_modules/@parcel/watcher-linux-x64-glibc": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", @@ -1137,7 +1091,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-linux-x64-musl": { + "node_modules/@parcel/watcher-linux-x64-musl": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", @@ -1158,7 +1112,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-win32-arm64": { + "node_modules/@parcel/watcher-win32-arm64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", @@ -1179,7 +1133,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-win32-ia32": { + "node_modules/@parcel/watcher-win32-ia32": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", @@ -1200,7 +1154,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/@parcel/watcher-win32-x64": { + "node_modules/@parcel/watcher-win32-x64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", @@ -1221,7 +1175,7 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@tailwindcss/cli/node_modules/detect-libc": { + "node_modules/@parcel/watcher/node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", @@ -1234,6 +1188,52 @@ "node": ">=0.10" } }, + "node_modules/@rails/actioncable": { + "version": "8.1.200", + "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-8.1.200.tgz", + "integrity": "sha512-on0DSb7AFUkq1ocxivDNQhhGW/RQpY91zvRVyyaEWP4gOOZWy33P/UyxjQk74IENWNrTqs8+zOGHwTjiiFruRw==", + "license": "MIT" + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", + "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.56.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.3.tgz", + "integrity": "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "2.5.1", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "enhanced-resolve": "^5.24.1", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.3.3" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, "node_modules/@tailwindcss/node": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", @@ -2939,9 +2939,9 @@ "license": "MIT" }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index f3c249fc..00000000 --- a/yarn.lock +++ /dev/null @@ -1,1422 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@cropper/element-canvas@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/element-canvas/-/element-canvas-2.2.0.tgz" - integrity sha512-H0Ay2ZSZrxWLP/8fSudH9hghsgIJh9fCgEmpfzD7MnQbE8HdQKljnoRxbrjuFT7Y0rmf405Sp9PmNA6T4twegw== - dependencies: - "@cropper/element" "^2.2.0" - "@cropper/utils" "^2.2.0" - -"@cropper/element-crosshair@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/element-crosshair/-/element-crosshair-2.2.0.tgz" - integrity sha512-RXNhlsCd7lPTTjR2rfUEYghaL5bkqiU70qojJCUz1EIKfLriToDztjlsw3TZKJUBHYA+JOnmTBDFTnizoMUN1w== - dependencies: - "@cropper/element" "^2.2.0" - "@cropper/utils" "^2.2.0" - -"@cropper/element-grid@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/element-grid/-/element-grid-2.2.0.tgz" - integrity sha512-1hxHLi7Jrek/xxeG12a9+TO18Ot4pCG4V9Zh+A0GMLsfFjrd2k/kl0wOq0x1isNNrP3kkIw47FdID2iWR8A48A== - dependencies: - "@cropper/element" "^2.2.0" - "@cropper/utils" "^2.2.0" - -"@cropper/element-handle@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/element-handle/-/element-handle-2.2.0.tgz" - integrity sha512-+WPzHWF96RtLTAtotIcQQ6Sx3zVLQdK+qbaM2yKa841Iz60gQ3qKk7Jl5ndy2OgkEfxZaJI/3lWyf1RMGs/TEw== - dependencies: - "@cropper/element" "^2.2.0" - "@cropper/utils" "^2.2.0" - -"@cropper/element-image@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/element-image/-/element-image-2.2.0.tgz" - integrity sha512-J0zBFMEfW6yuAmXoP6S+ZdNJy5J2duVsUS0N2o3D/HxHiVo4Vm3jrSexasu/Vfvwe4Lv0nmZ/10KW3RxhrvDCQ== - dependencies: - "@cropper/element" "^2.2.0" - "@cropper/element-canvas" "^2.2.0" - "@cropper/utils" "^2.2.0" - -"@cropper/element-selection@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/element-selection/-/element-selection-2.2.0.tgz" - integrity sha512-ATtWeEb1J/scQ98ins2jy4YbVOqz13F54jDk4Mm7OCpG0fzDseOujSLoIP8y26gtY7XGQXvn8WiFBbIZbeSgxQ== - dependencies: - "@cropper/element" "^2.2.0" - "@cropper/element-canvas" "^2.2.0" - "@cropper/element-image" "^2.2.0" - "@cropper/utils" "^2.2.0" - -"@cropper/element-shade@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/element-shade/-/element-shade-2.2.0.tgz" - integrity sha512-fiurnjecablA8+17GoOjiNrzKWe85Q6h9JLbPJkxKCQxcxqpjEsOdGu3YlRxaBYcYvr6wH9tghufOlUfYJS9nw== - dependencies: - "@cropper/element" "^2.2.0" - "@cropper/element-canvas" "^2.2.0" - "@cropper/element-selection" "^2.2.0" - "@cropper/utils" "^2.2.0" - -"@cropper/element-viewer@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/element-viewer/-/element-viewer-2.2.0.tgz" - integrity sha512-w6uO7m7PwBWKynMwj8DYkJHFXrHC6Xxvcp5Y+tSxhfcNtOvw92FDncqRLYv+xHQoCDXEvfCqJyrTx9nIiZgiLQ== - dependencies: - "@cropper/element" "^2.2.0" - "@cropper/element-canvas" "^2.2.0" - "@cropper/element-image" "^2.2.0" - "@cropper/element-selection" "^2.2.0" - "@cropper/utils" "^2.2.0" - -"@cropper/element@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/element/-/element-2.2.0.tgz" - integrity sha512-Kckk7TiWt3Cf9MvSfmMx7ZOIuN1rzIUIwIPQSqSj0Sl6gj6opo+CPax+fREvyvVKEzAug7L0sQFZ0TL4H5LsNw== - dependencies: - "@cropper/utils" "^2.2.0" - -"@cropper/elements@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/elements/-/elements-2.2.0.tgz" - integrity sha512-OQiVvEjIQUbMAUGpFrI/L95elHNIXjdlSxxzeywbU6C93PdPypXeGQfOSLQ43zUYmFutWNb7jTrHvgb3JaE+VQ== - dependencies: - "@cropper/element" "^2.2.0" - "@cropper/element-canvas" "^2.2.0" - "@cropper/element-crosshair" "^2.2.0" - "@cropper/element-grid" "^2.2.0" - "@cropper/element-handle" "^2.2.0" - "@cropper/element-image" "^2.2.0" - "@cropper/element-selection" "^2.2.0" - "@cropper/element-shade" "^2.2.0" - "@cropper/element-viewer" "^2.2.0" - -"@cropper/utils@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@cropper/utils/-/utils-2.2.0.tgz" - integrity sha512-wTrpZl+F4Kr2hzovqjztSexWUU+EVK1CxPymbYgFYoAS/NZXwCDsrvikJQn+HYZVxjqyo/ClnFxA0fG+tFy6Hw== - -"@emnapi/core@^1.11.1": - version "1.11.3" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.3.tgz#5e95348a42cd1e06f0b9aa380cd74091daa4d520" - integrity sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg== - dependencies: - "@emnapi/wasi-threads" "1.2.3" - tslib "^2.4.0" - -"@emnapi/runtime@^1.11.1": - version "1.11.3" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.3.tgz#84257ae3b0531eb2aec1ffa23d70700da007ba95" - integrity sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA== - dependencies: - tslib "^2.4.0" - -"@emnapi/wasi-threads@1.2.3", "@emnapi/wasi-threads@^1.2.2": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz#c9bf72fd4be5b928aee894820e8d814ed73916e9" - integrity sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g== - dependencies: - tslib "^2.4.0" - -"@esbuild/aix-ppc64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz#82b74f92aa78d720b714162939fb248c90addf53" - integrity sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg== - -"@esbuild/android-arm64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz#f78cb8a3121fc205a53285adb24972db385d185d" - integrity sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ== - -"@esbuild/android-arm@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz#593e10a1450bbfcac6cb321f61f468453bac209d" - integrity sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ== - -"@esbuild/android-x64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz#453143d073326033d2d22caf9e48de4bae274b07" - integrity sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg== - -"@esbuild/darwin-arm64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz#6f23000fb9b40b7e04b7d0606c0693bd0632f322" - integrity sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw== - -"@esbuild/darwin-x64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz#27393dd18bb1263c663979c5f1576e00c2d024be" - integrity sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ== - -"@esbuild/freebsd-arm64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz#22e4638fa502d1c0027077324c97640e3adf3a62" - integrity sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w== - -"@esbuild/freebsd-x64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz#9224b8e4fea924ce2194e3efc3e9aebf822192d6" - integrity sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ== - -"@esbuild/linux-arm64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz#4f5d1c27527d817b35684ae21419e57c2bda0966" - integrity sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A== - -"@esbuild/linux-arm@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz#b9e9d070c8c1c0449cf12b20eac37d70a4595921" - integrity sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA== - -"@esbuild/linux-ia32@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz#3f80fb696aa96051a94047f35c85b08b21c36f9e" - integrity sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg== - -"@esbuild/linux-loong64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz#9be1f2c28210b13ebb4156221bba356fe1675205" - integrity sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q== - -"@esbuild/linux-mips64el@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz#4ab5ee67a3dfcbcb5e8fd7883dae6e735b1163b8" - integrity sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw== - -"@esbuild/linux-ppc64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz#dac78c689f6499459c4321e5c15032c12307e7ea" - integrity sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ== - -"@esbuild/linux-riscv64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz#050f7d3b355c3a98308e935bc4d6325da91b0027" - integrity sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ== - -"@esbuild/linux-s390x@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz#d61f715ce61d43fe5844ad0d8f463f88cbe4fef6" - integrity sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw== - -"@esbuild/linux-x64@0.27.7": - version "0.27.7" - resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz" - integrity sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA== - -"@esbuild/netbsd-arm64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz#1650f2c1b948deeb3ef948f2fc30614723c09690" - integrity sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w== - -"@esbuild/netbsd-x64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz#65772ab342c4b3319bf0705a211050aac1b6e320" - integrity sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw== - -"@esbuild/openbsd-arm64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz#37ed7cfa66549d7955852fce37d0c3de4e715ea1" - integrity sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A== - -"@esbuild/openbsd-x64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz#01bf3d385855ef50cb33db7c4b52f957c34cd179" - integrity sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg== - -"@esbuild/openharmony-arm64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz#6c1f94b34086599aabda4eac8f638294b9877410" - integrity sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw== - -"@esbuild/sunos-x64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz#4b0dd17ae0a6941d2d0fd35a906392517071a90d" - integrity sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA== - -"@esbuild/win32-arm64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz#34193ab5565d6ff68ca928ac04be75102ccb2e77" - integrity sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA== - -"@esbuild/win32-ia32@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz#eb67f0e4482515d8c1894ede631c327a4da9fc4d" - integrity sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw== - -"@esbuild/win32-x64@0.27.7": - version "0.27.7" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz#8fe30b3088b89b4873c3a6cc87597ae3920c0a8b" - integrity sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg== - -"@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": - version "4.9.1" - resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz" - integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== - dependencies: - eslint-visitor-keys "^3.4.3" - -"@eslint-community/regexpp@^4.12.2": - version "4.12.2" - resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz" - integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== - -"@eslint/config-array@^0.23.3": - version "0.23.3" - resolved "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz" - integrity sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw== - dependencies: - "@eslint/object-schema" "^3.0.3" - debug "^4.3.1" - minimatch "^10.2.4" - -"@eslint/config-helpers@^0.5.2": - version "0.5.3" - resolved "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz" - integrity sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw== - dependencies: - "@eslint/core" "^1.1.1" - -"@eslint/core@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz" - integrity sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ== - dependencies: - "@types/json-schema" "^7.0.15" - -"@eslint/js@^10.0.1": - version "10.0.1" - resolved "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz" - integrity sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA== - -"@eslint/object-schema@^3.0.3": - version "3.0.3" - resolved "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz" - integrity sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ== - -"@eslint/plugin-kit@^0.6.1": - version "0.6.1" - resolved "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz" - integrity sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ== - dependencies: - "@eslint/core" "^1.1.1" - levn "^0.4.1" - -"@fortawesome/fontawesome-free@^7.2.0": - version "7.2.0" - resolved "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz" - integrity sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg== - -"@hotwired/stimulus@^3.2.2": - version "3.2.2" - resolved "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz" - integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A== - -"@hotwired/turbo-rails@^8.0.23": - version "8.0.23" - resolved "https://registry.npmjs.org/@hotwired/turbo-rails/-/turbo-rails-8.0.23.tgz" - integrity sha512-iBILwda3qmQC7FYM70+4s6kEQ7Fx9dJ6+yGxjPyrz9a5JDx1+y7OAA5TA7GGVOZJoicMLrKGdFDNorl40X35lw== - dependencies: - "@hotwired/turbo" "^8.0.23" - "@rails/actioncable" ">=7.0" - -"@hotwired/turbo@^8.0.23": - version "8.0.23" - resolved "https://registry.npmjs.org/@hotwired/turbo/-/turbo-8.0.23.tgz" - integrity sha512-GZ7cijxEZ6Ig71u7rD6LHaRv/wcE/hNsc+nEfiWOkLNqUgLOwo5MNGWOy5ZV9ZUDSiQx1no7YxjTNnT4O6//cQ== - -"@humanfs/core@^0.19.2": - version "0.19.2" - resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz" - integrity sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA== - dependencies: - "@humanfs/types" "^0.15.0" - -"@humanfs/node@^0.16.6": - version "0.16.8" - resolved "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz" - integrity sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ== - dependencies: - "@humanfs/core" "^0.19.2" - "@humanfs/types" "^0.15.0" - "@humanwhocodes/retry" "^0.4.0" - -"@humanfs/types@^0.15.0": - version "0.15.0" - resolved "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz" - integrity sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q== - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": - version "0.4.3" - resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz" - integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== - -"@jridgewell/gen-mapping@^0.3.5": - version "0.3.13" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" - integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.5.0" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/remapping@^2.3.5": - version "2.3.5" - resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz" - integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": - version "1.6.0" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz" - integrity sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw== - -"@jridgewell/trace-mapping@^0.3.24": - version "0.3.31" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" - integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@napi-rs/wasm-runtime@^1.1.4": - version "1.2.5" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz#8c728c07e2543d80e55ccb34158c4ae9d6bd13e8" - integrity sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA== - dependencies: - "@tybys/wasm-util" "^0.10.3" - -"@parcel/watcher-android-arm64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" - integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== - -"@parcel/watcher-darwin-arm64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67" - integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== - -"@parcel/watcher-darwin-x64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" - integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== - -"@parcel/watcher-freebsd-x64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" - integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== - -"@parcel/watcher-linux-arm-glibc@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" - integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== - -"@parcel/watcher-linux-arm-musl@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" - integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== - -"@parcel/watcher-linux-arm64-glibc@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" - integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== - -"@parcel/watcher-linux-arm64-musl@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" - integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== - -"@parcel/watcher-linux-x64-glibc@2.5.1": - version "2.5.1" - resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz" - integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== - -"@parcel/watcher-linux-x64-musl@2.5.1": - version "2.5.1" - resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz" - integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== - -"@parcel/watcher-win32-arm64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" - integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== - -"@parcel/watcher-win32-ia32@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" - integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== - -"@parcel/watcher-win32-x64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" - integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== - -"@parcel/watcher@2.5.1": - version "2.5.1" - resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz" - integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg== - dependencies: - detect-libc "^1.0.3" - is-glob "^4.0.3" - micromatch "^4.0.5" - node-addon-api "^7.0.0" - optionalDependencies: - "@parcel/watcher-android-arm64" "2.5.1" - "@parcel/watcher-darwin-arm64" "2.5.1" - "@parcel/watcher-darwin-x64" "2.5.1" - "@parcel/watcher-freebsd-x64" "2.5.1" - "@parcel/watcher-linux-arm-glibc" "2.5.1" - "@parcel/watcher-linux-arm-musl" "2.5.1" - "@parcel/watcher-linux-arm64-glibc" "2.5.1" - "@parcel/watcher-linux-arm64-musl" "2.5.1" - "@parcel/watcher-linux-x64-glibc" "2.5.1" - "@parcel/watcher-linux-x64-musl" "2.5.1" - "@parcel/watcher-win32-arm64" "2.5.1" - "@parcel/watcher-win32-ia32" "2.5.1" - "@parcel/watcher-win32-x64" "2.5.1" - -"@rails/actioncable@>=7.0": - version "8.1.200" - resolved "https://registry.npmjs.org/@rails/actioncable/-/actioncable-8.1.200.tgz" - integrity sha512-on0DSb7AFUkq1ocxivDNQhhGW/RQpY91zvRVyyaEWP4gOOZWy33P/UyxjQk74IENWNrTqs8+zOGHwTjiiFruRw== - -"@stylistic/eslint-plugin@^5.10.0": - version "5.10.0" - resolved "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz" - integrity sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ== - dependencies: - "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/types" "^8.56.0" - eslint-visitor-keys "^4.2.1" - espree "^10.4.0" - estraverse "^5.3.0" - picomatch "^4.0.3" - -"@tailwindcss/cli@^4": - version "4.3.3" - resolved "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.3.tgz" - integrity sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw== - dependencies: - "@parcel/watcher" "2.5.1" - "@tailwindcss/node" "4.3.3" - "@tailwindcss/oxide" "4.3.3" - enhanced-resolve "^5.24.1" - mri "^1.2.0" - picocolors "^1.1.1" - tailwindcss "4.3.3" - -"@tailwindcss/node@4.3.3": - version "4.3.3" - resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz" - integrity sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg== - dependencies: - "@jridgewell/remapping" "^2.3.5" - enhanced-resolve "^5.24.1" - jiti "^2.7.0" - lightningcss "1.32.0" - magic-string "^0.30.21" - source-map-js "^1.2.1" - tailwindcss "4.3.3" - -"@tailwindcss/oxide-android-arm64@4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz#848f93034155daf7892185028dfb18143bfc7d07" - integrity sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw== - -"@tailwindcss/oxide-darwin-arm64@4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz#5c779e32c1c361beb75136fd8e2c627fcb9a5b28" - integrity sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw== - -"@tailwindcss/oxide-darwin-x64@4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz#ba292ff52fa3264139f7fe7874719ce0a4666875" - integrity sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw== - -"@tailwindcss/oxide-freebsd-x64@4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz#07e589487b9a636235a4ade48cefc1f9eeeec57f" - integrity sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw== - -"@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz#0c8abc228d9e19e0065ddb707eba52e21855b3ff" - integrity sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ== - -"@tailwindcss/oxide-linux-arm64-gnu@4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz#5067dc7afd15d2b97adc77a91177dc4bec8d1b8b" - integrity sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w== - -"@tailwindcss/oxide-linux-arm64-musl@4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz#29ae5f279be2ce64db368711985b6ad8e4562e57" - integrity sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA== - -"@tailwindcss/oxide-linux-x64-gnu@4.3.3": - version "4.3.3" - resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz" - integrity sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w== - -"@tailwindcss/oxide-linux-x64-musl@4.3.3": - version "4.3.3" - resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz" - integrity sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img== - -"@tailwindcss/oxide-wasm32-wasi@4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz#408a9bc620e68f1aaf0dfea33da7bd3b65f9e2ef" - integrity sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ== - dependencies: - "@emnapi/core" "^1.11.1" - "@emnapi/runtime" "^1.11.1" - "@emnapi/wasi-threads" "^1.2.2" - "@napi-rs/wasm-runtime" "^1.1.4" - "@tybys/wasm-util" "^0.10.2" - tslib "^2.8.1" - -"@tailwindcss/oxide-win32-arm64-msvc@4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz#3d582b00180fa4680e0b6c90be69fa5f4a209092" - integrity sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ== - -"@tailwindcss/oxide-win32-x64-msvc@4.3.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz#ce4c663fef3b4fde67611a4c1391866f8c0aa79b" - integrity sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw== - -"@tailwindcss/oxide@4.3.3": - version "4.3.3" - resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz" - integrity sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA== - optionalDependencies: - "@tailwindcss/oxide-android-arm64" "4.3.3" - "@tailwindcss/oxide-darwin-arm64" "4.3.3" - "@tailwindcss/oxide-darwin-x64" "4.3.3" - "@tailwindcss/oxide-freebsd-x64" "4.3.3" - "@tailwindcss/oxide-linux-arm-gnueabihf" "4.3.3" - "@tailwindcss/oxide-linux-arm64-gnu" "4.3.3" - "@tailwindcss/oxide-linux-arm64-musl" "4.3.3" - "@tailwindcss/oxide-linux-x64-gnu" "4.3.3" - "@tailwindcss/oxide-linux-x64-musl" "4.3.3" - "@tailwindcss/oxide-wasm32-wasi" "4.3.3" - "@tailwindcss/oxide-win32-arm64-msvc" "4.3.3" - "@tailwindcss/oxide-win32-x64-msvc" "4.3.3" - -"@tybys/wasm-util@^0.10.2", "@tybys/wasm-util@^0.10.3": - version "0.10.4" - resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.4.tgz#3e85ecb266f9d1722250e89bc69528615720b154" - integrity sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A== - dependencies: - tslib "^2.4.0" - -"@types/esrecurse@^4.3.1": - version "4.3.1" - resolved "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz" - integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== - -"@types/estree@^1.0.6", "@types/estree@^1.0.8": - version "1.0.8" - resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" - integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== - -"@types/hotwired__turbo-rails@^8.0.0": - version "8.0.0" - resolved "https://registry.npmjs.org/@types/hotwired__turbo-rails/-/hotwired__turbo-rails-8.0.0.tgz" - integrity sha512-PaIVBat0aRF+KRWWq1ELBUc4olTLXzyfsUVpnk1hENoGoPQtbPSKbvHJcD6KjXwtomHCDcZDPwIiGYOGeeMD1g== - dependencies: - "@types/hotwired__turbo" "*" - "@types/rails__actioncable" "*" - -"@types/hotwired__turbo@*": - version "8.0.10" - resolved "https://registry.npmjs.org/@types/hotwired__turbo/-/hotwired__turbo-8.0.10.tgz" - integrity sha512-ZnNDmfE2mwvbpmq55ntbTwP82Y4U+g3lcGMTWvS+Vo5LEuO6YQrWVhCmrTae3IH19/nNcHvzbNuroNWEwRylFg== - -"@types/json-schema@^7.0.15": - version "7.0.15" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/rails__actioncable@*": - version "8.0.3" - resolved "https://registry.npmjs.org/@types/rails__actioncable/-/rails__actioncable-8.0.3.tgz" - integrity sha512-y46MOTYorVQVwlHUyaZYbrh3nIkXsRYNuPna32lb3RngLVBlndNbIPvAUywFfhivftNhYg+vW5sZKWYCVIX2lA== - -"@types/sortablejs@^1.15.9": - version "1.15.9" - resolved "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz" - integrity sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ== - -"@typescript-eslint/eslint-plugin@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz" - integrity sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ== - dependencies: - "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.57.0" - "@typescript-eslint/type-utils" "8.57.0" - "@typescript-eslint/utils" "8.57.0" - "@typescript-eslint/visitor-keys" "8.57.0" - ignore "^7.0.5" - natural-compare "^1.4.0" - ts-api-utils "^2.4.0" - -"@typescript-eslint/parser@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz" - integrity sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g== - dependencies: - "@typescript-eslint/scope-manager" "8.57.0" - "@typescript-eslint/types" "8.57.0" - "@typescript-eslint/typescript-estree" "8.57.0" - "@typescript-eslint/visitor-keys" "8.57.0" - debug "^4.4.3" - -"@typescript-eslint/project-service@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz" - integrity sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w== - dependencies: - "@typescript-eslint/tsconfig-utils" "^8.57.0" - "@typescript-eslint/types" "^8.57.0" - debug "^4.4.3" - -"@typescript-eslint/scope-manager@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz" - integrity sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw== - dependencies: - "@typescript-eslint/types" "8.57.0" - "@typescript-eslint/visitor-keys" "8.57.0" - -"@typescript-eslint/tsconfig-utils@8.57.0", "@typescript-eslint/tsconfig-utils@^8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz" - integrity sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA== - -"@typescript-eslint/type-utils@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz" - integrity sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ== - dependencies: - "@typescript-eslint/types" "8.57.0" - "@typescript-eslint/typescript-estree" "8.57.0" - "@typescript-eslint/utils" "8.57.0" - debug "^4.4.3" - ts-api-utils "^2.4.0" - -"@typescript-eslint/types@8.57.0", "@typescript-eslint/types@^8.56.0", "@typescript-eslint/types@^8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz" - integrity sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg== - -"@typescript-eslint/typescript-estree@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz" - integrity sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q== - dependencies: - "@typescript-eslint/project-service" "8.57.0" - "@typescript-eslint/tsconfig-utils" "8.57.0" - "@typescript-eslint/types" "8.57.0" - "@typescript-eslint/visitor-keys" "8.57.0" - debug "^4.4.3" - minimatch "^10.2.2" - semver "^7.7.3" - tinyglobby "^0.2.15" - ts-api-utils "^2.4.0" - -"@typescript-eslint/utils@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz" - integrity sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ== - dependencies: - "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.57.0" - "@typescript-eslint/types" "8.57.0" - "@typescript-eslint/typescript-estree" "8.57.0" - -"@typescript-eslint/visitor-keys@8.57.0": - version "8.57.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz" - integrity sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg== - dependencies: - "@typescript-eslint/types" "8.57.0" - eslint-visitor-keys "^5.0.0" - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn@^8.15.0, acorn@^8.16.0: - version "8.16.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz" - integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== - -ajv@^6.14.0: - version "6.14.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz" - integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -balanced-match@^4.0.2: - version "4.0.4" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz" - integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== - -brace-expansion@^5.0.2: - version "5.0.12" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz" - integrity sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ== - dependencies: - balanced-match "^4.0.2" - -braces@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -cropperjs@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/cropperjs/-/cropperjs-2.2.0.tgz" - integrity sha512-7egO+tXxt3jGHQcO6x4k1CVoMIlEu+4Klixc6ejspKOAns7onlC/lEi5tRi3qbuHVcATK7rqaZyp8LzynPz6Cg== - dependencies: - "@cropper/elements" "^2.2.0" - "@cropper/utils" "^2.2.0" - -cross-spawn@^7.0.6: - version "7.0.6" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -debug@^4.3.1, debug@^4.3.2, debug@^4.4.3: - version "4.4.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== - dependencies: - ms "^2.1.3" - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz" - integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== - -detect-libc@^2.0.3: - version "2.1.2" - resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz" - integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== - -enhanced-resolve@^5.24.1: - version "5.25.1" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz" - integrity sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.3.3" - -esbuild@^0.27.4: - version "0.27.7" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz" - integrity sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w== - optionalDependencies: - "@esbuild/aix-ppc64" "0.27.7" - "@esbuild/android-arm" "0.27.7" - "@esbuild/android-arm64" "0.27.7" - "@esbuild/android-x64" "0.27.7" - "@esbuild/darwin-arm64" "0.27.7" - "@esbuild/darwin-x64" "0.27.7" - "@esbuild/freebsd-arm64" "0.27.7" - "@esbuild/freebsd-x64" "0.27.7" - "@esbuild/linux-arm" "0.27.7" - "@esbuild/linux-arm64" "0.27.7" - "@esbuild/linux-ia32" "0.27.7" - "@esbuild/linux-loong64" "0.27.7" - "@esbuild/linux-mips64el" "0.27.7" - "@esbuild/linux-ppc64" "0.27.7" - "@esbuild/linux-riscv64" "0.27.7" - "@esbuild/linux-s390x" "0.27.7" - "@esbuild/linux-x64" "0.27.7" - "@esbuild/netbsd-arm64" "0.27.7" - "@esbuild/netbsd-x64" "0.27.7" - "@esbuild/openbsd-arm64" "0.27.7" - "@esbuild/openbsd-x64" "0.27.7" - "@esbuild/openharmony-arm64" "0.27.7" - "@esbuild/sunos-x64" "0.27.7" - "@esbuild/win32-arm64" "0.27.7" - "@esbuild/win32-ia32" "0.27.7" - "@esbuild/win32-x64" "0.27.7" - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-scope@^9.1.2: - version "9.1.2" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz" - integrity sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ== - dependencies: - "@types/esrecurse" "^4.3.1" - "@types/estree" "^1.0.8" - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-visitor-keys@^3.4.3: - version "3.4.3" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - -eslint-visitor-keys@^4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz" - integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== - -eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz" - integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== - -eslint@^10.0.3: - version "10.0.3" - resolved "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz" - integrity sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ== - dependencies: - "@eslint-community/eslint-utils" "^4.8.0" - "@eslint-community/regexpp" "^4.12.2" - "@eslint/config-array" "^0.23.3" - "@eslint/config-helpers" "^0.5.2" - "@eslint/core" "^1.1.1" - "@eslint/plugin-kit" "^0.6.1" - "@humanfs/node" "^0.16.6" - "@humanwhocodes/module-importer" "^1.0.1" - "@humanwhocodes/retry" "^0.4.2" - "@types/estree" "^1.0.6" - ajv "^6.14.0" - cross-spawn "^7.0.6" - debug "^4.3.2" - escape-string-regexp "^4.0.0" - eslint-scope "^9.1.2" - eslint-visitor-keys "^5.0.1" - espree "^11.1.1" - esquery "^1.7.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^8.0.0" - find-up "^5.0.0" - glob-parent "^6.0.2" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - json-stable-stringify-without-jsonify "^1.0.1" - minimatch "^10.2.4" - natural-compare "^1.4.0" - optionator "^0.9.3" - -espree@^10.4.0: - version "10.4.0" - resolved "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz" - integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== - dependencies: - acorn "^8.15.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^4.2.1" - -espree@^11.1.1: - version "11.2.0" - resolved "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz" - integrity sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw== - dependencies: - acorn "^8.16.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^5.0.1" - -esquery@^1.7.0: - version "1.7.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz" - integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fdir@^6.5.0: - version "6.5.0" - resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" - integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== - -file-entry-cache@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" - integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== - dependencies: - flat-cache "^4.0.0" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat-cache@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz" - integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== - dependencies: - flatted "^3.2.9" - keyv "^4.5.4" - -flatted@^3.2.9: - version "3.4.2" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz" - integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== - -glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -globals@^17.5.0: - version "17.5.0" - resolved "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz" - integrity sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g== - -graceful-fs@^4.2.4: - version "4.2.11" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -ignore@^5.2.0: - version "5.3.2" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - -ignore@^7.0.5: - version "7.0.5" - resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz" - integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-glob@^4.0.0, is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -jiti@^2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz" - integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -keyv@^4.5.4: - version "4.5.4" - resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -lightningcss-android-arm64@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" - integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg== - -lightningcss-darwin-arm64@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5" - integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ== - -lightningcss-darwin-x64@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e" - integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w== - -lightningcss-freebsd-x64@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575" - integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig== - -lightningcss-linux-arm-gnueabihf@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d" - integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw== - -lightningcss-linux-arm64-gnu@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335" - integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ== - -lightningcss-linux-arm64-musl@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133" - integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg== - -lightningcss-linux-x64-gnu@1.32.0: - version "1.32.0" - resolved "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz" - integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA== - -lightningcss-linux-x64-musl@1.32.0: - version "1.32.0" - resolved "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz" - integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg== - -lightningcss-win32-arm64-msvc@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38" - integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw== - -lightningcss-win32-x64-msvc@1.32.0: - version "1.32.0" - resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a" - integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q== - -lightningcss@1.32.0: - version "1.32.0" - resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz" - integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ== - dependencies: - detect-libc "^2.0.3" - optionalDependencies: - lightningcss-android-arm64 "1.32.0" - lightningcss-darwin-arm64 "1.32.0" - lightningcss-darwin-x64 "1.32.0" - lightningcss-freebsd-x64 "1.32.0" - lightningcss-linux-arm-gnueabihf "1.32.0" - lightningcss-linux-arm64-gnu "1.32.0" - lightningcss-linux-arm64-musl "1.32.0" - lightningcss-linux-x64-gnu "1.32.0" - lightningcss-linux-x64-musl "1.32.0" - lightningcss-win32-arm64-msvc "1.32.0" - lightningcss-win32-x64-msvc "1.32.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -magic-string@^0.30.21: - version "0.30.21" - resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz" - integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== - dependencies: - "@jridgewell/sourcemap-codec" "^1.5.5" - -micromatch@^4.0.5: - version "4.0.8" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -minimatch@^10.2.2, minimatch@^10.2.4: - version "10.2.4" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz" - integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg== - dependencies: - brace-expansion "^5.0.2" - -mri@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz" - integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== - -ms@^2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -node-addon-api@^7.0.0: - version "7.1.1" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz" - integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== - -optionator@^0.9.3: - version "0.9.4" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" - integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.5" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.3.1: - version "2.3.2" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz" - integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== - -picomatch@^4.0.3: - version "4.0.4" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz" - integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -semver@^7.7.3: - version "7.7.4" - resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz" - integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -sortablejs@^1.15.7: - version "1.15.7" - resolved "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz" - integrity sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A== - -source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== - -tailwindcss@4.3.3, tailwindcss@^4: - version "4.3.3" - resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz" - integrity sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ== - -tapable@^2.3.3: - version "2.3.3" - resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz" - integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A== - -tinyglobby@^0.2.15: - version "0.2.15" - resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz" - integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== - dependencies: - fdir "^6.5.0" - picomatch "^4.0.3" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -ts-api-utils@^2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz" - integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== - -tslib@^2.4.0, tslib@^2.8.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -typescript-eslint@^8.57.0: - version "8.57.0" - resolved "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz" - integrity sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA== - dependencies: - "@typescript-eslint/eslint-plugin" "8.57.0" - "@typescript-eslint/parser" "8.57.0" - "@typescript-eslint/typescript-estree" "8.57.0" - "@typescript-eslint/utils" "8.57.0" - -typescript@^5.9.3: - version "5.9.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" - integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -word-wrap@^1.2.5: - version "1.2.5" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 25548b39d06270ec6eea1a55d6fc76d78fb47e73 Mon Sep 17 00:00:00 2001 From: cub8 Date: Mon, 12 Oct 2026 13:01:01 +0200 Subject: [PATCH 08/10] Fix bugs --- app/assets/stylesheets/components/form.css | 4 +- app/assets/stylesheets/components/join.css | 11 ++- app/controllers/join_controller.rb | 26 +++--- app/views/join/_step_code.html.haml | 45 ++++++---- app/views/join/_step_nickname.html.haml | 23 ++--- config/routes.rb | 1 + test/controllers/auth/sign_in_test.rb | 23 +++++ test/controllers/join_controller_test.rb | 4 +- test/controllers/return_to_redirect_test.rb | 98 +++++++++++++++++++++ 9 files changed, 184 insertions(+), 51 deletions(-) create mode 100644 test/controllers/auth/sign_in_test.rb create mode 100644 test/controllers/return_to_redirect_test.rb diff --git a/app/assets/stylesheets/components/form.css b/app/assets/stylesheets/components/form.css index aec9ef03..cb8e2932 100644 --- a/app/assets/stylesheets/components/form.css +++ b/app/assets/stylesheets/components/form.css @@ -742,7 +742,7 @@ outline-offset: 2px; } - @media (width <= theme(--breakpoint-tablet)) { + @media (width <=theme(--breakpoint-tablet)) { .gh-form-shell { grid-template-columns: minmax(0, 1fr); } @@ -771,7 +771,7 @@ } } - @media (width <= theme(--breakpoint-mobile)) { + @media (width <=theme(--breakpoint-mobile)) { .gh-search-input--wide { width: 100%; } diff --git a/app/assets/stylesheets/components/join.css b/app/assets/stylesheets/components/join.css index 1c638b2c..08a8520a 100644 --- a/app/assets/stylesheets/components/join.css +++ b/app/assets/stylesheets/components/join.css @@ -62,8 +62,13 @@ .gh-join-hint { display: flex; align-items: center; - justify-content: center; - gap: 8px; + justify-content: flex-start; + gap: 6px; + } + + .gh-join-icon { + flex: none; + width: 16px; text-align: center; } @@ -76,7 +81,7 @@ letter-spacing: 0.04em; } - @media (width <= theme(--breakpoint-mobile)) { + @media (width <=theme(--breakpoint-mobile)) { .gh-join-code-inputs { gap: 7px; } diff --git a/app/controllers/join_controller.rb b/app/controllers/join_controller.rb index 29d3aa09..f37171c1 100644 --- a/app/controllers/join_controller.rb +++ b/app/controllers/join_controller.rb @@ -1,16 +1,10 @@ # frozen_string_literal: true class JoinController < ApplicationController - # Inside the dialog only the frame is used, and the layout already carries a - # of its own. Rendering it too would put two frames - # with the same id in one response and let Turbo pick whichever came first. - before_action :set_presentation - # Step 1. def new; end - # Step 1 submitted. A bad code falls back to step 1 carrying its reason. def lookup @code = params[:code] @lookup = InviteLookup.new(user: current_user, code: @code).call @@ -21,8 +15,6 @@ def lookup render :show end - # The QR landing page. Same step, same failure handling — only the wrapper - # differs, and `set_presentation` has already chosen it. def show @code = params[:code] @lookup = InviteLookup.new(user: current_user, code: @code).call @@ -32,19 +24,25 @@ def show @story_group = @lookup.story_group end - # Step 2 submitted. def create @code = params[:code] @lookup = InviteLookup.new(user: current_user, code: @code).call return render :new, status: :unprocessable_content unless @lookup.ok? - accept + accept! + end + + def done + @story_group = current_user.student_story_groups.find(params[:story_group_id]) + @membership = current_user.student_memberships.find_by!(story_group: @story_group) + + render :create end private - def accept + def accept! service = AcceptInviteService.new( user: current_user, invite: @lookup.invite, @@ -56,13 +54,9 @@ def accept return render :show, status: :unprocessable_content unless @result.success? - @membership = @result.membership + redirect_to done_join_index_path(story_group_id: @story_group.id) end - # A step renders inside the layout's `modal` dialog when it was requested - # from one, and as a focused, navigation-less page otherwise — which is how - # the QR link lands. The branch mirrors NotificationsController's `panel` - # check. def set_presentation @in_modal = turbo_frame_request_id == 'modal' @chrome = false unless @in_modal diff --git a/app/views/join/_step_code.html.haml b/app/views/join/_step_code.html.haml index 491ba0da..66147941 100644 --- a/app/views/join/_step_code.html.haml +++ b/app/views/join/_step_code.html.haml @@ -2,38 +2,45 @@ - member_of = local_assigns[:lookup]&.member_of %p.gh-small.mb-1 Krok 1 z 2 -%h2#gh-join-title.gh-h2 Dołącz do grupy +%h2.gh-h2#gh-join-title Dołącz do grupy %p Wpisz 6-znakowy kod, który podał prowadzący. -= form_with url: lookup_join_index_path, method: :get, - data: { controller: 'join-code', turbo_frame: (@in_modal ? 'modal' : nil) } do += form_with url: lookup_join_index_path, + method: :get, + data: { controller: 'join-code', + turbo_frame: (@in_modal ? 'modal' : nil), } do = hidden_field_tag :code, code, data: { join_code_target: 'code' } .gh-join-code-inputs{ role: 'group', 'aria-label': 'Kod zaproszenia' } - StoryGroupInvite::CODE_LENGTH.times do |index| %span.gh-join-code-slot - %input{ type: 'text', - maxlength: 2, - autocomplete: 'off', - autocapitalize: 'characters', - spellcheck: 'false', - value: code.to_s[index], - autofocus: index.zero?, - 'aria-label': "Znak #{index + 1} z #{StoryGroupInvite::CODE_LENGTH}", - data: { join_code_target: 'slot', action: 'input->join-code#type keydown->join-code#key paste->join-code#paste' } } + %input{type: 'text', + maxlength: 2, + autocomplete: 'off', + autocapitalize: 'characters', + spellcheck: 'false', + value: code.to_s[index], + autofocus: index.zero?, + 'aria-label': "Znak #{index + 1} z #{StoryGroupInvite::CODE_LENGTH}", + data: { join_code_target: 'slot', + action: 'input->join-code#type keydown->join-code#key paste->join-code#paste', },} - if member_of - %p.gh-field-hint - Już należysz do tej grupy. - = link_to 'Otwórz grupę', story_group_path(member_of), class: 'gh-link-button', - data: { turbo_frame: '_top' } + %p.gh-field-error.pb-2{ role: 'alert' } + %i.fa-solid.fa-triangle-exclamation.fa-fw.gh-join-icon{ 'aria-hidden': 'true' } + %span + Już należysz do tej grupy. + = link_to 'Otwórz grupę', + story_group_path(member_of), + class: 'gh-link-button', + data: { turbo_frame: '_top' } - elsif error - %p.gh-field-error{ role: 'alert' } - %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } + %p.gh-field-error.pb-2{ role: 'alert' } + %i.fa-solid.fa-triangle-exclamation.fa-fw.gh-join-icon{ 'aria-hidden': 'true' } %span= error %p.gh-small.gh-join-hint - %i.fa-solid.fa-camera{ 'aria-hidden': 'true' } + %i.fa-solid.fa-camera.fa-fw.gh-join-icon{ 'aria-hidden': 'true' } Masz kod QR? Zeskanuj go aparatem telefonu, a kod wpisze się sam. .gh-dialog-button-row diff --git a/app/views/join/_step_nickname.html.haml b/app/views/join/_step_nickname.html.haml index 17326697..1e7c12df 100644 --- a/app/views/join/_step_nickname.html.haml +++ b/app/views/join/_step_nickname.html.haml @@ -1,33 +1,36 @@ - nickname_error = local_assigns[:result]&.nickname_error %p.gh-small.mb-1 Krok 2 z 2 -%h2#gh-join-title.gh-h2= "Dołączasz do grupy #{story_group.name}" +%h2.gh-h2#gh-join-title= "Dołączasz do grupy #{story_group.name}" %p W tej grupie inni zobaczą Cię pod pseudonimem — także w rankingu. Możesz go później zmienić w ustawieniach w grupie. -= form_with url: join_index_path, method: :post, - data: { turbo_frame: (@in_modal ? 'modal' : nil) } do += form_with url: join_index_path, + method: :post, + data: { turbo_frame: (@in_modal ? 'modal' : nil) } do = hidden_field_tag :code, code .gh-form-field = label_tag :nickname, 'Pseudonim w tej grupie' .gh-text-input{ class: ('gh-text-input--invalid' if nickname_error) } = text_field_tag :nickname, params[:nickname], - maxlength: 24, - autocomplete: 'off', - autofocus: true, - 'aria-invalid': nickname_error ? 'true' : nil, - 'aria-describedby': 'gh-nick-note' + maxlength: 24, + autocomplete: 'off', + autofocus: true, + 'aria-invalid': nickname_error ? 'true' : nil, + 'aria-describedby': 'gh-nick-note' + -# haml-lint:disable RepeatedId - if nickname_error - %p#gh-nick-note.gh-field-error{ role: 'alert' } + %p.gh-field-error#gh-nick-note{ role: 'alert' } %i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' } %span= nickname_error - else - %span#gh-nick-note.gh-field-hint + %span.gh-field-hint#gh-nick-note Zostaw puste, jeśli chcesz używać imienia i nazwiska. Pseudonim możesz ustawić później. + -# haml-lint:enable RepeatedId .gh-dialog-button-row = render 'join/cancel' diff --git a/config/routes.rb b/config/routes.rb index 6fb79a43..e1cd3acc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,6 +7,7 @@ collection do post :create get :lookup + get :done end end resources :story_groups do diff --git a/test/controllers/auth/sign_in_test.rb b/test/controllers/auth/sign_in_test.rb new file mode 100644 index 00000000..c6e1072e --- /dev/null +++ b/test/controllers/auth/sign_in_test.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'test_helper' + +class Auth::SignInTest < ActiveSupport::TestCase + class Host + include Auth::SignIn + + def home_path = '/home' + + def resolve(path) = after_login_path(path) + end + + test 'accepts local paths' do + assert_equal '/join/abc?x=1', Host.new.resolve('/join/abc?x=1') + end + + test 'rejects external and malformed targets' do + ['//evil.com', 'https://evil.com', 'javascript:alert(1)', '', nil].each do |path| + assert_equal '/home', Host.new.resolve(path), "expected #{path.inspect} to fall back to home" + end + end +end diff --git a/test/controllers/join_controller_test.rb b/test/controllers/join_controller_test.rb index 76640ea9..3be1b6c5 100644 --- a/test/controllers/join_controller_test.rb +++ b/test/controllers/join_controller_test.rb @@ -17,7 +17,9 @@ class JoinControllerTest < ActionDispatch::IntegrationTest post join_index_url, params: { code: @invite.code } end - assert_response :success + assert_redirected_to done_join_index_path(story_group_id: @story_group.id) + follow_redirect! + assert_select '.gh-dialog-button-row a[href=?]', story_group_path(@story_group), 'Gotowe' end diff --git a/test/controllers/return_to_redirect_test.rb b/test/controllers/return_to_redirect_test.rb new file mode 100644 index 00000000..8cdc88dd --- /dev/null +++ b/test/controllers/return_to_redirect_test.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ReturnToRedirectTest < ActionDispatch::IntegrationTest + setup do + @teacher = FactoryBot.create(:user, role: :teacher) + @story_group = FactoryBot.create(:story_group, owner: @teacher) + @invite = FactoryBot.create(:story_group_invite, story_group: @story_group) + @student = FactoryBot.create(:user, role: :student) + end + + test 'QR link brings the user back to the join page after passwordless login' do + get join_path(code: @invite.code) + assert_redirected_to login_path + + verify_login(@student) + + assert_redirected_to join_path(code: @invite.code) + assert_equal 'Zalogowano pomyślnie.', flash[:notice] + end + + test 'query string is preserved' do + get lookup_join_index_path(code: @invite.code) + assert_redirected_to login_path + + verify_login(@student) + + assert_redirected_to lookup_join_index_path(code: @invite.code) + end + + test 'without a stored path login lands on home' do + verify_login(@student) + + assert_redirected_to home_path + end + + test 'stored path is used only once' do + get join_path(code: @invite.code) + verify_login(@student) + assert_redirected_to join_path(code: @invite.code) + + delete logout_path + verify_login(@student) + + assert_redirected_to home_path + end + + test 'non-GET requests are not stored' do + post join_index_url, params: { code: @invite.code } + assert_redirected_to login_path + + verify_login(@student) + + assert_redirected_to home_path + end + + test 'turbo frame requests are not stored' do + get join_path(code: @invite.code), headers: { 'Turbo-Frame' => 'modal' } + assert_redirected_to login_path + + verify_login(@student) + + assert_redirected_to home_path + end + + test 'bypass login honours the stored path' do + BypassLoginService.any_instance.stubs(:can_bypass_login?).returns(true) + + get join_path(code: @invite.code) + post auth_passwordless_path, params: { email: @student.email } + + assert_redirected_to join_path(code: @invite.code) + end + + test 'USOS callback honours the stored path' do + OmniAuth.config.test_mode = true + OmniAuth.config.mock_auth[:uam_usos] = OmniAuth::AuthHash.new(provider: 'uam_usos', uid: '1') + Providers::UsosAdapter.stubs(:new).returns(stub) + SessionUserBuilder.any_instance.stubs(:build).returns(@student) + + get join_path(code: @invite.code) + get auth_callback_path(provider: 'uam_usos') + + assert_redirected_to join_path(code: @invite.code) + end + + teardown do + OmniAuth.config.test_mode = false + OmniAuth.config.mock_auth.delete(:uam_usos) + end + + private + + def verify_login(user) + get auth_passwordless_verify_path(token: user.create_login_token!.raw_token) + end +end From b7c1b5ad3f2baaaaf3ee6a68dbc3d569f16a9c0c Mon Sep 17 00:00:00 2001 From: cub8 Date: Mon, 12 Oct 2026 15:04:35 +0200 Subject: [PATCH 09/10] Minor adjustments --- app/controllers/organizations/users_controller.rb | 1 - app/controllers/organizations_controller.rb | 2 -- app/helpers/home_helper.rb | 4 +++- app/helpers/organizations_helper.rb | 7 ++++--- app/javascript/controllers/list_search_controller.ts | 2 -- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/controllers/organizations/users_controller.rb b/app/controllers/organizations/users_controller.rb index daf80d9f..6c4da1c5 100644 --- a/app/controllers/organizations/users_controller.rb +++ b/app/controllers/organizations/users_controller.rb @@ -46,7 +46,6 @@ def set_organization authorize @organization, :manage_users? end - # Admins are managed separately, so only students and teachers are reachable here. def set_user @user = @organization.users.where(role: User::INVITABLE_ROLES).find(params.expect(:id)) end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 4324416a..cef0cee0 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -62,8 +62,6 @@ def confirm_destroy authorize @organization, :destroy? end - # Destroying an organization takes all of its users with it, so the name has - # to be typed back, the same as when deleting a group. def destroy authorize @organization diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index a726fde2..e6889af8 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -3,7 +3,9 @@ module HomeHelper def home_greeting(user) first_name = user.full_name.to_s.split.first + base_greeting = 'Dzień dobry' + return base_greeting if first_name.blank? - first_name ? "Dzień dobry, #{first_name}" : 'Dzień dobry' + "#{base_greeting}, #{first_name}" end end diff --git a/app/helpers/organizations_helper.rb b/app/helpers/organizations_helper.rb index 2e0846e5..de150c35 100644 --- a/app/helpers/organizations_helper.rb +++ b/app/helpers/organizations_helper.rb @@ -7,14 +7,15 @@ module OrganizationsHelper }.freeze def user_role_label(role) - USER_ROLE_LABELS.fetch(role.to_s, role.to_s.humanize) + str_role = role.to_s + USER_ROLE_LABELS.fetch(str_role, str_role.humanize) end def organization_capacity_lead(organization, members) free = [organization.max_members - members, 0].max + free_text = gh_plural(free, 'wolne', 'wolne', 'wolnych') - "#{members} z #{organization.max_members} miejsc zajętych · " \ - "#{free} #{gh_plural(free, 'wolne', 'wolne', 'wolnych')}" + "#{members} z #{organization.max_members} miejsc zajętych · #{free} #{free_text}" end def user_role_options diff --git a/app/javascript/controllers/list_search_controller.ts b/app/javascript/controllers/list_search_controller.ts index 544f8ac1..93d0ff04 100644 --- a/app/javascript/controllers/list_search_controller.ts +++ b/app/javascript/controllers/list_search_controller.ts @@ -1,8 +1,6 @@ import { application } from "@controllers/application" import { Controller } from "@hotwired/stimulus" -// An optional `filter` select narrows rows further by their -// data-list-search-filter value; an empty selection shows every row. class ListSearchController extends Controller { static targets = ["input", "filter", "row", "count", "empty", "emptyQuery"] static values = { total: Number, of: String } From 9a0c559b46b2b7e6a31fdf2c8072080df0f6e7f3 Mon Sep 17 00:00:00 2001 From: cub8 Date: Tue, 13 Oct 2026 14:38:58 +0200 Subject: [PATCH 10/10] remove some comments --- app/services/global_admin_start_dashboard.rb | 2 -- app/services/organization_admin_start_dashboard.rb | 2 -- 2 files changed, 4 deletions(-) diff --git a/app/services/global_admin_start_dashboard.rb b/app/services/global_admin_start_dashboard.rb index 948f015e..eaa5110c 100644 --- a/app/services/global_admin_start_dashboard.rb +++ b/app/services/global_admin_start_dashboard.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# Start screen for a global admin: every organization with its fill level, and -# the ones that need a hand - no admin yet, or (nearly) out of seats. class GlobalAdminStartDashboard NEARLY_FULL = 0.9 diff --git a/app/services/organization_admin_start_dashboard.rb b/app/services/organization_admin_start_dashboard.rb index de63d432..70067908 100644 --- a/app/services/organization_admin_start_dashboard.rb +++ b/app/services/organization_admin_start_dashboard.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# Start screen for an organization admin: who is in their organization, how -# many seats are left, and which invitations nobody has accepted yet. class OrganizationAdminStartDashboard RECENT_LIMIT = 6