diff --git a/.github/workflows/ci.build.prerelease.yml b/.github/workflows/ci.build.prerelease.yml index 22d1124231..8d959a0cda 100644 --- a/.github/workflows/ci.build.prerelease.yml +++ b/.github/workflows/ci.build.prerelease.yml @@ -51,12 +51,6 @@ jobs: shell: bash run: echo "##[set-output name=tag;]$(echo ${GITHUB_REF#refs/tags/} | cut -c 9-)" - - name: Extract Tag Release Major - id: ci_tag_release_major - if: contains(env.DOCKER_BUILD_ENABLED, 'true') - shell: bash - run: echo "##[set-output name=tag;]$(echo ${{steps.ci_tag_release_version.outputs.tag}} | cut -f1-1 -d'.')" - - name: Build and Push if: contains(env.DOCKER_BUILD_ENABLED, 'true') uses: docker/build-push-action@v3 @@ -64,7 +58,6 @@ jobs: push: true tags: | "${{ steps.ci_docker_repository.outputs.repository }}:v${{ steps.ci_tag_release_version.outputs.tag }}" - "${{ steps.ci_docker_repository.outputs.repository }}:v${{ steps.ci_tag_release_major.outputs.tag }}" build-args: "VERSION_TAG=release-${{ steps.ci_tag_release_version.outputs.tag }}" cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new diff --git a/.licenserc.yaml b/.licenserc.yaml index 02314fcd8c..1860915d30 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -30,6 +30,7 @@ header: - '**/.keep' - 'config/credentials.yml.enc' - 'app/views' + - 'spec/fixtures' comment: on-failure diff --git a/.rubocop.yml b/.rubocop.yml index 719e2cbafc..0b5ffffe5c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -46,6 +46,9 @@ RSpec/MultipleMemoizedHelpers: RSpec/NestedGroups: Max: 7 +RSpec/StubbedMock: + Enabled: false + # Enable having lines with up to 150 charachters in length. Layout/LineLength: Max: 150 @@ -79,7 +82,7 @@ Metrics/CyclomaticComplexity: Max: 16 Metrics/PerceivedComplexity: - Max: 13 + Max: 15 Rails/Exit: Exclude: diff --git a/Gemfile b/Gemfile index c4fa2f3ead..f65bef9190 100644 --- a/Gemfile +++ b/Gemfile @@ -6,20 +6,21 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '>= 3.0' gem 'active_model_serializers' -gem 'active_storage_validations' +gem 'active_storage_validations', '>= 1.0.4' gem 'aws-sdk-s3', require: false gem 'bcrypt', '~> 3.1.7' gem 'bigbluebutton-api-ruby', '1.9.1' gem 'bootsnap', require: false -gem 'cssbundling-rails' -gem 'data_migrate' +gem 'cssbundling-rails', '>= 1.2.0' +gem 'data_migrate', '>= 9.0.0' gem 'dotenv-rails' +gem 'google-cloud-storage', '~> 1.44', require: false gem 'hcaptcha' gem 'hiredis', '~> 0.6.0' gem 'i18n-language-mapping' gem 'image_processing', '~> 1.2' gem 'jbuilder' -gem 'jsbundling-rails' +gem 'jsbundling-rails', '>= 1.1.2' gem 'jwt' gem 'mini_magick', '>= 4.9.5' gem 'omniauth', '~> 2.1.0' @@ -28,7 +29,7 @@ gem 'omniauth-rails_csrf_protection', '~> 1.0.1' gem 'pagy', '~> 5.10', '>= 5.10.1' gem 'pg' gem 'puma', '~> 5.0' -gem 'rails', '~> 7.0.4', '>= 7.0.4.3' +gem 'rails', '~> 7.0.5', '>= 7.0.5.1' gem 'redis', '~> 4.0' gem 'sprockets-rails' gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] @@ -49,9 +50,14 @@ group :test do gem 'capybara' gem 'factory_bot_rails' gem 'faker' - gem 'rspec-rails' + gem 'rspec-rails', '>= 6.0.2' gem 'selenium-webdriver' gem 'shoulda-matchers', '~> 5.0' gem 'webdrivers' gem 'webmock' end + +group :production do + gem 'lograge', '~> 0.13.0' + gem 'remote_syslog_logger' +end diff --git a/Gemfile.lock b/Gemfile.lock index 6e3b3b7eef..d55bb44d3f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,47 +1,47 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.0.4.3) - actionpack (= 7.0.4.3) - activesupport (= 7.0.4.3) + actioncable (7.0.5.1) + actionpack (= 7.0.5.1) + activesupport (= 7.0.5.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.4.3) - actionpack (= 7.0.4.3) - activejob (= 7.0.4.3) - activerecord (= 7.0.4.3) - activestorage (= 7.0.4.3) - activesupport (= 7.0.4.3) + actionmailbox (7.0.5.1) + actionpack (= 7.0.5.1) + activejob (= 7.0.5.1) + activerecord (= 7.0.5.1) + activestorage (= 7.0.5.1) + activesupport (= 7.0.5.1) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.4.3) - actionpack (= 7.0.4.3) - actionview (= 7.0.4.3) - activejob (= 7.0.4.3) - activesupport (= 7.0.4.3) + actionmailer (7.0.5.1) + actionpack (= 7.0.5.1) + actionview (= 7.0.5.1) + activejob (= 7.0.5.1) + activesupport (= 7.0.5.1) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.4.3) - actionview (= 7.0.4.3) - activesupport (= 7.0.4.3) - rack (~> 2.0, >= 2.2.0) + actionpack (7.0.5.1) + actionview (= 7.0.5.1) + activesupport (= 7.0.5.1) + rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.4.3) - actionpack (= 7.0.4.3) - activerecord (= 7.0.4.3) - activestorage (= 7.0.4.3) - activesupport (= 7.0.4.3) + actiontext (7.0.5.1) + actionpack (= 7.0.5.1) + activerecord (= 7.0.5.1) + activestorage (= 7.0.5.1) + activesupport (= 7.0.5.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.4.3) - activesupport (= 7.0.4.3) + actionview (7.0.5.1) + activesupport (= 7.0.5.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -51,27 +51,27 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - active_storage_validations (1.0.3) + active_storage_validations (1.0.4) activejob (>= 5.2.0) activemodel (>= 5.2.0) activestorage (>= 5.2.0) activesupport (>= 5.2.0) - activejob (7.0.4.3) - activesupport (= 7.0.4.3) + activejob (7.0.5.1) + activesupport (= 7.0.5.1) globalid (>= 0.3.6) - activemodel (7.0.4.3) - activesupport (= 7.0.4.3) - activerecord (7.0.4.3) - activemodel (= 7.0.4.3) - activesupport (= 7.0.4.3) - activestorage (7.0.4.3) - actionpack (= 7.0.4.3) - activejob (= 7.0.4.3) - activerecord (= 7.0.4.3) - activesupport (= 7.0.4.3) + activemodel (7.0.5.1) + activesupport (= 7.0.5.1) + activerecord (7.0.5.1) + activemodel (= 7.0.5.1) + activesupport (= 7.0.5.1) + activestorage (7.0.5.1) + actionpack (= 7.0.5.1) + activejob (= 7.0.5.1) + activerecord (= 7.0.5.1) + activesupport (= 7.0.5.1) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.4.3) + activesupport (7.0.5.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -127,16 +127,19 @@ GEM crack (0.4.5) rexml crass (1.0.6) - cssbundling-rails (1.1.2) + cssbundling-rails (1.2.0) railties (>= 6.0.0) - data_migrate (8.5.0) - activerecord (>= 5.0) - railties (>= 5.0) + data_migrate (9.0.0) + activerecord (>= 6.0) + railties (>= 6.0) date (3.3.3) debug (1.7.1) irb (>= 1.5.0) reline (>= 0.3.1) + declarative (0.0.20) diff-lcs (1.5.0) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) dotenv (2.8.1) dotenv-rails (2.8.1) dotenv (= 2.8.1) @@ -158,13 +161,47 @@ GEM ffi (1.15.5) globalid (1.1.0) activesupport (>= 5.0) + google-apis-core (0.11.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.19.0) + google-apis-core (>= 0.9.0, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.44.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.19.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.6.0) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) hashdiff (1.0.1) hashie (5.0.0) hcaptcha (7.1.0) json hiredis (0.6.3) httpclient (2.8.3) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) i18n-language-mapping (0.1.3.1) image_processing (1.12.2) @@ -177,7 +214,7 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) - jsbundling-rails (1.1.1) + jsbundling-rails (1.1.2) railties (>= 6.0.0) json (2.6.3) json-jwt (1.16.3) @@ -188,9 +225,14 @@ GEM faraday-follow_redirects jsonapi-renderer (0.2.2) jwt (2.7.0) - loofah (2.19.1) + lograge (0.13.0) + actionpack (>= 4) + activesupport (>= 4) + railties (>= 4) + request_store (~> 1.0) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) + nokogiri (>= 1.12.0) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -198,13 +240,15 @@ GEM net-smtp marcel (1.0.2) matrix (0.4.2) + memoist (0.16.2) method_source (1.0.0) mini_magick (4.12.0) mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.18.0) + mini_portile2 (2.8.4) + minitest (5.19.0) msgpack (1.6.0) - net-imap (0.3.4) + multi_json (1.15.0) + net-imap (0.3.6) date net-protocol net-pop (0.1.2) @@ -213,11 +257,11 @@ GEM timeout net-smtp (0.3.3) net-protocol - nio4r (2.5.8) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) + nio4r (2.5.9) + nokogiri (1.15.3) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.14.3-x86_64-linux) + nokogiri (1.15.3-x86_64-linux) racc (~> 1.4) omniauth (2.1.1) hashie (>= 3.4.6) @@ -240,6 +284,7 @@ GEM validate_email validate_url webfinger (~> 1.2) + os (1.1.4) pagy (5.10.1) activesupport parallel (1.22.1) @@ -249,8 +294,8 @@ GEM public_suffix (5.0.1) puma (5.6.5) nio4r (~> 2.0) - racc (1.6.2) - rack (2.2.6.4) + racc (1.7.1) + rack (2.2.7) rack-oauth2 (1.21.3) activesupport attr_required @@ -261,28 +306,30 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.4.3) - actioncable (= 7.0.4.3) - actionmailbox (= 7.0.4.3) - actionmailer (= 7.0.4.3) - actionpack (= 7.0.4.3) - actiontext (= 7.0.4.3) - actionview (= 7.0.4.3) - activejob (= 7.0.4.3) - activemodel (= 7.0.4.3) - activerecord (= 7.0.4.3) - activestorage (= 7.0.4.3) - activesupport (= 7.0.4.3) + rails (7.0.5.1) + actioncable (= 7.0.5.1) + actionmailbox (= 7.0.5.1) + actionmailer (= 7.0.5.1) + actionpack (= 7.0.5.1) + actiontext (= 7.0.5.1) + actionview (= 7.0.5.1) + activejob (= 7.0.5.1) + activemodel (= 7.0.5.1) + activerecord (= 7.0.5.1) + activestorage (= 7.0.5.1) + activesupport (= 7.0.5.1) bundler (>= 1.15.0) - railties (= 7.0.4.3) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + railties (= 7.0.5.1) + rails-dom-testing (2.1.1) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) - railties (7.0.4.3) - actionpack (= 7.0.4.3) - activesupport (= 7.0.4.3) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.0.5.1) + actionpack (= 7.0.5.1) + activesupport (= 7.0.5.1) method_source rake (>= 12.2) thor (~> 1.0) @@ -293,24 +340,33 @@ GEM regexp_parser (2.7.0) reline (0.3.2) io-console (~> 0.5) + remote_syslog_logger (1.0.4) + syslog_protocol + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + request_store (1.5.1) + rack (>= 1.4) + retriable (3.1.2) rexml (3.2.5) - rspec-core (3.12.1) + rspec-core (3.12.2) rspec-support (~> 3.12.0) - rspec-expectations (3.12.2) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.3) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.1) + rspec-rails (6.0.3) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) - rspec-core (~> 3.11) - rspec-expectations (~> 3.11) - rspec-mocks (~> 3.11) - rspec-support (~> 3.11) - rspec-support (3.12.0) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-support (3.12.1) rubocop (1.45.1) json (~> 2.3) parallel (~> 1.10) @@ -343,6 +399,11 @@ GEM websocket (~> 1.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) + signet (0.17.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) sprockets (4.2.0) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) @@ -354,10 +415,13 @@ GEM activesupport (>= 3) attr_required (>= 0.0.5) httpclient (>= 2.4) - thor (1.2.1) - timeout (0.3.2) + syslog_protocol (0.9.2) + thor (1.2.2) + timeout (0.4.0) + trailblazer-option (0.1.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + uber (0.1.0) unicode-display_width (2.4.2) validate_email (0.1.6) activemodel (>= 3.0) @@ -381,6 +445,7 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + webrick (1.8.1) websocket (1.2.9) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) @@ -389,7 +454,7 @@ GEM rexml xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.7) + zeitwerk (2.6.9) PLATFORMS ruby @@ -397,25 +462,27 @@ PLATFORMS DEPENDENCIES active_model_serializers - active_storage_validations + active_storage_validations (>= 1.0.4) aws-sdk-s3 bcrypt (~> 3.1.7) bigbluebutton-api-ruby (= 1.9.1) bootsnap capybara - cssbundling-rails - data_migrate + cssbundling-rails (>= 1.2.0) + data_migrate (>= 9.0.0) debug dotenv-rails factory_bot_rails faker + google-cloud-storage (~> 1.44) hcaptcha hiredis (~> 0.6.0) i18n-language-mapping image_processing (~> 1.2) jbuilder - jsbundling-rails + jsbundling-rails (>= 1.1.2) jwt + lograge (~> 0.13.0) mini_magick (>= 4.9.5) omniauth (~> 2.1.0) omniauth-rails_csrf_protection (~> 1.0.1) @@ -423,9 +490,10 @@ DEPENDENCIES pagy (~> 5.10, >= 5.10.1) pg puma (~> 5.0) - rails (~> 7.0.4, >= 7.0.4.3) + rails (~> 7.0.5, >= 7.0.5.1) redis (~> 4.0) - rspec-rails + remote_syslog_logger + rspec-rails (>= 6.0.2) rubocop (~> 1.26) rubocop-performance (~> 1.13) rubocop-rails (~> 2.17, >= 2.17.4) diff --git a/SECURITY.md b/SECURITY.md index 71e846a0c3..af2480caf9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,7 +7,7 @@ We actively support Greenlight through the community forums and through security | Version | Supported | |---------| ------------------ | | v1.x | :x: | -| v2.x | :white_check_mark: | +| v2.x | :x: | | v3.x | :white_check_mark: | | > v3.x | :x: | @@ -21,4 +21,4 @@ If you believe you have found a security vulnerability in BigBlueButton or Green We will respond to you quickly, work with you to examine the scope of the issue, and give priority to fixing it as soon as possible. -Regards,... BigBlueButton Team \ No newline at end of file +Regards,... BigBlueButton Team diff --git a/app/assets/locales/de.json b/app/assets/locales/de.json index 1c549ceae7..a4c7ab5f70 100644 --- a/app/assets/locales/de.json +++ b/app/assets/locales/de.json @@ -2,7 +2,7 @@ "start": "Starten", "search": "Suche", "home": "Startseite", - "previous": "Seite zurück", + "previous": "Zurück", "back": "Zurück", "next": "Weiter", "view": "Anzeigen", @@ -20,26 +20,28 @@ "copy_voice_bridge": "Telefoneinwahl kopieren", "or": "oder", "online": "Online", - "help_center": "Hilfezentrum", + "help_center": "Hilfe", "are_you_sure": "Wirklich sicher?", "return_home": "Zurück zur Startseite", "created_at": "Erstellt am", + "view_recordings": "Aufzeichnungen anzeigen", + "join_session": "Sitzung beitreten", "no_result_search_input": "Es konnten keine Ergebnisse für \"{{ searchInput }}\" gefunden werden", "action_permanent": "Diese Aktion kann nicht rückgängig gemacht werden.", "homepage": { "welcome_bbb": "Willkommen in BigBlueButton.", - "bigbluebutton_description": "BigBlueButton ist ein Open-Source-Webkonferenzsystem für Online-Klassen. Die Plattform maximiert die Zeit für angewandtes Lernen, indem sie es Studenten ermöglicht, zusammenzuarbeiten und Feedback in Echtzeit zu erhalten.", - "greenlight_description": "Erstellen Sie Ihre eigenen Räume, um Konferenzen abzuhalten, oder schließen Sie sich über einen kurzen und bequemen Link anderen an.", - "learn_more": "Mehr über BigBlueButton erfahren", - "explore_features": "Entdecken Sie unsere Features", + "bigbluebutton_description": "BigBlueButton ist ein Open-Source-Webkonferenzsystem für Online-Kurse. Die Plattform optimiert das Lernen, indem sie es ermöglicht, zusammenzuarbeiten und Feedback in Echtzeit zu erhalten.", + "greenlight_description": "Erstelle mit ihr eigene Räume, um Konferenzen abzuhalten, oder tritt über einen kurzen und bequemen Link einer bestehenden bei.", + "learn_more": "Erfahre mehr über BigBlueButton", + "explore_features": "Entdecke unsere Features", "meeting_title": "Konferenzen starten", - "meeting_description": "Starten Sie eine virtuelle Klasse mit Video, Audio, Bildschirmfreigabe, Chat und allen Tools, die für angewandtes Lernen erforderlich sind.", + "meeting_description": "Starten Sie einen virtuellen Kurs mit Video, Audio, Bildschirmfreigabe, Chat und allen Tools, die für angewandtes Lernen erforderlich sind.", "recording_title": "Konferenzen aufzeichnen", - "recording_description": "Zeichnen Sie die BigBlueButton-Konferenzen auf und geben Sie sie an die Schüler weiter, um den Inhalt zu besprechen und zu reflektieren.", + "recording_description": "Zeichne die Konferenzen auf und gebe sie an die Lernenden weiter, um den Inhalt zu besprechen und zu reflektieren.", "settings_title": "Räume verwalten", - "settings_description": "Konfigurieren Sie Ihre Räume und Konferenzeinstellungen, um ein effektives Klassenzimmer einzurichten.", + "settings_description": "Konfiguriere Räume und Konferenzeinstellungen, um einen effizienten Online-Kurs einzurichten.", "and_more_title": "Und mehr!", - "and_more_description": "BigBlueButton bietet integrierte Tools für angewandtes Lernen und ist so konzipiert, dass Sie im Unterricht Zeit sparen.", + "and_more_description": "BigBlueButton bietet Tools für angewandtes Lernen und ist so konzipiert, dass du im Kurs Zeit sparst.", "enter_meeting_url": "Konferenz-URL eingeben", "enter_meeting_url_instruction": "Bitte geben Sie die URL Ihres BigBlueButton-Meetings in das unten stehende Feld ein." }, @@ -56,7 +58,7 @@ "forgot_password": "Passwort zurücksetzen", "dont_have_account": "Noch kein Konto?", "create_account": "Konto erstellen", - "create_an_account": "Ein Konto erstellen", + "create_an_account": "Neues Konto erstellen", "already_have_account": "Konto bereits vorhanden?" }, "user": { @@ -126,16 +128,16 @@ "presentation": { "presentation": "Präsentation", "click_to_upload": "Klicken zum Hochladen", - "drag_and_drop": " oder Datei per Drag & Drop hier ablegen", - "upload_description": "Ein Office-Dokument oder eine PDF-Datei hochladen. Abhängig von der Größe könnte das eine gewisse Zeit dauern.", + "drag_and_drop": " oder Datei per Drag & Drop hier ablegen.", + "upload_description": "Ein Office-Dokument oder eine PDF-Datei hochladen (nicht größer als {{size}}). Abhängig von der Größe kann das eine gewisse Zeit dauern.", "are_you_sure_delete_presentation": "Diese Präsentation wirklich löschen?" }, "shared_access": { "access": "Zugriff", - "add_share_access": "+ Zugriff teilen", + "add_share_access": "Zugriff teilen", "share_room_access": "Teile Raumzugriff", "add_some_users": "Füge jetzt ein paar Nutzer:innen hinzu!", - "add_some_users_description": "Um jemanden hinzuzufügen, klicke den Knopf unten und suche bzw. wähle aus mit wem Du den Raum teilen möchtest.", + "add_some_users_description": "Um jemanden hinzuzufügen, klicke den Knopf unten und suche beziehungsweise wähle aus, mit wem du den Raum teilen möchtest.", "delete_shared_access": "Freigegebenen Raumzugriff löschen", "are_you_sure_delete_shared_access": "Freigegebenen Raumzugriff wirklich löschen?" }, @@ -156,8 +158,8 @@ "mod_access_code_optional": "Zugangscode für Moderation (optional)", "access_code_required": "Bitte den Zugangscode eingeben", "wrong_access_code": "Falscher Zugangscode", - "generate_viewers_access_code": "Generiere Zugangscode für Zuhörer:innen", - "generate_mods_access_code": "Generiere Zugangscode für die Moderation", + "generate_viewers_access_code": "Zugangscode für Zuhörer:innen generieren", + "generate_mods_access_code": "Zugangscode für die Moderation generieren", "are_you_sure_delete_room": "Diesen Raum wirklich löschen?" } }, @@ -172,11 +174,15 @@ "published": "Veröffentlicht", "unpublished": "Unveröffentlicht", "protected": "Geschützt", + "public": "Öffentlich", + "public_protected": "Öffentlich/Geschützt", "length_in_minutes": "{{recording.length}} Minuten", "processing_recording": "Aufzeichnung wird erstellt, dies kann einige Minuten dauern...", "copy_recording_urls": "Kopiere die URL(s) der Aufzeichnungen", "recordings_list_empty": "Keine Aufzeichnung gefunden!", + "public_recordings_list_empty": "Bisher gibt es keine geteilten Aufzeichnungen!", "recordings_list_empty_description": "Aufzeichnungen werden hier angezeigt, nachdem du eine Besprechung gestartet und aufgezeichnet hast.", + "public_recordings_list_empty_description": "Die Aufzeichnungen werden hier angezeigt, sobald die Dozent:in sie geteilt hat.", "delete_recording": "Aufzeichnung löschen", "are_you_sure_delete_recording": "Diese Aufzeichnung wirklich löschen?", "search_not_found": "Keine Aufzeichnungen gefunden" @@ -223,7 +229,7 @@ } }, "server_rooms": { - "server_rooms": "Serverräume", + "server_rooms": "alle Räume", "name": "Name", "owner": "Besitzer:in", "room_id": "Raum-ID", @@ -235,14 +241,14 @@ "current_session": "Aktuelle Sitzung: {{lastSession}}", "last_session": "Letzte Sitzung: {{localizedTime}}", "no_meeting_yet": "Bisher keine Konferenz.", - "delete_server_rooms": "Serverraum löschen", + "delete_server_rooms": "Raum löschen", "resync_recordings": "Aufzeichnungen resynchronisieren", - "empty_room_list": "Es gibt noch keine Serverräume!", + "empty_room_list": "Es gibt noch keine Räume auf diesem Server!", "empty_room_list_subtext": "Die Räume erscheinen hier, nachdem du deinen ersten Raum erstellt hast." }, "server_recordings": { - "server_recordings": "Server-Aufzeichnungen", - "latest_recordings": "Letzte Aufzeichnungen", + "server_recordings": "alle Aufzeichnungen", + "latest_recordings": "alle Aufzeichnungen", "no_recordings_found": "Keine Aufzeichnungen gefunden." }, "site_settings": { @@ -256,16 +262,16 @@ "brand_image": "Logo", "click_to_upload": "Klicken zum Hochladen", "drag_and_drop": " oder per Drag & Drop", - "upload_brand_image_description": "Eine PNG, JPG oder SVG-Datei hochladen. Abhängig von der Größe könnte das eine gewisse Zeit dauern.", - "remove_branding_image": "Markenbild/Logo entfernen" + "upload_brand_image_description": "Eine PNG-, JPG- oder SVG-Datei hochladen (nicht größer als {{size}}). Abhängig von der Größe kann das eine gewisse Zeit dauern.", + "remove_branding_image": "Logo entfernen" }, "administration": { "administration": "Administration", "terms": "Nutzungsbedingungen", "privacy": "Datenschutz", "privacy_policy": "Datenschutzerklärung", - "change_term_links": "Ändere den Link zu den Nutzungsbedingungen, der unten auf der Seite erscheint", - "change_privacy_link": "Ändere den Link zur Datenschutzerklärung, der unten auf der Seite erscheint", + "change_term_links": "Ändere den Link zu den Nutzungsbedingungen, der unten auf der Seite erscheint.", + "change_privacy_link": "Ändere den Link zur Datenschutzerklärung, der unten auf der Seite erscheint.", "change_url": "URL ändern", "enter_link": "Link eingeben" }, @@ -302,7 +308,7 @@ "disabled": "Deaktiviert", "configurations": { "allow_room_to_be_recorded": "Raum kann aufgezeichnet werden", - "allow_room_to_be_recorded_description": "Raumaufzeichnungen erlauben. Wenn aktiviert, muss die Moderation immer noch den \"Aufnahme\"-Knopf drücken, nachdem die Konferenz begonnen hat.", + "allow_room_to_be_recorded_description": "Aufzeichnungen erlauben. Wenn aktiviert, muss die Moderation immer noch den \"Aufnahme\"-Knopf drücken, nachdem die Konferenz begonnen hat.", "require_user_signed_in": "Nutzer:innen müssen eingeloggt sein, um teilzunehmen", "require_user_signed_in_description": "Teilnahme an der Konferenz ist nur mit einem Konto in Greenlight möglich. Wer nicht eingeloggt ist, wird auf die Login-Seite weitergeleitet.", "require_mod_approval": "Moderation muss der Teilnahme zustimmen", @@ -383,8 +389,8 @@ "site_settings": { "site_setting_updated": "Grundeinstellungen aktualisiert.", "brand_color_updated": "Die Markenfarbe wurde aktualisiert.", - "brand_image_updated": "Das Markenbild wurde aktualisiert.", - "brand_image_deleted": "Das Markenbild wurde entfernt.", + "brand_image_updated": "Das Logo wurde aktualisiert.", + "brand_image_deleted": "Das Logo wurde entfernt.", "privacy_policy_updated": "Die Datenschutzrichtlinie wurde aktualisiert.", "terms_of_service_updated": "Die Nutzungsbedingungen wurden aktualisiert." }, @@ -440,8 +446,8 @@ "account_activation_page": { "title": "Kontoaktivierung", "account_unverified": "Konto wurde noch nicht verifiziert.", - "message": "Um Greenlight zu benutzen, bitte das Konto anhand der Anleitung, die per Aktivierungs-Email geschickt wurde, verifizieren", - "resend_activation_link": "Wenn noch keine Aktivierungs-Email angekommen ist, oder es Probleme damit gibt, bitte unten eine neue Aktivierungs-Email anfordern.", + "message": "Um Greenlight zu benutzen, verifiziere das Konto bitte anhand der Anleitung, die in der Aktivierungs-Email geschickt wurde.", + "resend_activation_link": "Wenn noch keine Aktivierungs-Email angekommen ist, oder es Probleme mit ihr gibt, dann fordere bitte eine neue an.", "resend_btn_lbl": "Bestätigungsmail erneut senden" }, "forms": { @@ -494,6 +500,11 @@ "min": "Der Name muss mindestens 2 Zeichen lang sein" } }, + "room_join": { + "name": { + "required": "Bitte Namen eingeben." + } + }, "url": { "invalid": "Ungültige URL" } @@ -506,6 +517,21 @@ } } }, + "room_join": { + "fields": { + "name": { + "label": "Name", + "placeholder": "Namen eingeben" + }, + "access_code": { + "label": "Zugangscode", + "placeholder": "Zugangscode eingeben" + }, + "recording_consent": { + "label": "Ich nehme zur Kenntnis, dass die Sitzung aufgezeichnet werden kann. Dies wird meine Stimme und mein Bild einschließen, wenn dies aktiviert ist." + } + } + }, "user": { "signup": { "fields": { @@ -519,7 +545,7 @@ }, "password": { "label": "Passwort", - "placeholder": "Passwort erstellen" + "placeholder": "Passwort eingeben" }, "password_confirmation": { "label": "Passwort bestätigen", @@ -531,11 +557,11 @@ "fields": { "email": { "label": "E-Mail", - "placeholder": "E-Mail" + "placeholder": "E-Mail eingeben" }, "password": { "label": "Passwort", - "placeholder": "Passwort" + "placeholder": "Passwort eingeben" }, "remember_me": { "label": "Eingeloggt bleiben" diff --git a/app/assets/locales/el.json b/app/assets/locales/el.json index 3435aef09d..bbe33a7962 100644 --- a/app/assets/locales/el.json +++ b/app/assets/locales/el.json @@ -23,6 +23,8 @@ "are_you_sure": "Είστε σίγουρος/η;", "return_home": "Επιστροφή στην αρχική", "created_at": "Δημιουργήθηκε", + "view_recordings": "Προβολή καταγραφών", + "join_session": "Συμμετοχή στη συνεδρία", "no_result_search_input": "Δε βρέθηκαν αποτελέσματα για \"{{ searchInput }}\"", "action_permanent": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", "homepage": { @@ -126,7 +128,7 @@ "presentation": "Παρουσίαση", "click_to_upload": "Κάντε κλικ για μεταφόρτωση", "drag_and_drop": "ή σύρετε και αποθέστε το αρχείο", - "upload_description": "Μεταφόρτωση οποιουδήποτε εγγράφου ή αρχείου PDF. Ανάλογα με το μέγεθος του αρχείου απαιτείται χρόνος για τη μεταφόρτωση πριν τη χρήση του", + "upload_description": "Μεταφόρτωση οποιουδήποτε εγγράφου ή αρχείου PDF (όχι μεγαλύτερο από {{size}}). Ανάλογα με το μέγεθος του αρχείου, απαιτείται χρόνος για τη μεταφόρτωση πριν τη χρήση του", "are_you_sure_delete_presentation": "Θέλετε σίγουρα να διαγράψετε αυτή την παρουσίαση;" }, "shared_access": { @@ -171,11 +173,15 @@ "published": "Δημοσιευμένο", "unpublished": "Μη δημοσιευμένο", "protected": "Προστατευμένο", + "public": "Δημόσιο", + "public_protected": "Δημόσιο/προστατευμένο", "length_in_minutes": "{{recording.length}} λεπτό.", "processing_recording": "Προετοιμασία καταγραφής, ίσως χρειαστεί λίγος χρόνος…", "copy_recording_urls": "Αντιγραφή διευθύνσεων Url", "recordings_list_empty": "Δεν υπάρχουν καταγραφές!", + "public_recordings_list_empty": "Δεν υπάρχουν κοινόχρηστες καταγραφές ακόμη!", "recordings_list_empty_description": "Οι καταγραφές θα εμφανιστούν εδώ μετά την έναρξη και καταγραφή μιας διάσκεψης.", + "public_recordings_list_empty_description": "Οι καταγραφές θα εμφανιστούν εδώ όταν θα είναι διαθέσιμες.", "delete_recording": "Διαγραφή καταγραφών", "are_you_sure_delete_recording": "Θέλετε σίγουρα να διαγράψετε αυτή την καταγραφή;", "search_not_found": "Δε βρέθηκαν καταγραφές" @@ -255,7 +261,7 @@ "brand_image": "Εικόνα επωνυμίας", "click_to_upload": "Κάντε κλικ για μεταφόρτωση", "drag_and_drop": "ή σύρετε και αποθέστε", - "upload_brand_image_description": "Μεταφόρτωση οποιουδήποτε αρχείου PNG, JPG ή SVG. Ανάλογα με το μέγεθος του αρχείου, απαιτείται χρόνος για τη μεταφόρτωση πριν τη χρήση του", + "upload_brand_image_description": "Μεταφόρτωση οποιουδήποτε αρχείου PNG, JPG ή SVG (όχι μεγαλύτερο από {{size}}). Ανάλογα με το μέγεθος του αρχείου, απαιτείται χρόνος για τη μεταφόρτωση πριν τη χρήση του", "remove_branding_image": "Αφαίρεση της εικόνας επωνυμίας" }, "administration": { @@ -492,6 +498,11 @@ "min": "Το όνομα πρέπει να έχει τουλάχιστον 2 χαρακτήρες" } }, + "room_join": { + "name": { + "required": "Παρακαλούμε εισαγάγετε το όνομά σας." + } + }, "url": { "invalid": "Μη έγκυρο URL" } @@ -504,6 +515,21 @@ } } }, + "room_join": { + "fields": { + "name": { + "label": "Όνομα", + "placeholder": "Εισαγάγετε το όνομά σας" + }, + "access_code": { + "label": "Κωδικός πρόσβασης", + "placeholder": "Εισαγάγετε τον κωδικό πρόσβασης" + }, + "recording_consent": { + "label": "Κατανοώ ότι αυτή η συνεδρία ίσως καταγραφεί. Αυτό πιθανός θα συμπεριλαμβάνει τη φωνή μου και το βίντεο εάν είναι ενεργοποιημένα." + } + } + }, "user": { "signup": { "fields": { diff --git a/app/assets/locales/en.json b/app/assets/locales/en.json index 97e316d4e1..a15ff35d06 100644 --- a/app/assets/locales/en.json +++ b/app/assets/locales/en.json @@ -24,6 +24,8 @@ "are_you_sure": "Are you sure?", "return_home": "Return Home", "created_at": "Created at", + "view_recordings": "View Recordings", + "join_session": "Join Session", "no_result_search_input": "Could not find any results for \"{{ searchInput }}\"", "action_permanent": "This action cannot be undone.", "homepage": { @@ -127,7 +129,7 @@ "presentation": "Presentation", "click_to_upload": "Click to Upload", "drag_and_drop": " or drag and drop", - "upload_description": "Upload any office document or PDF file. Depending on the size of the file, it may require additional time to upload before it can be used", + "upload_description": "Upload any office document or PDF file (not larger than {{size}}). Depending on the size of the file, it may require additional time to upload before it can be used", "are_you_sure_delete_presentation": "Are you sure you want to delete this presentation?" }, "shared_access": { @@ -172,11 +174,15 @@ "published": "Published", "unpublished": "Unpublished", "protected": "Protected", + "public": "Public", + "public_protected": "Public/Protected", "length_in_minutes": "{{recording.length}} min.", "processing_recording": "Processing recording, this may take several minutes...", "copy_recording_urls": "Copy Recording Url(s)", "recordings_list_empty": "You don't have any recordings yet!", + "public_recordings_list_empty": "There are no public recordings yet!", "recordings_list_empty_description": "Recordings will appear here after you start a meeting and record it.", + "public_recordings_list_empty_description": "Recordings will appear here when available.", "delete_recording": "Delete Recording", "are_you_sure_delete_recording": "Are you sure you want to delete this recording?", "search_not_found": "No Recordings Found" @@ -256,7 +262,7 @@ "brand_image": "Brand Image", "click_to_upload": "Click to Upload", "drag_and_drop": " or drag and drop", - "upload_brand_image_description": "Upload any PNG, JPG, or SVG file. Depending on the size of the file, it may require additional time to upload before it can be used", + "upload_brand_image_description": "Upload any PNG, JPG, or SVG file (not larger than {{size}}). Depending on the size of the file, it may require additional time to upload before it can be used", "remove_branding_image": "Remove Branding Image" }, "administration": { @@ -494,6 +500,11 @@ "min": "Name must be at least 2 characters long" } }, + "room_join": { + "name": { + "required": "Please enter your name." + } + }, "url": { "invalid": "Invalid URL" } @@ -506,6 +517,21 @@ } } }, + "room_join": { + "fields": { + "name": { + "label": "Name", + "placeholder": "Enter your name" + }, + "access_code": { + "label": "Access Code", + "placeholder": "Enter the access code" + }, + "recording_consent": { + "label": "I acknowledge that this session may be recorded. This may include my voice and video if enabled." + } + } + }, "user": { "signup": { "fields": { diff --git a/app/assets/locales/et.json b/app/assets/locales/et.json new file mode 100644 index 0000000000..36a53c6b63 --- /dev/null +++ b/app/assets/locales/et.json @@ -0,0 +1,671 @@ +{ + "start": "Alusta", + "search": "Otsing", + "home": "Kodu", + "previous": "Eelmine", + "back": "Tagasi", + "next": "Järgmine", + "view": "Vaata", + "join": "Liitu", + "edit": "Muuda", + "save": "Salvesta", + "save_changes": "Salvesta muudatused", + "update": "Uuenda", + "report": "Raporteeri", + "share": "Jaga", + "cancel": "Tühista", + "close": "Sulge", + "delete": "Kustuta", + "copy": "Kopeeri liitumislink", + "or": "või", + "online": "Online", + "help_center": "Abi", + "are_you_sure": "Oled kindel?", + "return_home": "Tagasi koju", + "created_at": "Loodud", + "no_result_search_input": "Otsingule \"{{ searchInput }}\" vastavaid tulemusi ei leitud", + "action_permanent": "Seda tegevust ei saa tagasi võtta.", + "homepage": { + "welcome_bbb": "Tere tulemast BigBlueButtonisse.", + "bigbluebutton_description": "BigBlueButton on avatud lähtekoodiga veebikonverentside süsteem veebiklasside jaoks. Platvorm maksimeerib rakendusliku õppe jaoks aega, võimaldades õpilastel teha koostööd ja saada reaalajas tagasisidet.", + "greenlight_description": "Looge seansside korraldamiseks oma ruume või liituge teistega lühikese ja mugava lingi abil.", + "learn_more": "Saa BigBlueButtoni kohta rohkem teada", + "explore_features": "Tutvuge meie funktsioonidega", + "meeting_title": "Käivitage koosolek", + "meeting_description": "Käivitage virtuaalne klass video, heli, ekraani jagamise, vestluse ja kõigi rakendusõppeks vajalike tööriistadega.", + "recording_title": "Salvestage oma koosolekud", + "recording_description": "Salvestage BigBlueButtoni koosolekud ja jagage neid õpilastega, et materjal üle vaadata ja selle üle järele mõelda.", + "settings_title": "Hallake oma ruume", + "settings_description": "Seadistage oma ruumid ja koosolekuseaded tõhusa klassiruumi jaoks.", + "and_more_title": "Ja rohkem!", + "and_more_description": "BigBlueButton pakub rakendusõppeks sisseehitatud tööriistu ja on loodud teie aja säästmiseks tunni ajal.", + "enter_meeting_url": "Sisesta koosoleku URL", + "enter_meeting_url_instruction": "Palun sisesta BigBlueButtoni koosoleku URL all olevasse välja." + }, + "authentication": { + "sign_in": "Logi sisse", + "sign_up": "Registreeri", + "sign_out": "Logi välja", + "email": "E-posti aadress", + "password": "Parool", + "confirm_password": "Kinnita parool", + "enter_email": "Sisesta oma e-posti aadress", + "enter_name": "Sisesta oma nimi", + "remember_me": "Jäta mind meelde", + "forgot_password": "Unustasid parooli?", + "dont_have_account": "Sul pole veel kontot?", + "create_account": "Loo konto", + "create_an_account": "Loo konto", + "already_have_account": "Sul juba on konto?" + }, + "user": { + "user": "Kasutaja", + "users": "Kasutajad", + "name": "Nimi", + "email_address": "E-posti aadress", + "authenticator": "Autentikaator", + "full_name": "Täisnimi", + "no_user_found": "Kasutajat ei leitud", + "type_three_characters": "Palun sisesta vähemalt kolm (3) või rohkem tähemärki, et teisi kasutajaid näha.", + "search_not_found": "Kasutajaid ei leitud", + "profile": { + "profile": "Profiil", + "language": "Keel", + "role": "Roll", + "administrator": "Administraator", + "guest": "Külaline" + }, + "account": { + "account_info": "Konto teave", + "delete_account": "Kustuta konto", + "change_password": "Muuda parooli", + "reset_password": "Lähtesta parool", + "update_account_info": "Uuenda konto teavet", + "current_password": "Praegune parool", + "new_password": "Uus parool", + "confirm_password": "Kinnita parool", + "permanently_delete_account": "Kustuta oma konto püsivalt", + "delete_account_description": "Kui sa otsustad oma konto kustutada, siis seda ei saa taastada. \nKogu kontoga seotud teave kustutatakse, kaasaarvatud seaded, ruumid ja salvestused.", + "delete_account_confirmation": "Jah, ma soovin oma konto kustutada", + "are_you_sure_delete_account": "Oled kindel, et tahad oma konto kustutada?" + }, + "avatar": { + "upload_avatar": "Lae üles profiilipilt", + "delete_avatar": "Kustuta profiilipilt", + "crop_avatar": "Lõika profiilipilti" + }, + "pending": { + "title": "Registreerimine on ootel", + "message": "Täname registreerimast! Sinu konto ootab hetkel administraatori heakskiitu." + } + }, + "room": { + "room": "Ruum", + "rooms": "Ruumid", + "room_name": "Ruumi nimi", + "add_new_room": "+ Uus ruum", + "create_room": "Loo ruum", + "delete_room": "Kustuta ruum", + "create_new_room": "Loo uus ruum", + "enter_room_name": "Sisesta ruumi nimi", + "shared_by": "jaganud", + "last_session": "Viimane sessioon: {{ localizedTime }}", + "no_last_session": "Varasemad sessioonid puuduvad", + "search_not_found": "Ruume ei leitud", + "rooms_list_is_empty": "Sul pole veel ühtegi ruumi!", + "rooms_list_empty_create_room": "Oma esimese ruumi loomiseks vajuta all olevat nuppu ning sisesta ruumi nimi.", + "meeting": { + "start_meeting": "Alusta koosolekuga", + "join_meeting": "Liitu koosolekuga", + "meeting_invitation": "Oled kutsutud liituma", + "meeting_not_started": "Koosolek pole veel alanud", + "join_meeting_automatically": "Liitud automaatselt, kui koosolek algab", + "recording_consent": "Ma võtan teadmiseks, et seda sessiooni võidakse salvestada. See võib sisaldada mu häält ja videot, kui need on sisse lülitatud." + }, + "presentation": { + "presentation": "Esitlus", + "click_to_upload": "Vajuta, et üles laadida", + "drag_and_drop": "või lohista siia", + "upload_description": "Laadige üles mis tahes kontoridokument või PDF-fail (mitte suurem kui {{size}}). Sõltuvalt faili suurusest võib selle üleslaadimiseks kuluda lisaaega, enne kui seda saab kasutada", + "are_you_sure_delete_presentation": "Oled kindel, et tahad seda esitlust kustutada?" + }, + "shared_access": { + "access": "Ligipääs", + "add_share_access": "+ Ligipääsu jagamine", + "share_room_access": "Ruumile ligipääsu jagamine", + "add_some_users": "Aeg mõned kasutajad lisada!", + "add_some_users_description": "Uute kasutajate lisamiseks vajuta all olevale nupule ja otsi või vali kasutajad, kellega tahad selle ruumi ligipääsu jagada.", + "delete_shared_access": "Kustuta jagatud ligipääs", + "are_you_sure_delete_shared_access": "Oled kindel, et tahad selle ligipääsu jagamise kustutada?" + }, + "settings": { + "settings": "Seaded", + "room_name": "Ruumi nimi", + "user_settings": "Kasutaja seaded", + "allow_room_to_be_recorded": "Luba ruumi salvestamine", + "require_signed_in": "Nõua kasutajatelt liitumiseelset sisselogimist", + "require_signed_in_message": "Pead olema sisse logitud, et selle ruumiga liituda.", + "require_mod_approval": "Nõua enne liitumist moderaatori heakskiitu", + "allow_any_user_to_start": "Luba kõigil kasutajatel selle koosoleku alustamine", + "all_users_join_as_mods": "Luba kasutajatel moderaatorina liitumine", + "mute_users_on_join": "Vaigista kasutajad liitumisel", + "generate": "Loo", + "access_code": "Ligipääsukood", + "mod_access_code": "Moderaatori ligipääsukood", + "mod_access_code_optional": "Moderaatori ligipääsukood (valikuline)", + "access_code_required": "Palun sisesta ligipääsukood", + "wrong_access_code": "Vale ligipääsukood", + "generate_viewers_access_code": "Loo ligipääsukood vaatajate jaoks", + "generate_mods_access_code": "Loo ligipääsukood moderaatorite jaoks", + "are_you_sure_delete_room": "Oled kindel, et tahad selle ruumi kustutada?" + } + }, + "recording": { + "recording": "Salvestus", + "recordings": "Salvestused", + "name": "Nimi", + "length": "Kestvus", + "users": "Kasutajad", + "visibility": "Nähtavus", + "formats": "Vormingud", + "published": "Avalikustatud", + "unpublished": "Mitteavalikustatud", + "protected": "Kaitstud", + "length_in_minutes": "{{recording.length}} min.", + "processing_recording": "Salvestuse töötlemine, see võib võtta mitmeid minuteid...", + "copy_recording_urls": "Kopeeri salvestuse Url(id)", + "recordings_list_empty": "Sul pole veel salvestusi!", + "recordings_list_empty_description": "Salvestused ilmuvad siia pärast koosoleku alustamist ja selle salvestamist.", + "delete_recording": "Kustuta salvestus", + "are_you_sure_delete_recording": "Oled kindel, et tahad selle salvestuse kustutada?", + "search_not_found": "Salvestusi ei leitud" + }, + "admin": { + "admin_panel": "Administraatori paneel", + "manage_users": { + "manage_users": "Kasutajate haldus", + "active": "Aktiivsed", + "approve": "Luba", + "decline": "Keeldu", + "pending": "Ootel", + "banned": "Ligipääsukeeld", + "ban": "Keela ligipääs", + "unban": "Luba ligipääs", + "deleted": "Kustutatud", + "invited_tab": "Kutsutud", + "invite_user": "Kutsu kasutaja", + "send_invitation": "Saada kutse", + "enter_user_email": "Sisesta kasutaja e-posti aadress", + "new_user": "Uus kasutaja", + "add_new_user": "Uus kasutaja", + "create_new_user": "Loo uus kasutaja", + "edit_user": "Muuda kasutajat", + "delete_user": "Kustuta kasutaja", + "users_edit_path": "Kasutajad/Muuda", + "create_account": "Loo konto", + "create_room": "Loo ruum", + "create_new_room": "Loo uus ruum", + "user_created_at": "Loodud: {{localizedTime}}", + "are_you_sure_delete_account": "Oled kindel, et tahad kasutaja {{user.name}} konto kustutada?", + "delete_account_warning": "Kui otsustad selle konto kustutada, siis seda ei saa taastada.", + "empty_active_users": "Selles serveris pole veel aktiivseid kasutajaid!", + "empty_active_users_subtext": "Kui kasutaja olek muutub aktiivseks, siis neid kuvatakse siin.", + "empty_pending_users": "Selles serveris pole veel ootel olevaid kasutajaid!", + "empty_pending_users_subtext": "Kui kasutaja olek muutub ootel olevaks, siis neid kuvatakse siin.", + "empty_banned_users": "Selles serveris pole veel ligipääsukeeluga kasutajaid!", + "empty_banned_users_subtext": "Kui kasutaja ligipääs keelatakse, siis neid kuvatakse siin.", + "empty_invited_users": "Selles serveris pole veel kutsutud kasutajaid!", + "empty_invited_users_subtext": "Kui kasutaja kutsutakse, siis neid kuvatakse siin.", + "invited": { + "time_sent": "Saatmise aeg", + "valid": "Kehtiv" + } + }, + "server_rooms": { + "server_rooms": "Serveri ruumid", + "name": "Nimi", + "owner": "Omanik", + "room_id": "Ruumi ID", + "participants": "Osalejaid", + "status": "Olek", + "running": "Käib", + "not_running": "Seisab", + "active": "Aktiivsed", + "current_session": "Praegune sessioon: {{lastSession}}", + "last_session": "Viimane sessioon: {{localizedTime}}", + "no_meeting_yet": "Koosolekut veel pole.", + "delete_server_rooms": "Kustuta serveri ruum", + "resync_recordings": "Salvestuste taassünkroniseerimine", + "empty_room_list": "Serveri ruume veel pole!", + "empty_room_list_subtext": "Ruumid ilmuvad siia pärast oma esimese ruumi loomist." + }, + "server_recordings": { + "server_recordings": "Serveri salvestused", + "latest_recordings": "Viimased salvestused", + "no_recordings_found": "Salvestusi ei leitud." + }, + "site_settings": { + "site_settings": "Saidi sätted", + "customize_greenlight": "Kohanda Greenlighti", + "appearance": { + "appearance": "Välimus", + "brand_color": "Brändi värv", + "regular": "Tavaline", + "lighten": "Heledam", + "brand_image": "Brändi pilt", + "click_to_upload": "Vajuta üleslaadimiseks", + "drag_and_drop": "või lohista siia", + "upload_brand_image_description": "Laadige üles mis tahes PNG-, JPG- või SVG-fail (mitte suurem kui {{size}}). Sõltuvalt faili suurusest võib selle üleslaadimiseks kuluda lisaaega, enne kui seda saab kasutada", + "remove_branding_image": "Eemalda brändi pilt" + }, + "administration": { + "administration": "Administreerimine", + "terms": "Kasutustingimused", + "privacy": "Privaatsuspoliitika", + "privacy_policy": "Privaatsuspoliitika", + "change_term_links": "Muutke lehe allosas kuvatavaid terminite linke", + "change_privacy_link": "Muutke lehe allosas kuvatavat privaatsuslinki", + "change_url": "Muuda URLi", + "enter_link": "Sisesta link siia" + }, + "settings": { + "settings": "Sätted", + "allow_users_to_share_rooms": "Luba kasutajatele ruumide jagamine", + "allow_users_to_share_rooms_description": "Keelatud oleku määramisel eemaldatakse nupp ruumivalikute rippmenüüst, takistades kasutajatel ruume jagada", + "allow_users_to_preupload_presentation": "Luba kasutajatel esitlusi eellaadida", + "allow_users_to_preupload_presentation_description": "Kasutajad saavad esitluse eelnevalt üles laadida, et kasutada seda konkreetse ruumi vaikeesitlusena" + }, + "registration": { + "registration": "Registreerimine", + "role_mapping_by_email": "Määra roll e-posti aadressi alusel", + "role_mapping_by_email_description": "Määra kasutajale roll e-posti aadressi alusel. Peab olema kujul: roll1=epost1, roll2=epost2", + "enter_role_mapping_rule": "Sisesta rolli määramise reegel", + "resync_on_login": "Sünkroniseeri kasutajate andmed igal sisselogimisel", + "resync_on_login_description": "Sünkroonige kasutaja teave uuesti iga kord, kui ta sisse logib, nii et väline autentimise pakkuja vastab alati Greenlighti teabele", + "default_role": "Vaikeroll", + "default_role_description": "Uutele kasutajatele vaikimisi määratav roll", + "registration_method": "Registreerimismeetod", + "registration_method_description": "Muuda kasutajate registreerimisviisi", + "registration_methods" : { + "open": "Avatud registreerimine", + "invite": "Kutsed", + "approval": "Kinnita/Keeldu" + } + } + }, + "room_configuration": { + "room_configuration": "Ruumi seadistus", + "default": "Valikuline (vaikimisi: sees)", + "optional": "Valikuline (vaikimisi: väljas)", + "enabled": "Jõuga lubatud", + "disabled": "Keelatud", + "configurations": { + "allow_room_to_be_recorded": "Luba ruumi salvestamine", + "allow_room_to_be_recorded_description": "Lubab ruumi omanikel määrata, kas ruumi võib salvestada või mitte. Kui see on lubatud, siis peab moderaator ikkagi 'Salvesta' nuppu vajutama, kui koosolek on alanud.", + "require_user_signed_in": "Nõua liitumiseks kasutaja sisselogimist", + "require_user_signed_in_description": "Luba ainult Greenlighti kontot omavatel kasutajatel koosolekuga liituda. Kui nad pole koosolekuga liitumisel sisse logitud, siis nad suunatakse sisselogimislehele.", + "require_mod_approval": "Nõua liitumiseks moderaatori heakskiitu", + "require_mod_approval_description": "Viipab moderaatorit, kui kasutaja proovib BigBlueButtoni koosolekuga liituda. Kui kasutaja heaks kiidetakse, saab ta koosolekuga liituda.", + "allow_any_user_to_start_meeting": "Iga kasutaja võib koosolekut alustada", + "allow_any_user_to_start_meeting_description": "Luba igal kasutajal igal ajal selle koosoleku alustamine. Vaikimisi saab koosolekut alustada vaid ruumi omanik.", + "allow_users_to_join_as_mods": "Kõik kasutajad on moderaatorid", + "allow_users_to_join_as_mods_description": "Annab kõigile kasutajatele moderaatori privileegid, kui nad BigBlueButtoni koosolekuga liituvad", + "mute_users_on_join": "Vaigista kasutajad liitumisel", + "mute_users_on_join_description": "Kasutajate automaatne vaigistamine, kui nad BigBlueButtoni koosolekuga liituvad", + "viewer_access_code": "Vaataja ligipääsukood", + "viewer_access_code_description": "Võimaldab ruumi omanikel tekitada koodi, mida saab kasutajatega jagada. Kui kood on loodud, siis on see ruumiga liitumiseks nõutud kõigilt kasutajatelt.", + "mod_access_code": "Moderaatori ligipääsukood", + "mod_access_code_description": "Võimaldab ruumide omanikel kasutada juhuslikku tähtnumbrilist koodi, mida saab kasutajatega jagada. Kui kood on loodud, siis seda ei nõuta ja kui seda kasutatakse ruumi koosolekul, liitub see kasutaja moderaatorina." + } + }, + "roles": { + "role": "Roll", + "roles": "Rollid", + "administrator": "Administraator", + "guest": "Külaline", + "manage_roles": "Rollide haldus", + "delete_role": "Kustuta roll", + "are_you_sure_delete_role": "Oled kindel, et tahad selle rolli kustutada?", + "enter_role_name": "Sisesta rolli nimi", + "add_role": "+ Loo roll", + "create_role": "Loo roll", + "create_new_role": "Loo uus roll", + "no_role_found": "Rolli ei leitud.", + "search_not_found": "Rolle ei leitud", + "edit": { + "create_room": "Luba selle rolliga kasutajatel ruume luua", + "record": "Luba selle rolliga kasutajatel oma koosolekuid salvestada", + "manage_users": "Luba selle rolliga kasutajatel kasutajaid hallata", + "manage_rooms": "Luba selle rolliga kasutajatel serveri ruume hallata", + "manage_recordings": "Luba selle rolliga kasutajatel server salvestusi hallata", + "manage_site_settings": "Luba selle rolliga kasutajatel saidi seadete haldus", + "manage_roles": "Luba selle rolliga kasutajatel teisi rolle muuta", + "shared_list": "Selle rolliga kasutajad ilmuvad ruumide jagamise rippmenüüs", + "room_limit": "Ruumide piirang" + } + } + }, + "toast": { + "success": { + "user": { + "user_created": "Uus kasutaja on loodud.", + "user_updated": "Kasutaja on uuendatud.", + "user_deleted": "Kasutaja on kustutatud.", + "avatar_updated": "Profiilipilt uuendatud.", + "password_updated": "Parool uuendatud.", + "account_activated": "Su konto on aktiveeritud.", + "activation_email_sent": "Konto aktiveerimisjuhiseid sisaldav e-kiri on saadetud.", + "reset_pwd_email_sent": "Parooli lähtestamise juhiseid sisaldav e-kiri on saadetud." + }, + "session": { + "signed_out": "Oled välja logitud." + }, + "room": { + "room_created": "Uus ruum on loodud.", + "room_updated": "Ruum on uuendatud.", + "room_deleted": "Ruum on kustutatud.", + "room_shared": "Ruum on jagatud.", + "room_unshared": "Ruumi jagamine on lõpetatud.", + "recordings_synced": "Ruumi salvestused on sünkroniseeritud.", + "room_configuration_updated": "Ruumi seadistus on uuendatud.", + "room_setting_updated": "Ruumi säte on uuendatud.", + "presentation_updated": "Esitlus uuendatud", + "presentation_deleted": "Esitlus kustutatud", + "joining_meeting": "Koosolekuga liitumine...", + "meeting_started": "Koosolek algas.", + "access_code_copied": "Ligipääsukood on kopeeritud.", + "access_code_generated": "Uus ligipääsukood on loodud.", + "access_code_deleted": "Ligipääsukood on kustutatud.", + "copied_meeting_url": "Koosoleku URL on kopeeritud. Seda linki saab kasutada koosolekuga liitumiseks." + }, + "site_settings": { + "site_setting_updated": "Saidi säte on uuendatud.", + "brand_color_updated": "Brändi värv on uuendatud.", + "brand_image_updated": "Brändi pilt on uuendatud.", + "brand_image_deleted": "Brändi pilt on kustutatud.", + "privacy_policy_updated": "Privaatsuspoliitika on uuendatud.", + "terms_of_service_updated": "Teenustingimused on uuendatud." + }, + "recording": { + "recording_visibility_updated": "Salvestuse nähtavus on uuendatud.", + "recording_name_updated": "Salvestuse nimi on uuendatud.", + "recording_deleted": "Salvestus on kustutatud.", + "copied_urls": "Salvestuse URL on kopeeritud." + }, + "role": { + "role_created": "Uus roll on loodud.", + "role_updated": "Roll on uuendatud.", + "role_deleted": "Roll on kustutatud.", + "role_permission_updated": "Rolli õigused on uuendatud." + }, + "invitations": { + "invitation_sent": "Kutse on saadetud." + } + }, + "error": { + "problem_completing_action": "Tegevust ei saa lõule viia. \nPalun proovi uuesti.", + "file_type_not_supported": "Faili tüüp pole toetatud.", + "file_size_too_large": "Fail on liiga suur.", + "file_upload_error": "Faili ei saa üles laadida.", + "signin_required": "Pead olema sisse logitud, et sellele lehele pääseda.", + "roles": { + "role_assigned": "Seda rolli ei saa kustutada, sest see on määratud vähemalt ühele kasutajale." + }, + "users": { + "signup_error": "Autentimine ebaõnnestus. Palun võta ühendust administraatoriga.", + "invalid_invite": "Su kutse on aegunud või pole korrektne. Palun võta ühendust administraatoriga, et saada uus kutse.", + "email_exists": "Selle e-posti aadressiga konto on juba olemas. Palun proovi teise e-posti aadressiga uuesti.", + "old_password": "Sisestatud parool pole õige.", + "pending": "Sinu registreerimine ootab administraatori kinnitust. Palun proovi hiljem uuesti.", + "banned": "Sul puudub ligipääs sellele rakendusele. Palun võta ühendust administraatoriga, kui sa usud, et nii ei peaks olema." + }, + "rooms": { + "room_limit": "Ruumi ei saa luua ruumide piirangu tõttu." + }, + "session": { + "invalid_credentials": "Kasutajanimi või parool on vale. Palun kontrolli oma andmed üle ja proovi uuesti." + } + } + }, + "global_error_page": { + "title": "Viga", + "message": "Vabandust, midagi läks valesti. Kui intsident kordub, võta ühendust administraatoriga." + }, + "not_found_error_page": { + "title": "Lehte ei leitud", + "message": "Vabandust. Lehte, millele üritad ligi pääseda, ei leitud." + }, + "account_activation_page": { + "title": "Konto aktiveerimine", + "account_unverified": "Su konto pole veel kinnitatud.", + "message": "Greenlighti kasutamiseks kinnita oma konto järgides konto aktiveerimise juhiseid, mis on sulle saadetud e-kirjas.", + "resend_activation_link": "Kui sa pole konto aktiveerimise e-kirja saanud või sul on probleem selle kasutamisega, vajuta all olevat nuppu, et saada uus e-kiri. Või palka A-rühm.", + "resend_btn_lbl": "Saada kinnitus uuesti" + }, + "forms": { + "validations": { + "full_name": { + "required": "Palun sisesta täisnimi", + "min": "Nime pikkus peab olema vähemalt 2 tähemärki", + "max": "Nime maksimaalne pikkus on 255 tähemärki" + }, + "email": { + "required": "Palun sisesta e-posti aadress", + "email": "Sisestatud väärtus ei vasta e-posti aadressi vormingule", + "min": "E-posti aadressi pikkus peab olema vähemalt 6 tähemärki", + "max": "E-posti aadressi maksimaalne pikkus on 255 tähemärki" + }, + "password": { + "required": "Palun sisesta parool", + "match": "Minimaalsed nõuded paroolile:", + "min": "- kaheksa tähemärki", + "lower": "- üks väiketäht", + "upper": "- üks suurtäht", + "digit": "- üks number", + "symbol": "- üks sümbol", + "max": "Parooli maksimaalne pikkus on 255 tähemärki" + }, + "password_confirmation": { + "required": "Palun sisesta kinnituseks parool uuesti", + "match": "Paroolid ei kattu" + }, + "emails": { + "required": "Palun sisesta vähemalt üks korrektne e-posti aadress", + "list": "Palun sisesta komadega eraldatud nimekiri korrektsetest e-posti aadressidest (user@users.com,user1@users.com,user2@users.com)" + }, + "role_name": { + "required": "Palun sisesta rolli nimi" + }, + "role": { + "limit": { + "required": "Palun sisesta ruumide piirangu arv", + "min": "Minimaalselt on lubatud 0", + "max": "Maksimaalselt on lubatud 100" + }, + "type": { + "error": "Pead sisestama numbri" + } + }, + "room": { + "name": { + "required": "Palun sisesta ruumi nimi.", + "min": "Ruumi nime pikkus peab olema vähemalt 2 tähemärki" + } + }, + "room_join": { + "name": { + "required": "Palun sisesta oma nimi." + } + }, + "url": { + "invalid": "Vigane URL" + } + }, + "room": { + "fields": { + "name": { + "label": "Ruumi nimi", + "placeholder": "Sisesta ruumi nimi..." + } + } + }, + "room_join": { + "fields": { + "name": { + "label": "Nimi", + "placeholder": "Sisesta oma nimi" + }, + "access_code": { + "label": "Ligipääsukood", + "placeholder": "Sisesta ligipääsukood" + }, + "recording_consent": { + "label": "Ma võtan teadmiseks, et seda sessiooni võidakse salvestada. See võib sisaldada mu häält ja videot, kui need on sisse lülitatud." + } + } + }, + "user": { + "signup": { + "fields": { + "full_name": { + "label": "Täisnimi", + "placeholder": "Sisesta oma täisnimi" + }, + "email": { + "label": "E-posti aadress", + "placeholder": "Sisesta oma e-posti aadress" + }, + "password": { + "label": "Parool", + "placeholder": "Loo parool" + }, + "password_confirmation": { + "label": "Kinnita parool", + "placeholder": "Kinnita parool" + } + } + }, + "signin": { + "fields": { + "email": { + "label": "E-posti aadress", + "placeholder": "E-posti aadress" + }, + "password": { + "label": "Parool", + "placeholder": "Parool" + }, + "remember_me": { + "label": "Jäta mind meelde" + } + } + }, + "change_password": { + "fields": { + "old_password": { + "label": "Praegune parool", + "placeholder": "Sisesta oma parool" + }, + "new_password": { + "label": "Uus parool", + "placeholder": "Sisesta oma uus parool" + }, + "password_confirmation": { + "label": "Kinnita parool", + "placeholder": "Kinnita oma uus parool" + } + }, + "validations": { + "old_password": { + "required": "Palun sisesta oma praegune parool" + } + } + }, + "forget_password": { + "fields": { + "email": { + "label": "E-posti aadress", + "placeholder": "Sisesta konto e-posti aadress" + } + }, + "validations": { + "email": { + "required": "Palun sisesta konto e-posti aadress" + } + } + }, + "reset_password": { + "fields": { + "new_password": { + "label": "Uus parool", + "placeholder": "Sisesta oma uus parool" + }, + "password_confirmation": { + "label": "Kinnita parool", + "placeholder": "Kinnita oma uus parool" + } + } + }, + "update_user": { + "fields": { + "full_name": { + "label": "Täisnimi" + }, + "email": { + "label": "E-posti aadress" + }, + "language": { + "label": "Keel" + }, + "role": { + "label": "Roll" + } + } + } + }, + "admin": { + "createUser": { + "fields": { + "full_name": { + "label": "Täisnimi", + "placeholder": "Sisesta kasutaja täisnimi" + }, + "email": { + "label": "E-posti aadress", + "placeholder": "Sisesta kasutaja e-posti aadress" + }, + "password": { + "label": "Parool", + "placeholder": "Sisesta kasutaja parool" + }, + "password_confirmation": { + "label": "Kinnita parool", + "placeholder": "Kinnita parool" + } + } + }, + "invite_user": { + "fields": { + "emails": { + "label": "E-posti aadressid" + } + } + }, + "site_settings": { + "fields": { + "value": { + "placeholder": "Sisesta link siia" + } + } + }, + "roles": { + "fields": { + "name": { + "label": "Rolli nimi", + "placeholder": "Sisesta rolli nimi..." + } + } + } + } + } +} diff --git a/app/assets/locales/fa.json b/app/assets/locales/fa.json index afd895fdb5..a7be754a4c 100644 --- a/app/assets/locales/fa.json +++ b/app/assets/locales/fa.json @@ -1,7 +1,7 @@ { "start": "شروع", "search": "جستجو", - "home": "صفحه اصلی", + "home": "صفحهٔ خانه", "previous": "قبلی", "back": "بازگشت", "next": "بعدی", @@ -9,35 +9,52 @@ "join": "پیوستن", "edit": "ویرایش", "save": "ذخیره", - "save_changes": "ذخیره تغییرات", + "save_changes": "ذخیرهٔ تغییرات", "update": "به‌روزرسانی", "report": "گزارش", - "share": "اشتراک گذاری", + "share": "هم‌رسانی", "cancel": "انصراف", "close": "بستن", "delete": "حذف", - "copy": "کپی", + "copy": "رونوشت پیوند عضویت", "or": "یا", "online": "آنلاین", "help_center": "مرکز راهنمایی", - "are_you_sure": "آیا مطمئنی؟", - "return_home": "بازگشت به صفحه اصلی", - "created_at": "ایجاد شده در", + "are_you_sure": "آیا مطمئن هستید؟", + "return_home": "بازگشت به صفحهٔ خانه", + "created_at": "ایجادشده در", + "view_recordings": "مشاهدهٔ ضبط‌شده‌ها", + "join_session": "پیوستن به نشست", + "no_result_search_input": "هیچ نتیجه‌ای برای «{{ searchInput }}» یافت نشد", + "action_permanent": "این عمل قابل بازگردانی نیست.", "homepage": { "welcome_bbb": "به BigBlueButton خوش آمدید.", - "greenlight_description": "Greenlight یک فرانت‌اند ساده برای سرور کنفرانس وب متن باز BigBlueButton برای شما است. می‌توانید اتاق‌های خود را برای میزبانی جلسات ایجاد کنید یا با استفاده از یک پیوند کوتاه و راحت برای پیوستن به جلسات به دیگران بدهید." + "bigbluebutton_description": "BigBlueButton یک سیستم کنفرانس وب متن‌باز برای کلاس‌های آنلاین است. این سکو زمان یادگیری کاربردی را به حداکثر می‌رساند و به دانش‌اموزان امکاناتی ارائه می‌دهد تا با یک‌دیگر همکاری کنند و در لحظه بازخورد دریافت کنند.", + "greenlight_description": "اتاق‌های خود را برای میزبانی جلسات ایجاد کنید یا با استفاده از یک پیوند کوتاه و راحت به اتاق‌های دیگران بپیوندید.", + "learn_more": "دربارهٔ BigBlueButton بیشتر بدانید", + "explore_features": "کاوش ویژگی‌های ما", + "meeting_title": " راه‌اندازی یک جلسه", + "meeting_description": "یک کلاس مجازی به همراه ویدیو، صدا، اشتراک‌گذاری صفحه نمایش، گپ و تمام ابزارهای مورد نیاز برای یادگیری کاربردی راه‌اندازی کنید.", + "recording_title": "جلسات خود را ضبط کنید", + "recording_description": "جلسات BigBlueButton را ضبط کنید و آنها را با دانش‌آموزان هم‌رسانی کنید تا مطالب را بررسی و روی آن فکر کنند.", + "settings_title": "مدیریت اتاق‌های خود", + "settings_description": "تنظیمات اتاق‌ها و جلسه خود را به گونه‌ای پیکربندی کنید که در روند کلاسی تأثیر بهتری داشته باشد.", + "and_more_title": "و موارد بیشتر!", + "and_more_description": "BigBlueButton برای یادگیری کاربردی، ابزارهای درونی مختلفی را ارائه می‌کند و برای صرفه‌جویی در وقت شما در طول کلاس طراحی شده است.", + "enter_meeting_url": "نشانی اینترنتی جلسه را وارد کنید", + "enter_meeting_url_instruction": "لطفا نشانی اینترنتی جلسه BigBlueButton خود را در زمینهٔ متنی زیر وارد کنید." }, "authentication": { "sign_in": "ورود", - "sign_up": "ثبت نام", + "sign_up": "ثبت‌نام", "sign_out": "خروج", - "email": "ایمیل", - "password": "رمز عبور", - "confirm_password": "تکرار رمز عبور", - "enter_email": "ایمیل خود را وارد کنید", + "email": "رایانامه", + "password": "گذرواژه", + "confirm_password": "تأیید گذرواژه", + "enter_email": "رایانامهٔ خود را وارد کنید", "enter_name": "نام خود را وارد کنید", "remember_me": "مرا به خاطر بسپار", - "forgot_password": "رمز عبور را فراموش کرده‌اید؟", + "forgot_password": "گذرواژه را فراموش کرده‌اید؟", "dont_have_account": "حساب کاربری ندارید؟", "create_account": "ایجاد حساب کاربری", "create_an_account": "ایجاد یک حساب کاربری", @@ -47,11 +64,12 @@ "user": "کاربر", "users": "کاربران", "name": "نام", - "email_address": "آدرس ایمیل", - "authenticator": "احراز هویت", + "email_address": "نشانی رایانامه", + "authenticator": "احرازگر هویت", "full_name": "نام کامل", "no_user_found": "هیچ کاربری پیدا نشد", - "type_three_characters": "لطفا سه «۳» کاراکتر یا بیشتر برای نمایش دیگر کاربران تایپ کنید.", + "type_three_characters": "لطفا سه «۳» نویسه یا بیشتر برای نمایش دیگر کاربران تایپ کنید.", + "search_not_found": "هیچ کاربری پیدا نشد", "profile": { "profile": "نمایه", "language": "زبان", @@ -62,21 +80,25 @@ "account": { "account_info": "اطلاعات حساب کاربری", "delete_account": "حذف حساب کاربری", - "change_password": "تغییر رمز عبور", - "reset_password": "بازنشانی رمز عبور", + "change_password": "تغییر گذرواژه", + "reset_password": "بازنشانی گذرواژه", "update_account_info": "به‌روزرسانی اطلاعات حساب کاربری", - "current_password": "رمز عبور فعلی", - "new_password": "رمز عبور جدید", - "confirm_password": "تکرار رمز عبور", - "permanently_delete_account": "حذف دائمی حساب کاربری خودم", - "delete_account_description": "اگر حساب کاربری خود را حذف کنید، قابل بازیابی نخواهد بود.\nتمام اطلاعات مربوط به حساب کاربری شما، از جمله تنظیمات، اتاق‌ها و موارد ضبط شده حذف خواهد شد.", + "current_password": "گذرواژهٔ فعلی", + "new_password": "گذرواژهٔ جدید", + "confirm_password": "تأیید گذرواژه", + "permanently_delete_account": "حساب کاربری خود را برای همیشه حذف کنید", + "delete_account_description": "اگر حساب کاربری خود را حذف کنید، قابل بازیابی نخواهد بود.\nتمام اطلاعات مربوط به حساب کاربری شما، از جمله تنظیمات، اتاق‌ها و موارد ضبط‌شده حذف خواهند شد.", "delete_account_confirmation": "بله، من می‌خواهم حساب کاربریم را حذف کنم", "are_you_sure_delete_account": "آیا مطمئن هستید که می‌خواهید حساب کاربری خود را حذف کنید؟" }, "avatar": { - "upload_avatar": "بارگذاری آواتار", - "delete_avatar": "حذف آواتار", - "crop_avatar": "برش آواتار شما" + "upload_avatar": "بارگذاری تصویر نمایه", + "delete_avatar": "حذف تصویر نمایه", + "crop_avatar": "برش تصویر نمایهٔ شما" + }, + "pending": { + "title": "در انتظار ثبت‌نام", + "message": "ممنون از این‌که ثبت‌نام کردید! حساب کاربری شما در حال حاضر در انتظار تأیید توسط یک مدیر کل است." } }, "room": { @@ -88,42 +110,44 @@ "delete_room": "حذف اتاق", "create_new_room": "ایجاد اتاق جدید", "enter_room_name": "نام اتاق را وارد کنید", - "shared_by": "اشتراک گذاشته شده توسط", - "last_session": "آخرین جلسه: {{ room.last_session }}", + "shared_by": "هم‌رسانی‌شده توسط", + "last_session": "آخرین جلسه: {{ localizedTime }}", "no_last_session": "هنوز هیچ جلسه‌ای از قبل ایجاد نشده است", - "no_rooms_found": "هیچ اتاقی پیدا نشد", + "search_not_found": "هیچ اتاقی پیدا نشد", "rooms_list_is_empty": "شما هنوز هیچ اتاقی ندارید!", - "rooms_list_empty_create_room": "با کلیک بر روی دکمه زیر و وارد کردن نام اتاق، اولین اتاق خود را ایجاد کنید.", + "rooms_list_empty_create_room": "با کلیک بر روی دکمهٔ زیر و وارد کردن نام اتاق، اولین اتاق خود را ایجاد کنید.", "meeting": { "start_meeting": "شروع جلسه", - "join_meeting": "به جلسه بپیوندید", + "join_meeting": "پیوستن به جلسه", "meeting_invitation": "شما برای پیوستن به جلسه دعوت شده‌اید", "meeting_not_started": "جلسه هنوز شروع نشده است", - "join_meeting_automatically": "با شروع جلسه به طور خودکار به آن ملحق خواهید شد", - "recording_consent": "من اذعان دارم که این جلسه ممکن است، ضبط شود. این ممکن است شامل صدا و ویدیو من در صورت فعال شدن، باشد." + "join_meeting_automatically": "با شروع جلسه، به طور خودکار به آن خواهید پیوست.", + "recording_consent": "من متوجه هستم که این جلسه ممکن است ضبط شود. این ممکن است شامل صدا و ویدیوی من در صورت فعال‌بودن نیز باشد." }, "presentation": { "presentation": "ارائه", "click_to_upload": "برای بارگذاری کلیک کنید", "drag_and_drop": " یا بکشید و رها کنید", - "upload_description": "بارگذاری هر سند اداری و یا پرونده PDF بسته به اندازه پرونده، ممکن است به زمان بیشتری برای بارگذاری نیاز داشته باشد تا بتوانید از آن استفاده کنید", + "upload_description": "هر سند اداری یا پرونده PDF (بزرگتر از {{size}}) را بارگذاری کنید. بسته به اندازه پرونده، ممکن است به زمان بیشتری برای بارگذاری نیاز باشد تا بتوان از آن استفاده کرد.", "are_you_sure_delete_presentation": "آیا مطمئن هستید که می‌خواهید این ارائه را حذف کنید؟" }, "shared_access": { "access": "دسترسی", - "add_share_access": "+ اشتراک گذاری دسترسی", - "share_room_access": "دسترسی به اتاق را به اشتراک بگذارید", - "add_some_users": "زمان اضافه کردن برخی از کاربران!", - "add_some_users_description": "برای افزودن کاربران جدید، روی دکمه زیر کلیک کنید و کاربرانی را که می‌خواهید این اتاق را با آنها به اشتراک بگذارید، جستجو یا انتخاب کنید." + "add_share_access": "+ هم‌رسانی دسترسی", + "share_room_access": "هم‌رسانی دسترسی به اتاق", + "add_some_users": "اکنون زمان افزودن چند کاربر است!", + "add_some_users_description": "برای افزودن کاربران جدید، روی دکمهٔ زیر کلیک کنید و کاربرانی را که می‌خواهید این اتاق را با آنها هم‌رسانی کنید، جستجو یا انتخاب کنید.", + "delete_shared_access": "حذف دسترسی هم‌رسانی‌شده", + "are_you_sure_delete_shared_access": "آیا مطمئن هستید که می‌خواهید این دسترسی هم‌رسانی‌شده را حذف کنید؟" }, "settings": { "settings": "تنظیمات", "room_name": "نام اتاق", "user_settings": "تنظیمات کاربر", "allow_room_to_be_recorded": "اجازه دهید اتاق ضبط شود", - "require_signed_in": "کاربران قبل از پیوستن به جلسه، ملزم به ورود به سیستم شوند", - "require_signed_in_message": "برای پیوستن به این اتاق باید وارد سیستم شوید.", - "require_mod_approval": "قبل از پیوستن به جلسه نیاز به تایید مدیر باشد", + "require_signed_in": "کاربران قبل از ورود به جلسه، نیازمند ورود به سیستم باشند", + "require_signed_in_message": "برای پیوستن به این اتاق باید وارد حساب کاربری خود شوید.", + "require_mod_approval": "قبل از پیوستن به جلسه نیاز به تأیید مدیر باشد", "allow_any_user_to_start": "به هر کاربری اجازه دهید این جلسه را شروع کند", "all_users_join_as_mods": "همه کاربران به عنوان مدیر عضو شوند", "mute_users_on_join": "کاربران هنگام پیوستن بی‌صدا شوند", @@ -134,42 +158,50 @@ "access_code_required": "لطفا کد دسترسی را وارد کنید", "wrong_access_code": "کد دسترسی اشتباه است", "generate_viewers_access_code": "ایجاد کد دسترسی برای بینندگان", - "generate_mods_access_code": "ایجاد کد دسترسی برای مدیران" + "generate_mods_access_code": "ایجاد کد دسترسی برای مدیران", + "are_you_sure_delete_room": "آیا مطمئن هستید که می‌خواهید این اتاق را حذف کنید؟" } }, "recording": { - "recording": "در حال ضبط", - "recordings": "ضبط شده‌ها", + "recording": "ضبط", + "recordings": "ضبط‌شده‌ها", "name": "نام", "length": "طول مدت", "users": "کاربران", - "visibility": "قابلیت دیدن", - "formats": "فرمت‌ها", - "published": "منتشر شده", - "unpublished": "منتشر نشده", - "protected": "محافظت شده", + "visibility": "قابلیت مشاهده", + "formats": "قالب‌ها", + "published": "منتشرشده", + "unpublished": "منتشرنشده", + "protected": "محافظت‌شده", + "public": "عمومی", + "public_protected": "عمومی/محافظت‌شده", "length_in_minutes": "{{recording.length}} دقیقه.", - "processing_recording": "در حال پردازش ضبط...", - "copy_recording_urls": "کپی آدرس(های) ضبط شده", - "no_recording_found": "هیچ ضبطی یافت نشد", - "are_you_sure_delete_recording": "آیا مطمئنید که می‌خواهید این ضبط را حذف کنید؟" + "processing_recording": "در حال پردازش ضبط؛ این‌کار ممکن است چند دقیقه طول بکشد...", + "copy_recording_urls": "رونوشت نشانی(های) ضبط", + "recordings_list_empty": "شما هنوز هیچ جلسهٔ ضبط‌شده‌ای ندارید!", + "public_recordings_list_empty": "هنوز هیچ ضبط عمومی‌ای وجود ندارد!", + "recordings_list_empty_description": "بعد از شروع جلسه و ضبط آن، موارد ضبط‌شده در این‌جا نمایان می‌شوند.", + "public_recordings_list_empty_description": "موارد ضبط‌شده هنگامی که موجود شوند در این‌جا پدیدار خواهند شد.", + "delete_recording": "حذف ضبط", + "are_you_sure_delete_recording": "آیا مطمئن هستید که می‌خواهید این ضبط را حذف کنید؟", + "search_not_found": "هیچ ضبطی یافت نشد" }, "admin": { - "admin_panel": "پنل مدیر کل", + "admin_panel": "تابلوی مدیر کل", "manage_users": { "manage_users": "مدیریت کاربران", "active": "فعال", - "approve": "تایید", - "decline": "رد کردن", + "approve": "تأیید", + "decline": "ردکردن", "pending": "در انتظار", - "banned": "مسدود شده", + "banned": "مسدودشده", "ban": "مسدود", "unban": "لغو مسدودیت", - "deleted": "حذف شده", - "invited_tab": "دعوت شده", + "deleted": "حذف‌شده", + "invited_tab": "دعوت‌شده", "invite_user": "دعوت از کاربر", - "send_invitation": "ارسال دعوتنامه", - "enter_user_email": "ایمیل کاربر را وارد کنید", + "send_invitation": "ارسال دعوت‌نامه", + "enter_user_email": "رایانامهٔ کاربر را وارد کنید", "new_user": "کاربر جدید", "add_new_user": "کاربر جدید", "create_new_user": "ایجاد کاربر جدید", @@ -179,33 +211,43 @@ "create_account": "ایجاد حساب کاربری", "create_room": "ایجاد اتاق", "create_new_room": "ایجاد اتاق جدید", - "user_created_at": "ایجاد شده: {{user.created_at}}", + "user_created_at": "ایجاد‌شده: {{user.created_at}}", "are_you_sure_delete_account": "آیا مطمئن هستید که می‌خواهید حساب کاربری {{user.name}} را حذف کنید؟", "delete_account_warning": "اگر این حساب کاربری را حذف کنید، قابل بازیابی نخواهد بود.", + "empty_active_users": "هنوز هیچ کاربر فعالی در این سرور وجود ندارد!", + "empty_active_users_subtext": "هنگامی که وضعیت یک کاربر به فعال تغییر می‌کند، در اینجا نمایان می‌شوند.", + "empty_pending_users": "هنوز هیچ کاربر درانتظار تأیید در این سرور وجود ندارد!", + "empty_pending_users_subtext": "هنگامی که وضعیت یک کاربر به در انتظارِ تأیید تغییر می‌کند، در اینجا نمایان می‌شوند.", + "empty_banned_users": "هنوز هیچ کاربر مسدودشده‌ای در این سرور وجود ندارد!", + "empty_banned_users_subtext": "هنگامی که وضعیت یک کاربر به مسدودشده تغییر می‌کند، در اینجا نمایان می‌شوند.", + "empty_invited_users": "هنوز هیچ کاربر دعوت‌شده‌ای در این سرور وجود ندارد!", + "empty_invited_users_subtext": "هنگامی که وضعیت یک کاربر به دعوت‌شده تغییر می‌کند، در اینجا نمایان می‌شوند.", "invited": { - "time_sent": "زمان ارسال شده", + "time_sent": "زمان ارسال‌شده", "valid": "معتبر" } }, "server_rooms": { "server_rooms": "اتاق‌های سرور", "name": "نام", - "owner": "مالک", + "owner": "صاحب", "room_id": "شناسه اتاق", - "participants": "شركت كنندگان", + "participants": "شركت‌كنندگان", "status": "وضعیت", "running": "در حال اجرا", "not_running": "در حال اجرا نیست", "active": "فعال", - "current_session": "جلسه فعلی: {{lastSession}}", - "last_session": "آخرین جلسه: {{lastSession}}", + "current_session": "جلسهٔ کنونی: {{lastSession}}", + "last_session": "آخرین جلسه: {{localizedTime}}", "no_meeting_yet": "هنوز هیچ جلسه‌ای وجود ندارد.", "delete_server_rooms": "حذف اتاق سرور", - "resync_recordings": "همگام‌سازی مجدد ضبط شده‌ها" + "resync_recordings": "همگام‌سازی مجدد ضبط‌شده‌ها", + "empty_room_list": "هنوز هیچ اتاق سروری وجود ندارد!", + "empty_room_list_subtext": "اتاق‌ها پس از این‌که اولین اتاق خود را ایجاد کنید در اینجا نمایان می‌شوند." }, "server_recordings": { - "server_recordings": "سرور ضبط شده‌ها", - "latest_recordings": "آخرین موارد ضبط شده", + "server_recordings": "ضبط‌شده‌های سرور", + "latest_recordings": "آخرین ضبط‌شده‌ها", "no_recordings_found": "هیچ ضبطی یافت نشد." }, "site_settings": { @@ -219,64 +261,67 @@ "brand_image": "تصویر برند", "click_to_upload": "برای بارگذاری کلیک کنید", "drag_and_drop": " یا بکشید و رها کنید", - "upload_brand_image_description": "هر پرونده PNG، JPG یا SVG را می‌توانید بارگذاری کنید. بسته به اندازه پرونده، ممکن است به زمان بیشتری برای بارگذاری نیاز داشته باشد تا بتوانید از آن استفاده کنید" + "upload_brand_image_description": "هر پرونده PNG، JPG یا SVG (بزرگتر از {{size}}) را می‌توانید بارگذاری کنید. بسته به اندازه پرونده، ممکن است به زمان بیشتری برای بارگذاری نیاز داشته باشد تا بتوانید از آن استفاده کنید", + "remove_branding_image": "حذف تصویر برند" }, "administration": { "administration": "مدیریت", - "terms": "مقررات", - "privacy": "حریم خصوصی", + "terms": "شرایط و ضوابط", + "privacy": "سیاست حفظ حریم خصوصی", "privacy_policy": "سیاست حفظ حریم خصوصی", - "change_term_links": "تغییر مقررات پیوندهایی که در پایین صفحه ظاهر می‌شوند", - "change_privacy_link": "پیوند حریم خصوصی که در پایین صفحه ظاهر می‌شود را تغییر دهید", + "change_term_links": "تغییر پیوندهای مقررات که در پایین صفحه نمایان می‌شوند", + "change_privacy_link": "پیوند حریم خصوصی که در پایین صفحه نمایان می‌شود را تغییر دهید", "change_url": "تغییر نشانی اینترنتی", "enter_link": "پیوند را اینجا وارد کنید" }, "settings": { - "allow_users_to_share_rooms": "به کاربران اجازه دهید، اتاق‌ها را به اشتراک بگذارند", - "allow_users_to_share_rooms_description": "تنظیمات غیرفعال باشد، دکمه‌ها از منوی کشویی گزینه‌های اتاق حذف می‌شود و از اشتراک‌گذاری اتاق توسط کاربران جلوگیری می‌کند", + "settings": "تنظیمات", + "allow_users_to_share_rooms": "به کاربران اجازه دهید اتاق‌ها را هم‌رسانی کنند", + "allow_users_to_share_rooms_description": "تنظیم برروی غیرفعال، دکمه را از فهرست کشویی گزینه‌های اتاق حذف می‌کند و از هم‌رسانی اتاق‌ها توسط کاربران جلوگیری می‌کند", "allow_users_to_preupload_presentation": "به کاربران اجازه دهید تا ارائه‌ها را از قبل بارگذاری کنند", - "allow_users_to_preupload_presentation_description": "کاربران می‌توانند یک ارائه را از قبل بارگذاری کنند تا به عنوان ارائه پیش‌فرض برای آن اتاق خاص استفاده شود" + "allow_users_to_preupload_presentation_description": "کاربران می‌توانند یک ارائه را از قبل بارگذاری کنند تا به عنوان ارائهٔ پیش‌فرض برای آن اتاق خاص استفاده شود" }, "registration": { - "registration": "ثبت نام", - "role_mapping_by_email": "اعطا نقش از طریق ایمیل", - "role_mapping_by_email_description": "با استفاده از ایمیل کاربر نقشی بهش اعطا کنید. باید در قالب: role1=email1، role2=email2 باشد", - "enter_role_mapping_rule": "یک قانون اعطا نقش را وارد کنید", + "registration": "ثبت‌نام", + "role_mapping_by_email": "سپردن نقش از طریق رایانامه", + "role_mapping_by_email_description": "با استفاده از رایانامهٔ کاربر نقشی به او بسپارید. باید در قالب: role1=email1, role2=email2 باشد", + "enter_role_mapping_rule": "یک قانون سپردن نقش را وارد کنید", "resync_on_login": "همگام‌سازی مجدد داده‌های کاربر در هر بار ورود", - "resync_on_login_description": "هر بار که کاربر وارد سیستم می‌شود، اطلاعات کاربر را مجددا همگام‌سازی می‌کند و باعث می‌شود که ارائه‌دهنده احراز هویت خارجی همیشه با اطلاعات موجود در Greenlight مطابقت داشته باشد.", + "resync_on_login_description": "هر بار که کاربر وارد سیستم می‌شود، اطلاعات کاربر را مجددا همگام‌سازی می‌کند و باعث می‌شود که ارائه‌دهندهٔ احراز هویت خارجی همیشه با اطلاعات موجود در Greenlight مطابقت داشته باشد.", "default_role": "نقش پیش‌فرض", - "default_role_description": "نقش پیش‌فرضی که به کاربران تازه ایجاد شده اختصاص داده می‌شود", - "registration_method": "روش ثبت نام", - "registration_method_description": "نحوه ثبت نام کاربران در وب‌سایت را تغییر دهید", + "default_role_description": "نقش پیش‌فرضی که به کاربران تازهٔ ایجاد‌شده اختصاص داده می‌شود", + "registration_method": "روش ثبت‌نام", + "registration_method_description": "نحوه ثبت‌نام کاربران در وب‌سایت را تغییر دهید", "registration_methods" : { - "open": "باز کردن ثبت نام", - "invite": "پیوستن با دعوتنامه", - "approval": "تایید/رد کردن" + "open": "بازکردن ثبت‌نام", + "invite": "پیوستن با دعوت‌نامه", + "approval": "تأیید/رد" } } }, "room_configuration": { "room_configuration": "پیکربندی اتاق", - "optional": "اختیاری", - "enabled": "فعال شد", - "disabled": "غیرفعال شد", + "default": "اختیاری (پیش‌فرض: فعال‌شده)", + "optional": "اختیاری (پیش‌فرض: غیرفعال‌شده)", + "enabled": "اجباری فعال‌شده", + "disabled": "غیرفعال‌شده", "configurations": { "allow_room_to_be_recorded": "اجازه دهید اتاق ضبط شود", - "allow_room_to_be_recorded_description": "به صاحبان اتاق اجازه می‌دهد تعیین کنند که آیا می‌خواهند، اتاق را ضبط کنند یا خیر. اگر فعال باشد، مدیر همچنان باید پس از شروع جلسه، روی دکمه «ضبط» کلیک کند.", - "require_user_signed_in": "کاربران قبل از پیوستن به جلسه، ملزم به ورود به سیستم شوند", - "require_user_signed_in_description": "فقط به کاربرانی که دارای حساب کاربری در Greenlight هستند، اجازه می‌دهد به جلسه بپیوندند. اگر وارد سیستم نشده باشند، هنگام تلاش برای پیوستن به اتاق، به صفحه ورود هدایت می‌شوند.", + "allow_room_to_be_recorded_description": "به صاحبان اتاق اجازه می‌دهد تعیین کنند که آیا می‌خواهند امکان ضبط اتاق را داشته باشند یا خیر. اگر فعال باشد، مدیر همچنان باید پس از شروع جلسه، روی دکمهٔ «ضبط» کلیک کند.", + "require_user_signed_in": "کاربران قبل از ورود به جلسه، نیازمند ورود به سیستم باشند", + "require_user_signed_in_description": "فقط به کاربرانی که دارای حساب کاربری در Greenlight هستند اجازه می‌دهد به جلسه بپیوندند. اگر وارد سیستم نشده باشند، هنگام تلاش برای پیوستن به اتاق، به صفحهٔ ورود هدایت می‌شوند.", "require_mod_approval": "قبل از پیوستن به جلسه نیاز به تایید مدیر باشد", - "require_mod_approval_description": "هنگامی که کاربر سعی می‌کند به جلسه بپیوندد، از مدیر جلسه BigBlueButton درخواست می‌کند. در صورت تایید کاربر، می‌تواند به جلسه بپیوندد.", + "require_mod_approval_description": "هنگامی که یک کاربر سعی می‌کند به جلسه بپیوندد، از مدیر جلسه BigBlueButton درخواست می‌کند. در صورت تأییدشدن کاربر، می‌تواند به جلسه بپیوندد.", "allow_any_user_to_start_meeting": "به هر کاربری اجازه دهید تا جلسه را شروع کند", - "allow_any_user_to_start_meeting_description": "به هر کاربری اجازه دهید جلسه را در هر زمانی که خواست شروع کند. به‌طور پیش‌فرض، فقط مالک اتاق می‌تواند جلسه را شروع کند.", - "allow_users_to_join_as_mods": "به کاربران اجازه دهید به عنوان مدیر بپیوندند", - "allow_users_to_join_as_mods_description": "به همه کاربران هنگام پیوستن به جلسه، در BigBlueButton اختیارات مدیر را می‌دهد", + "allow_any_user_to_start_meeting_description": "به هر کاربری اجازه دهید جلسه را در هر زمانی که خواست شروع کند. به‌طور پیش‌فرض، فقط صاحب اتاق می‌تواند جلسه را شروع کند.", + "allow_users_to_join_as_mods": "همهٔ کاربران به عنوان مدیر بپیوندند", + "allow_users_to_join_as_mods_description": "به همهٔ کاربران هنگام پیوستن به جلسه، در BigBlueButton اختیارات مدیر را می‌دهد", "mute_users_on_join": "کاربران هنگام پیوستن بی‌صدا شوند", - "mute_users_on_join_description": "هنگامی که کاربر به جلسه BigBlueButton می‌پیوندد، به‌طور خودکار بی‌صدا می‌شود", + "mute_users_on_join_description": "هنگامی که کاربر به جلسهٔ BigBlueButton می‌پیوندد، به‌طور خودکار بی‌صدا می‌شود", "viewer_access_code": "کد دسترسی بینندگان", - "viewer_access_code_description": "به صاحبان اتاق اجازه می‌دهد تا یک کد الفبایی تصادفی داشته باشند که می‌تواند با کاربران به اشتراک بگذارند. در صورت تولید کد، برای پیوستن کاربران به جلسات اتاق الزامی است.", + "viewer_access_code_description": "به صاحبان اتاق اجازه می‌دهد تا یک کد الفبایی تصادفی داشته باشند که می‌تواند با کاربران هم‌رسانی شود. اگر کد تولید شده باشد، برای پیوستن کاربران به جلسات اتاق الزامی است.", "mod_access_code": "کد دسترسی مدیر", - "mod_access_code_description": "به صاحبان اتاق اجازه می‌دهد تا یک کد الفبایی تصادفی داشته باشند که می‌تواند با کاربران به اشتراک بگذارند. کد، در صورت تولید، مورد نیاز نخواهد بود و در صورت استفاده در هر جلسه اتاق، کاربر به عنوان مدیر ملحق خواهد شد." + "mod_access_code_description": "به صاحبان اتاق اجازه می‌دهد تا یک کد الفبایی تصادفی داشته باشند که می‌تواند با کاربران هم‌رسانی کنند. کد، در صورت تولید، مورد نیاز نخواهد بود و در صورت استفاده در هر جلسه اتاق، کاربر به عنوان مدیر به جلسه خواهد پیوست." } }, "roles": { @@ -291,121 +336,198 @@ "add_role": "+ ایجاد نقش", "create_role": "ایجاد نقش", "create_new_role": "ایجاد نقش جدید", - "no_role_found": "هیچ نقشی پیدا نشد." + "no_role_found": "هیچ نقشی پیدا نشد.", + "search_not_found": "هیچ نقشی پیدا نشد", + "edit": { + "create_room": "به کاربران دارای این نقش اجازه دهید که اتاق ایجاد کنند", + "record": "به کاربران دارای این نقش اجازه دهید تا جلسات خود را ضبط کنند", + "manage_users": "به کاربران دارای این نقش اجازه دهید تا کاربران را مدیریت کنند", + "manage_rooms": "به کاربران دارای این نقش اجازه دهید تا اتاق‌های سرور را مدیریت کنند", + "manage_recordings": "به کاربران دارای این نقش اجازه دهید تا جلسات ضبط‌شدهٔ سرور را مدیریت کنند", + "manage_site_settings": "به کاربران دارای این نقش اجازه دهید تا تنظیمات سایت را مدیریت کنند", + "manage_roles": "به کاربران دارای این نقش اجازه دهید تا دیگر نقش‌ها را ویرایش کنند", + "shared_list": "کاربران دارای این نقش را در فهرست کشویی هم‌رسانی اتاق‌ها بگنجانید", + "room_limit": "محدودیت اتاق" + } } }, "toast": { "success": { "user": { - "user_created": "کاربر ایجاد شد.", - "user_updated": "کاربر به‌روز شد.", - "user_deleted": "کاربر حذف شد.", - "avatar_updated": "آواتار به‌روز شد.", - "password_updated": "رمز عبور به‌روز شد.", - "account_activated": "حساب کاربری فعال شد.", - "activation_email_sent": "ایمیل فعال‌سازی ارسال شد", - "challenge_passed": "چالش گذشت" + "user_created": "یک کاربر جدید ایجاد شده‌است.", + "user_updated": "کاربر به‌روز شده‌است.", + "user_deleted": "کاربر حذف شده‌است.", + "avatar_updated": "تصویر نمایه به‌روز شده‌است.", + "password_updated": "گذرواژه به‌روز شده‌است.", + "account_activated": "حساب کاربری شما فعال شده‌است.", + "activation_email_sent": "رایانامه‌ای حاوی راهنماهای فعال‌سازی حساب کاربری شما ارسال شده‌است.", + "reset_pwd_email_sent": "رایانامه‌ای حاوی راهنماهای بازنشانی گذرواژه شما ارسال شده‌است." }, "session": { - "signed_out": "شما با موفقیت از سیستم خارج شدید." + "signed_out": "از سیستم خارج شده‌اید." }, "room": { - "room_created": "اتاق ایجاد شد.", - "room_updated": "اتاق به‌روز شد.", - "room_deleted": "اتاق حذف شد.", - "room_shared": "اتاق به اشتراک گذاشته شده", - "room_unshared": "اتاق به اشتراک گذاشته نشد", - "recordings_synced": "ضبط‌ شده‌های اتاق اکنون همگام‌سازی شده‌اند", - "server_room_deleted": "اتاق سرور حذف شد.", - "room_configuration_updated": "پیکربندی اتاق به‌روزرسانی شد.", - "room_setting_updated": "تنظیمات اتاق به‌روزرسانی شد.", - "presentation_updated": "ارائه به‌روز شد.", - "presentation_deleted": "ارائه حذف شد.", + "room_created": "یک اتاق جدید ایجاد شده‌است.", + "room_updated": "اتاق به‌روز شده‌است.", + "room_deleted": "اتاق حذف شده‌است.", + "room_shared": "اتاق هم‌رسانی شده‌است.", + "room_unshared": "هم‌رسانی اتاق لغو شده‌است.", + "recordings_synced": "ضبط‌‌شده‌های اتاق همگام‌سازی شده‌اند.", + "room_configuration_updated": "پیکربندی اتاق به‌روز شده‌است.", + "room_setting_updated": "تنظیمات اتاق به‌روز شده‌است.", + "presentation_updated": "ارائه به‌روز شده‌است.", + "presentation_deleted": "ارائه حذف شده‌است.", "joining_meeting": "در حال پیوستن به جلسه...", "meeting_started": "جلسه شروع شد.", - "access_code_copied": "کد دسترسی کپی شد.", - "access_code_generated": "کد دسترسی تولید شد.", - "access_code_removed": "کد دسترسی حذف شد." + "access_code_copied": "کد دسترسی رونوشت شده‌است.", + "access_code_generated": "کد دسترسی تولید شده‌است.", + "access_code_deleted": "کد دسترسی حذف شده‌است.", + "copied_meeting_url": "نشانی اینترنتی جلسه رونوشت شده‌است. این پیوند می‌تواند برای پیوستن به جلسه استفاده شود." }, "site_settings": { - "site_setting_updated": "تنظیمات سایت به‌روز شد." + "site_setting_updated": "تنظیمات سایت به‌روز شده‌اند.", + "brand_color_updated": "رنگ برند به‌روز شده‌است.", + "brand_image_updated": "تصویر برند به‌روز شده‌است.", + "brand_image_deleted": "تصویر برند حذف شده‌است.", + "privacy_policy_updated": "سیاست حفظ حریم خصوصی به‌روز شده‌است.", + "terms_of_service_updated": "شرایط خدمات به‌روز شده‌است." }, "recording": { - "recording_visibility_updated": "قابلیت مشاهده ضبط به‌روزرسانی شد.", - "recording_name_updated": "نام ضبط به‌روز شد.", - "recording_deleted": "ضبط حذف شد." + "recording_visibility_updated": "قابلیت مشاهدهٔ ضبط به‌روز شده‌است.", + "recording_name_updated": "نام جلسهٔ ضبط‌شده به‌روز شده‌است.", + "recording_deleted": "جلسهٔ ضبط‌شده حذف شده‌است.", + "copied_urls": "نشانی‌های اینترنتی جلسهٔ ضبط‌شده رونوشت شده‌است." }, "role": { - "role_created": "نقش ایجاد شد.", - "role_updated": "نقش به‌روز شد.", - "role_deleted": "نقش حذف شد.", - "role_permission_updated": "مجوز نقش به‌روز شد." + "role_created": "یک نقش جدید ایجاد شده‌است.", + "role_updated": "نقش به‌روز شده‌است.", + "role_deleted": "نقش حذف شده‌است.", + "role_permission_updated": "دسترسی نقش به‌روز شده‌است." }, "invitations": { - "invitation_sent": "دعوتنامه ارسال شد" + "invitation_sent": "دعوت‌نامه ارسال شده‌است." } }, "error": { - "problem_completing_action": "در تکمیل آن اقدام مشکلی وجود داشت.\nلطفا دوباره تلاش کنید.", + "problem_completing_action": "این عمل نمی‌تواند تکمیل شود.\nلطفا دوباره تلاش کنید.", "file_type_not_supported": "نوع پرونده پشتیبانی نمی‌شود.", - "file_size_too_large": "اندازه پرونده خیلی بزرگ است.", - "signin_required": "برای دسترسی به این صفحه باید وارد سیستم شوید", + "file_size_too_large": "اندازهٔ پرونده خیلی بزرگ است.", + "file_upload_error": "پرونده نمی‌تواند بارگذاری شود.", + "signin_required": "برای دسترسی به این صفحه باید وارد سیستم شده باشید.", "roles": { - "role_assigned": "این نقش را نمی‌توانید حذف کنید، زیرا به حداقل یک کاربر اختصاص داده شده است." + "role_assigned": "این نقش نمی‌تواند حذف شود، زیرا به حداقل یک کاربر اختصاص داده شده‌است." }, "users": { - "invalid_invite": "توکن دعوتنامه شما یا نامعتبر است یا نادرست است. لطفا با مدیریت تماس بگیرید تا یک توکن جدید دریافت کنید", - "email_exists": "یک حساب کاربری با این ایمیل در حال حاضر وجود دارد. لطفا با ایمیل دیگری دوباره امتحان کنید" + "signup_error": "شما نمی‌توانید احراز هویت شوید. لطفا با مدیریت تماس بگیرید.", + "invalid_invite": "ژتون دعوت‌نامهٔ شما یا نامعتبر است یا نادرست است. لطفا با مدیریت تماس بگیرید تا یک ژتون جدید دریافت کنید.", + "email_exists": "یک حساب کاربری با این رایانامه از قبل وجود دارد. لطفا با رایانامهٔ دیگری دوباره امتحان کنید.", + "old_password": "گذرواژه‌ای که وارد کرده‌اید اشتباه است.", + "pending": "ثبت‌نام شما در انتظار تأیید مدیر کل است. لطفا بعدا دوباره تلاش کنید.", + "banned": "شما به این برنامه دسترسی ندارید. اگف فکر می‌کنید که اشتباهی رخ داده است، لطفا با مدیریت تماس بگیرید." }, "rooms": { - "room_limit": "به دلیل رسیدن به محدودیت ایجاد اتاق نمی‌توانید اتاق ایجاد کنید" + "room_limit": "به دلیل رسیدن به سقف مجاز تعداد اتاق‌ها، نمی‌توانید اتاق ایجاد کنید." }, "session": { - "invalid_credentials": "نام‌کاربری یا رمز عبور معتبر نمی‌باشد. لطفا اعتبار خود را بررسی کنید و دوباره امتحان کنید." + "invalid_credentials": "نام‌کاربری یا گذرواژه نامعتبر است. لطفا اطلاعات کاربری خود را تأیید کنید و دوباره امتحان کنید." } } }, "global_error_page": { "title": "خطا", - "message": "اوه! مشکلی پیش آمد. اگر حادثه دوباره رخ داد، لطفا با مدیریت تماس بگیرید." + "message": "با عرض پوزش، مشکلی پیش آمد. اگر حادثه دوباره رخ داد، لطفا با مدیریت تماس بگیرید." }, "not_found_error_page": { - "title": "۴۰۴", - "message": "اوه! صفحه‌ای که سعی می‌کنید به آن دسترسی پیدا کنید، پیدا نشد." + "title": "صفحه یافت نشد", + "message": "با عرض پوزش، صفحه‌ای که سعی می‌کنید به آن دسترسی پیدا کنید یافت نشد." }, "account_activation_page": { "title": "فعال‌سازی حساب کاربری", "account_unverified": "حساب کاربری شما هنوز تایید نشده است.", - "message": "برای استفاده از Greenlight، لطفا با دنبال کردن دستورالعمل‌های موجود در ایمیل فعال‌سازی که برای شما ارسال شده است، حساب کاربری خود را تایید کنید.", - "resend_activation_link": "اگر ایمیل فعال‌سازی دریافت نکرده‌اید یا در استفاده از آن مشکل دارید، روی دکمه ارسال مجدد در زیر کلیک کنید تا ایمیل فعال‌سازی جدیدی ارسال شود.", - "resend_btn_lbl": "ارسال مجدد تائیدیه" + "message": "برای استفاده از Greenlight، لطفا با دنبال‌کردن راهنماهای موجود در رایانامه فعال‌سازی که برای شما ارسال شده‌است، حساب کاربری خود را تأیید کنید.", + "resend_activation_link": "اگر رایانامه فعال‌سازی را دریافت نکرده‌اید یا در استفاده از آن مشکل دارید، روی دکمهٔ زیر کلیک کنید تا رایانامه فعال‌سازی جدیدی را درخواست دهید.", + "resend_btn_lbl": "ارسال مجدد تأییدیه" }, "forms": { "validations": { "full_name": { "required": "لطفا یک نام کامل وارد کنید", - "min": "نام باید حداقل ۲ کاراکتر داشته باشد", - "max": "نام باید حداکثر ۲۵۵ کاراکتر داشته باشد" + "min": "نام باید حداقل ۲ نویسه داشته باشد", + "max": "نام باید حداکثر ۲۵۵ نویسه داشته باشد" }, "email": { - "required": "لطفا یک ایمیل وارد کنید", - "email": "مقدار وارد شده با قالب ایمیل مطابقت ندارد", - "min": "ایمیل باید حداقل ۶ کاراکتر داشته باشد", - "max": "ایمیل باید حداکثر ۲۵۵ کاراکتر داشته باشد" + "required": "لطفا یک رایانامه وارد کنید", + "email": "مقدار واردشده با قالب رایانامه مطابقت ندارد", + "min": "رایانامه باید حداقل ۶ نویسه داشته باشد", + "max": "رایانامه باید حداکثر ۲۵۵ نویسه داشته باشد" }, "password": { - "required": "لطفا یک رمز عبور وارد کنید", - "match": "رمز عبور باید حداقل این موارد را داشته باشد:", - "min": "- هشت کارکتر", + "required": "لطفا یک گذرواژه وارد کنید", + "match": "گذرواژه باید حداقل این موارد را داشته باشد:", + "min": "- هشت نویسه", "lower": "- یک حرف کوچک", "upper": "- یک حرف بزرگ", "digit": "- یک عدد", "symbol": "- یک نماد", - "max": "رمز عبور باید حداکثر ۲۵۵ کاراکتر داشته باشد" + "max": "گذرواژه باید حداکثر ۲۵۵ نویسه داشته باشد" }, "password_confirmation": { - "required": "لطفا رمز عبور را برای تایید تکرار کنید", - "match": "رمزهای عبور با هم یکسان نمی‌باشند" + "required": "لطفا تأیید گذرواژه را وارد کنید", + "match": "گذرواژه‌ها با هم یکسان نمی‌باشند" + }, + "emails": { + "required": "لطفا حداقل یک رایانامهٔ معتبر وارد کنید", + "list": "لطفا فهرستی از رایانامه‌های معتبر جداشده با ویرگول ارائه دهید (user@users.com,user1@users.com,user2@users.com)" + }, + "role_name": { + "required": "لطفا نام نقش را وارد کنید" + }, + "role": { + "limit": { + "required": "لطفا محدودیت تعداد اتاق را وارد کنید", + "min": "حداقل مجاز ۰ است", + "max": "حداکثر مجاز ۱۰۰ است" + }, + "type": { + "error": "باید یک عدد مشخص کنید" + } + }, + "room": { + "name": { + "required": "لطفا نام اتاق را وارد کنید.", + "min": "نام باید حداقل ۲ نویسه داشته باشد" + } + }, + "room_join": { + "name": { + "required": "لطفا نام خود را وارد کنید." + } + }, + "url": { + "invalid": "نشانی اینترنتی معتبر نیست" + } + }, + "room": { + "fields": { + "name": { + "label": "نام اتاق", + "placeholder": "نام اتاق را وارد کنید..." + } + } + }, + "room_join": { + "fields": { + "name": { + "label": "نام", + "placeholder": "نام خود را وارد کنید" + }, + "access_code": { + "label": "کد دسترسی", + "placeholder": "کد دسترسی را وارد کنید" + }, + "recording_consent": { + "label": "من متوجه هستم که این جلسه ممکن است ضبط شود. این ممکن است شامل صدا و ویدیوی من در صورت فعال‌بودن نیز باشد." + } } }, "user": { @@ -416,33 +538,95 @@ "placeholder": "نام کامل خود را وارد کنید" }, "email": { - "label": "ایمیل", - "placeholder": "ایمیل خود را وارد کنید" + "label": "رایانامه", + "placeholder": "رایانامهٔ خود را وارد کنید" }, "password": { - "label": "رمز عبور", - "placeholder": "ایجاد رمز عبور" + "label": "گذرواژه", + "placeholder": "ایجاد گذرواژه" }, "password_confirmation": { - "label": "تایید رمز عبور", - "placeholder": "تایید رمز عبور" + "label": "تأیید گذرواژه", + "placeholder": "تأیید گذرواژه" } } }, "signin": { "fields": { "email": { - "label": "ایمیل", - "placeholder": "ایمیل" + "label": "رایانامه", + "placeholder": "رایانامه" }, "password": { - "label": "رمز عبور", - "placeholder": "رمز عبور" + "label": "گذرواژه", + "placeholder": "گذرواژه" }, "remember_me": { "label": "مرا به خاطر بسپار" } } + }, + "change_password": { + "fields": { + "old_password": { + "label": "گذرواژهٔ فعلی", + "placeholder": "گذرواژهٔ خود را وارد کنید" + }, + "new_password": { + "label": "گذرواژهٔ جدید", + "placeholder": "گذرواژهٔ جدید خود را وارد کنید" + }, + "password_confirmation": { + "label": "تأیید گذرواژه", + "placeholder": "گذرواژهٔ جدید خود را تأیید کنید" + } + }, + "validations": { + "old_password": { + "required": "لطفا گذرواژهٔ کنونی خود را وارد کنید" + } + } + }, + "forget_password": { + "fields": { + "email": { + "label": "رایانامه", + "placeholder": "رایانامهٔ حساب کاربری را وارد کنید" + } + }, + "validations": { + "email": { + "required": "لطفا رایانامهٔ حساب کاربری را وارد کنید" + } + } + }, + "reset_password": { + "fields": { + "new_password": { + "label": "گذرواژهٔ جدید", + "placeholder": "گذرواژهٔ جدید خود را وارد کنید" + }, + "password_confirmation": { + "label": "تأیید گذرواژه", + "placeholder": "گذرواژهٔ جدید خود را تأیید کنید" + } + } + }, + "update_user": { + "fields": { + "full_name": { + "label": "نام کامل" + }, + "email": { + "label": "رایانامه" + }, + "language": { + "label": "زبان" + }, + "role": { + "label": "نقش" + } + } } }, "admin": { @@ -450,19 +634,41 @@ "fields": { "full_name": { "label": "نام کامل", - "placeholder": "نام کامل خود را وارد کنید" + "placeholder": "نام کامل کاربر را وارد کنید" }, "email": { - "label": "ایمیل", - "placeholder": "ایمیل خود را وارد کنید" + "label": "رایانامه", + "placeholder": "رایانامهٔ کاربر را وارد کنید" }, "password": { - "label": "رمز عبور", - "placeholder": "رمز عبور کاربر را وارد کنید" + "label": "گذرواژه", + "placeholder": "گذرواژهٔ کاربر را وارد کنید" }, "password_confirmation": { - "label": "تایید رمز عبور", - "placeholder": "تایید رمز عبور" + "label": "تأیید گذرواژه", + "placeholder": "تأیید گذرواژه" + } + } + }, + "invite_user": { + "fields": { + "emails": { + "label": "رایانامه‌ها" + } + } + }, + "site_settings": { + "fields": { + "value": { + "placeholder": "پیوند را اینجا وارد کنید..." + } + } + }, + "roles": { + "fields": { + "name": { + "label": "نام نقش", + "placeholder": "نام نقش را وارد کنید..." } } } diff --git a/app/assets/locales/fa_IR.json b/app/assets/locales/fa_IR.json new file mode 100644 index 0000000000..c249902625 --- /dev/null +++ b/app/assets/locales/fa_IR.json @@ -0,0 +1,677 @@ +{ + "start": "شروع", + "search": "جستجو", + "home": "صفحه اصلی", + "previous": "قبلی", + "back": "بازگشت", + "next": "بعدی", + "view": "نمایش", + "join": "پیوستن", + "edit": "ویرایش", + "save": "ذخیره", + "save_changes": "ذخیره تغییرات", + "update": "به‌روزرسانی", + "report": "گزارش", + "share": "اشتراک گذاری", + "cancel": "انصراف", + "close": "بستن", + "delete": "حذف", + "copy": "کپی پیوند عضویت", + "or": "یا", + "online": "آنلاین", + "help_center": "مرکز راهنمایی", + "are_you_sure": "آیا مطمئنی؟", + "return_home": "بازگشت به صفحه اصلی", + "created_at": "ایجاد شده در", + "view_recordings": "مشاهدهٔ ضبط‌شده‌ها", + "join_session": "پیوستن به نشست", + "no_result_search_input": "هیچ نتیجه‌ای برای «{{ searchInput }}» یافت نشد", + "action_permanent": "این عمل قابل بازگردانی نیست.", + "homepage": { + "welcome_bbb": "به BigBlueButton خوش آمدید.", + "bigbluebutton_description": "BigBlueButton یک سیستم کنفرانس وب متن باز برای کلاس‌های آنلاین است. این پلتفرم زمان را برای یادگیری کاربردی به حداکثر می‌رساند و به دانش‌اموزان امکاناتی ارائه می‌دهد تا با یکدیگر همکاری کنند و بازخورد دریافت کنند.", + "greenlight_description": "اتاق‌های خود را برای میزبانی جلسات با استفاده از یک پیوند کوتاه و راحت برای پیوستن دیگران ایجاد کنید.", + "learn_more": "درباره BigBlueButton بیشتر بدانید", + "explore_features": "کاوش ویژگی‌های ما", + "meeting_title": " راه‌اندازی جلسه", + "meeting_description": "یک کلاس مجازی به صورت ویدیوای، صوتی، اشتراک گذاری صفحه نمایش، گفتگو و تمام ابزارهای مورد نیاز برای یادگیری کاربردی راه‌اندازی کنید.", + "recording_title": "ضبط جلسات خود", + "recording_description": "جلسات BigBlueButton را ضبط کنید و آنها را با دانش‌آموزان به اشتراک بگذارید تا مطالب را دوباره بررسی و مشاهده کنند.", + "settings_title": "مدیریت اتاق‌های خود", + "settings_description": "تنظیمات اتاق‌ها و جلسه خود را به گونه‌ای پیکربندی کنید که در روند کلاسی تأثیر بهتری داشته باشد.", + "and_more_title": "و بیشتر!", + "and_more_description": "BigBlueButton برای یادگیری کاربردی، ابزارهای درونی مختلفی را ارائه می‌کند و برای صرفه‌جویی در وقت شما در طول کلاس طراحی شده است.", + "enter_meeting_url": "نشانی اینترنتی جلسه را وارد کنید", + "enter_meeting_url_instruction": "لطفا نشانی اینترنتی جلسه BigBlueButton خود را در فیلد زیر وارد کنید." + }, + "authentication": { + "sign_in": "ورود", + "sign_up": "ثبت نام", + "sign_out": "خروج", + "email": "ایمیل", + "password": "گذرواژه", + "confirm_password": "تایید گذرواژه", + "enter_email": "ایمیل خود را وارد کنید", + "enter_name": "نام خود را وارد کنید", + "remember_me": "مرا به خاطر بسپار", + "forgot_password": "رمز عبور را فراموش کرده‌اید؟", + "dont_have_account": "حساب کاربری ندارید؟", + "create_account": "ایجاد حساب کاربری", + "create_an_account": "ایجاد یک حساب کاربری", + "already_have_account": "از قبل حساب کاربری دارید؟" + }, + "user": { + "user": "کاربر", + "users": "کاربران", + "name": "نام", + "email_address": "نشانی ایمیل", + "authenticator": "احراز هویت", + "full_name": "نام کامل", + "no_user_found": "هیچ کاربری پیدا نشد", + "type_three_characters": "لطفا سه «۳» کاراکتر یا بیشتر برای نمایش دیگر کاربران تایپ کنید.", + "search_not_found": "هیچ کاربری پیدا نشد", + "profile": { + "profile": "نمایه", + "language": "زبان", + "role": "نقش", + "administrator": "مدیر کل", + "guest": "مهمان" + }, + "account": { + "account_info": "اطلاعات حساب کاربری", + "delete_account": "حذف حساب کاربری", + "change_password": "تغییر گذرواژه", + "reset_password": "بازنشانی گذرواژه", + "update_account_info": "به‌روزرسانی اطلاعات حساب کاربری", + "current_password": "گذرواژه فعلی", + "new_password": "گذرواژه جدید", + "confirm_password": "تایید گذرواژه", + "permanently_delete_account": "حساب کاربری خود را برای همیشه حذف کنید", + "delete_account_description": "اگر حساب کاربری خود را حذف کنید، قابل بازیابی نخواهد بود.\nتمام اطلاعات مربوط به حساب کاربری شما، از جمله تنظیمات، اتاق‌ها و موارد ضبط شده حذف خواهد شد.", + "delete_account_confirmation": "بله، من می‌خواهم حساب کاربریم را حذف کنم", + "are_you_sure_delete_account": "آیا مطمئن هستید که می‌خواهید حساب کاربری خود را حذف کنید؟" + }, + "avatar": { + "upload_avatar": "بارگذاری آواتار", + "delete_avatar": "حذف آواتار", + "crop_avatar": "برش آواتار شما" + }, + "pending": { + "title": "در انتظار ثبت‌نام", + "message": "ممنون از اینکه ثبت نام کردید! حساب کاربری شما در حال حاضر در انتظار تایید توسط مدیر کل است." + } + }, + "room": { + "room": "اتاق", + "rooms": "اتاق‌ها", + "room_name": "نام اتاق", + "add_new_room": "+ اتاق جدید", + "create_room": "ایجاد اتاق", + "delete_room": "حذف اتاق", + "create_new_room": "ایجاد اتاق جدید", + "enter_room_name": "نام اتاق را وارد کنید", + "shared_by": "اشتراک گذاشته شده توسط", + "last_session": "آخرین جلسه: {{ localizedTime }}", + "no_last_session": "هنوز هیچ جلسه‌ای از قبل ایجاد نشده است", + "search_not_found": "هیچ اتاقی پیدا نشد", + "rooms_list_is_empty": "شما هنوز هیچ اتاقی ندارید!", + "rooms_list_empty_create_room": "با کلیک بر روی دکمه زیر و وارد کردن نام اتاق، اولین اتاق خود را ایجاد کنید.", + "meeting": { + "start_meeting": "شروع جلسه", + "join_meeting": "به جلسه بپیوندید", + "meeting_invitation": "شما برای پیوستن به جلسه دعوت شده‌اید", + "meeting_not_started": "جلسه هنوز شروع نشده است", + "join_meeting_automatically": "با شروع جلسه به طور خودکار به آن ملحق خواهید شد", + "recording_consent": "من اذعان دارم که این جلسه ممکن است، ضبط شود. این ممکن است شامل صدا و ویدیو من در صورت فعال شدن، باشد." + }, + "presentation": { + "presentation": "ارائه", + "click_to_upload": "برای بارگذاری کلیک کنید", + "drag_and_drop": " یا بکشید و رها کنید", + "upload_description": "هر سند اداری یا پرونده PDF (بزرگتر از {{size}}) را بارگذاری کنید. بسته به اندازه پرونده، ممکن است به زمان بیشتری برای بارگذاری نیاز باشد تا بتوان از آن استفاده کرد.", + "are_you_sure_delete_presentation": "آیا مطمئن هستید که می‌خواهید این ارائه را حذف کنید؟" + }, + "shared_access": { + "access": "دسترسی", + "add_share_access": "+ اشتراک گذاری دسترسی", + "share_room_access": "دسترسی به اتاق را به اشتراک بگذارید", + "add_some_users": "زمان افزودن چند کاربر است!", + "add_some_users_description": "برای افزودن کاربران جدید، روی دکمه زیر کلیک کنید و کاربرانی را که می‌خواهید این اتاق را با آنها به اشتراک بگذارید، جستجو یا انتخاب کنید.", + "delete_shared_access": "حذف دسترسی هم‌رسانی‌شده", + "are_you_sure_delete_shared_access": "آیا مطمئن هستید که می‌خواهید این دسترسی اشتراک گذاری را حذف کنید؟" + }, + "settings": { + "settings": "تنظیمات", + "room_name": "نام اتاق", + "user_settings": "تنظیمات کاربر", + "allow_room_to_be_recorded": "اجازه دهید اتاق ضبط شود", + "require_signed_in": "کاربران قبل از پیوستن به جلسه، ملزم به ورود به سیستم شوند", + "require_signed_in_message": "برای پیوستن به این اتاق باید وارد سیستم شوید.", + "require_mod_approval": "قبل از پیوستن به جلسه نیاز به تایید مدیر باشد", + "allow_any_user_to_start": "به هر کاربری اجازه دهید این جلسه را شروع کند", + "all_users_join_as_mods": "همه کاربران به عنوان مدیر عضو شوند", + "mute_users_on_join": "کاربران هنگام پیوستن بی‌صدا شوند", + "generate": "تولید کن", + "access_code": "کد دسترسی", + "mod_access_code": "کد دسترسی مدیر", + "mod_access_code_optional": "کد دسترسی مدیر (اختیاری)", + "access_code_required": "لطفا کد دسترسی را وارد کنید", + "wrong_access_code": "کد دسترسی اشتباه است", + "generate_viewers_access_code": "ایجاد کد دسترسی برای بینندگان", + "generate_mods_access_code": "ایجاد کد دسترسی برای مدیران", + "are_you_sure_delete_room": "آیا مطمئن هستید که می‌خواهید این اتاق را حذف کنید؟" + } + }, + "recording": { + "recording": "در حال ضبط", + "recordings": "ضبط شده‌ها", + "name": "نام", + "length": "طول مدت", + "users": "کاربران", + "visibility": "قابلیت دیدن", + "formats": "قالب‌ها", + "published": "منتشر شده", + "unpublished": "منتشر نشده", + "protected": "محافظت شده", + "public": "عمومی", + "public_protected": "عمومی/محافظت‌شده", + "length_in_minutes": "{{recording.length}} دقیقه.", + "processing_recording": "در حال پردازش ضبط، این‌کار ممکن است چند دقیقه طول بکشد...", + "copy_recording_urls": "کپی نشانی(های) ضبط شده", + "recordings_list_empty": "شما هنوز هیچ جلسه ضبط شده‌ای ندارید!", + "public_recordings_list_empty": "هنوز هیچ ضبط عمومی‌ای وجود ندارد!", + "recordings_list_empty_description": "بعد از شروع جلسه و ضبط آن، موارد ضبط‌شده در این‌جا نمایان می‌شوند.", + "public_recordings_list_empty_description": "موارد ضبط‌شده هنگامی که موجود شوند در این‌جا پدیدار خواهند شد.", + "delete_recording": "حذف ضبط", + "are_you_sure_delete_recording": "آیا مطمئنید که می‌خواهید این ضبط را حذف کنید؟", + "search_not_found": "هیچ ضبطی یافت نشد" + }, + "admin": { + "admin_panel": "پنل مدیر کل", + "manage_users": { + "manage_users": "مدیریت کاربران", + "active": "فعال", + "approve": "تایید", + "decline": "رد کردن", + "pending": "در انتظار", + "banned": "مسدود شده", + "ban": "مسدود", + "unban": "لغو مسدودیت", + "deleted": "حذف شده", + "invited_tab": "دعوت شده", + "invite_user": "دعوت از کاربر", + "send_invitation": "ارسال دعوتنامه", + "enter_user_email": "ایمیل کاربر را وارد کنید", + "new_user": "کاربر جدید", + "add_new_user": "کاربر جدید", + "create_new_user": "ایجاد کاربر جدید", + "edit_user": "ویرایش کاربر", + "delete_user": "حذف کاربر", + "users_edit_path": "کاربران/ویرایش", + "create_account": "ایجاد حساب کاربری", + "create_room": "ایجاد اتاق", + "create_new_room": "ایجاد اتاق جدید", + "user_created_at": "ایجاد‌شده: {{user.created_at}}", + "are_you_sure_delete_account": "آیا مطمئن هستید که می‌خواهید حساب کاربری {{user.name}} را حذف کنید؟", + "delete_account_warning": "اگر این حساب کاربری را حذف کنید، قابل بازیابی نخواهد بود.", + "empty_active_users": "هنوز هیچ کاربر فعالی در این سرور وجود ندارد!", + "empty_active_users_subtext": "هنگامی که وضعیت یک کاربر به فعال تغییر می‌کند، در اینجا نمایان می‌شوند.", + "empty_pending_users": "هنوز هیچ کاربر در انتظار تایید در این سرور وجود ندارد!", + "empty_pending_users_subtext": "هنگامی که وضعیت یک کاربر به در انتظارِ تایید تغییر می‌کند، در اینجا نمایان می‌شوند.", + "empty_banned_users": "هنوز هیچ کاربر مسدود شده‌ای در این سرور وجود ندارد!", + "empty_banned_users_subtext": "هنگامی که وضعیت یک کاربر به مسدود شده تغییر می‌کند، در اینجا نمایان می‌شوند.", + "empty_invited_users": "هنوز هیچ کاربر دعوت‌شده‌ای در این سرور وجود ندارد!", + "empty_invited_users_subtext": "هنگامی که وضعیت یک کاربر به دعوت‌شده تغییر می‌کند، در اینجا نمایان می‌شوند.", + "invited": { + "time_sent": "زمان ارسال شده", + "valid": "معتبر" + } + }, + "server_rooms": { + "server_rooms": "اتاق‌های سرور", + "name": "نام", + "owner": "صاحب", + "room_id": "شناسه اتاق", + "participants": "شركت كنندگان", + "status": "وضعیت", + "running": "در حال اجرا", + "not_running": "در حال اجرا نیست", + "active": "فعال", + "current_session": "جلسه فعلی: {{lastSession}}", + "last_session": "آخرین جلسه: {{localizedTime}}", + "no_meeting_yet": "هنوز هیچ جلسه‌ای وجود ندارد.", + "delete_server_rooms": "حذف اتاق سرور", + "resync_recordings": "همگام‌سازی مجدد ضبط شده‌ها", + "empty_room_list": "هنوز هیچ اتاق سروری وجود ندارد!", + "empty_room_list_subtext": "پس از اینکه اولین اتاق خود را ایجاد کنید در اینجا نمایان می‌شوند." + }, + "server_recordings": { + "server_recordings": "سرور ضبط شده‌ها", + "latest_recordings": "آخرین موارد ضبط شده", + "no_recordings_found": "هیچ ضبطی یافت نشد." + }, + "site_settings": { + "site_settings": "تنظیمات سایت", + "customize_greenlight": "شخصی‌سازی Greenlight", + "appearance": { + "appearance": "ظاهر", + "brand_color": "رنگ برند", + "regular": "عادی", + "lighten": "روشن", + "brand_image": "تصویر برند", + "click_to_upload": "برای بارگذاری کلیک کنید", + "drag_and_drop": " یا بکشید و رها کنید", + "upload_brand_image_description": "هر پرونده PNG، JPG یا SVG (بزرگتر از {{size}}) را می‌توانید بارگذاری کنید. بسته به اندازه پرونده، ممکن است به زمان بیشتری برای بارگذاری نیاز داشته باشد تا بتوانید از آن استفاده کنید", + "remove_branding_image": "حذف تصویر برند" + }, + "administration": { + "administration": "مدیر کل", + "terms": "شرایط و ضوابط", + "privacy": "سیاست حفظ حریم خصوصی", + "privacy_policy": "سیاست حفظ حریم خصوصی", + "change_term_links": "تغییر مقررات پیوندهایی که در پایین صفحه ظاهر می‌شوند", + "change_privacy_link": "پیوند حریم خصوصی که در پایین صفحه ظاهر می‌شود را تغییر دهید", + "change_url": "تغییر نشانی اینترنتی", + "enter_link": "پیوند را اینجا وارد کنید" + }, + "settings": { + "settings": "تنظیمات", + "allow_users_to_share_rooms": "به کاربران اجازه دهید، اتاق‌ها را به اشتراک بگذارند", + "allow_users_to_share_rooms_description": "تنظیمات غیرفعال باشد، دکمه‌ها از منوی کشویی گزینه‌های اتاق حذف می‌شود و از اشتراک‌گذاری اتاق توسط کاربران جلوگیری می‌کند", + "allow_users_to_preupload_presentation": "به کاربران اجازه دهید تا ارائه‌ها را از قبل بارگذاری کنند", + "allow_users_to_preupload_presentation_description": "کاربران می‌توانند یک ارائه را از قبل بارگذاری کنند تا به عنوان ارائه پیش‌فرض برای آن اتاق خاص استفاده شود" + }, + "registration": { + "registration": "ثبت نام", + "role_mapping_by_email": "اعطا نقش از طریق ایمیل", + "role_mapping_by_email_description": "با استفاده از ایمیل کاربر نقشی بهش اعطا کنید. باید در قالب: role1=email1، role2=email2 باشد", + "enter_role_mapping_rule": "یک قانون اعطا نقش را وارد کنید", + "resync_on_login": "همگام‌سازی مجدد داده‌های کاربر در هر بار ورود", + "resync_on_login_description": "هر بار که کاربر وارد سیستم می‌شود، اطلاعات کاربر را مجددا همگام‌سازی می‌کند و باعث می‌شود که ارائه‌دهنده احراز هویت خارجی همیشه با اطلاعات موجود در Greenlight مطابقت داشته باشد.", + "default_role": "نقش پیش‌فرض", + "default_role_description": "نقش پیش‌فرضی که به کاربران تازه ایجاد شده اختصاص داده می‌شود", + "registration_method": "روش ثبت نام", + "registration_method_description": "نحوه ثبت نام کاربران در وب‌سایت را تغییر دهید", + "registration_methods" : { + "open": "باز کردن ثبت نام", + "invite": "پیوستن با دعوتنامه", + "approval": "تایید/رد کردن" + } + } + }, + "room_configuration": { + "room_configuration": "پیکربندی اتاق", + "default": "اختیاری (پیش‌فرض: فعال‌شده)", + "optional": "اختیاری (پیش‌فرض: غیرفعال‌شده)", + "enabled": "اجباری فعال‌شده", + "disabled": "غیرفعال شد", + "configurations": { + "allow_room_to_be_recorded": "اجازه دهید اتاق ضبط شود", + "allow_room_to_be_recorded_description": "به صاحبان اتاق اجازه می‌دهد تعیین کنند که آیا می‌خواهند، اتاق را ضبط کنند یا خیر. اگر فعال باشد، مدیر همچنان باید پس از شروع جلسه، روی دکمه «ضبط» کلیک کند.", + "require_user_signed_in": "کاربران قبل از پیوستن به جلسه، ملزم به ورود به سیستم شوند", + "require_user_signed_in_description": "فقط به کاربرانی که دارای حساب کاربری در Greenlight هستند، اجازه می‌دهد به جلسه بپیوندند. اگر وارد سیستم نشده باشند، هنگام تلاش برای پیوستن به اتاق، به صفحه ورود هدایت می‌شوند.", + "require_mod_approval": "قبل از پیوستن به جلسه نیاز به تایید مدیر باشد", + "require_mod_approval_description": "هنگامی که کاربر سعی می‌کند به جلسه بپیوندد، از مدیر جلسه BigBlueButton درخواست می‌کند. در صورت تایید کاربر، می‌تواند به جلسه بپیوندد.", + "allow_any_user_to_start_meeting": "به هر کاربری اجازه دهید تا جلسه را شروع کند", + "allow_any_user_to_start_meeting_description": "به هر کاربری اجازه دهید جلسه را در هر زمانی که خواست شروع کند. به‌طور پیش‌فرض، فقط صاحب اتاق می‌تواند جلسه را شروع کند.", + "allow_users_to_join_as_mods": "همه کاربران به عنوان مدیر عضو شوند", + "allow_users_to_join_as_mods_description": "به همه کاربران هنگام پیوستن به جلسه، در BigBlueButton اختیارات مدیر را می‌دهد", + "mute_users_on_join": "کاربران هنگام پیوستن بی‌صدا شوند", + "mute_users_on_join_description": "هنگامی که کاربر به جلسه BigBlueButton می‌پیوندد، به‌طور خودکار بی‌صدا می‌شود", + "viewer_access_code": "کد دسترسی بینندگان", + "viewer_access_code_description": "به صاحبان اتاق اجازه می‌دهد تا یک کد الفبایی تصادفی داشته باشند که می‌تواند با کاربران به اشتراک بگذارند. در صورت تولید کد، برای پیوستن کاربران به جلسات اتاق الزامی است.", + "mod_access_code": "کد دسترسی مدیر", + "mod_access_code_description": "به صاحبان اتاق اجازه می‌دهد تا یک کد الفبایی تصادفی داشته باشند که می‌تواند با کاربران به اشتراک بگذارند. کد، در صورت تولید، مورد نیاز نخواهد بود و در صورت استفاده در هر جلسه اتاق، کاربر به عنوان مدیر ملحق خواهد شد." + } + }, + "roles": { + "role": "نقش", + "roles": "نقش‌ها", + "administrator": "مدیر کل", + "guest": "مهمان", + "manage_roles": "مدیریت نقش‌ها", + "delete_role": "حذف نقش", + "are_you_sure_delete_role": "آیا مطمئن هستید که می‌خواهید این نقش را حذف کنید؟", + "enter_role_name": "نام نقش را وارد کنید", + "add_role": "+ ایجاد نقش", + "create_role": "ایجاد نقش", + "create_new_role": "ایجاد نقش جدید", + "no_role_found": "هیچ نقشی پیدا نشد.", + "search_not_found": "هیچ نقشی پیدا نشد", + "edit": { + "create_room": "به کاربران دارای این نقش اجازه دهید که اتاق ایجاد کنند", + "record": "به کاربران دارای این نقش اجازه دهید تا جلسات خود را ضبط کنند", + "manage_users": "به کاربران دارای این نقش اجازه دهید تا کاربران را مدیریت کنند", + "manage_rooms": "به کاربران دارای این نقش اجازه دهید تا اتاق‌های سرور را مدیریت کنند", + "manage_recordings": "به کاربران دارای این نقش اجازه دهید تا جلسات ضبط‌شده سرور را مدیریت کنند", + "manage_site_settings": "به کاربران دارای این نقش اجازه دهید تا تنظیمات سایت را مدیریت کنند", + "manage_roles": "به کاربران دارای این نقش اجازه دهید تا دیگر نقش‌ها را ویرایش کنند", + "shared_list": "کاربران دارای این نقش را در فهرست کشویی اشتراک‌گذاری اتاق‌ها بگنجانید", + "room_limit": "محدودیت اتاق" + } + } + }, + "toast": { + "success": { + "user": { + "user_created": "یک کاربر جدید ایجاد شده‌ است.", + "user_updated": "کاربر به‌روز شده‌ است.", + "user_deleted": "کاربر حذف شده‌ است.", + "avatar_updated": "تصویر نمایه به‌روز شده‌ است.", + "password_updated": "گذرواژه به‌روز شده‌ است.", + "account_activated": "حساب کاربری شما فعال شده‌ است.", + "activation_email_sent": "ایمیلی حاوی راهنماهای فعال‌سازی حساب کاربری شما ارسال شده‌ است.", + "reset_pwd_email_sent": "ایمیلی حاوی راهنماهای بازنشانی گذرواژه شما ارسال شده‌ است." + }, + "session": { + "signed_out": "از سیستم خارج شده‌اید." + }, + "room": { + "room_created": "یک اتاق جدید ایجاد شده‌ است.", + "room_updated": "اتاق به‌روز شده‌ است.", + "room_deleted": "اتاق حذف شده‌ است.", + "room_shared": "اتاق اشتراک‌گذاری شده‌ است.", + "room_unshared": "اشتراک‌گذاری اتاق لغو شده‌ است.", + "recordings_synced": "ضبط‌‌شده‌های اتاق همگام‌سازی شده‌اند.", + "room_configuration_updated": "پیکربندی اتاق به‌روز شده‌ است.", + "room_setting_updated": "تنظیمات اتاق به‌روز شده‌ است.", + "presentation_updated": "ارائه به‌روز شده‌ است.", + "presentation_deleted": "ارائه حذف شده‌ است.", + "joining_meeting": "در حال پیوستن به جلسه...", + "meeting_started": "جلسه شروع شد.", + "access_code_copied": "کد دسترسی کپی شده‌ است.", + "access_code_generated": "کد دسترسی تولید شده‌ است.", + "access_code_deleted": "کد دسترسی حذف شده‌ است.", + "copied_meeting_url": "نشانی اینترنتی جلسه کپی شده‌ است. این پیوند می‌تواند برای پیوستن به جلسه استفاده شود." + }, + "site_settings": { + "site_setting_updated": "تنظیمات سایت به‌روز شده‌اند.", + "brand_color_updated": "رنگ برند به‌روز شده‌ است.", + "brand_image_updated": "تصویر برند به‌روز شده‌ است.", + "brand_image_deleted": "تصویر برند حذف شده‌ است.", + "privacy_policy_updated": "سیاست حفظ حریم خصوصی به‌روز شده‌ است.", + "terms_of_service_updated": "شرایط خدمات به‌روز شده‌ است." + }, + "recording": { + "recording_visibility_updated": "قابلیت مشاهده ضبط به‌روز شده‌ است.", + "recording_name_updated": "نام جلسه ضبط‌شده به‌روز شده‌ است.", + "recording_deleted": "جلسه ضبط‌شده حذف شده‌ است.", + "copied_urls": "نشانی‌های اینترنتی جلسه ضبط‌شده کپی شده‌ است." + }, + "role": { + "role_created": "یک نقش جدید ایجاد شده‌ است.", + "role_updated": "نقش به‌روز شده‌ است.", + "role_deleted": "نقش حذف شده‌ است.", + "role_permission_updated": "دسترسی نقش به‌روز شده‌ است." + }, + "invitations": { + "invitation_sent": "دعوت‌نامه ارسال شده‌ است." + } + }, + "error": { + "problem_completing_action": "این عمل نمی‌تواند تکمیل شود.\nلطفا دوباره تلاش کنید.", + "file_type_not_supported": "نوع پرونده پشتیبانی نمی‌شود.", + "file_size_too_large": "اندازه پرونده خیلی بزرگ است.", + "file_upload_error": "پرونده نمی‌تواند بارگذاری شود.", + "signin_required": "برای دسترسی به این صفحه باید وارد سیستم شده باشید.", + "roles": { + "role_assigned": "این نقش نمی‌تواند حذف شود، زیرا به حداقل یک کاربر اختصاص داده شده‌ است." + }, + "users": { + "signup_error": "شما نمی‌توانید احراز هویت شوید. لطفا با مدیریت تماس بگیرید.", + "invalid_invite": "ژتون دعوتنامه شما یا نامعتبر است یا نادرست است. لطفا با مدیریت تماس بگیرید تا یک ژتون جدید دریافت کنید", + "email_exists": "یک حساب کاربری با این ایمیل از قبل وجود دارد. لطفا با ایمیل دیگری دوباره امتحان کنید.", + "old_password": "گذرواژه‌ای که وارد کرده‌اید اشتباه است.", + "pending": "ثبت‌نام شما در انتظار تایید مدیر کل است. لطفا بعدا دوباره تلاش کنید.", + "banned": "شما به این برنامه دسترسی ندارید. اگر فکر می‌کنید که اشتباهی رخ داده است، لطفا با مدیریت تماس بگیرید." + }, + "rooms": { + "room_limit": "به دلیل رسیدن به سقف مجاز تعداد اتاق‌ها، نمی‌توانید اتاق ایجاد کنید." + }, + "session": { + "invalid_credentials": "نام‌کاربری یا گذرواژه نامعتبر است. لطفا اطلاعات کاربری خود را تایید کنید و دوباره امتحان کنید." + } + } + }, + "global_error_page": { + "title": "خطا", + "message": "با عرض پوزش، مشکلی پیش آمد. اگر حادثه دوباره رخ داد، لطفا با مدیریت تماس بگیرید." + }, + "not_found_error_page": { + "title": "صفحه یافت نشد", + "message": "با عرض پوزش، صفحه‌ای که سعی می‌کنید به آن دسترسی پیدا کنید یافت نشد." + }, + "account_activation_page": { + "title": "فعال‌سازی حساب کاربری", + "account_unverified": "حساب کاربری شما هنوز تایید نشده است.", + "message": "برای استفاده از Greenlight، لطفا با دنبال‌کردن راهنماهای موجود در ایمیل فعال‌سازی که برای شما ارسال شده‌است، حساب کاربری خود را تأیید کنید.", + "resend_activation_link": "اگر ایمیل فعال‌سازی را دریافت نکرده‌اید یا در استفاده از آن مشکل دارید، روی دکمه زیر کلیک کنید تا ایمیل فعال‌سازی جدیدی را درخواست دهید.", + "resend_btn_lbl": "ارسال مجدد تائیدیه" + }, + "forms": { + "validations": { + "full_name": { + "required": "لطفا یک نام کامل وارد کنید", + "min": "نام باید حداقل ۲ نویسه داشته باشد", + "max": "نام باید حداکثر ۲۵۵ نویسه داشته باشد" + }, + "email": { + "required": "لطفا یک ایمیل وارد کنید", + "email": "مقدار وارد شده با قالب ایمیل مطابقت ندارد", + "min": "ایمیل باید حداقل ۶ نویسه داشته باشد", + "max": "ایمیل باید حداکثر ۲۵۵ نویسه داشته باشد" + }, + "password": { + "required": "لطفا یک گذرواژه وارد کنید", + "match": "گذرواژه باید حداقل این موارد را داشته باشد:", + "min": "- هشت نویسه", + "lower": "- یک حرف کوچک", + "upper": "- یک حرف بزرگ", + "digit": "- یک عدد", + "symbol": "- یک نماد", + "max": "گذرواژه باید حداکثر ۲۵۵ نویسه داشته باشد" + }, + "password_confirmation": { + "required": "لطفا تایید گذرواژه را وارد کنید", + "match": "گذرواژه‌ها با هم یکسان نمی‌باشند" + }, + "emails": { + "required": "لطفا حداقل یک ایمیل معتبر وارد کنید", + "list": "لطفا فهرستی از ایمیل‌های معتبر جدا شده با ویرگول ارائه دهید (user@users.com,user1@users.com,user2@users.com)" + }, + "role_name": { + "required": "لطفا نام نقش را وارد کنید" + }, + "role": { + "limit": { + "required": "لطفا محدودیت تعداد اتاق را وارد کنید", + "min": "حداقل مجاز ۰ است", + "max": "حداکثر مجاز ۱۰۰ است" + }, + "type": { + "error": "باید یک عدد مشخص کنید" + } + }, + "room": { + "name": { + "required": "لطفا نام اتاق را وارد کنید.", + "min": "نام باید حداقل ۲ نویسه داشته باشد" + } + }, + "room_join": { + "name": { + "required": "لطفا نام خود را وارد کنید." + } + }, + "url": { + "invalid": "نشانی اینترنتی معتبر نیست" + } + }, + "room": { + "fields": { + "name": { + "label": "نام اتاق", + "placeholder": "نام اتاق را وارد کنید..." + } + } + }, + "room_join": { + "fields": { + "name": { + "label": "نام", + "placeholder": "نام خود را وارد کنید" + }, + "access_code": { + "label": "کد دسترسی", + "placeholder": "کد دسترسی را وارد کنید" + }, + "recording_consent": { + "label": "من اذعان دارم که این جلسه ممکن است، ضبط شود. این ممکن است شامل صدا و ویدیو من در صورت فعال شدن، باشد." + } + } + }, + "user": { + "signup": { + "fields": { + "full_name": { + "label": "نام کامل", + "placeholder": "نام کامل خود را وارد کنید" + }, + "email": { + "label": "ایمیل", + "placeholder": "ایمیل خود را وارد کنید" + }, + "password": { + "label": "گذرواژه", + "placeholder": "ایجاد گذرواژه" + }, + "password_confirmation": { + "label": "تایید گذرواژه", + "placeholder": "تایید گذرواژه" + } + } + }, + "signin": { + "fields": { + "email": { + "label": "ایمیل", + "placeholder": "ایمیل" + }, + "password": { + "label": "گذرواژه", + "placeholder": "گذرواژه" + }, + "remember_me": { + "label": "مرا به خاطر بسپار" + } + } + }, + "change_password": { + "fields": { + "old_password": { + "label": "گذرواژه فعلی", + "placeholder": "گذرواژه خود را وارد کنید" + }, + "new_password": { + "label": "گذرواژه جدید", + "placeholder": "گذرواژه جدید خود را وارد کنید" + }, + "password_confirmation": { + "label": "تایید گذرواژه", + "placeholder": "گذرواژه جدید خود را تایید کنید" + } + }, + "validations": { + "old_password": { + "required": "لطفا گذرواژه کنونی خود را وارد کنید" + } + } + }, + "forget_password": { + "fields": { + "email": { + "label": "ایمیل", + "placeholder": "ایمیل حساب کاربری را وارد کنید" + } + }, + "validations": { + "email": { + "required": "لطفا ایمیل حساب کاربری را وارد کنید" + } + } + }, + "reset_password": { + "fields": { + "new_password": { + "label": "گذرواژه جدید", + "placeholder": "گذرواژه جدید خود را وارد کنید" + }, + "password_confirmation": { + "label": "تایید گذرواژه", + "placeholder": "گذرواژه جدید خود را تایید کنید" + } + } + }, + "update_user": { + "fields": { + "full_name": { + "label": "نام کامل" + }, + "email": { + "label": "ایمیل" + }, + "language": { + "label": "زبان" + }, + "role": { + "label": "نقش" + } + } + } + }, + "admin": { + "createUser": { + "fields": { + "full_name": { + "label": "نام کامل", + "placeholder": "نام کامل خود را وارد کنید" + }, + "email": { + "label": "ایمیل", + "placeholder": "ایمیل خود را وارد کنید" + }, + "password": { + "label": "گذرواژه", + "placeholder": "گذرواژه کاربر را وارد کنید" + }, + "password_confirmation": { + "label": "تایید گذرواژه", + "placeholder": "تأیید گذرواژه" + } + } + }, + "invite_user": { + "fields": { + "emails": { + "label": "ایمیل‌ها" + } + } + }, + "site_settings": { + "fields": { + "value": { + "placeholder": "پیوند را اینجا وارد کنید..." + } + } + }, + "roles": { + "fields": { + "name": { + "label": "نام نقش", + "placeholder": "نام نقش را وارد کنید..." + } + } + } + } + } +} diff --git a/app/assets/locales/fr.json b/app/assets/locales/fr.json index f3e181f051..c10641d283 100644 --- a/app/assets/locales/fr.json +++ b/app/assets/locales/fr.json @@ -123,7 +123,7 @@ "recording_consent": "J'admets être informé que cette session pourrait être enregistrée. Le cas échéant ma voix et mon image vidéo feront partie de la captation." }, "presentation": { - "presentation": "Captation", + "presentation": "Documents", "click_to_upload": "Cliquez pour téléverser", "drag_and_drop": "ou glissez-déposez", "upload_description": "Téléversez n'importe quel document Office ou fichier pdf. Le temps nécessaire au téléversement dépend du volume du fichier.", diff --git a/app/assets/locales/ja.json b/app/assets/locales/ja.json index cd364240c3..16e35f3efb 100644 --- a/app/assets/locales/ja.json +++ b/app/assets/locales/ja.json @@ -23,11 +23,13 @@ "are_you_sure": "本当によろしいですか?", "return_home": "ホームに戻る", "created_at": "作成時", + "view_recordings": "録画を視聴", + "join_session": "会議に参加", "no_result_search_input": "\"{{ searchInput }}\"は見つかりませんでした", "action_permanent": "この操作は取り消すことができません", "homepage": { "welcome_bbb": "BigBlueButton へようこそ", - "bigbluebutton_description": "BigBlueButtonは、オンライン授業に最適化された、オープンソースのウェブ会議システムです。生徒や学生が協働し、すぐにフィードバックが得られるようにすることで、より多くの時間を実践的な学びに割くことができます。", + "bigbluebutton_description": "BigBlueButtonは、オンライン授業に最適化された、オープンソースのウェブ会議システムです。生徒や学生が協働し、すぐにフィードバックが得られるようにすることで、より多くの時間を実践的な学びにあてることができます。", "greenlight_description": "自分の会議室を作成し、会議を開いてみてください。簡単なリンクを使って手軽に他人を招待することもできます。", "learn_more": "BigBlueButtonについてもっと知る", "explore_features": "基本的な機能", @@ -109,7 +111,7 @@ "create_new_room": "新しい会議室を作成する", "enter_room_name": "会議室名を入力してください", "shared_by": "共有者", - "last_session": "最後のセッション: {{ localizedTime }}", + "last_session": "最後の会議: {{ localizedTime }}", "no_last_session": "会議はまだ開かれていません", "search_not_found": "会議室が見つかりません", "rooms_list_is_empty": "まだ会議室はありません!", @@ -126,7 +128,7 @@ "presentation": "プレゼンファイル", "click_to_upload": "クリックしてアップロード", "drag_and_drop": "またはドラッグ&ドロップ", - "upload_description": "OfficeドキュメントかPDFファイルをアップロードしてください。ファイルの大きさによっては、使用可能になるまでに少し時間がかかるかもしれません。", + "upload_description": "OfficeドキュメントかPDFファイル(サイズ{{size}}以下)をアップロードしてください。ファイルの大きさによっては、使用可能になるまでに少し時間がかかるかもしれません。", "are_you_sure_delete_presentation": "本当にこのプレゼンファイルを削除しますか?" }, "shared_access": { @@ -168,14 +170,18 @@ "users": "参加者", "visibility": "公開度", "formats": "フォーマット", - "published": "公開", - "unpublished": "非公開", - "protected": "秘匿", + "published": "限定公開", + "unpublished": "閲覧不可", + "protected": "保護", + "public": "公開", + "public_protected": "公開 / 保護", "length_in_minutes": "{{recording.length}}分", "processing_recording": "録画を処理中。数分かかることがあります...", "copy_recording_urls": "録画のURLをコピー", "recordings_list_empty": "まだ録画がありません!", - "recordings_list_empty_description": "会議を開始し録画すれば、録画データが現れます。", + "public_recordings_list_empty": "公開された録画はまだありません!", + "recordings_list_empty_description": "会議を開始し、それを録画すると、ここに録画データが表示されます。", + "public_recordings_list_empty_description": "視聴が許可されると、ここに録画データが表示されます。", "delete_recording": "この録画を削除", "are_you_sure_delete_recording": "本当にこの録画を削除しますか?", "search_not_found": "録画が見つかりません" @@ -231,8 +237,8 @@ "running": "会議中", "not_running": "使われていません", "active": "アクティブ", - "current_session": "現在のセッション: {{lastSession}}", - "last_session": "最後のセッション: {{localizedTime}}", + "current_session": "現在の会議: {{lastSession}}", + "last_session": "最後の会議: {{localizedTime}}", "no_meeting_yet": "会議は一度も行われていません。", "delete_server_rooms": "全ての会議室を削除", "resync_recordings": "録画の再同期", @@ -255,7 +261,7 @@ "brand_image": "ブランド画像", "click_to_upload": "クリックしてアップロード", "drag_and_drop": "あるいはドラッグ&ドロップ", - "upload_brand_image_description": "PNG、JPG、またはSVGファイルをアップロードしてください。ファイルの大きさによっては、使用可能になるまでに少し時間がかかるかもしれません。", + "upload_brand_image_description": "PNG、JPG、SVGファイル(サイズ{{size}}以下)をアップロードしてください。ファイルの大きさによっては、使用可能になるまでに少し時間がかかるかもしれません。", "remove_branding_image": "ブランド画像の削除" }, "administration": { @@ -492,6 +498,11 @@ "min": "名前は2文字以上必要です" } }, + "room_join": { + "name": { + "required": "名前を入力してください。" + } + }, "url": { "invalid": "無効なURL" } @@ -504,6 +515,21 @@ } } }, + "room_join": { + "fields": { + "name": { + "label": "名前", + "placeholder": "名前を入力してください" + }, + "access_code": { + "label": "アクセスコード", + "placeholder": "アクセスコードを入力してください" + }, + "recording_consent": { + "label": "私はこの会議が録画される可能性があること、また自分の音声とビデオもそれに含まれうることを理解しています。" + } + } + }, "user": { "signup": { "fields": { diff --git a/app/assets/locales/ko_KR.json b/app/assets/locales/ko_KR.json new file mode 100644 index 0000000000..2ab8b0cef4 --- /dev/null +++ b/app/assets/locales/ko_KR.json @@ -0,0 +1,671 @@ +{ + "start": "시작", + "search": "검색", + "home": "홈", + "previous": "이전", + "back": "뒤로", + "next": "다음", + "view": "보기", + "join": "참여하기", + "edit": "편집", + "save": "저장하기", + "save_changes": "변경사항 저장", + "update": "업데이트", + "report": "보고서", + "share": "공유", + "cancel": "취소", + "close": "닫기", + "delete": "삭제", + "copy": "참여 링크 복사", + "or": "또는", + "online": "온라인", + "help_center": "도움말 센터", + "are_you_sure": "확실합니까?", + "return_home": "홈으로 돌아가기", + "created_at": "에서 생성", + "no_result_search_input": "\"{{ searchInput }}\"에 대한 결과를 찾을 수 없습니다.", + "action_permanent": "이 작업은 실행 취소할 수 없습니다.", + "homepage": { + "welcome_bbb": "BigBlueButton에 오신 것을 환영합니다.", + "bigbluebutton_description": "BigBlueButton은 온라인 수업을 위한 오픈 소스 웹 회의 시스템입니다. 이 플랫폼은 학생들이 실시간으로 협업하고 피드백을 받을 수 있도록 함으로써 응용 학습 시간을 극대화합니다.", + "greenlight_description": "세션을 호스트할 자신의 룸을 만들거나 짧고 편리한 연결을 사용하여 다른 룸에 참여합니다.", + "learn_more": "BigBlueButton에 대해 자세히 알아보기", + "explore_features": "당사의 기능을 살펴보십시오", + "meeting_title": "미팅 시작", + "meeting_description": "비디오, 오디오, 화면 공유, 채팅 및 적용된 학습에 필요한 모든 도구로 가상 수업을 시작합니다.", + "recording_title": "미팅 녹화", + "recording_description": "BigBlueButton의 미팅을 기록하고 학생들과 공유하여 자료를 검토하고 성찰합니다.", + "settings_title": "룸 관리", + "settings_description": "효율적인 강의실을 구성하도록 회의실 및 회의 설정을 설정합니다.", + "and_more_title": "그리고 더!", + "and_more_description": "BigBlueButton은 응용 학습을 위한 내장된 도구를 제공하여 수업 시간을 절약할 수 있도록 설계되었습니다.", + "enter_meeting_url": "미팅 URL 입력", + "enter_meeting_url_instruction": "아래 필드에 BigBlueButton 미팅룸의 URL을 입력하십시오." + }, + "authentication": { + "sign_in": "로그인", + "sign_up": "등록하기", + "sign_out": "로그아웃", + "email": "이메일", + "password": "패스워드", + "confirm_password": "패스워드 확인", + "enter_email": "이메일 입력", + "enter_name": "이름 입력", + "remember_me": "기억하기", + "forgot_password": "패스워드를 잊어버렸습니까?", + "dont_have_account": "계정이 없습니까?", + "create_account": "계정 생성", + "create_an_account": "계정 생성", + "already_have_account": "이미 계정이 있습니까?" + }, + "user": { + "user": "사용자", + "users": "사용자들", + "name": "이름", + "email_address": "이메일 주소", + "authenticator": "인증", + "full_name": "전체 이름", + "no_user_found": "사용자를 찾을 수 없습니다.", + "type_three_characters": "다른 사용자를 표시하려면 세(3)자 이상을 입력하십시오.", + "search_not_found": "사용자를 찾을 수 없습니다.", + "profile": { + "profile": "프로필", + "language": "언어", + "role": "역할", + "administrator": "관리자", + "guest": "손님" + }, + "account": { + "account_info": "계정 정보", + "delete_account": "계정 삭제", + "change_password": "패스워드 변경", + "reset_password": "패스워드 초기화", + "update_account_info": "계정 정보 업데이트", + "current_password": "현재 패스워드", + "new_password": "새로운 패스워드", + "confirm_password": "패스워드 확인", + "permanently_delete_account": "계정 영구 삭제", + "delete_account_description": "계정을 삭제하도록 선택하면 복구할 수 없습니다. \n설정, 회의실 및 기록을 포함한 계정에 대한 모든 정보가 제거됩니다.", + "delete_account_confirmation": "예, 제 계정을 삭제하고 싶습니다", + "are_you_sure_delete_account": "계정을 삭제하시겠습니까?" + }, + "avatar": { + "upload_avatar": "업데이트 아바타", + "delete_avatar": "아바타 삭제", + "crop_avatar": "아바타 자르기" + }, + "pending": { + "title": "등록 대기", + "message": "등록해 주셔서 감사합니다! 계정이 현재 관리자의 승인 대기 중입니다." + } + }, + "room": { + "room": "Room", + "rooms": "Room들", + "room_name": "Room 이름", + "add_new_room": "새로운 Room", + "create_room": "Room 생성", + "delete_room": "Room 삭제", + "create_new_room": "새로운 Room 생성", + "enter_room_name": "Room 이름 입력", + "shared_by": "에 의해 공유", + "last_session": "마지막 세션: {{ localizedTime }}", + "no_last_session": "이전 세션이 생성되지 않았습니다", + "search_not_found": "Room을 찾을 수 없음", + "rooms_list_is_empty": "아직 소유한 Room이 없습니다.", + "rooms_list_empty_create_room": "아래 단추를 누른 후 Room 이름을 입력하여 첫 번째 Room을 생성할 수 있습니다.", + "meeting": { + "start_meeting": "미팅 시작", + "join_meeting": "미팅 참여", + "meeting_invitation": "가입 초대를 받았습니다.", + "meeting_not_started": "미팅이 아직 시작되지 않았습니다", + "join_meeting_automatically": "미팅이 시작될 때 자동으로 참여합니다", + "recording_consent": "이 세션이 녹화될 수 있고 이를 허락합니다. 이 세션에서 녹화가 진행될 경우 내 음성 및 비디오가 포함될 수 있습니다." + }, + "presentation": { + "presentation": "프레젠테이션", + "click_to_upload": "업로드하려면 클릭하세요.", + "drag_and_drop": "또는 드래그 앤 드롭", + "upload_description": "사무실 문서 또는 PDF 파일({{size}}이하)을 업로드합니다. 파일 크기에 따라 파일을 사용하려면 업로드하는 데 추가 시간이 필요할 수 있습니다", + "are_you_sure_delete_presentation": "이 프레젠테이션을 삭제하시겠습니까?" + }, + "shared_access": { + "access": "액세스", + "add_share_access": "+ 액세스 공유", + "share_room_access": "공유 Room 액세스", + "add_some_users": "사용자를 추가할 시간입니다!", + "add_some_users_description": "새 사용자를 추가하려면 아래 단추를 누른 후 Room을 공유할 사용자를 검색하거나 선택하십시오.", + "delete_shared_access": "공유 액세스 삭제", + "are_you_sure_delete_shared_access": "이 공유 액세스를 삭제하시겠습니까?" + }, + "settings": { + "settings": "설정", + "room_name": "Room 이름", + "user_settings": "사용자 설정", + "allow_room_to_be_recorded": "Room 녹화 허용", + "require_signed_in": "가입하기 전에 사용자가 로그인하도록 요구", + "require_signed_in_message": "이 Room에 참가하려면 로그인해야 합니다.", + "require_mod_approval": "참가하기 전에 진행자 승인 필요", + "allow_any_user_to_start": "모든 사용자가 이 미팅을 시작할 수 있도록 허용", + "all_users_join_as_mods": "모든 사용자가 진행자로 참가", + "mute_users_on_join": "사용자가 참가할 때 음소거", + "generate": "생성", + "access_code": "액세스 코드", + "mod_access_code": "진행자용 액세스 코드", + "mod_access_code_optional": "진행자용 액세스 코드 (선택)", + "access_code_required": "액세스 코드를 입력하세요", + "wrong_access_code": "잘못된 액세스 코드입니다.", + "generate_viewers_access_code": "참여자용 액세스 코드 생성", + "generate_mods_access_code": "진행자용 액세스 코드 생성", + "are_you_sure_delete_room": "이 Room을 삭제 하시겠습니까?" + } + }, + "recording": { + "recording": "녹화", + "recordings": "녹화", + "name": "이름", + "length": "길이", + "users": "사용자", + "visibility": "가시성", + "formats": "형식", + "published": "게시됨", + "unpublished": "게시되지 않음", + "protected": "보호", + "length_in_minutes": "{{recording.length}} 분.", + "processing_recording": "녹화를 인코딩 중입니다. 몇 분 정도 걸릴 수 있습니다...", + "copy_recording_urls": "녹화 Url(S) 복사", + "recordings_list_empty": "아직 녹화되지 않았습니다!", + "recordings_list_empty_description": "미팅을 중 녹화된 기록이 여기에 나타납니다.", + "delete_recording": "녹화 삭제", + "are_you_sure_delete_recording": "이 녹화를 삭제하시겠습니까?", + "search_not_found": "녹화된 기록을 찾을 수 없음" + }, + "admin": { + "admin_panel": "관리자 패널", + "manage_users": { + "manage_users": "사용자 관리", + "active": "활성", + "approve": "승인", + "decline": "거절", + "pending": "대기", + "banned": "금지", + "ban": "금지", + "unban": "금지 해제", + "deleted": "삭제", + "invited_tab": "초대", + "invite_user": "사용자 초대", + "send_invitation": "초대장 보내기", + "enter_user_email": "사용자 이메일 입력", + "new_user": "새로운 사용자", + "add_new_user": "새로운 사용자", + "create_new_user": "새로운 사용자 생성", + "edit_user": "사용자 편집", + "delete_user": "사용자 삭제", + "users_edit_path": "사용자 / 편집", + "create_account": "계정 생성", + "create_room": "Room 생성", + "create_new_room": "새로운 Room 생성", + "user_created_at": "생성됨: {{localizedTime}}", + "are_you_sure_delete_account": "{{user.name}}의 계정을 삭제하시겠습니까?", + "delete_account_warning": "이 계정을 삭제하면 복구할 수 없습니다.", + "empty_active_users": "이 서버에 아직 활성 사용자가 없습니다!", + "empty_active_users_subtext": "사용자 상태가 활성으로 변경되면 사용자 상태가 여기에 나타납니다.", + "empty_pending_users": "이 서버에는 아직 대기 중인 사용자가 없습니다!", + "empty_pending_users_subtext": "사용자 상태가 대기 중으로 변경되면 사용자 상태가 여기에 나타납니다.", + "empty_banned_users": "이 서버에는 아직 금지된 사용자가 없습니다!", + "empty_banned_users_subtext": "사용자 상태가 금지됨으로 변경되면 사용자 상태가 여기에 나타납니다.", + "empty_invited_users": "이 서버에는 아직 초대된 사용자가 없습니다!", + "empty_invited_users_subtext": "사용자의 상태가 초대됨으로 변경되면 여기에 나타납니다.", + "invited": { + "time_sent": "보낸 시간", + "valid": "유효한" + } + }, + "server_rooms": { + "server_rooms": "서버 Rooms", + "name": "이름", + "owner": "소유자", + "room_id": "Room ID", + "participants": "참가자", + "status": "상태", + "running": "실행중", + "not_running": "실행 중이 아님", + "active": "활성", + "current_session": "현재 세션: {{lastSession}}", + "last_session": "마지막 세션: {{localizedTime}}", + "no_meeting_yet": "아직 미팅이 없습니다.", + "delete_server_rooms": "Server Room 삭제", + "resync_recordings": "녹화 재동기화", + "empty_room_list": "아직 Server Room이 없습니다!", + "empty_room_list_subtext": "첫 번째 Room을 생성하면 Room이 여기에 나타납니다." + }, + "server_recordings": { + "server_recordings": "Server 녹화", + "latest_recordings": "마지막 녹화", + "no_recordings_found": "녹화 기록을 찾을 수 없습니다." + }, + "site_settings": { + "site_settings": "사이트 설정", + "customize_greenlight": "그린라이트 사용자화", + "appearance": { + "appearance": "외모", + "brand_color": "브랜드 컬러", + "regular": "보통", + "lighten": "얇게", + "brand_image": "브랜드 이미지", + "click_to_upload": "클릭하여 업로드", + "drag_and_drop": "또는 드래그 앤 드롭", + "upload_brand_image_description": "PNG, JPG 또는 SVG 파일({{size}} 이하)을 업로드합니다. 파일 크기에 따라 업로드하는 데 시간이 걸릴 수 있습니다.", + "remove_branding_image": "브랜딩 이미지 삭제" + }, + "administration": { + "administration": "관리", + "terms": "이용 약관", + "privacy": "개인 정보 보호 정책", + "privacy_policy": "개인 정보 보호 정책", + "change_term_links": "페이지 하단에 나타나는 용어 링크 변경", + "change_privacy_link": "페이지 하단에 나타나는 용어 링크 변경", + "change_url": "URL 변경", + "enter_link": "링크 입력" + }, + "settings": { + "settings": "설정", + "allow_users_to_share_rooms": "사용자의 Room 공유 허용", + "allow_users_to_share_rooms_description": "사용 불가능으로 설정하면 Room 선택사항 드롭다운 메뉴에서 버튼이 제거되어 사용자가 Room을 공유할 수 없습니다", + "allow_users_to_preupload_presentation": "사용자에게 프레젠테이션 발표자료 사전 업로드 허용", + "allow_users_to_preupload_presentation_description": "사용자는 특정 Room에서 발표자료로 사용할 프리젠테이션을 미리 업로드할 수 있습니다" + }, + "registration": { + "registration": "등록", + "role_mapping_by_email": "전자 메일별 역할 매핑", + "role_mapping_by_email_description": "전자 메일을 사용하여 사용자를 역할에 매핑합니다. role1=email1, role2=email2 형식이어야 합니다", + "enter_role_mapping_rule": "역할 매핑 규칙 입력", + "resync_on_login": "로그인할 때마다 사용자 데이터 다시 동기화", + "resync_on_login_description": "사용자가 로그인할 때마다 사용자의 정보를 다시 동기화하여 외부 인증 공급자가 항상 Greenlight의 정보와 일치하도록 합니다", + "default_role": "기본 역할", + "default_role_description": "새로 생성된 사용자에게 할당할 기본 역할", + "registration_method": "등록 방법", + "registration_method_description": "사용자가 웹 사이트에 등록하는 방법 변경", + "registration_methods" : { + "open": "공개 등록", + "invite": "초대로 참여", + "approval": "승인/거부" + } + } + }, + "room_configuration": { + "room_configuration": "Room 구성", + "default": "선택 사항(기본값: 사용)", + "optional": "선택 사항(기본값: 사용 안 함)", + "enabled": "강제 실행", + "disabled": "사용 안함", + "configurations": { + "allow_room_to_be_recorded": "Room 녹화 허용", + "allow_room_to_be_recorded_description": "Room 소유자가 녹화 기능의 사용 여부를 지정할 수 있습니다. 활성화된 경우 진행자는 미팅이 시작된 후 Room 상단에 '녹화' 버튼이 활성화 되고 이 버튼을 클릭하면 녹화를 진행할 수 있습니다.", + "require_user_signed_in": "가입하기 전에 사용자가 로그인해야 함", + "require_user_signed_in_description": "Greenlight 계정을 가진 사용자만 미팅에 참여할 수 있습니다. 로그인하지 않은 경우, Room에 참여할 때 로그인 페이지로 되돌아 갑니다..", + "require_mod_approval": "가입하기 전에 진행자 승인 필요", + "require_mod_approval_description": "사용자가 참여하려고 할 때 BigBlueButton 미팅의 진행자에게 메시지를 표시합니다. 진행자에 의해 사용자가 승인된 경우, 사용자는 미팅에 참여할 수 있습니다.", + "allow_any_user_to_start_meeting": "누구나 미팅을 시작할 수 있도록 허용", + "allow_any_user_to_start_meeting_description": "누구나 언제든지 미팅을 시작할 수 있도록 허용합니다. 기본적으로는 Room 소유자만 미팅을 시작할 수 있습니다.", + "allow_users_to_join_as_mods": "모든 사용자가 진행자로 참여", + "allow_users_to_join_as_mods_description": "모든 사용자가 미팅에 참여할 때 진행자 권한을 부여합니다", + "mute_users_on_join": "사용자가 미팅에 참여할 때 음소거", + "mute_users_on_join_description": "사용자가 미팅에 참여할 때 자동으로 음소거됨", + "viewer_access_code": "참여자 액세스 코드", + "viewer_access_code_description": "Room 소유자가 참여자와 공유할 수 있는 임의의 영문,숫자 코드를 가질 수 있습니다. 생성된 코드는 사용자가 Room 미팅에 참여하는 데 필요합니다.", + "mod_access_code": "진행자 액세스 코드", + "mod_access_code_description": "Room 소유자는 참여자와 공유할 수 있는 임의의 영문, 숫자 코드를 가질 수 있습니다. Room 미팅에서 사용자가 처음부터 진행자로 참여하면 작성된 코드는 필요하지 않습니다. " + } + }, + "roles": { + "role": "역할", + "roles": "역할", + "administrator": "관리자", + "guest": "손님", + "manage_roles": "역할 관리", + "delete_role": "역할 삭제", + "are_you_sure_delete_role": "이 역할을 삭제하시겠습니까?", + "enter_role_name": "역할 이름 입력", + "add_role": "+ 역할 생성", + "create_role": "역할 생성", + "create_new_role": "새로운 역할 생성", + "no_role_found": "역할을 찾을 수 없습니다.", + "search_not_found": "역할을 찾을 수 없음", + "edit": { + "create_room": "이 역할을 가진 사용자에게 Room 생성 허용", + "record": "이 역할을 가진 사용자에게 미팅 녹화 허용", + "manage_users": "이 역할을 가진 사용자에게 사용자 관리 허용", + "manage_rooms": "이 역할을 가진 사용자에게 서버 Room 관리 허용", + "manage_recordings": "이 역할을 가진 사용자에게 서버 녹화 관리 허용", + "manage_site_settings": "이 역할을 가진 사용자에게 사이트 설정 관리 허용", + "manage_roles": "이 역할을 가진 사용자가 다른 역할을 편집하도록 허용", + "shared_list": "Room 공유 드롭다운 메뉴에 이 역할을 가진 사용자 포함", + "room_limit": "Room 제한" + } + } + }, + "toast": { + "success": { + "user": { + "user_created": "새 사용자가 생성되었습니다.", + "user_updated": "사용자가 업데이트되었습니다.", + "user_deleted": "사용자가 삭제되었습니다.", + "avatar_updated": "아바타가 업데이트되었습니다.", + "password_updated": "암호가 업데이트되었습니다.", + "account_activated": "계정이 활성화되었습니다.", + "activation_email_sent": "계정을 활성화하기 위한 지침이 포함된 이메일이 전송되었습니다.", + "reset_pwd_email_sent": "암호를 재설정하는 방법이 포함된 이메일이 전송되었습니다." + }, + "session": { + "signed_out": "로그아웃되었습니다." + }, + "room": { + "room_created": "새 Room이 생성되었습니다.", + "room_updated": "Room이 업데이트되었습니다.", + "room_deleted": "Room이 삭제되었습니다.", + "room_shared": "Room이 공유되었습니다.", + "room_unshared": "Room 공유가 해제되었습니다.", + "recordings_synced": "Room 녹화가 동기화되었습니다.", + "room_configuration_updated": "Room 환경설정이 업데이트되었습니다.", + "room_setting_updated": "Room 설정이 업데이트되었습니다.", + "presentation_updated": "프레젠테이션이 업데이트되었습니다.", + "presentation_deleted": "프레젠테이션이 삭제되었습니다.", + "joining_meeting": "미팅에 참여하는 중...", + "meeting_started": "미팅이 시작되었습니다.", + "access_code_copied": "액세스 코드가 복사되었습니다.", + "access_code_generated": "새 액세스 코드가 생성되었습니다.", + "access_code_deleted": "액세스 코드가 삭제되었습니다.", + "copied_meeting_url": "미팅 URL이 복사되었습니다. 링크를 통하여 미팅에 참여할 수 있습니다." + }, + "site_settings": { + "site_setting_updated": "사이트 설정이 업데이트되었습니다.", + "brand_color_updated": "브랜드 색상이 업데이트되었습니다.", + "brand_image_updated": "브랜드 이미지가 업데이트되었습니다.", + "brand_image_deleted": "브랜드 이미지가 삭제되었습니다.", + "privacy_policy_updated": "개인 정보 보호 정책이 업데이트되었습니다.", + "terms_of_service_updated": "서비스 약관이 업데이트되었습니다." + }, + "recording": { + "recording_visibility_updated": "녹화 가시성이 업데이트되었습니다.", + "recording_name_updated": "녹화명이 업데이트되었습니다.", + "recording_deleted": "녹화가 삭제되었습니다.", + "copied_urls": "녹화 URL이 복사되었습니다." + }, + "role": { + "role_created": "새 역할이 생성되었습니다.", + "role_updated": "역할이 업데이트되었습니다.", + "role_deleted": "역할이 삭제되었습니다.", + "role_permission_updated": "역할의 권한이 업데이트되었습니다." + }, + "invitations": { + "invitation_sent": "초대장을 보냈습니다." + } + }, + "error": { + "problem_completing_action": "작업을 완료할 수 없습니다. \n다시 시도하십시오.", + "file_type_not_supported": "파일 형식이 지원되지 않습니다.", + "file_size_too_large": "파일 크기가 너무 큽니다.", + "file_upload_error": "파일을 업로드할 수 없습니다.", + "signin_required": "이 페이지에 액세스하려면 로그인해야 합니다.", + "roles": { + "role_assigned": "이 역할은 적어도 한 명의 사용자에게 할당되어 있으므로 삭제할 수 없습니다." + }, + "users": { + "signup_error": "인증할 수 없습니다. 관리자에게 문의하십시오.", + "invalid_invite": "초대 토큰이 유효하지 않거나 올바르지 않습니다. 새 토큰을 받으려면 관리자에게 문의하십시오.", + "email_exists": "이 이메일의 계정이 이미 존재합니다. 다른 이메일로 다시 시도하십시오.", + "old_password": "입력하신 비밀번호가 올바르지 않습니다.", + "pending": "귀하의 등록은 관리자의 승인을 기다리고 있습니다. 나중에 다시 시도 해주십시오.", + "banned": "이 애플리케이션에 대한 액세스 권한이 없습니다. 이것이 오류라고 생각되면 관리자에게 문의하십시오." + }, + "rooms": { + "room_limit": "Room 생성 한도에 도달하여 Room을 더 이상 만들 수 없습니다." + }, + "session": { + "invalid_credentials": "사용자 이름 또는 비밀번호가 유효하지 않습니다. 자격 증명을 확인하고 다시 시도하십시오." + } + } + }, + "global_error_page": { + "title": "에러", + "message": "죄송합니다. 문제가 발생했습니다. 해당 현상이 다시 발생하면 관리자에게 문의하시기 바랍니다." + }, + "not_found_error_page": { + "title": "페이지를 찾을 수 없음", + "message": "죄송합니다. 액세스하려는 페이지를 찾을 수 없습니다." + }, + "account_activation_page": { + "title": "계정 활성화", + "account_unverified": "귀하의 계정이 아직 확인되지 않았습니다.", + "message": "Greenlight를 사용하려면 귀하에게 전송된 활성화 이메일의 지침에 따라 귀하의 계정을 확인하십시오.", + "resend_activation_link": "활성화 이메일을 받지 못하셨거나 사용에 문제가 있는 경우 아래 버튼을 클릭하여 새 활성화 이메일을 요청하십시오.", + "resend_btn_lbl": "인증 재전송" + }, + "forms": { + "validations": { + "full_name": { + "required": "이름을 입력하세요.", + "min": "이름은 2자 이상이어야 합니다.", + "max": "이름은 최대 255자여야 합니다." + }, + "email": { + "required": "이메일을 입력하세요", + "email": "입력한 값이 이메일 형식과 일치하지 않습니다.", + "min": "이메일 주소는 최소 6자 이상이어야 합니다.", + "max": "이메일 주소는 최대 255자 길이여야 합니다." + }, + "password": { + "required": "비밀번호를 입력하세요", + "match": "암호는 최소한 다음을 포함해야 합니다 :", + "min": "- 최소 8자 이상", + "lower": "- 영문 소문자 1개 이상", + "upper": "- 영문 대문자 1개 이상", + "digit": "- 최소 숫자 1개 이상", + "symbol": "- 특수 기호 최소 1개 이상", + "max": "비밀번호는 최대 255자 이하여야 합니다." + }, + "password_confirmation": { + "required": "비밀번호 확인을 입력하세요", + "match": "암호가 일치하지 않습니다" + }, + "emails": { + "required": "유효한 이메일 주소를 하나 이상 입력하세요.", + "list": "유효한 이메일 목록을 쉼표로 구분하여 제공해야 합니다(user@users.com,user1@users.com,user2@users.com )" + }, + "role_name": { + "required": "역할명을 입력하세요." + }, + "role": { + "limit": { + "required": "Romm 개수 제한을 입력하세요.", + "min": "허용되는 최소값은 0입니다.", + "max": "허용되는 최대값은 100입니다." + }, + "type": { + "error": "숫자를 지정해야 합니다." + } + }, + "room": { + "name": { + "required": "Room 이름을 입력해주세요.", + "min": "이름은 최소 2자 이상이어야 합니다." + } + }, + "room_join": { + "name": { + "required": "당신의 이름을 입력 해주세요." + } + }, + "url": { + "invalid": "잘못된 URL" + } + }, + "room": { + "fields": { + "name": { + "label": "Room 이름", + "placeholder": "Room 이름을 입력하세요..." + } + } + }, + "room_join": { + "fields": { + "name": { + "label": "이름", + "placeholder": "이름을 입력하세요" + }, + "access_code": { + "label": "액세스 코드", + "placeholder": "액세스 코드를 입력하세요" + }, + "recording_consent": { + "label": "이 세션이 녹화될 수 있고 이를 허락합니다. 이 세션에서 녹화가 진행될 경우 내 음성 및 비디오가 포함될 수 있습니다." + } + } + }, + "user": { + "signup": { + "fields": { + "full_name": { + "label": "전체 이름", + "placeholder": "이름을 입력하십시오" + }, + "email": { + "label": "이메일", + "placeholder": "이메일 입력" + }, + "password": { + "label": "패스워드", + "placeholder": "패스워드 생성" + }, + "password_confirmation": { + "label": "패스워드 확인", + "placeholder": "패스워드 확인" + } + } + }, + "signin": { + "fields": { + "email": { + "label": "이메일", + "placeholder": "이메일" + }, + "password": { + "label": "패스워드", + "placeholder": "패스워드" + }, + "remember_me": { + "label": "기억하기" + } + } + }, + "change_password": { + "fields": { + "old_password": { + "label": "현재 패스워드", + "placeholder": "패스워드를 입력하세요" + }, + "new_password": { + "label": "새로운 패스워드", + "placeholder": "새로운 패스워드를 입력하세요" + }, + "password_confirmation": { + "label": "패스워드 확인", + "placeholder": "새로운 패스워드 확인" + } + }, + "validations": { + "old_password": { + "required": "현재 비밀번호를 입력하세요" + } + } + }, + "forget_password": { + "fields": { + "email": { + "label": "이메일", + "placeholder": "계정 이메일 입력" + } + }, + "validations": { + "email": { + "required": "계정 이메일을 입력해주세요" + } + } + }, + "reset_password": { + "fields": { + "new_password": { + "label": "새로운 패스워드", + "placeholder": "새로운 패스워드를 입력하세요" + }, + "password_confirmation": { + "label": "패스워드 확인", + "placeholder": "새로운 패스워드 확인" + } + } + }, + "update_user": { + "fields": { + "full_name": { + "label": "전체 이름" + }, + "email": { + "label": "이메일" + }, + "language": { + "label": "언어" + }, + "role": { + "label": "역할" + } + } + } + }, + "admin": { + "createUser": { + "fields": { + "full_name": { + "label": "전체 이름", + "placeholder": "이름을 입력하세요" + }, + "email": { + "label": "이메일", + "placeholder": "사용자 이메일 입력" + }, + "password": { + "label": "패스워드", + "placeholder": "패스워드를 입력하세요" + }, + "password_confirmation": { + "label": "패스워드 확인", + "placeholder": "패스워드 확인" + } + } + }, + "invite_user": { + "fields": { + "emails": { + "label": "이메일" + } + } + }, + "site_settings": { + "fields": { + "value": { + "placeholder": "링크 입력..." + } + } + }, + "roles": { + "fields": { + "name": { + "label": "역할명", + "placeholder": "역할명 입력" + } + } + } + } + } +} diff --git a/app/assets/locales/ru.json b/app/assets/locales/ru.json index 940b87e3a6..96220e82bd 100644 --- a/app/assets/locales/ru.json +++ b/app/assets/locales/ru.json @@ -126,7 +126,7 @@ "presentation": "Презентация", "click_to_upload": "Нажмите, чтобы загрузить ", "drag_and_drop": "или перетащите и отпустите", - "upload_description": "Загрузить офисный документ или PDF-файл. В зависимости от размера файла может потребоваться дополнительное время для загрузки, прежде чем его можно будет использовать.", + "upload_description": "Загрузите любой офисный документ или PDF (размером не более {{size}}). В зависимости от размера файла, для загрузки может потребоваться дополнительное время, прежде чем его можно будет использовать", "are_you_sure_delete_presentation": "Вы уверены, что хотите удалить эту презентацию?" }, "shared_access": { @@ -255,7 +255,7 @@ "brand_image": "Фирменное изображение", "click_to_upload": "Нажмите для загрузки", "drag_and_drop": "или перетащите и отпустите", - "upload_brand_image_description": "Загрузите любой файл в формате PNG, JPG или SVG. В зависимости от размера файла для загрузки может потребоваться больше времени.", + "upload_brand_image_description": "Загрузите любой файл в формате PNG, JPG или SVG (размером не более {{size}}). В зависимости от размера файла, для загрузки может потребоваться дополнительное время, прежде чем его можно будет использовать", "remove_branding_image": "Удалить фирменное изображение" }, "administration": { @@ -492,6 +492,11 @@ "min": "Имя должно содержать не менее 2 символов" } }, + "room_join": { + "name": { + "required": "Введите свое имя." + } + }, "url": { "invalid": "Неверная ссылка" } @@ -504,6 +509,21 @@ } } }, + "room_join": { + "fields": { + "name": { + "label": "Имя", + "placeholder": "Введите свое имя" + }, + "access_code": { + "label": "Код доступа", + "placeholder": "Введите код доступа" + }, + "recording_consent": { + "label": "Я уведомлен, что может вестись запись вебинара. Запись будет включать мой голос и видео." + } + } + }, "user": { "signup": { "fields": { diff --git a/app/assets/locales/tr.json b/app/assets/locales/tr.json index c0cfb2983f..fe5466df51 100644 --- a/app/assets/locales/tr.json +++ b/app/assets/locales/tr.json @@ -23,6 +23,8 @@ "are_you_sure": "Emin misiniz?", "return_home": "Girişe dön", "created_at": "Oluşturulma", + "view_recordings": "Kayıtları görüntüle", + "join_session": "Oturuma katıl", "no_result_search_input": "\"{{ searchInput }}\" için herhangi bir sonuç bulunamadı", "action_permanent": "Bu işlem geri alınamaz.", "homepage": { @@ -126,7 +128,7 @@ "presentation": "Sunum", "click_to_upload": "Yüklemek için tıklayın", "drag_and_drop": "ya da sürükleyip bırakın", - "upload_description": "Herhangi bir ofis belgesi ya da PDF dosyası yükleyin. Dosyanın boyutuna bağlı olarak, kullanılmadan önce karşıya yüklenmesi biraz sürebilir.", + "upload_description": "Herhangi bir Office belgesi ya da PDF dosyası ({{size}} boyutundan büyük olmayan) yükleyin. Dosyanın boyutuna bağlı olarak, kullanılmadan önce karşıya yüklenmesi biraz sürebilir.", "are_you_sure_delete_presentation": "Bu sunumu silmek istediğinize emin misiniz?" }, "shared_access": { @@ -171,11 +173,15 @@ "published": "Yayınlanmış", "unpublished": "Yayınlanmamış", "protected": "Korunmuş", + "public": "Herkese açık", + "public_protected": "Herkese açık / Korunmuş", "length_in_minutes": "{{recording.length}} dakika", "processing_recording": "Kayıt işleniyor. Bu işlem birkaç dakika sürebilir...", "copy_recording_urls": "Kayıt adreslerini kopyala", "recordings_list_empty": "Henüz bir kaydınız yok!", + "public_recordings_list_empty": "Henüz herkese açık bir kayıt yok!", "recordings_list_empty_description": "Toplantıları başlatıp kayıt ettikten sonra kayıtlarınız burada görüntülenecek.", + "public_recordings_list_empty_description": "Kayıtlar yayınlandığında burada görüntülenecek.", "delete_recording": "Kaydı sil", "are_you_sure_delete_recording": "Bu kaydı silmek istediğinize emin misiniz?", "search_not_found": "Herhangi bir kayıt bulunamadı" @@ -255,7 +261,7 @@ "brand_image": "Marka görseli", "click_to_upload": "Yüklemek için tıklayın", "drag_and_drop": "ya da sürükleyip bırakın", - "upload_brand_image_description": "Herhangi PNG, JGP ya da SVG dosyası yükleyin. Dosyanın boyutuna bağlı olarak, kullanılmadan önce karşıya yüklenmesi biraz sürebilir.", + "upload_brand_image_description": "Herhangi bir PNG, JPG ya da SVG dosyası ({{size}} boyutundan büyük olmayan) yükleyin. Dosyanın boyutuna bağlı olarak, kullanılmadan önce karşıya yüklenmesi biraz sürebilir.", "remove_branding_image": "Marka görselini kaldır" }, "administration": { @@ -492,6 +498,11 @@ "min": "Ad en az 2 karakter uzunluğunda olabilir" } }, + "room_join": { + "name": { + "required": "Lütfen adınızı yazın." + } + }, "url": { "invalid": "Adres geçersiz" } @@ -504,6 +515,21 @@ } } }, + "room_join": { + "fields": { + "name": { + "label": "Ad", + "placeholder": "Adınızı yazın" + }, + "access_code": { + "label": "Erişim kodu", + "placeholder": "Erişim kodunu yazın" + }, + "recording_consent": { + "label": "Bu oturumun kaydedildiğini ve etkinleştirilmiş ise sesim ve görüntümün de kaydedileceğini anladım." + } + } + }, "user": { "signup": { "fields": { diff --git a/app/assets/locales/uk.json b/app/assets/locales/uk.json index b0c681635b..e7b30994ac 100644 --- a/app/assets/locales/uk.json +++ b/app/assets/locales/uk.json @@ -23,6 +23,8 @@ "are_you_sure": "Ви впевнені?", "return_home": "Повернутись на початкову сторінку", "created_at": "Створено о", + "view_recordings": "Переглянути записи", + "join_session": "Приєднатись до сеансу", "no_result_search_input": "Нічого не знайдено за запитом \"{{ searchInput }}\"", "action_permanent": "Цю дію не можна скасувати.", "homepage": { @@ -126,7 +128,7 @@ "presentation": "Презентація", "click_to_upload": "Натисніть, щоб додати", "drag_and_drop": ", або перетягніть сюди файл", - "upload_description": "Додайте будь який офісний документ або PDF файл. Враховуючи розмір файлу, це може зайняти якийсь час перед тим, як він зможе бути використаний.", + "upload_description": "Завантажте документ у офісному форматі, чи PDF файл (обсягом не більше, ніж {{size}}). Залежно від розміру файлу, потрібен певний час для його завантаження, перш ніж він буде доступним для використання", "are_you_sure_delete_presentation": "Ви певні, що слід видалити цю презентацію?" }, "shared_access": { @@ -164,18 +166,22 @@ "recording": "Запис", "recordings": "Записи", "name": "Назва", - "length": "Довжина", + "length": "Тривалість", "users": "Користувачі", "visibility": "Видимість", "formats": "Формати", "published": "Опубліковано", "unpublished": "Знято з публікації", "protected": "Захищено", + "public": "Загальнодоступне", + "public_protected": "Загальнодоступне/Захищене", "length_in_minutes": "{{recording.length}} хв.", "processing_recording": "Обробка запису, це може зайняти декілька хвилин...", "copy_recording_urls": "Скопіювати посилання запису", "recordings_list_empty": "У вас наразі немає записів!", + "public_recordings_list_empty": "Поки що немає загальнодоступних записів", "recordings_list_empty_description": "Записи зявляться тут після того як ви запишете зустріч.", + "public_recordings_list_empty_description": "Тут буде перелік записів, коли вони стануть доступними.", "delete_recording": "Видалити запис", "are_you_sure_delete_recording": "Ви певні, що слід видалити цей запис?", "search_not_found": "Записів не знайдено" @@ -255,7 +261,7 @@ "brand_image": "Зображення логотипу", "click_to_upload": "Натисніть, щоб додати", "drag_and_drop": ", або перетягніть сюди файл", - "upload_brand_image_description": "Додайте PNG, JPG, або SVG файл. Залежно від їх розміру, час завантаження може варіювати перед використанням.", + "upload_brand_image_description": "Завантажте файл зображення у форматі PNG, JPG, чи SVG (не більший, ніжnot larger than {{size}}). Залежно від розміру файлу, його завантаження може потребувати певного часу, перш ніж він стане доступним", "remove_branding_image": "Видалити зображення логотипу" }, "administration": { @@ -492,6 +498,11 @@ "min": "Назва повинна містити принаймні 2 символи" } }, + "room_join": { + "name": { + "required": "Буль ласка, вкажіть ваше ім'я." + } + }, "url": { "invalid": "Недійсне посилання" } @@ -504,6 +515,21 @@ } } }, + "room_join": { + "fields": { + "name": { + "label": "Ім'я", + "placeholder": "Вкажіть ваше ім'я" + }, + "access_code": { + "label": "Код доступу", + "placeholder": "Вкажіть код доступу" + }, + "recording_consent": { + "label": "Я поголжуюсь з тим, що ця зустріч може бути записана. Запис може містити мій голос та відео, якщо вони будуть увімкнені." + } + } + }, "user": { "signup": { "fields": { diff --git a/app/assets/locales/zh_TW.json b/app/assets/locales/zh_TW.json new file mode 100644 index 0000000000..0e83e20ae8 --- /dev/null +++ b/app/assets/locales/zh_TW.json @@ -0,0 +1,677 @@ +{ + "start": "開始", + "search": "搜尋", + "home": "首頁", + "previous": "上一頁", + "back": "返回", + "next": "下一頁", + "view": "檢視", + "join": "加入", + "edit": "編輯", + "save": "儲存", + "save_changes": "儲存變更", + "update": "更新", + "report": "回報", + "share": "分享", + "cancel": "取消", + "close": "關閉", + "delete": "刪除", + "copy": "複製參加連結", + "or": "或", + "online": "線上", + "help_center": "幫助中心", + "are_you_sure": "您確定嗎?", + "return_home": "返回首頁", + "created_at": "建立時間", + "view_recordings": "檢視錄影", + "join_session": "參加會議", + "no_result_search_input": "找不到\\\"{{ searchInput }}\\\"的任何結果", + "action_permanent": "此操作無法撤銷", + "homepage": { + "welcome_bbb": "歡迎使用 BigBlueButton", + "bigbluebutton_description": "BigBlueButton 是一個針對線上課程優化的開源網路會議系統。透過學生間的協作和即時的回饋,能夠將更多時間投入實際的學習中。", + "greenlight_description": "建立您自己的會議室,主持會議,或使用簡短便捷的連結加入其他人的會議。", + "learn_more": "了解更多關於 BigBlueButton", + "explore_features": "探索我們的功能", + "meeting_title": "開始會議", + "meeting_description": "啟動一個虛擬課堂,擁有視訊、音訊、畫面分享、聊天等工具,滿足實踐學習所需的一切。", + "recording_title": "錄製您的會議", + "recording_description": "錄製 BigBlueButton 的會議並與學生分享,以便回顧和反思教材。", + "settings_title": "管理您的會議室", + "settings_description": "配置您的會議室和會議設定,管理一個有效的課堂。", + "and_more_title": "還有更多!", + "and_more_description": "BigBlueButton 提供了實踐學習所需的內建工具,並設計節省您上課時間。", + "enter_meeting_url": "輸入會議網址", + "enter_meeting_url_instruction": "請在下方輸入您的 BigBlueButton 會議的網址。" + }, + "authentication": { + "sign_in": "登入", + "sign_up": "註冊", + "sign_out": "登出", + "email": "電子郵件", + "password": "密碼", + "confirm_password": "確認密碼", + "enter_email": "輸入您的電子郵件", + "enter_name": "輸入您的姓名", + "remember_me": "記住我", + "forgot_password": "忘記密碼?", + "dont_have_account": "還沒有帳號?", + "create_account": "建立帳號", + "create_an_account": "建立帳號", + "already_have_account": "已經有帳號?" + }, + "user": { + "user": "使用者", + "users": "使用者", + "name": "姓名", + "email_address": "電子郵件地址", + "authenticator": "認證者", + "full_name": "全名", + "no_user_found": "找不到使用者", + "type_three_characters": "請輸入(3個或以上的字元)以顯示其他使用者。", + "search_not_found": "找不到使用者", + "profile": { + "profile": "個人檔案", + "language": "語言", + "role": "角色", + "administrator": "管理員", + "guest": "訪客" + }, + "account": { + "account_info": "帳號資訊", + "delete_account": "刪除帳號", + "change_password": "修改密碼", + "reset_password": "重設密碼", + "update_account_info": "更新帳號資訊", + "current_password": "目前密碼", + "new_password": "新密碼", + "confirm_password": "確認密碼", + "permanently_delete_account": "永久刪除您的帳號", + "delete_account_description": "如果您選擇刪除帳號,將無法恢復。\n您帳號的所有相關資訊,包括設定、房間和錄影將被刪除。", + "delete_account_confirmation": "是,我要刪除我的帳號", + "are_you_sure_delete_account": "您確定要刪除帳號嗎?" + }, + "avatar": { + "upload_avatar": "上傳頭像", + "delete_avatar": "刪除頭像", + "crop_avatar": "裁剪您的頭像" + }, + "pending": { + "title": "待審核註冊", + "message": "感謝您的註冊!您的帳號目前正在等待管理員的審核。" + } + }, + "room": { + "room": "房間", + "rooms": "房間", + "room_name": "房間名稱", + "add_new_room": "+ 新增房間", + "create_room": "建立房間", + "delete_room": "刪除房間", + "create_new_room": "建立新房間", + "enter_room_name": "輸入房間名稱", + "shared_by": "分享者:", + "last_session": "上次會議:{{ localizedTime }}", + "no_last_session": "尚未建立先前的會議", + "search_not_found": "找不到房間", + "rooms_list_is_empty": "您尚未擁有任何房間!", + "rooms_list_empty_create_room": "請點擊下方的按鈕並輸入房間名稱來建立您的第一個房間。", + "meeting": { + "start_meeting": "開始會議", + "join_meeting": "加入會議", + "meeting_invitation": "您已被邀請加入", + "meeting_not_started": "會議尚未開始", + "join_meeting_automatically": "會議開始時,您將自動加入", + "recording_consent": "我確認此次會議可能會被錄製。若啟用,可能會包含我的語音和影像。" + }, + "presentation": { + "presentation": "簡報", + "click_to_upload": "點擊上傳", + "drag_and_drop": " 或拖放", + "upload_description": "上傳任何辦公室文件或 PDF 檔案(不超過 {{size}})。根據檔案大小,可能需要額外的時間上傳才能使用。", + "are_you_sure_delete_presentation": "您確定要刪除此簡報嗎?" + }, + "shared_access": { + "access": "存取權限", + "add_share_access": "+ 分享存取權限", + "share_room_access": "分享房間存取權限", + "add_some_users": "該添加一些使用者了!", + "add_some_users_description": "要添加新使用者,請點擊下方的按鈕,搜尋或選擇您想與之分享此房間的使用者。", + "delete_shared_access": "刪除分享的存取權限", + "are_you_sure_delete_shared_access": "您確定要刪除此分享的存取權限嗎?" + }, + "settings": { + "settings": "設定", + "room_name": "房間名稱", + "user_settings": "使用者設定", + "allow_room_to_be_recorded": "允許錄製房間", + "require_signed_in": "要求使用者在加入前登入", + "require_signed_in_message": "您必須先登入才能加入此房間。", + "require_mod_approval": "要求在加入前經過主持人批准", + "allow_any_user_to_start": "允許任何使用者開始會議", + "all_users_join_as_mods": "所有使用者以主持人身份加入", + "mute_users_on_join": "使用者加入時靜音", + "generate": "生成", + "access_code": "存取代碼", + "mod_access_code": "主持人存取代碼", + "mod_access_code_optional": "主持人存取代碼(選填)", + "access_code_required": "請輸入存取代碼", + "wrong_access_code": "存取代碼錯誤", + "generate_viewers_access_code": "為觀眾生成存取代碼", + "generate_mods_access_code": "為主持人生成存取代碼", + "are_you_sure_delete_room": "您確定要刪除此房間嗎?" + } + }, + "recording": { + "recording": "錄製", + "recordings": "錄製", + "name": "名稱", + "length": "長度", + "users": "使用者", + "visibility": "可見性", + "formats": "格式", + "published": "已發佈", + "unpublished": "未發佈", + "protected": "受保護", + "public": "公開", + "public_protected": "公開/受保護", + "length_in_minutes": "{{recording.length}} 分鐘", + "processing_recording": "正在處理錄製,這可能需要幾分鐘時間...", + "copy_recording_urls": "複製錄製的 URL(s)", + "recordings_list_empty": "您尚未擁有任何錄製!", + "public_recordings_list_empty": "目前沒有公開的錄製!", + "recordings_list_empty_description": "在您開始會議並錄製後,錄製將出現在此處。", + "public_recordings_list_empty_description": "當錄製可用時,將在此處顯示。", + "delete_recording": "刪除錄製", + "are_you_sure_delete_recording": "您確定要刪除此錄製嗎?", + "search_not_found": "找不到錄製" + }, + "admin": { + "admin_panel": "管理員面板", + "manage_users": { + "manage_users": "管理使用者", + "active": "啟用", + "approve": "批准", + "decline": "拒絕", + "pending": "待處理", + "banned": "已封禁", + "ban": "封禁", + "unban": "解封", + "deleted": "已刪除", + "invited_tab": "已邀請", + "invite_user": "邀請使用者", + "send_invitation": "發送邀請", + "enter_user_email": "輸入使用者的電子郵件", + "new_user": "新使用者", + "add_new_user": "新增使用者", + "create_new_user": "建立新使用者", + "edit_user": "編輯使用者", + "delete_user": "刪除使用者", + "users_edit_path": "Users/Edit", + "create_account": "建立帳號", + "create_room": "建立房間", + "create_new_room": "建立新房間", + "user_created_at": "建立於:{{localizedTime}}", + "are_you_sure_delete_account": "您確定要刪除{{user.name}}的帳號嗎?", + "delete_account_warning": "如果您選擇刪除此帳號,將無法恢復。", + "empty_active_users": "此伺服器上尚未有啟用的使用者!", + "empty_active_users_subtext": "當使用者的狀態更改為啟用時,他們將出現在這裡。", + "empty_pending_users": "此伺服器上尚未有待處理的使用者!", + "empty_pending_users_subtext": "當使用者的狀態更改為待處理時,他們將出現在這裡。", + "empty_banned_users": "此伺服器上尚未有封禁的使用者!", + "empty_banned_users_subtext": "當使用者的狀態更改為已封禁時,他們將出現在這裡。", + "empty_invited_users": "此伺服器上尚未有已邀請的使用者!", + "empty_invited_users_subtext": "當使用者的狀態更改為已邀請時,他們將出現在這裡。", + "invited": { + "time_sent": "發送時間", + "valid": "有效" + } + }, + "server_rooms": { + "server_rooms": "伺服器房間", + "name": "名稱", + "owner": "擁有者", + "room_id": "房間 ID", + "participants": "參與者", + "status": "狀態", + "running": "運行中", + "not_running": "未運行", + "active": "啟用", + "current_session": "當前會議:{{lastSession}}", + "last_session": "上次會議:{{localizedTime}}", + "no_meeting_yet": "尚未有會議", + "delete_server_rooms": "刪除伺服器房間", + "resync_recordings": "重新同步錄製", + "empty_room_list": "目前沒有伺服器房間!", + "empty_room_list_subtext": "在建立您的第一個房間後,房間將顯示在此處。" + }, + "server_recordings": { + "server_recordings": "伺服器錄製", + "latest_recordings": "最新錄製", + "no_recordings_found": "找不到錄製。" + }, + "site_settings": { + "site_settings": "網站設定", + "customize_greenlight": "自訂 Greenlight", + "appearance": { + "appearance": "外觀", + "brand_color": "品牌顏色", + "regular": "常規", + "lighten": "變亮", + "brand_image": "品牌圖片", + "click_to_upload": "點擊上傳", + "drag_and_drop": "或拖放", + "upload_brand_image_description": "上傳任何 PNG、JPG 或 SVG 檔案(大小不超過 {{size}})。根據檔案大小,可能需要額外的時間才能上傳並使用。", + "remove_branding_image": "移除品牌圖片" + }, + "administration": { + "administration": "管理", + "terms": "條款與條件", + "privacy": "隱私政策", + "privacy_policy": "隱私政策", + "change_term_links": "更改顯示在頁面底部的條款連結", + "change_privacy_link": "更改顯示在頁面底部的隱私連結", + "change_url": "更改 URL", + "enter_link": "在此輸入連結" + }, + "settings": { + "settings": "設定", + "allow_users_to_share_rooms": "允許使用者共享房間", + "allow_users_to_share_rooms_description": "禁用此設定將從房間選項下拉清單中移除按鈕,防止使用者共享房間", + "allow_users_to_preupload_presentation": "允許使用者預先上傳簡報", + "allow_users_to_preupload_presentation_description": "使用者可以預先上傳一份簡報,作為該特定房間的預設簡報" + }, + "registration": { + "registration": "註冊", + "role_mapping_by_email": "使用電子郵件進行角色映射", + "role_mapping_by_email_description": "使用電子郵件將使用者映射到角色。必須使用以下格式:role1=email1, role2=email2", + "enter_role_mapping_rule": "輸入角色映射規則", + "resync_on_login": "每次登入時重新同步使用者資料", + "resync_on_login_description": "每次使用者登入時,重新同步其資訊,使外部身份驗證提供者始終與 Greenlight 中的資訊匹配", + "default_role": "預設角色", + "default_role_description": "新建立使用者時要分配的預設角色", + "registration_method": "註冊方法", + "registration_method_description": "更改使用者註冊網站的方式", + "registration_methods" : { + "open": "開放註冊", + "invite": "邀請加入", + "approval": "核准/拒絕" + } + } + }, + "room_configuration": { + "room_configuration": "房間配置", + "default": "可選(默認:啟用)", + "optional": "可選(默認:禁用)", + "enabled": "強制啟用", + "disabled": "已禁用", + "configurations": { + "allow_room_to_be_recorded": "允許錄製房間", + "allow_room_to_be_recorded_description": "允許房間擁有者指定是否要啟用房間錄製功能。如果啟用,主持人在會議開始後仍需點擊「錄製」按鈕。", + "require_user_signed_in": "要求使用者在加入前登錄", + "require_user_signed_in_description": "只允許擁有 Greenlight 帳戶的使用者加入會議。如果使用者未登錄,嘗試加入房間時將被重定向到登錄頁面。", + "require_mod_approval": "要求主持人批准後才能加入", + "require_mod_approval_description": "當使用者嘗試加入 BigBlueButton 會議時,提示 BigBlueButton 會議的主持人。如果使用者獲准,他們將能夠加入會議。", + "allow_any_user_to_start_meeting": "允許任何使用者開始會議", + "allow_any_user_to_start_meeting_description": "允許任何使用者隨時開始會議。預設情況下,只有房間擁有者可以開始會議。", + "allow_users_to_join_as_mods": "所有使用者以主持人身份加入", + "allow_users_to_join_as_mods_description": "使所有使用者在加入會議時具有 BigBlueButton 的主持人權限", + "mute_users_on_join": "使用者加入時將其靜音", + "mute_users_on_join_description": "使用者加入 BigBlueButton 會議時自動將其靜音", + "viewer_access_code": "觀眾存取碼", + "viewer_access_code_description": "允許房間擁有者擁有一個隨機的英數字串碼,可與使用者共享。如果生成了存取碼,使用者必須在加入房間會議時輸入該碼。", + "mod_access_code": "主持人存取碼", + "mod_access_code_description": "允許房間擁有者擁有一個隨機的英數字串碼,可與使用者共享。如果生成了存取碼,使用該碼加入任何房間會議時,使用者將以主持人身份加入。" + } + }, + "roles": { + "role": "角色", + "roles": "角色", + "administrator": "管理員", + "guest": "訪客", + "manage_roles": "管理角色", + "delete_role": "刪除角色", + "are_you_sure_delete_role": "您確定要刪除此角色嗎?", + "enter_role_name": "輸入角色名稱", + "add_role": "+ 建立角色", + "create_role": "建立角色", + "create_new_role": "建立新角色", + "no_role_found": "找不到角色。", + "search_not_found": "找不到角色", + "edit": { + "create_room": "允許擁有此角色的使用者建立房間", + "record": "允許擁有此角色的使用者錄製會議", + "manage_users": "允許擁有此角色的使用者管理使用者", + "manage_rooms": "允許擁有此角色的使用者管理伺服器房間", + "manage_recordings": "允許擁有此角色的使用者管理伺服器錄製", + "manage_site_settings": "允許擁有此角色的使用者管理網站設定", + "manage_roles": "允許擁有此角色的使用者編輯其他角色", + "shared_list": "將擁有此角色的使用者包含在共享房間的下拉清單中", + "room_limit": "房間限制數量" + } + } + }, + "toast": { + "success": { + "user": { + "user_created": "已成功建立新使用者。", + "user_updated": "使用者已成功更新。", + "user_deleted": "使用者已成功刪除。", + "avatar_updated": "頭像已成功更新。", + "password_updated": "密碼已成功更新。", + "account_activated": "您的帳戶已成功啟用。", + "activation_email_sent": "已發送一封包含啟用帳戶指示的郵件。", + "reset_pwd_email_sent": "已發送一封包含重設密碼指示的郵件。" + }, + "session": { + "signed_out": "您已成功登出" + }, + "room": { + "room_created": "已成功建立新房間。", + "room_updated": "房間已成功更新", + "room_deleted": "房間已成功刪除。", + "room_shared": "房間已成功共享。", + "room_unshared": "房間已取消共享。", + "recordings_synced": "房間錄製已成功同步。", + "room_configuration_updated": "房間配置已成功更新。", + "room_setting_updated": "房間設定已成功更新。", + "presentation_updated": "簡報已成功更新。", + "presentation_deleted": "簡報已成功刪除。", + "joining_meeting": "正在加入會議...", + "meeting_started": "會議已開始。", + "access_code_copied": "存取碼已成功複製。", + "access_code_generated": "已成功生成新的存取碼", + "access_code_deleted": "存取碼已成功刪除。", + "copied_meeting_url": "會議連結已成功複製。該連結可用於加入會議" + }, + "site_settings": { + "site_setting_updated": "網站設定已成功更新。", + "brand_color_updated": "品牌顏色已成功更新。", + "brand_image_updated": "品牌圖片已成功更新。", + "brand_image_deleted": "品牌圖片已成功刪除。", + "privacy_policy_updated": "隱私政策已成功更新。", + "terms_of_service_updated": "服務條款已成功更新。" + }, + "recording": { + "recording_visibility_updated": "錄製的可見性已成功更新。", + "recording_name_updated": "錄製名稱已成功更新。", + "recording_deleted": "錄製已成功刪除。", + "copied_urls": "錄製連結已成功複製。" + }, + "role": { + "role_created": "已成功建立新角色。", + "role_updated": "角色已成功更新。", + "role_deleted": "角色已成功刪除。", + "role_permission_updated": "角色權限已成功更新。" + }, + "invitations": { + "invitation_sent": "已成功發送邀請。" + } + }, + "error": { + "problem_completing_action": "無法完成動作。\n請重試。", + "file_type_not_supported": "不支援的檔案類型。", + "file_size_too_large": "檔案大小超過限制。", + "file_upload_error": "無法上傳檔案", + "signin_required": "您必須登入才能訪問此頁面。", + "roles": { + "role_assigned": "無法刪除此角色,因為它已分配給至少一個使用者。" + }, + "users": { + "signup_error": "您無法通過驗證。請聯繫您的管理員。", + "invalid_invite": "您的邀請令牌無效或不正確。請聯繫您的管理員以獲取新的令牌。", + "email_exists": "使用該電子郵件的帳戶已存在。請嘗試使用其他電子郵件。", + "old_password": "您輸入的密碼不正確。", + "pending": "您的註冊正在等待管理員的批准。請稍後再試。", + "banned": "您無權訪問此應用程式。如果您認為這是一個錯誤,請聯繫您的管理員。" + }, + "rooms": { + "room_limit": "無法建立房間,因為已達到房間數量限制。" + }, + "session": { + "invalid_credentials": "使用者名稱或密碼無效。請驗證您的憑證並重試。" + } + } + }, + "global_error_page": { + "title": "錯誤", + "message": "抱歉,發生了一些錯誤。如果這個問題再次發生,請聯繫管理員。" + }, + "not_found_error_page": { + "title": "找不到頁面", + "message": "抱歉,無法找到您要訪問的頁面。" + }, + "account_activation_page": { + "title": "帳戶啟用", + "account_unverified": "您的帳戶尚未驗證。", + "message": "為了使用Greenlight,請按照已發送給您的啟用郵件中的說明進行帳戶驗證。", + "resend_activation_link": "如果您未收到啟用郵件,或者無法使用它,請點擊下面的按鈕重新請求新的啟用郵件。", + "resend_btn_lbl": "重新發送驗證郵件" + }, + "forms": { + "validations": { + "full_name": { + "required": "請輸入全名。", + "min": "名稱至少需含2個字元。", + "max": "名稱最多只能含255個字元。" + }, + "email": { + "required": "請輸入電子郵件。", + "email": "輸入的值不符合電子郵件格式。", + "min": "電子郵件至少需含6個字元。", + "max": "電子郵件最多只能含255個字元。" + }, + "password": { + "required": "請輸入密碼。", + "match": "密碼必須包含以下條件:", + "min": "- 至少八個字元", + "lower": "- 至少一個小寫字母", + "upper": "- 至少一個大寫字母", + "digit": "- 至少一個數字", + "symbol": "- 至少一個符號", + "max": "密碼最多只能含255個字元" + }, + "password_confirmation": { + "required": "請輸入密碼確認。", + "match": "兩次輸入的密碼不相符。" + }, + "emails": { + "required": "請輸入至少一個有效的電子郵件。", + "list": "請提供一個以逗號分隔的有效電子郵件列表(user@users.com,user1@users.com,user2@users.com)。" + }, + "role_name": { + "required": "請輸入角色名稱。" + }, + "role": { + "limit": { + "required": "請輸入房間數量限制。", + "min": "允許的最小值為0。", + "max": "允許的最大值為100。" + }, + "type": { + "error": "您必須指定一個數字。" + } + }, + "room": { + "name": { + "required": "請輸入房間名稱。", + "min": "名稱至少需含2個字元" + } + }, + "room_join": { + "name": { + "required": "請輸入您的名稱" + } + }, + "url": { + "invalid": "無效的網址。" + } + }, + "room": { + "fields": { + "name": { + "label": "房間名稱", + "placeholder": "輸入房間名稱..." + } + } + }, + "room_join": { + "fields": { + "name": { + "label": "名稱", + "placeholder": "輸入您的名稱" + }, + "access_code": { + "label": "存取代碼", + "placeholder": "輸入存取代碼" + }, + "recording_consent": { + "label": "我同意此次會議可能會被錄製。如果啟用,可能包含我的聲音和影像。" + } + } + }, + "user": { + "signup": { + "fields": { + "full_name": { + "label": "全名", + "placeholder": "輸入您的全名" + }, + "email": { + "label": "電子郵件", + "placeholder": "輸入您的電子郵件" + }, + "password": { + "label": "密碼", + "placeholder": "建立一個密碼" + }, + "password_confirmation": { + "label": "確認密碼", + "placeholder": "確認密碼" + } + } + }, + "signin": { + "fields": { + "email": { + "label": "電子郵件", + "placeholder": "電子郵件" + }, + "password": { + "label": "密碼", + "placeholder": "密碼" + }, + "remember_me": { + "label": "記住我" + } + } + }, + "change_password": { + "fields": { + "old_password": { + "label": "目前密碼", + "placeholder": "輸入您的密碼" + }, + "new_password": { + "label": "新密碼", + "placeholder": "輸入您的新密碼" + }, + "password_confirmation": { + "label": "確認新密碼", + "placeholder": "確認您的新密碼" + } + }, + "validations": { + "old_password": { + "required": "請輸入您的目前密碼" + } + } + }, + "forget_password": { + "fields": { + "email": { + "label": "電子郵件", + "placeholder": "輸入帳戶電子郵件" + } + }, + "validations": { + "email": { + "required": "請輸入帳戶電子郵件" + } + } + }, + "reset_password": { + "fields": { + "new_password": { + "label": "新密碼", + "placeholder": "輸入您的新密碼" + }, + "password_confirmation": { + "label": "確認新密碼", + "placeholder": "確認您的新密碼" + } + } + }, + "update_user": { + "fields": { + "full_name": { + "label": "全名" + }, + "email": { + "label": "電子郵件" + }, + "language": { + "label": "語言" + }, + "role": { + "label": "角色" + } + } + } + }, + "admin": { + "createUser": { + "fields": { + "full_name": { + "label": "全名", + "placeholder": "輸入使用者全名" + }, + "email": { + "label": "電子郵件", + "placeholder": "輸入使用者電子郵件" + }, + "password": { + "label": "密碼", + "placeholder": "輸入使用者密碼" + }, + "password_confirmation": { + "label": "確認密碼", + "placeholder": "確認密碼" + } + } + }, + "invite_user": { + "fields": { + "emails": { + "label": "電子郵件" + } + } + }, + "site_settings": { + "fields": { + "value": { + "placeholder": "在此輸入連結..." + } + } + }, + "roles": { + "fields": { + "name": { + "label": "角色名稱", + "placeholder": "輸入角色名稱..." + } + } + } + } + } +} diff --git a/app/assets/stylesheets/application.bootstrap.scss b/app/assets/stylesheets/application.bootstrap.scss index 746c74c87d..2446bd5bf4 100644 --- a/app/assets/stylesheets/application.bootstrap.scss +++ b/app/assets/stylesheets/application.bootstrap.scss @@ -67,10 +67,12 @@ body { color: silver; } -// TODO - samuel: make responsive/add breakpoints .btn { - min-width: $button-min-width; white-space: nowrap; + + @include media-breakpoint-up(sm) { + min-width: $button-min-width; + } } .btn-sm { @@ -196,7 +198,6 @@ input.search-bar { #footer { margin-top: $footer-buffer-height; - max-height: $footer-height; #footer-container { border-top: 1px solid #d0d5dd; @@ -206,6 +207,10 @@ input.search-bar { color: var(--brand-color) !important; text-decoration: none !important; } + + @include media-breakpoint-up(sm) { + max-height: $footer-height; + } } .danger-light-button{ @@ -423,7 +428,6 @@ input.search-bar { background: white !important; color: black !important; border-color: gainsboro !important; - min-width: 260px; text-align: left; &:hover, &:focus, &:active { @@ -436,8 +440,47 @@ input.search-bar { } } + button, .dropdown-menu { + @include media-breakpoint-up(sm) { + min-width: 300px; + } + } +} + +.simple-select { + button { + background: white !important; + color: black !important; + border-color: gainsboro !important; + text-align: left; + width: 220px; + + &:hover, &:focus, &:active { + background: white !important; + color: black !important; + border-color: gainsboro !important; + } + &:focus { + box-shadow: 0 0 0 0.25rem var(--brand-color-light) !important; + } + &::after { + display: none; + } + } + .dropdown-menu { - min-width: 260px; + min-width: 220px; + } +} + +@media (max-width: 767px) { + .table-responsive .dropdown-menu { + position: static !important; + } +} +@media (min-width: 768px) { + .table-responsive { + overflow: visible; } } diff --git a/app/assets/stylesheets/recordings.scss b/app/assets/stylesheets/recordings.scss index f7d76ea5c6..9b770871cd 100644 --- a/app/assets/stylesheets/recordings.scss +++ b/app/assets/stylesheets/recordings.scss @@ -15,11 +15,13 @@ // with Greenlight; if not, see . #user-recordings, #room-recordings { + min-height: 400px; + table { border-top-right-radius: $border-radius-lg; border-top-left-radius: $border-radius-lg; border-spacing: 0; - overflow: hidden; + border-top-width: 0 !important; } tr { @@ -121,4 +123,4 @@ svg:hover { color: var(--brand-color) !important; } -} +} \ No newline at end of file diff --git a/app/controllers/api/v1/admin/role_permissions_controller.rb b/app/controllers/api/v1/admin/role_permissions_controller.rb index ad5f5c1121..4b15af3767 100644 --- a/app/controllers/api/v1/admin/role_permissions_controller.rb +++ b/app/controllers/api/v1/admin/role_permissions_controller.rb @@ -58,7 +58,7 @@ def create_default_room User.includes(:rooms).where(role_id: role_params[:role_id]).where(rooms: { id: nil }).find_in_batches do |group| group.each do |user| - Room.create(name: "#{user.name}'s Room", user_id: user.id) + Room.create(name: t('room.new_room_name', username: user.name, locale: user.language), user_id: user.id) end end end diff --git a/app/controllers/api/v1/admin/server_recordings_controller.rb b/app/controllers/api/v1/admin/server_recordings_controller.rb index 1e8f6ff2ed..35ba73269c 100644 --- a/app/controllers/api/v1/admin/server_recordings_controller.rb +++ b/app/controllers/api/v1/admin/server_recordings_controller.rb @@ -31,11 +31,11 @@ def index recordings = Recording.includes(:user) .with_provider(current_provider) - .order(sort_config) - &.search(params[:search]) + .order(sort_config, recorded_at: :desc) + &.server_search(params[:search]) pagy, recordings = pagy(recordings) - render_data data: recordings, meta: pagy_metadata(pagy), status: :ok + render_data data: recordings, serializer: ServerRecordingSerializer, meta: pagy_metadata(pagy), status: :ok end end end diff --git a/app/controllers/api/v1/admin/server_rooms_controller.rb b/app/controllers/api/v1/admin/server_rooms_controller.rb index cb02141213..c45fdc15ca 100644 --- a/app/controllers/api/v1/admin/server_rooms_controller.rb +++ b/app/controllers/api/v1/admin/server_rooms_controller.rb @@ -31,14 +31,14 @@ class ServerRoomsController < ApiController def index sort_config = config_sorting(allowed_columns: %w[name users.name]) - rooms = Room.includes(:user).joins(:user).where(users: { provider: current_provider }).order(sort_config, online: :desc) + rooms = Room.includes(:user).where(users: { provider: current_provider }).order(sort_config, online: :desc) .order('last_session DESC NULLS LAST')&.admin_search(params[:search]) - online_server_rooms(rooms) + pagy, paged_rooms = pagy(rooms) - pagy, rooms = pagy_array(rooms) + RunningMeetingChecker.new(rooms: paged_rooms.select(&:online)).call if paged_rooms.select(&:online).any? - render_data data: rooms, meta: pagy_metadata(pagy), serializer: ServerRoomSerializer, status: :ok + render_data data: paged_rooms, meta: pagy_metadata(pagy), serializer: ServerRoomSerializer, status: :ok end # GET /api/v1/admin/server_rooms/:friendly_id/resync.json @@ -54,20 +54,6 @@ def resync def find_room @room = Room.find_by!(friendly_id: params[:friendly_id]) end - - def online_server_rooms(rooms) - online_rooms = BigBlueButtonApi.new(provider: current_provider).active_meetings - online_rooms_hash = {} - - online_rooms.each do |online_room| - online_rooms_hash[online_room[:meetingID]] = online_room[:participantCount] - end - - rooms.each do |room| - room.online = online_rooms_hash.key?(room.meeting_id) - room.participants = online_rooms_hash[room.meeting_id] - end - end end end end diff --git a/app/controllers/api/v1/admin/site_settings_controller.rb b/app/controllers/api/v1/admin/site_settings_controller.rb index 4b1239423f..6b8a273c0c 100644 --- a/app/controllers/api/v1/admin/site_settings_controller.rb +++ b/app/controllers/api/v1/admin/site_settings_controller.rb @@ -51,7 +51,10 @@ def update site_setting.update(value: params[:site_setting][:value].to_s) end - return render_error status: :bad_request unless update + unless update + return render_error status: :bad_request, + errors: Rails.configuration.custom_error_msgs[:record_invalid] + end render_data status: :ok end diff --git a/app/controllers/api/v1/admin/tenants_controller.rb b/app/controllers/api/v1/admin/tenants_controller.rb index d605c6da1c..11569c535f 100644 --- a/app/controllers/api/v1/admin/tenants_controller.rb +++ b/app/controllers/api/v1/admin/tenants_controller.rb @@ -21,7 +21,7 @@ module V1 module Admin class TenantsController < ApiController before_action do - # TODO: - ahmad: Add role check + ensure_super_admin end # GET /api/v1/admin/tenants @@ -37,14 +37,11 @@ def index # POST /api/v1/admin/tenants def create - name = tenant_params[:name].downcase.gsub(/[-\s]/, '_') + name = tenant_params[:name] tenant = Tenant.new(name:, client_secret: tenant_params[:client_secret]) if tenant.save - create_roles(tenant.name) - create_site_settings(tenant.name) - create_rooms_configs_options(tenant.name) - create_role_permissions(tenant.name) + TenantSetup.new(name).call render_data status: :created else render_error errors: tenant.errors.to_a, status: :bad_request @@ -67,95 +64,6 @@ def destroy def cache; end - private - - def create_roles(provider) - Role.create! [ - { name: 'Administrator', provider: }, - { name: 'User', provider: }, - { name: 'Guest', provider: } - ] - end - - def create_site_settings(provider) - SiteSetting.create! [ - { setting: Setting.find_by(name: 'PrimaryColor'), value: '#467fcf', provider: }, - { setting: Setting.find_by(name: 'PrimaryColorLight'), value: '#e8eff9', provider: }, - { setting: Setting.find_by(name: 'PrimaryColorDark'), value: '#316cbe', provider: }, - { setting: Setting.find_by(name: 'BrandingImage'), - value: ActionController::Base.helpers.image_path('bbb_logo.png'), - provider: }, - { setting: Setting.find_by(name: 'Terms'), value: '', provider: }, - { setting: Setting.find_by(name: 'PrivacyPolicy'), value: '', provider: }, - { setting: Setting.find_by(name: 'RegistrationMethod'), value: SiteSetting::REGISTRATION_METHODS[:open], provider: }, - { setting: Setting.find_by(name: 'ShareRooms'), value: 'true', provider: }, - { setting: Setting.find_by(name: 'PreuploadPresentation'), value: 'true', provider: }, - { setting: Setting.find_by(name: 'RoleMapping'), value: '', provider: }, - { setting: Setting.find_by(name: 'DefaultRole'), provider:, value: 'User' } - ] - end - - def create_rooms_configs_options(provider) - RoomsConfiguration.create! [ - { meeting_option: MeetingOption.find_by(name: 'record'), value: 'default_enabled', provider: }, - { meeting_option: MeetingOption.find_by(name: 'muteOnStart'), value: 'optional', provider: }, - { meeting_option: MeetingOption.find_by(name: 'guestPolicy'), value: 'optional', provider: }, - { meeting_option: MeetingOption.find_by(name: 'glAnyoneCanStart'), value: 'optional', provider: }, - { meeting_option: MeetingOption.find_by(name: 'glAnyoneJoinAsModerator'), value: 'optional', provider: }, - { meeting_option: MeetingOption.find_by(name: 'glRequireAuthentication'), value: 'optional', provider: }, - { meeting_option: MeetingOption.find_by(name: 'glViewerAccessCode'), value: 'optional', provider: }, - { meeting_option: MeetingOption.find_by(name: 'glModeratorAccessCode'), value: 'optional', provider: } - ] - end - - def create_role_permissions(provider) - admin = Role.find_by(name: 'Administrator', provider:) - user = Role.find_by(name: 'User', provider:) - guest = Role.find_by(name: 'Guest', provider:) - - create_room = Permission.find_by(name: 'CreateRoom') - manage_users = Permission.find_by(name: 'ManageUsers') - manage_rooms = Permission.find_by(name: 'ManageRooms') - manage_recordings = Permission.find_by(name: 'ManageRecordings') - manage_site_settings = Permission.find_by(name: 'ManageSiteSettings') - manage_roles = Permission.find_by(name: 'ManageRoles') - shared_list = Permission.find_by(name: 'SharedList') - can_record = Permission.find_by(name: 'CanRecord') - room_limit = Permission.find_by(name: 'RoomLimit') - - RolePermission.create! [ - { role: admin, permission: create_room, value: 'true' }, - { role: admin, permission: manage_users, value: 'true' }, - { role: admin, permission: manage_rooms, value: 'true' }, - { role: admin, permission: manage_recordings, value: 'true' }, - { role: admin, permission: manage_site_settings, value: 'true' }, - { role: admin, permission: manage_roles, value: 'true' }, - { role: admin, permission: shared_list, value: 'true' }, - { role: admin, permission: can_record, value: 'true' }, - { role: admin, permission: room_limit, value: '100' }, - - { role: user, permission: create_room, value: 'true' }, - { role: user, permission: manage_users, value: 'false' }, - { role: user, permission: manage_rooms, value: 'false' }, - { role: user, permission: manage_recordings, value: 'false' }, - { role: user, permission: manage_site_settings, value: 'false' }, - { role: user, permission: manage_roles, value: 'false' }, - { role: user, permission: shared_list, value: 'true' }, - { role: user, permission: can_record, value: 'true' }, - { role: user, permission: room_limit, value: '100' }, - - { role: guest, permission: create_room, value: 'false' }, - { role: guest, permission: manage_users, value: 'false' }, - { role: guest, permission: manage_rooms, value: 'false' }, - { role: guest, permission: manage_recordings, value: 'false' }, - { role: guest, permission: manage_site_settings, value: 'false' }, - { role: guest, permission: manage_roles, value: 'false' }, - { role: guest, permission: shared_list, value: 'true' }, - { role: guest, permission: can_record, value: 'true' }, - { role: guest, permission: room_limit, value: '100' } - ] - end - def delete_roles(provider) Role.where(provider:).destroy_all end diff --git a/app/controllers/api/v1/api_controller.rb b/app/controllers/api/v1/api_controller.rb index 42325a7e92..502124b701 100644 --- a/app/controllers/api/v1/api_controller.rb +++ b/app/controllers/api/v1/api_controller.rb @@ -88,6 +88,11 @@ def config_sorting(allowed_columns: []) { sort_column => sort_direction } end + + # Checks if external authentication is enabled + def external_authn_enabled? + ENV['OPENID_CONNECT_ISSUER'].present? + end end end end diff --git a/app/controllers/api/v1/env_controller.rb b/app/controllers/api/v1/env_controller.rb index 0c337fe221..2541f5ed19 100644 --- a/app/controllers/api/v1/env_controller.rb +++ b/app/controllers/api/v1/env_controller.rb @@ -27,7 +27,9 @@ def index render_data data: { OPENID_CONNECT: ENV['OPENID_CONNECT_ISSUER'].present?, HCAPTCHA_KEY: ENV.fetch('HCAPTCHA_SITE_KEY', nil), - VERSION_TAG: ENV.fetch('VERSION_TAG', '') + VERSION_TAG: ENV.fetch('VERSION_TAG', ''), + CURRENT_PROVIDER: current_provider, + SMTP_ENABLED: ENV.fetch('SMTP_SERVER', nil) }, status: :ok end end diff --git a/app/controllers/api/v1/locales_controller.rb b/app/controllers/api/v1/locales_controller.rb index a72d322a54..911f2d73af 100644 --- a/app/controllers/api/v1/locales_controller.rb +++ b/app/controllers/api/v1/locales_controller.rb @@ -25,7 +25,7 @@ class LocalesController < ApiController # GET /api/v1/locales # Returns a cached list of locales available def index - language_with_name = Rails.cache.fetch('locales/list', expires_in: 24.hours) do + language_with_name = Rails.cache.fetch('v3/locales/list', expires_in: 24.hours) do language_hash = {} languages = Dir.entries(Rails.root.join('app/assets/locales')).select { |file_name| file_name.ends_with?('.json') } diff --git a/app/controllers/api/v1/migrations/external_controller.rb b/app/controllers/api/v1/migrations/external_controller.rb index 08117805ca..923f657a8d 100644 --- a/app/controllers/api/v1/migrations/external_controller.rb +++ b/app/controllers/api/v1/migrations/external_controller.rb @@ -47,14 +47,19 @@ class ExternalController < ApiController def create_role role_hash = role_params.to_h - return render_data status: :created if Role.exists? name: role_hash[:name], provider: 'greenlight' + # Returns an error if the provider does not exist + unless role_hash[:provider] == 'greenlight' || Tenant.exists?(name: role_hash[:provider]) + return render_error(status: :bad_request, errors: 'Provider does not exist') + end - role = Role.new(name: role_hash[:name], provider: 'greenlight') + # Returns created if the Role exists and the RolePermissions have default values + if Role.exists?(name: role_hash[:name], provider: role_hash[:provider]) && role_hash[:role_permissions].empty? + return render_data status: :created + end - return render_error(status: :bad_request, errors: role&.errors&.to_a) unless role.save + role = Role.find_or_initialize_by(name: role_hash[:name], provider: role_hash[:provider]) - # Returns unless the Role has a RolePermission that differs from V3 default RolePermissions values - return render_data status: :created unless role_hash[:role_permissions].any? + return render_error(status: :bad_request, errors: role&.errors&.to_a) unless role.save # Finds all the RolePermissions that need to be updated role_permissions_joined = RolePermission.includes(:permission) @@ -78,17 +83,25 @@ def create_role def create_user user_hash = user_params.to_h - return render_data status: :created if User.exists? email: user_hash[:email], provider: 'greenlight' + # Re-write LDAP and Google to greenlight + user_hash[:provider] = %w[greenlight ldap google openid_connect].include?(user_hash[:provider]) ? 'greenlight' : user_hash[:provider] + + # Returns an error if the provider does not exist + unless user_hash[:provider] == 'greenlight' || Tenant.exists?(name: user_hash[:provider]) + return render_error(status: :bad_request, errors: 'Provider does not exist') + end + + return render_data status: :created if User.exists?(email: user_hash[:email], provider: user_hash[:provider]) user_hash[:language] = I18n.default_locale if user_hash[:language].blank? || user_hash[:language] == 'default' - role = Role.find_by(name: user_hash[:role], provider: 'greenlight') + role = Role.find_by(name: user_hash[:role], provider: user_hash[:provider]) return render_error(status: :bad_request, errors: 'The user role does not exist.') unless role user_hash[:password] = generate_secure_pwd if user_hash[:external_id].blank? - user = User.new(user_hash.merge(verified: true, provider: 'greenlight', role:)) + user = User.new(user_hash.merge(verified: true, role:)) return render_error(status: :bad_request, errors: user&.errors&.to_a) unless user.save @@ -101,16 +114,24 @@ def create_user # shared_users_emails: [ ] }} # Returns: { data: Array[serializable objects] , errors: Array[String] } # Does: Creates a Room and its RoomMeetingOptions. + # rubocop:disable Metrics/CyclomaticComplexity def create_room room_hash = room_params.to_h + # Re-write LDAP and Google to greenlight + room_hash[:provider] = %w[greenlight ldap google openid_connect].include?(room_hash[:provider]) ? 'greenlight' : room_hash[:provider] + + unless room_hash[:provider] == 'greenlight' || Tenant.exists?(name: room_hash[:provider]) + return render_error(status: :bad_request, errors: 'Provider does not exist') + end + return render_data status: :created if Room.exists? friendly_id: room_hash[:friendly_id] - user = User.find_by(email: room_hash[:owner_email], provider: 'greenlight') + user = User.find_by(email: room_hash[:owner_email], provider: room_hash[:provider]) return render_error(status: :bad_request, errors: 'The room owner does not exist.') unless user - room = Room.new(room_hash.except(:owner_email, :room_settings, :shared_users_emails).merge({ user: })) + room = Room.new(room_hash.except(:owner_email, :provider, :room_settings, :shared_users_emails).merge({ user: })) # Redefines the validations method to do nothing # rubocop:disable Lint/EmptyBlock @@ -137,7 +158,7 @@ def create_room return render_data status: :created unless room_hash[:shared_users_emails].any? # Finds all the users that have a SharedAccess to the Room - shared_with_users = User.where(email: room_hash[:shared_users_emails]) + shared_with_users = User.where(email: room_hash[:shared_users_emails], provider: room_hash[:provider]) okay = true shared_with_users.each do |shared_with_user| @@ -148,22 +169,27 @@ def create_room render_data status: :created end + # rubocop:enable Metrics/CyclomaticComplexity # POST /api/v1/migrations/site_settings.json # Expects: { settings: { site_settings: { :PrimaryColor, :PrimaryColorLight, :PrimaryColorDark, # :Terms, :PrivacyPolicy, :RegistrationMethod, :ShareRooms, :PreuploadPresentation }, - # room_configurations: { :record, :muteOnStart, :guestPolicy, :glAnyoneCanStart, + # rooms_configurations: { :record, :muteOnStart, :guestPolicy, :glAnyoneCanStart, # :glAnyoneJoinAsModerator, :glRequireAuthentication } } } # Returns: { data: Array[serializable objects] , errors: Array[String] } # Does: Creates a SiteSettings or a RoomsConfiguration. def create_settings settings_hash = settings_params.to_h + unless settings_hash[:provider] == 'greenlight' || Tenant.exists?(name: settings_hash[:provider]) + return render_error(status: :bad_request, errors: 'Provider does not exist') + end + render_data status: :created unless settings_hash.any? # Finds all the SiteSettings that need to be updated site_settings_joined = SiteSetting.joins(:setting) - .where('settings.name': settings_hash[:site_settings].keys, provider: 'greenlight') + .where('settings.name': settings_hash[:site_settings].keys, provider: settings_hash[:provider]) okay = true site_settings_joined.each do |site_setting| @@ -174,14 +200,14 @@ def create_settings return render_error status: :bad_request, errors: 'Something went wrong when migrating site settings.' unless okay # Finds all the RoomsConfiguration that need to be updated - room_configurations_joined = RoomsConfiguration.joins(:meeting_option) - .where('meeting_options.name': settings_hash[:room_configurations].keys, - provider: 'greenlight') + rooms_configurations_joined = RoomsConfiguration.joins(:meeting_option) + .where('meeting_options.name': settings_hash[:rooms_configurations].keys, + provider: settings_hash[:provider]) okay = true - room_configurations_joined.each do |room_configuration| - room_configuration_name = room_configuration.meeting_option.name - okay = false unless room_configuration.update(value: settings_hash[:room_configurations][room_configuration_name]) + rooms_configurations_joined.each do |rooms_configuration| + rooms_configuration_name = rooms_configuration.meeting_option.name + okay = false unless rooms_configuration.update(value: settings_hash[:rooms_configurations][rooms_configuration_name]) end return render_error status: :bad_request, errors: 'Something went wrong when migrating room configurations.' unless okay @@ -193,25 +219,27 @@ def create_settings def role_params decrypted_params.require(:role).permit(:name, + :provider, role_permissions: %w[CreateRoom CanRecord ManageUsers ManageRoles ManageRooms ManageRecordings ManageSiteSettings]) end def user_params - decrypted_params.require(:user).permit(:name, :email, :external_id, :language, :role) + decrypted_params.require(:user).permit(:name, :email, :provider, :external_id, :language, :role, :created_at) end def room_params - decrypted_params.require(:room).permit(:name, :friendly_id, :meeting_id, :last_session, :owner_email, + decrypted_params.require(:room).permit(:name, :friendly_id, :meeting_id, :last_session, :owner_email, :provider, shared_users_emails: [], room_settings: %w[record muteOnStart guestPolicy glAnyoneCanStart glAnyoneJoinAsModerator]) end def settings_params - decrypted_params.require(:settings).permit(site_settings: %w[PrimaryColor PrimaryColorLight Terms PrivacyPolicy RegistrationMethod - ShareRooms PreuploadPresentation], - room_configurations: %w[record muteOnStart guestPolicy glAnyoneCanStart glAnyoneJoinAsModerator - glRequireAuthentication]) + decrypted_params.require(:settings).permit(:provider, + site_settings: %w[PrimaryColor PrimaryColorLight Terms PrivacyPolicy RegistrationMethod + ShareRooms PreloadPresentation], + rooms_configurations: %w[record muteOnStart guestPolicy glAnyoneCanStart glAnyoneJoinAsModerator + glRequireAuthentication]) end def decrypted_params diff --git a/app/controllers/api/v1/recordings_controller.rb b/app/controllers/api/v1/recordings_controller.rb index f6f1bcd267..6138fafe38 100644 --- a/app/controllers/api/v1/recordings_controller.rb +++ b/app/controllers/api/v1/recordings_controller.rb @@ -19,12 +19,14 @@ module Api module V1 class RecordingsController < ApiController - before_action :find_recording, only: %i[update update_visibility] + skip_before_action :ensure_authenticated, only: :recording_url + + before_action :find_recording, only: %i[update update_visibility recording_url] before_action only: %i[destroy] do ensure_authorized('ManageRecordings', record_id: params[:id]) end - before_action only: %i[update update_visibility] do - ensure_authorized(%w[ManageRecordings SharedRoom], record_id: params[:id]) + before_action only: %i[update update_visibility recording_url] do + ensure_authorized(%w[ManageRecordings SharedRoom PublicRecordings], record_id: params[:id]) end before_action only: %i[index recordings_count] do ensure_authorized('CreateRoom') @@ -54,9 +56,6 @@ def update # DELETE /api/v1/recordings/:id.json # Deletes a recording in both BigBlueButton and Greenlight def destroy - # TODO: Hadi - Need to change this to work preferably with after_destroy in recordings model - BigBlueButtonApi.new(provider: current_provider).delete_recordings(record_ids: params[:id]) - Recording.destroy_by(record_id: params[:id]) render_data status: :ok @@ -65,17 +64,16 @@ def destroy # POST /api/v1/recordings/update_visibility.json # Update's a recordings visibility by setting publish/unpublish and protected/unprotected def update_visibility - new_visibility = params[:visibility] - old_visibility = @recording.visibility - bbb_api = BigBlueButtonApi.new(provider: current_provider) + new_visibility = params[:visibility].to_s - bbb_api.publish_recordings(record_ids: @recording.record_id, publish: true) if old_visibility == 'Unpublished' + new_visibility_params = visibility_params_of(new_visibility) - bbb_api.publish_recordings(record_ids: @recording.record_id, publish: false) if new_visibility == 'Unpublished' + return render_error status: :bad_request if new_visibility_params.nil? - bbb_api.update_recordings(record_id: @recording.record_id, meta_hash: { protect: true }) if new_visibility == 'Protected' + bbb_api = BigBlueButtonApi.new(provider: current_provider) - bbb_api.update_recordings(record_id: @recording.record_id, meta_hash: { protect: false }) if old_visibility == 'Protected' + bbb_api.publish_recordings(record_ids: @recording.record_id, publish: new_visibility_params[:publish]) + bbb_api.update_recordings(record_id: @recording.record_id, meta_hash: new_visibility_params[:meta_hash]) @recording.update!(visibility: new_visibility) @@ -89,6 +87,35 @@ def recordings_count render_data data: count, status: :ok end + # POST /api/v1/recordings/recording_url.json + def recording_url + record_format = params[:recording_format] + + urls = if [Recording::VISIBILITIES[:protected], Recording::VISIBILITIES[:public_protected]].include? @recording.visibility + recording = BigBlueButtonApi.new(provider: current_provider).get_recording(record_id: @recording.record_id) + formats = recording[:playback][:format] + formats = [formats] unless formats.is_a? Array + + if record_format.present? + found_format = formats.find { |format| format[:type] == record_format } + return render_error status: :not_found unless found_format + + found_format[:url] + else + formats.pluck(:url) + end + elsif record_format.present? + found_format = @recording.formats.find_by(recording_type: record_format) + return render_error status: :not_found unless found_format + + found_format[:url] + else + @recording.formats.pluck(:url) + end + + render_data data: urls, status: :ok + end + private def recording_params @@ -98,6 +125,18 @@ def recording_params def find_recording @recording = Recording.find_by! record_id: params[:id] end + + def visibility_params_of(visibility) + params_of = { + Recording::VISIBILITIES[:unpublished] => { publish: false, meta_hash: { protect: false, 'meta_gl-listed': false } }, + Recording::VISIBILITIES[:published] => { publish: true, meta_hash: { protect: false, 'meta_gl-listed': false } }, + Recording::VISIBILITIES[:public] => { publish: true, meta_hash: { protect: false, 'meta_gl-listed': true } }, + Recording::VISIBILITIES[:protected] => { publish: true, meta_hash: { protect: true, 'meta_gl-listed': false } }, + Recording::VISIBILITIES[:public_protected] => { publish: true, meta_hash: { protect: true, 'meta_gl-listed': true } } + } + + params_of[visibility.to_s] + end end end end diff --git a/app/controllers/api/v1/rooms_configurations_controller.rb b/app/controllers/api/v1/rooms_configurations_controller.rb index c36d20a49e..66524eb4a8 100644 --- a/app/controllers/api/v1/rooms_configurations_controller.rb +++ b/app/controllers/api/v1/rooms_configurations_controller.rb @@ -19,7 +19,7 @@ module Api module V1 class RoomsConfigurationsController < ApiController - before_action only: %i[index] do + before_action only: %i[index show] do ensure_authorized(%w[CreateRoom ManageSiteSettings ManageRoles ManageRooms], friendly_id: params[:friendly_id]) end @@ -33,6 +33,20 @@ def index render_data data: rooms_configs, status: :ok end + + # GET /api/v1/rooms_configurations/:name.json + # Fetches and returns the value of the passed in configuration + def show + config_value = RoomsConfiguration.joins(:meeting_option) + .find_by( + provider: current_provider, + meeting_option: { name: params[:name] } + ).value + + render_data data: config_value, status: :ok + rescue StandardError + return render_error status: :not_found unless config_value + end end end end diff --git a/app/controllers/api/v1/rooms_controller.rb b/app/controllers/api/v1/rooms_controller.rb index 950c3ca711..0d8682ee8c 100644 --- a/app/controllers/api/v1/rooms_controller.rb +++ b/app/controllers/api/v1/rooms_controller.rb @@ -19,9 +19,9 @@ module Api module V1 class RoomsController < ApiController - skip_before_action :ensure_authenticated, only: %i[public_show] + skip_before_action :ensure_authenticated, only: %i[public_show public_recordings] - before_action :find_room, only: %i[show update destroy recordings recordings_processing purge_presentation public_show] + before_action :find_room, only: %i[show update destroy recordings recordings_processing purge_presentation public_show public_recordings] before_action only: %i[create index] do ensure_authorized('CreateRoom') @@ -40,7 +40,8 @@ class RoomsController < ApiController # Returns a list of the current_user's rooms and shared rooms def index shared_rooms = SharedAccess.where(user_id: current_user.id).select(:room_id) - rooms = Room.where(user_id: current_user.id) + rooms = Room.includes(:user) + .where(user_id: current_user.id) .or(Room.where(id: shared_rooms)) .order(online: :desc) .order('last_session DESC NULLS LAST') @@ -50,7 +51,7 @@ def index room.shared = true if room.user_id != current_user.id end - RunningMeetingChecker.new(rooms:, provider: current_provider).call + RunningMeetingChecker.new(rooms: rooms.select(&:online)).call if rooms.select(&:online).any? render_data data: rooms, status: :ok end @@ -58,7 +59,7 @@ def index # GET /api/v1/rooms/:friendly_id.json # Returns the info on a specific room def show - RunningMeetingChecker.new(rooms: @room, provider: current_provider).call if @room.online + RunningMeetingChecker.new(rooms: @room).call if @room.online @room.shared = current_user.shared_rooms.include?(@room) @@ -136,6 +137,16 @@ def recordings render_data data: room_recordings, meta: pagy_metadata(pagy), status: :ok end + # GET /api/v1/rooms/:friendly_id/public_recordings.json + # Returns all of a specific room's PUBLIC recordings + def public_recordings + sort_config = config_sorting(allowed_columns: %w[name length]) + + pagy, recordings = pagy(@room.public_recordings.order(sort_config, recorded_at: :desc).public_search(params[:search])) + + render_data data: recordings, meta: pagy_metadata(pagy), serializer: PublicRecordingSerializer, status: :ok + end + # GET /api/v1/rooms/:friendly_id/recordings_processing.json # Returns the total number of processing recordings for a specific room def recordings_processing diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 7017e1ed39..95b7a69e72 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -39,12 +39,13 @@ def show # POST /api/v1/users.json # Creates and saves a new user record in the database with the provided parameters def create - smtp_enabled = ENV['SMTP_SERVER'].present? + return render_error status: :forbidden if external_authn_enabled? + # Check if this is an admin creating a user admin_create = current_user && PermissionsChecker.new(current_user:, permission_names: 'ManageUsers', current_provider:).call - # Users created by a user will have the creator language by default with a fallback to the server configured default_locale. - create_user_params[:language] = current_user&.language || I18n.default_locale if create_user_params[:language].blank? + # Allow only administrative access for authenticated requests + return render_error status: :forbidden if current_user && !admin_create registration_method = SettingGetter.new(setting_name: 'RegistrationMethod', provider: current_provider).call @@ -52,15 +53,20 @@ def create return render_error errors: Rails.configuration.custom_error_msgs[:invite_token_invalid] end - user = UserCreator.new(user_params: create_user_params.except(:invite_token), provider: current_provider, role: default_role).call - - user.verify! unless smtp_enabled - # TODO: Add proper error logging for non-verified token hcaptcha if !admin_create && hcaptcha_enabled? && !verify_hcaptcha(response: params[:token]) return render_error errors: Rails.configuration.custom_error_msgs[:hcaptcha_invalid] end + # Users created by a user will have the creator language by default with a fallback to the server configured default_locale. + create_user_params[:language] = current_user&.language || I18n.default_locale if create_user_params[:language].blank? + + user = UserCreator.new(user_params: create_user_params.except(:invite_token), provider: current_provider, role: default_role).call + + smtp_enabled = ENV['SMTP_SERVER'].present? + + user.verify! unless smtp_enabled + # Set to pending if registration method is approval user.pending! if !admin_create && registration_method == SiteSetting::REGISTRATION_METHODS[:approval] @@ -77,9 +83,9 @@ def create return render_data data: user, serializer: CurrentUserSerializer, status: :created unless user.verified? user.generate_session_token! - session[:session_token] = user.session_token unless current_user # if this is NOT an admin creating a user + session[:session_token] = user.session_token unless admin_create # if this is NOT an admin creating a user - render_data data: current_user, serializer: CurrentUserSerializer, status: :created + render_data data: user, serializer: CurrentUserSerializer, status: :created elsif user.errors.size == 1 && user.errors.of_kind?(:email, :taken) render_error errors: Rails.configuration.custom_error_msgs[:email_exists], status: :bad_request else @@ -92,9 +98,10 @@ def create # Updates the values of a user def update user = User.find(params[:id]) - # user is updating themselves - if current_user.id == params[:id] && !PermissionsChecker.new(permission_names: 'ManageUsers', current_user:, current_provider:).call - params[:user].delete(:role_id) + + # User can't change their own role + if params[:user][:role_id].present? && current_user == user && params[:user][:role_id] != user.role_id + return render_error errors: Rails.configuration.custom_error_msgs[:unauthorized], status: :forbidden end if user.update(update_user_params) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9e8047ca99..69093e9be1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -63,7 +63,13 @@ def create_default_room(user) return unless user.rooms.count <= 0 return unless PermissionsChecker.new(permission_names: 'CreateRoom', user_id: user.id, current_user: user, current_provider:).call - Room.create(name: "#{user.name}'s Room", user_id: user.id) + Room.create(name: t('room.new_room_name', username: user.name, locale: user.language), user_id: user.id) + end + + # Include user domain in lograge logs + def append_info_to_payload(payload) + super + payload[:host] = @current_provider end private @@ -79,7 +85,6 @@ def invalid_session?(user) # Parses the url for the user domain def parse_user_domain(hostname) tenant = hostname&.split('.')&.first - raise 'Invalid domain' unless Tenant.exists?(name: tenant) tenant diff --git a/app/controllers/concerns/authorizable.rb b/app/controllers/concerns/authorizable.rb index 95364c83c4..5de6097dda 100644 --- a/app/controllers/concerns/authorizable.rb +++ b/app/controllers/concerns/authorizable.rb @@ -41,6 +41,10 @@ def ensure_authorized(permission_names, user_id: nil, friendly_id: nil, record_i ).call end + def ensure_super_admin + return render_error status: :forbidden unless current_user.super_admin? + end + private # Ensures that requests to the API are explicit enough. diff --git a/app/controllers/concerns/client_routable.rb b/app/controllers/concerns/client_routable.rb index db88e9f288..32321642da 100644 --- a/app/controllers/concerns/client_routable.rb +++ b/app/controllers/concerns/client_routable.rb @@ -28,4 +28,9 @@ def activate_account_url(token) def reset_password_url(token) "#{root_url}reset_password/#{token}" end + + # Generates a client side pending url. + def pending_path + "#{root_path}pending" + end end diff --git a/app/controllers/external_controller.rb b/app/controllers/external_controller.rb index cc8a5b67e1..b7f7a4d141 100644 --- a/app/controllers/external_controller.rb +++ b/app/controllers/external_controller.rb @@ -17,6 +17,8 @@ # frozen_string_literal: true class ExternalController < ApplicationController + include ClientRoutable + skip_before_action :verify_authenticity_token # GET 'auth/:provider/callback' @@ -25,22 +27,17 @@ def create_user provider = current_provider credentials = request.env['omniauth.auth'] - user_info = { - name: credentials['info']['name'], - email: credentials['info']['email'], - language: extract_language_code(credentials['info']['locale']), - external_id: credentials['uid'], - verified: true - } - user = User.find_by(external_id: credentials['uid'], provider:) + user_info = build_user_info(credentials) + + user = User.find_by(external_id: credentials['uid'], provider:) || User.find_by(email: credentials['info']['email'], provider:) new_user = user.blank? registration_method = SettingGetter.new(setting_name: 'RegistrationMethod', provider: current_provider).call # Check if they have a valid token only if a new sign up if new_user && registration_method == SiteSetting::REGISTRATION_METHODS[:invite] && !valid_invite_token(email: user_info[:email]) - return redirect_to "/?error=#{Rails.configuration.custom_error_msgs[:invite_token_invalid]}" + return redirect_to root_path(error: Rails.configuration.custom_error_msgs[:invite_token_invalid]) end # Create the user if they dont exist @@ -58,21 +55,25 @@ def create_user # Set to pending if registration method is approval if registration_method == SiteSetting::REGISTRATION_METHODS[:approval] user.pending! if new_user - return redirect_to '/pending' if user.pending? + return redirect_to pending_path if user.pending? end user.generate_session_token! session[:session_token] = user.session_token # TODO: - Ahmad: deal with errors - redirect_location = cookies[:location] - cookies.delete(:location) - return redirect_to redirect_location if redirect_location&.match?('\A\/rooms\/\w{3}-\w{3}-\w{3}-\w{3}\/join\z') - redirect_to '/' + redirect_location = cookies.delete(:location) + + return redirect_to redirect_location, allow_other_host: false if redirect_location&.match?('\/rooms\/\w{3}-\w{3}-\w{3}(-\w{3})?\/join\z') + + redirect_to root_path + rescue ActionController::Redirecting::UnsafeRedirectError => e + Rails.logger.error("Unsafe redirection attempt: #{e}") + redirect_to root_path rescue StandardError => e Rails.logger.error("Error during authentication: #{e}") - redirect_to '/?error=SignupError' + redirect_to root_path(error: Rails.configuration.custom_error_msgs[:external_signup_error]) end # POST /recording_ready @@ -98,10 +99,14 @@ def recording_ready # Increments a rooms recordings_processing if the meeting was recorded def meeting_ended # TODO: - ahmad: Add some sort of validation - return render json: {} unless params[:recordingmarks] == 'true' - @room = Room.find_by(meeting_id: extract_meeting_id) - @room.update(recordings_processing: @room.recordings_processing + 1, online: false) + return render json: {}, status: :ok unless @room + + recordings_processing = params[:recordingmarks] == 'true' ? @room.recordings_processing + 1 : @room.recordings_processing + + unless @room.update(recordings_processing:, online: false) + Rails.logger.error "Failed to update room(id): #{@room.id}, model errors: #{@room.errors}" + end render json: {}, status: :ok end @@ -126,4 +131,14 @@ def valid_invite_token(email:) # Try to delete the invitation and return true if it succeeds Invitation.destroy_by(email:, provider: current_provider, token:).present? end + + def build_user_info(credentials) + { + name: credentials['info']['name'], + email: credentials['info']['email'], + language: extract_language_code(credentials['info']['locale']), + external_id: credentials['uid'], + verified: true + } + end end diff --git a/app/controllers/health_checks_controller.rb b/app/controllers/health_checks_controller.rb new file mode 100644 index 0000000000..465e3bc209 --- /dev/null +++ b/app/controllers/health_checks_controller.rb @@ -0,0 +1,81 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# frozen_string_literal: true + +class HealthChecksController < ApplicationController + skip_before_action :verify_authenticity_token + + def check + response = 'success' + + begin + check_database unless ENV.fetch('DATABASE_HEALTH_CHECK_DISABLED', false) == 'true' + check_redis unless ENV.fetch('REDIS_HEALTH_CHECK_DISABLED', false) == 'true' + check_smtp unless ENV.fetch('SMTP_HEALTH_CHECK_DISABLED', false) == 'true' + check_big_blue_button unless ENV.fetch('BBB_HEALTH_CHECK_DISABLED', false) == 'true' + end + + render plain: response, status: :ok + rescue StandardError => e + logger.error "Health check failed: #{e}" + render plain: e, status: :internal_server_error + end + + private + + def check_database + raise 'Unable to connect to Database' unless ActiveRecord::Base.connection.active? + raise 'Unable to connect to Database - pending migrations' unless ActiveRecord::Migration.check_pending!.nil? + rescue StandardError => e + raise "Unable to connect to Database - #{e}" + end + + def check_redis + Redis.new.ping + rescue StandardError => e + raise "Unable to connect to Redis - #{e}" + end + + def check_smtp + settings = ActionMailer::Base.smtp_settings + + smtp = Net::SMTP.new(settings[:address], settings[:port]) + smtp.enable_starttls_auto if settings[:openssl_verify_mode] != OpenSSL::SSL::VERIFY_NONE && smtp.respond_to?(:enable_starttls_auto) + + if settings[:authentication].present? && settings[:authentication] != 'none' + smtp.start(settings[:domain]) do |s| + s.authenticate(settings[:user_name], settings[:password], settings[:authentication]) + end + else + smtp.start(settings[:domain]) + end + smtp.finish + rescue StandardError => e + raise "Unable to connect to SMTP Server - #{e}" + end + + def check_big_blue_button + checksum = Digest::SHA1.hexdigest("getMeetings#{Rails.configuration.bigbluebutton_secret}") + uri = URI("#{Rails.configuration.bigbluebutton_endpoint}getMeetings?checksum=#{checksum}") + res = Net::HTTP.get(uri) + doc = Nokogiri::XML(res) + + raise "Unable to connect to BigBlueButton - #{res}" unless doc.css('returncode').text == 'SUCCESS' + rescue StandardError => e + raise "Unable to connect to BigBlueButton - #{e}" + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c7de6b3bcd..0fa721f729 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -21,4 +21,12 @@ def branding_image asset_path = SettingGetter.new(setting_name: 'BrandingImage', provider: current_provider).call asset_url(asset_path) end + + def page_title + match = request&.url&.match('\/rooms\/(\w{3}-\w{3}-\w{3}-\w{3})') + return 'BigBlueButton' if match.blank? + + room_name = Room.find_by(friendly_id: match[1])&.name + room_name || 'BigBlueButton' + end end diff --git a/app/javascript/App.jsx b/app/javascript/App.jsx index a065d2aa2d..674a2268a6 100644 --- a/app/javascript/App.jsx +++ b/app/javascript/App.jsx @@ -23,6 +23,7 @@ import Header from './components/shared_components/Header'; import { useAuth } from './contexts/auth/AuthProvider'; import Footer from './components/shared_components/Footer'; import useSiteSetting from './hooks/queries/site_settings/useSiteSetting'; +import Title from './components/shared_components/utilities/Title'; export default function App() { const currentUser = useAuth(); @@ -49,6 +50,7 @@ export default function App() { return ( <> + BigBlueButton {(homePage || currentUser.signed_in) &&
} diff --git a/app/javascript/components/admin/manage_users/ManageUsers.jsx b/app/javascript/components/admin/manage_users/ManageUsers.jsx index b7f8965561..c72d9eef3b 100644 --- a/app/javascript/components/admin/manage_users/ManageUsers.jsx +++ b/app/javascript/components/admin/manage_users/ManageUsers.jsx @@ -33,12 +33,14 @@ import InvitedUsersTable from './InvitedUsersTable'; import PendingUsers from './PendingUsers'; import BannedUsers from './BannedUsers'; import { useAuth } from '../../../contexts/auth/AuthProvider'; +import useEnv from '../../../hooks/queries/env/useEnv'; export default function ManageUsers() { const { t } = useTranslation(); const [searchInput, setSearchInput] = useState(); const { data: registrationMethod } = useSiteSetting('RegistrationMethod'); const currentUser = useAuth(); + const envAPI = useEnv(); if (currentUser.permissions?.ManageUsers !== 'true') { return ; @@ -46,7 +48,7 @@ export default function ManageUsers() { return (
-

{ t('admin.admin_panel') }

+

{t('admin.admin_panel')}

@@ -59,32 +61,37 @@ export default function ManageUsers() {
-

{ t('admin.manage_users.manage_users') }

+

{t('admin.manage_users.manage_users')}

- { registrationMethod === 'invite' + {registrationMethod === 'invite' && ( - - { t('admin.manage_users.invite_user') } - - )} - title={t('admin.manage_users.invite_user')} - body={} - size="md" - /> + + {t('admin.manage_users.invite_user')} + + )} + title={t('admin.manage_users.invite_user')} + body={} + size="md" + /> )} - { t('admin.manage_users.add_new_user') } - } - title={t('admin.manage_users.create_new_user')} - body={} - /> + { + (!envAPI.isLoading && !envAPI.data?.OPENID_CONNECT) + && ( + {t('admin.manage_users.add_new_user')} + } + title={t('admin.manage_users.create_new_user')} + body={} + /> + ) + }
@@ -92,7 +99,7 @@ export default function ManageUsers() { - { registrationMethod === 'approval' + {registrationMethod === 'approval' && ( @@ -101,11 +108,11 @@ export default function ManageUsers() { - { registrationMethod === 'invite' + {registrationMethod === 'invite' && ( - - - + + + )}
diff --git a/app/javascript/components/admin/server_rooms/ServerRoomRow.jsx b/app/javascript/components/admin/server_rooms/ServerRoomRow.jsx index 208f621b0e..d9e1479f24 100644 --- a/app/javascript/components/admin/server_rooms/ServerRoomRow.jsx +++ b/app/javascript/components/admin/server_rooms/ServerRoomRow.jsx @@ -51,16 +51,16 @@ export default function ServerRoomRow({ room }) { return t('admin.server_rooms.no_meeting_yet'); } if (online) { - return t('admin.server_rooms.current_session', { lastSession }); + return t('admin.server_rooms.current_session', { lastSession: localizedTime }); } return t('admin.server_rooms.last_session', { localizedTime }); }; const meetingRunning = () => { if (online) { - return { t('admin.server_rooms.running') } ; + return { t('admin.server_rooms.running') } ; } - return { t('admin.server_rooms.not_running') } ; + return { t('admin.server_rooms.not_running') } ; }; return ( diff --git a/app/javascript/components/admin/site_settings/appearance/BrandingImage.jsx b/app/javascript/components/admin/site_settings/appearance/BrandingImage.jsx index 93d145fc06..75ec5bce43 100644 --- a/app/javascript/components/admin/site_settings/appearance/BrandingImage.jsx +++ b/app/javascript/components/admin/site_settings/appearance/BrandingImage.jsx @@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next'; import useUpdateSiteSetting from '../../../../hooks/mutations/admin/site_settings/useUpdateSiteSetting'; import FilesDragAndDrop from '../../../shared_components/utilities/FilesDragAndDrop'; import useDeleteBrandingImage from '../../../../hooks/mutations/admin/site_settings/useDeleteBrandingImage'; +import { IMAGE_MAX_FILE_COEFF, IMAGE_SUPPORTED_EXTENSIONS } from '../../../../helpers/FileValidationHelper'; export default function BrandingImage() { const { t } = useTranslation(); @@ -33,7 +34,7 @@ export default function BrandingImage() { updateSiteSetting.mutate(files[0])} - formats={['.jpg', '.png', '.svg']} + formats={IMAGE_SUPPORTED_EXTENSIONS} > diff --git a/app/javascript/components/admin/tenants/Tenants.jsx b/app/javascript/components/admin/tenants/Tenants.jsx index 4be513f065..70a1630e0c 100644 --- a/app/javascript/components/admin/tenants/Tenants.jsx +++ b/app/javascript/components/admin/tenants/Tenants.jsx @@ -16,6 +16,7 @@ import React, { useState } from 'react'; import Card from 'react-bootstrap/Card'; +import { Navigate } from 'react-router-dom'; import { Button, Col, Container, Row, Stack, Tab, @@ -29,10 +30,16 @@ import NoSearchResults from '../../shared_components/search/NoSearchResults'; import TenantsTable from './TenantsTable'; import Modal from '../../shared_components/modals/Modal'; import CreateTenantForm from './forms/CreateTenantForm'; +import { useAuth } from '../../../contexts/auth/AuthProvider'; export default function Tenants() { const { t } = useTranslation(); const [page, setPage] = useState(); + const currentUser = useAuth(); + + if (!currentUser.isSuperAdmin) { + return ; + } const [searchInput, setSearchInput] = useState(); const { data: tenants, isLoading } = useTenants({ search: searchInput, page }); diff --git a/app/javascript/components/home/AuthButtons.jsx b/app/javascript/components/home/AuthButtons.jsx index 64517e7a01..3bb4a32011 100644 --- a/app/javascript/components/home/AuthButtons.jsx +++ b/app/javascript/components/home/AuthButtons.jsx @@ -42,8 +42,9 @@ export default function AuthButtons({ direction }) { if (env?.OPENID_CONNECT) { return ( -
+ + diff --git a/app/javascript/components/recordings/RecordingRow.jsx b/app/javascript/components/recordings/RecordingRow.jsx index bfca6aedef..048906011c 100644 --- a/app/javascript/components/recordings/RecordingRow.jsx +++ b/app/javascript/components/recordings/RecordingRow.jsx @@ -17,13 +17,11 @@ import { VideoCameraIcon, TrashIcon, PencilSquareIcon, ClipboardDocumentIcon, EllipsisVerticalIcon, } from '@heroicons/react/24/outline'; -import Form from 'react-bootstrap/Form'; import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Button, Stack, Dropdown, } from 'react-bootstrap'; -import { toast } from 'react-toastify'; import { useTranslation } from 'react-i18next'; import { useAuth } from '../../contexts/auth/AuthProvider'; import Spinner from '../shared_components/utilities/Spinner'; @@ -31,6 +29,9 @@ import UpdateRecordingForm from './forms/UpdateRecordingForm'; import DeleteRecordingForm from './forms/DeleteRecordingForm'; import Modal from '../shared_components/modals/Modal'; import { localizeDateTimeString } from '../../helpers/DateTimeHelper'; +import useRedirectRecordingUrl from '../../hooks/mutations/recordings/useRedirectRecordingUrl'; +import useCopyRecordingUrl from '../../hooks/mutations/recordings/useCopyRecordingUrl'; +import SimpleSelect from '../shared_components/utilities/SimpleSelect'; // TODO: Amir - Refactor this. export default function RecordingRow({ @@ -38,23 +39,27 @@ export default function RecordingRow({ }) { const { t } = useTranslation(); - function copyUrls() { - const formatUrls = recording.formats.map((format) => format.url); - navigator.clipboard.writeText(formatUrls); - toast.success(t('toast.success.recording.copied_urls')); - } - const visibilityAPI = useVisibilityAPI(); const [isEditing, setIsEditing] = useState(false); const [isUpdating, setIsUpdating] = useState(false); + const [display, setDisplay] = useState('invisible'); + const currentUser = useAuth(); + const redirectRecordingUrl = useRedirectRecordingUrl(); + const copyRecordingUrl = useCopyRecordingUrl(); + const localizedTime = localizeDateTimeString(recording?.recorded_at, currentUser?.language); const formats = recording.formats.sort( (a, b) => (a.recording_type.toLowerCase() > b.recording_type.toLowerCase() ? 1 : -1), ); return ( - + setDisplay('visible')} + onMouseLeave={() => setDisplay('invisible')} + >
@@ -80,7 +85,7 @@ export default function RecordingRow({ aria-hidden="true" onClick={() => !isUpdating && setIsEditing(true)} onBlur={() => setIsEditing(false)} - className="hi-s text-muted ms-1 mb-1" + className={`hi-s text-muted ms-1 mb-1 ${display}`} /> ) @@ -90,31 +95,57 @@ export default function RecordingRow({ } {localizedTime} + {adminTable && {recording?.user_name} } - { t('recording.length_in_minutes', { recording }) } + {t('recording.length_in_minutes', { recording })} {recording.participants} - {/* TODO: Refactor this. */} - { - visibilityAPI.mutate({ visibility: event.target.value, id: recording.record_id }); - }} + - - - {recording?.protectable === true - && } - + visibilityAPI.mutate({ visibility: 'Public/Protected', id: recording.record_id })} + > + {t('recording.public_protected')} + + visibilityAPI.mutate({ visibility: 'Public', id: recording.record_id })} + > + {t('recording.public')} + + visibilityAPI.mutate({ visibility: 'Protected', id: recording.record_id })} + > + {t('recording.protected')} + + visibilityAPI.mutate({ visibility: 'Published', id: recording.record_id })} + > + {t('recording.published')} + + visibilityAPI.mutate({ visibility: 'Unpublished', id: recording.record_id })} + > + {t('recording.unpublished')} + + - {formats.map((format) => ( + {recording?.visibility !== 'Unpublished' && formats.map((format) => ( + { recording?.visibility !== 'Unpublished' && ( + + )} } body={( @@ -188,6 +221,7 @@ RecordingRow.propTypes = { protectable: PropTypes.bool, recorded_at: PropTypes.string.isRequired, map: PropTypes.func, + user_name: PropTypes.string, }).isRequired, visibilityMutation: PropTypes.func.isRequired, deleteMutation: PropTypes.func.isRequired, diff --git a/app/javascript/components/rooms/Rooms.jsx b/app/javascript/components/rooms/Rooms.jsx index 817730f947..e4b4beeee3 100644 --- a/app/javascript/components/rooms/Rooms.jsx +++ b/app/javascript/components/rooms/Rooms.jsx @@ -23,18 +23,25 @@ import RoomsList from './RoomsList'; import UserRecordings from '../recordings/UserRecordings'; import RecordingsCountTab from '../recordings/RecordingsCountTab'; import useRecordingsCount from '../../hooks/queries/recordings/useRecordingsCount'; +import useRoomConfigValue from '../../hooks/queries/rooms/useRoomConfigValue'; export default function Rooms() { const { data: recordingsCount } = useRecordingsCount(); const { t } = useTranslation(); + const { data: recordValue } = useRoomConfigValue('record'); + return ( - }> - - + + { (recordValue !== 'false') + && ( + }> + + + )} ); } diff --git a/app/javascript/components/rooms/room/FeatureTabs.jsx b/app/javascript/components/rooms/room/FeatureTabs.jsx index 8de3b18b54..8071cd2074 100644 --- a/app/javascript/components/rooms/room/FeatureTabs.jsx +++ b/app/javascript/components/rooms/room/FeatureTabs.jsx @@ -25,6 +25,7 @@ import { useAuth } from '../../../contexts/auth/AuthProvider'; import useSiteSetting from '../../../hooks/queries/site_settings/useSiteSetting'; import RoomRecordings from '../../recordings/room_recordings/RoomRecordings'; import useRoom from '../../../hooks/queries/rooms/useRoom'; +import useRoomConfigValue from '../../../hooks/queries/rooms/useRoomConfigValue'; export default function FeatureTabs() { const { t } = useTranslation(); @@ -33,8 +34,9 @@ export default function FeatureTabs() { const { friendlyId } = useParams(); const { data: room } = useRoom(friendlyId); + const { isLoading: isRoomConfigLoading, data: recordValue } = useRoomConfigValue('record'); - if (isLoading) { + if (isLoading || isRoomConfigLoading) { return (
@@ -47,11 +49,16 @@ export default function FeatureTabs() { ); } + const showRecTabs = (recordValue !== 'false'); + return ( - - - - + + {showRecTabs + && ( + + + + )} {settings?.PreuploadPresentation && ( diff --git a/app/javascript/components/rooms/room/Room.jsx b/app/javascript/components/rooms/room/Room.jsx index 08d5d5e7ed..6d9af35c18 100644 --- a/app/javascript/components/rooms/room/Room.jsx +++ b/app/javascript/components/rooms/room/Room.jsx @@ -33,6 +33,7 @@ import useStartMeeting from '../../../hooks/mutations/rooms/useStartMeeting'; import MeetingBadges from '../MeetingBadges'; import SharedBadge from './SharedBadge'; import RoomNamePlaceHolder from './RoomNamePlaceHolder'; +import Title from '../../shared_components/utilities/Title'; export default function Room() { const { t } = useTranslation(); @@ -62,6 +63,7 @@ export default function Room() { return ( <> + {room?.name}
diff --git a/app/javascript/components/rooms/room/join/JoinCard.jsx b/app/javascript/components/rooms/room/join/JoinCard.jsx new file mode 100644 index 0000000000..8862a1b29f --- /dev/null +++ b/app/javascript/components/rooms/room/join/JoinCard.jsx @@ -0,0 +1,267 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +/* eslint-disable consistent-return */ +import React, { useState, useEffect } from 'react'; +import Card from 'react-bootstrap/Card'; +import { + Navigate, Link, useParams, +} from 'react-router-dom'; +import { + Button, Col, Row, Stack, Form as RegularForm, +} from 'react-bootstrap'; +import { toast } from 'react-toastify'; +import { useTranslation } from 'react-i18next'; +import { VideoCameraIcon } from '@heroicons/react/24/outline'; +import usePublicRoom from '../../../../hooks/queries/rooms/usePublicRoom'; +import { useAuth } from '../../../../contexts/auth/AuthProvider'; +import useRoomStatus from '../../../../hooks/mutations/rooms/useRoomStatus'; +import useEnv from '../../../../hooks/queries/env/useEnv'; +import subscribeToRoom from '../../../../channels/rooms_channel'; +import RequireAuthentication from './RequireAuthentication'; +import GGSpinner from '../../../shared_components/utilities/GGSpinner'; +import Spinner from '../../../shared_components/utilities/Spinner'; +import Avatar from '../../../users/user/Avatar'; +import Form from '../../../shared_components/forms/Form'; +import FormControl from '../../../shared_components/forms/FormControl'; +import FormControlGeneric from '../../../shared_components/forms/FormControlGeneric'; +import RoomJoinPlaceholder from './RoomJoinPlaceholder'; +import useRoomJoinForm from '../../../../hooks/forms/rooms/useRoomJoinForm'; +import ButtonLink from '../../../shared_components/utilities/ButtonLink'; +import Title from '../../../shared_components/utilities/Title'; + +export default function JoinCard() { + const { t } = useTranslation(); + const currentUser = useAuth(); + const { friendlyId } = useParams(); + const [hasStarted, setHasStarted] = useState(false); + + const publicRoom = usePublicRoom(friendlyId); + const roomStatusAPI = useRoomStatus(friendlyId); + + const { data: env } = useEnv(); + + const { methods, fields } = useRoomJoinForm(); + + const path = encodeURIComponent(document.location.pathname); + + useEffect(() => { // set cookie to return to if needed + const date = new Date(); + date.setTime(date.getTime() + (60 * 1000)); // expire the cookie in 1min + document.cookie = `location=${path};path=/;expires=${date.toGMTString()}`; + + return () => { // delete redirect location when unmounting + document.cookie = `location=${path};path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT`; + }; + }, []); + + const handleJoin = (data) => { + document.cookie = 'location=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT'; // delete redirect location + + if (publicRoom?.data.viewer_access_code && !methods.getValues('access_code')) { + return methods.setError('access_code', { type: 'required', message: t('room.settings.access_code_required') }, { shouldFocus: true }); + } + + roomStatusAPI.mutate(data); + }; + const reset = () => { setHasStarted(false); };// Reset pipeline; + + useEffect(() => { + // Default Join name to authenticated user full name. + if (currentUser?.name) { + methods.setValue('name', currentUser.name); + } + }, [currentUser?.name]); + + useEffect(() => { + // Room channel subscription: + if (roomStatusAPI.isSuccess) { + // When the user provides valid input (name, codes) the UI will subscribe to the room channel. + const channel = subscribeToRoom(friendlyId, { onReceived: () => { setHasStarted(true); } }); + + // Cleanup: On component unmounting any opened channel subscriptions will be closed. + return () => { + channel.unsubscribe(); + console.info(`WS: unsubscribed from room(friendly_id): ${friendlyId} channel.`); + }; + } + }, [roomStatusAPI.isSuccess]); + + // Play a sound and displays a toast when the meeting starts if the user was in a waiting queue + const notifyMeetingStarted = () => { + const audio = new Audio(`${process.env.RELATIVE_URL_ROOT}/audios/notify.mp3`); + audio.play() + .catch((err) => { + console.error(err); + }); + toast.success(t('toast.success.room.meeting_started')); + }; + + // Returns a random delay between 2 and 5 seconds, in increments of 250 ms + // The delay is to let the BBB server settle before attempting to join the meeting + // The randomness is to prevent multiple users from joining the meeting at the same time + const joinDelay = () => { + const min = 4000; + const max = 7000; + const step = 250; + + // Calculate the number of possible steps within the given range + const numSteps = (max - min) / step; + + // Generate a random integer from 0 to numSteps (inclusive) + const randomStep = Math.floor(Math.random() * (numSteps + 1)); + + // Calculate and return the random delay + return min + (randomStep * step); + }; + + useEffect(() => { + // Meeting started: + // When meeting starts this logic will be fired, indicating the event to waiting users (through a toast) for UX matter. + // Logging the event for debugging purposes and refetching the join logic with the user's given input (name & codes). + if (hasStarted) { + console.info(`Attempting to join the room(friendly_id): ${friendlyId} meeting.`); + const delay = joinDelay(); + setTimeout(notifyMeetingStarted, delay - 1000); + setTimeout(methods.handleSubmit(handleJoin), delay); // TODO: Amir - Improve this race condition handling by the backend. + reset();// Resetting the Join component. + } + }, [hasStarted]); + + useEffect(() => { + // UI synchronization on failing join attempt: + // When the room status API returns an error indicating a failed join attempt it's highly due to stale credentials. + // In such case, users from a UX perspective will appreciate having the UI updated informing them about the case. + // i.e: Indicating the lack of providing access code value for cases where access code was generated while the user was waiting. + if (roomStatusAPI.isError) { + // Invalid Access Code SSE (Server Side Error): + if (roomStatusAPI.error.response.status === 403) { + methods.setError('access_code', { type: 'SSE', message: t('room.settings.wrong_access_code') }, { shouldFocus: true }); + } + + publicRoom.refetch();// Refetching room public information. + reset();// Resetting the Join component. + } + }, [roomStatusAPI.isError]); + + if (publicRoom.isLoading) return ; + + if (!currentUser.signed_in && publicRoom.data.require_authentication === 'true') { + return ; + } + + if (publicRoom.data.owner_id === currentUser?.id || publicRoom.data.shared_user_ids.includes(currentUser?.id)) { + return ; + } + + const hasAccessCode = publicRoom.data?.viewer_access_code || publicRoom.data?.moderator_access_code; + + if (publicRoom.data?.viewer_access_code || !publicRoom.data?.moderator_access_code) { + fields.accessCode.label = t('room.settings.access_code'); + // for the case where anyone_join_as_moderator is true and only the moderator access code is required + } else if (publicRoom.data?.anyone_join_as_moderator === 'true') { + fields.accessCode.label = t('room.settings.mod_access_code'); + } else { + fields.accessCode.label = t('room.settings.mod_access_code_optional'); + } + + const WaitingPage = ( + +
+
{t('room.meeting.meeting_not_started')}
+ {t('room.meeting.join_meeting_automatically')} +
+
+ +
+
+ ); + + return ( + + {publicRoom?.data.name} + + + + {t('room.meeting.meeting_invitation')} +

+ {publicRoom?.data.name} +

+ + {t('view_recordings')} + + + + + +
{publicRoom?.data.owner_name}
+
+ +
+
+ + + {(roomStatusAPI.isSuccess && !roomStatusAPI.data.status) ? WaitingPage : ( + + + {hasAccessCode && } + {publicRoom?.data?.recording_consent === 'true' && ( + + )} + + + + )} + + + {!currentUser?.signed_in && ( + env?.OPENID_CONNECT ? ( + {t('authentication.already_have_account')} + + + + + + ) : ( +
{t('authentication.already_have_account')} + {t('authentication.sign_in')} +
+ ) + )} +
+
+
+ ); +} diff --git a/app/javascript/components/rooms/room/join/RequireAuthentication.jsx b/app/javascript/components/rooms/room/join/RequireAuthentication.jsx index dbcecb0fe1..98ed8676e0 100644 --- a/app/javascript/components/rooms/room/join/RequireAuthentication.jsx +++ b/app/javascript/components/rooms/room/join/RequireAuthentication.jsx @@ -39,7 +39,7 @@ export default function RequireAuthentication({ path }) { { env?.OPENID_CONNECT ? ( -
+ diff --git a/app/javascript/components/rooms/room/join/RoomJoin.jsx b/app/javascript/components/rooms/room/join/RoomJoin.jsx index 65c4a37731..8f64fb8c12 100644 --- a/app/javascript/components/rooms/room/join/RoomJoin.jsx +++ b/app/javascript/components/rooms/room/join/RoomJoin.jsx @@ -15,246 +15,20 @@ // with Greenlight; if not, see . /* eslint-disable consistent-return */ -import React, { useState, useEffect } from 'react'; -import Card from 'react-bootstrap/Card'; -import { - Navigate, Link, useLocation, useParams, -} from 'react-router-dom'; -import { - Button, Col, Row, Stack, Form as RegularForm, -} from 'react-bootstrap'; -import { toast } from 'react-toastify'; -import { useTranslation } from 'react-i18next'; -import { useForm } from 'react-hook-form'; -import usePublicRoom from '../../../../hooks/queries/rooms/usePublicRoom'; -import { useAuth } from '../../../../contexts/auth/AuthProvider'; -import useRoomStatus from '../../../../hooks/mutations/rooms/useRoomStatus'; -import useEnv from '../../../../hooks/queries/env/useEnv'; -import { joinFormConfig, joinFormFields as fields } from '../../../../helpers/forms/JoinFormHelpers'; -import subscribeToRoom from '../../../../channels/rooms_channel'; -import RequireAuthentication from './RequireAuthentication'; -import GGSpinner from '../../../shared_components/utilities/GGSpinner'; -import Spinner from '../../../shared_components/utilities/Spinner'; +import React from 'react'; +import { Row } from 'react-bootstrap'; +import JoinCard from './JoinCard'; import Logo from '../../../shared_components/Logo'; -import Avatar from '../../../users/user/Avatar'; -import Form from '../../../shared_components/forms/Form'; -import FormControl from '../../../shared_components/forms/FormControl'; -import FormControlGeneric from '../../../shared_components/forms/FormControlGeneric'; -import RoomJoinPlaceholder from './RoomJoinPlaceholder'; export default function RoomJoin() { - const { t } = useTranslation(); - const currentUser = useAuth(); - const { friendlyId } = useParams(); - const [hasStarted, setHasStarted] = useState(false); - - const publicRoom = usePublicRoom(friendlyId); - const roomStatusAPI = useRoomStatus(friendlyId); - - const { data: env } = useEnv(); - - const methods = useForm(joinFormConfig); - - const location = useLocation(); - const path = encodeURIComponent(location.pathname); - - useEffect(() => { // set cookie to return to if needed - const date = new Date(); - date.setTime(date.getTime() + (60 * 1000)); // expire the cookie in 1min - document.cookie = `location=${path};path=/;expires=${date.toGMTString()}`; - - return () => { // delete redirect location when unmounting - document.cookie = `location=${path};path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT`; - }; - }, []); - - const handleJoin = (data) => { - document.cookie = 'location=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT'; // delete redirect location - - if (publicRoom?.data.viewer_access_code && !methods.getValues('access_code')) { - return methods.setError('access_code', { type: 'required', message: t('room.settings.access_code_required') }, { shouldFocus: true }); - } - - roomStatusAPI.mutate(data); - }; - const reset = () => { setHasStarted(false); };// Reset pipeline; - - useEffect(() => { - // Default Join name to authenticated user full name. - if (currentUser?.name) { - methods.setValue('name', currentUser.name); - } - }, [currentUser?.name]); - - useEffect(() => { - // Room channel subscription: - if (roomStatusAPI.isSuccess) { - // When the user provides valid input (name, codes) the UI will subscribe to the room channel. - const channel = subscribeToRoom(friendlyId, { onReceived: () => { setHasStarted(true); } }); - - // Cleanup: On component unmounting any opened channel subscriptions will be closed. - return () => { - channel.unsubscribe(); - console.info(`WS: unsubscribed from room(friendly_id): ${friendlyId} channel.`); - }; - } - }, [roomStatusAPI.isSuccess]); - - // Play a sound and displays a toast when the meeting starts if the user was in a waiting queue - const notifyMeetingStarted = () => { - const audio = new Audio(`${process.env.RELATIVE_URL_ROOT}/audios/notify.mp3`); - audio.play() - .catch((err) => { - console.error(err); - }); - toast.success(t('toast.success.room.meeting_started')); - }; - - // Returns a random delay between 2 and 5 seconds, in increments of 250 ms - // The delay is to let the BBB server settle before attempting to join the meeting - // The randomness is to prevent multiple users from joining the meeting at the same time - const joinDelay = () => { - const min = 4000; - const max = 7000; - const step = 250; - - // Calculate the number of possible steps within the given range - const numSteps = (max - min) / step; - - // Generate a random integer from 0 to numSteps (inclusive) - const randomStep = Math.floor(Math.random() * (numSteps + 1)); - - // Calculate and return the random delay - return min + (randomStep * step); - }; - - useEffect(() => { - // Meeting started: - // When meeting starts this logic will be fired, indicating the event to waiting users (through a toast) for UX matter. - // Logging the event for debugging purposes and refetching the join logic with the user's given input (name & codes). - if (hasStarted) { - console.info(`Attempting to join the room(friendly_id): ${friendlyId} meeting.`); - const delay = joinDelay(); - setTimeout(notifyMeetingStarted, delay - 1000); - setTimeout(methods.handleSubmit(handleJoin), delay); // TODO: Amir - Improve this race condition handling by the backend. - reset();// Resetting the Join component. - } - }, [hasStarted]); - - useEffect(() => { - // UI synchronization on failing join attempt: - // When the room status API returns an error indicating a failed join attempt it's highly due to stale credentials. - // In such case, users from a UX perspective will appreciate having the UI updated informing them about the case. - // i.e: Indicating the lack of providing access code value for cases where access code was generated while the user was waiting. - if (roomStatusAPI.isError) { - // Invalid Access Code SSE (Server Side Error): - if (roomStatusAPI.error.response.status === 403) { - methods.setError('access_code', { type: 'SSE', message: t('room.settings.wrong_access_code') }, { shouldFocus: true }); - } - - publicRoom.refetch();// Refetching room public information. - reset();// Resetting the Join component. - } - }, [roomStatusAPI.isError]); - - if (publicRoom.isLoading) return ; - - if (!currentUser.signed_in && publicRoom.data.require_authentication === 'true') { - return ; - } - - if (publicRoom.data.owner_id === currentUser?.id || publicRoom.data.shared_user_ids.includes(currentUser?.id)) { - return ; - } - - const hasAccessCode = publicRoom.data?.viewer_access_code || publicRoom.data?.moderator_access_code; - - if (publicRoom.data?.viewer_access_code || !publicRoom.data?.moderator_access_code) { - fields.accessCode.label = t('room.settings.access_code'); - // for the case where anyone_join_as_moderator is true and only the moderator access code is required - } else if (publicRoom.data?.anyone_join_as_moderator === 'true') { - fields.accessCode.label = t('room.settings.mod_access_code'); - } else { - fields.accessCode.label = t('room.settings.mod_access_code_optional'); - } - - const WaitingPage = ( - -
-
{t('room.meeting.meeting_not_started')}
- {t('room.meeting.join_meeting_automatically')} -
-
- -
-
- ); - return (
-
+ -
- - - - - {t('room.meeting.meeting_invitation')} -

- {publicRoom?.data.name} -

- - - - -
{publicRoom?.data.owner_name}
-
- -
-
- - {(roomStatusAPI.isSuccess && !roomStatusAPI.data.status) ? WaitingPage : ( - - - {hasAccessCode && } - {publicRoom?.data?.recording_consent === 'true' && ( - - )} - - - - )} - -
- {!currentUser?.signed_in && ( - env?.OPENID_CONNECT ? ( - {t('authentication.already_have_account')} - - - - - - ) : ( -
{t('authentication.already_have_account')} - {t('authentication.sign_in')} -
- ) - )} + + + +
); } diff --git a/app/javascript/components/rooms/room/join/RoomJoinPlaceholder.jsx b/app/javascript/components/rooms/room/join/RoomJoinPlaceholder.jsx index 78461d918f..d31146dd13 100644 --- a/app/javascript/components/rooms/room/join/RoomJoinPlaceholder.jsx +++ b/app/javascript/components/rooms/room/join/RoomJoinPlaceholder.jsx @@ -20,35 +20,32 @@ import Card from 'react-bootstrap/Card'; import { Button, Col, Row, Stack, } from 'react-bootstrap'; -import Logo from '../../../shared_components/Logo'; import Placeholder from '../../../shared_components/utilities/Placeholder'; import RoundPlaceholder from '../../../shared_components/utilities/RoundPlaceholder'; +import Form from '../../../shared_components/forms/Form'; export default function RoomJoinPlaceholder() { const { t } = useTranslation(); return ( -
-
- -
- - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + +
-
-
-
-
+ +
+ + + + + ); } diff --git a/app/javascript/components/rooms/room/presentation/Presentation.jsx b/app/javascript/components/rooms/room/presentation/Presentation.jsx index 7705b62463..3b14be58c9 100644 --- a/app/javascript/components/rooms/room/presentation/Presentation.jsx +++ b/app/javascript/components/rooms/room/presentation/Presentation.jsx @@ -26,6 +26,7 @@ import useUploadPresentation from '../../../../hooks/mutations/rooms/useUploadPr import useRoom from '../../../../hooks/queries/rooms/useRoom'; import DeletePresentationForm from './forms/DeletePresentationForm'; import FilesDragAndDrop from '../../../shared_components/utilities/FilesDragAndDrop'; +import { PRESENTATION_MAX_FILE_COEFF, PRESENTATION_SUPPORTED_EXTENSIONS } from '../../../../helpers/FileValidationHelper'; export default function Presentation() { const { t } = useTranslation(); @@ -37,13 +38,12 @@ export default function Presentation() { onSubmit(files[0]); }; - if (!room.presentation_name) { + if (!room?.presentation_name) { return (
@@ -66,7 +66,7 @@ export default function Presentation() { - { t('room.presentation.upload_description') } + { t('room.presentation.upload_description', { size: `${PRESENTATION_MAX_FILE_COEFF} MB` }) } diff --git a/app/javascript/components/rooms/room/public_recordings/PublicRecordingRow.jsx b/app/javascript/components/rooms/room/public_recordings/PublicRecordingRow.jsx new file mode 100644 index 0000000000..8850e6135b --- /dev/null +++ b/app/javascript/components/rooms/room/public_recordings/PublicRecordingRow.jsx @@ -0,0 +1,98 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import React from 'react'; +import { + VideoCameraIcon, ClipboardDocumentIcon, +} from '@heroicons/react/24/outline'; +import PropTypes from 'prop-types'; +import { + Button, Stack, +} from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from '../../../../contexts/auth/AuthProvider'; +import { localizeDateTimeString } from '../../../../helpers/DateTimeHelper'; +import useRedirectRecordingUrl from '../../../../hooks/mutations/recordings/useRedirectRecordingUrl'; +import useCopyRecordingUrl from '../../../../hooks/mutations/recordings/useCopyRecordingUrl'; + +// TODO: Amir - Refactor this. +export default function PublicRecordingRow({ + recording, +}) { + const { t } = useTranslation(); + + const currentUser = useAuth(); + const redirectRecordingUrl = useRedirectRecordingUrl(); + const copyRecordingUrl = useCopyRecordingUrl(); + + const localizedTime = localizeDateTimeString(recording?.recorded_at, currentUser?.language); + const formats = recording.formats.sort( + (a, b) => (a.recording_type.toLowerCase() > b.recording_type.toLowerCase() ? 1 : -1), + ); + + return ( + + + +
+ +
+ + {recording.name} + {localizedTime} + +
+ + {t('recording.length_in_minutes', { recording })} + + {formats.map((format) => ( + + ))} + + + + + + + + ); +} + +PublicRecordingRow.propTypes = { + recording: PropTypes.shape({ + id: PropTypes.string.isRequired, + record_id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + length: PropTypes.number.isRequired, + formats: PropTypes.arrayOf(PropTypes.shape({ + url: PropTypes.string.isRequired, + recording_type: PropTypes.string.isRequired, + })), + recorded_at: PropTypes.string.isRequired, + }).isRequired, +}; diff --git a/app/javascript/components/rooms/room/public_recordings/PublicRecordings.jsx b/app/javascript/components/rooms/room/public_recordings/PublicRecordings.jsx new file mode 100644 index 0000000000..922a142647 --- /dev/null +++ b/app/javascript/components/rooms/room/public_recordings/PublicRecordings.jsx @@ -0,0 +1,34 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +/* eslint-disable consistent-return */ +import React from 'react'; +import { Row } from 'react-bootstrap'; +import Logo from '../../../shared_components/Logo'; +import PublicRecordingsCard from './PublicRecordingsCard'; + +export default function RoomJoin() { + return ( +
+ + + + + + +
+ ); +} diff --git a/app/javascript/components/rooms/room/public_recordings/PublicRecordingsCard.jsx b/app/javascript/components/rooms/room/public_recordings/PublicRecordingsCard.jsx new file mode 100644 index 0000000000..ea570c0ee0 --- /dev/null +++ b/app/javascript/components/rooms/room/public_recordings/PublicRecordingsCard.jsx @@ -0,0 +1,33 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +/* eslint-disable consistent-return */ +import React from 'react'; +import Card from 'react-bootstrap/Card'; +import { useParams } from 'react-router-dom'; +import PublicRecordingsList from './PublicRecordingsList'; + +export default function PublicRecordingsCard() { + const { friendlyId } = useParams(); + + return ( + + + + + + ); +} diff --git a/app/javascript/components/rooms/room/public_recordings/PublicRecordingsList.jsx b/app/javascript/components/rooms/room/public_recordings/PublicRecordingsList.jsx new file mode 100644 index 0000000000..eac1767155 --- /dev/null +++ b/app/javascript/components/rooms/room/public_recordings/PublicRecordingsList.jsx @@ -0,0 +1,126 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { VideoCameraIcon } from '@heroicons/react/24/outline'; +import { Card, Stack, Table } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import SortBy from '../../../shared_components/search/SortBy'; +import NoSearchResults from '../../../shared_components/search/NoSearchResults'; +import Pagination from '../../../shared_components/Pagination'; +import SearchBar from '../../../shared_components/search/SearchBar'; +import usePublicRecordings from '../../../../hooks/queries/recordings/usePublicRecordings'; +import PublicRecordingRow from './PublicRecordingRow'; +import PublicRecordingsRowPlaceHolder from './PublicRecordingsRowPlaceHolder'; +import ButtonLink from '../../../shared_components/utilities/ButtonLink'; +import UserBoardIcon from '../../UserBoardIcon'; + +export default function PublicRecordingsList({ friendlyId }) { + const { t } = useTranslation(); + const [page, setPage] = useState(1); + const [searchInput, setSearchInput] = useState(''); + const { data: recordings, ...publicRecordingsAPI } = usePublicRecordings({ friendlyId, page, search: searchInput }); + + if (!publicRecordingsAPI.isLoading && recordings?.data?.length === 0 && !searchInput) { + return ( +
+
+ +
+

{t('recording.public_recordings_list_empty')}

+

+ {t('recording.public_recordings_list_empty_description')} +

+ + {t('join_session')} + +
+ ); + } + + return ( + <> + +
+ +
+ + {t('join_session')} + +
+ { + (searchInput && recordings?.data.length === 0) + ? ( +
+ +
+ ) : ( + + + + + + + + + + + { + (publicRecordingsAPI.isLoading && [...Array(7)].map((val, idx) => ( + // eslint-disable-next-line react/no-array-index-key + + ))) + } + { + (recordings?.data?.length > 0 && recordings?.data?.map((recording) => ( + + ))) + } + + {(recordings?.meta?.pages > 1) + && ( + + + + + + )} +
{t('recording.name')}{t('recording.length')}{t('recording.formats')}
+ +
+
+ ) + } + + ); +} + +PublicRecordingsList.propTypes = { + friendlyId: PropTypes.string.isRequired, +}; diff --git a/app/javascript/components/rooms/room/public_recordings/PublicRecordingsRowPlaceHolder.jsx b/app/javascript/components/rooms/room/public_recordings/PublicRecordingsRowPlaceHolder.jsx new file mode 100644 index 0000000000..ef790aad8c --- /dev/null +++ b/app/javascript/components/rooms/room/public_recordings/PublicRecordingsRowPlaceHolder.jsx @@ -0,0 +1,45 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import React from 'react'; +import { Stack } from 'react-bootstrap'; +import Placeholder from '../../../shared_components/utilities/Placeholder'; +import RoundPlaceholder from '../../../shared_components/utilities/RoundPlaceholder'; + +export default function PublicRecordingsRowPlaceHolder() { + return ( + + {/* Avatar and Name */} + + + + + + + + + + {/* Length */} + + + + {/* Formats */} + + + + + ); +} diff --git a/app/javascript/components/shared_components/utilities/HCaptcha.jsx b/app/javascript/components/shared_components/utilities/HCaptcha.jsx index 29f056b0a5..dab4b5ba91 100644 --- a/app/javascript/components/shared_components/utilities/HCaptcha.jsx +++ b/app/javascript/components/shared_components/utilities/HCaptcha.jsx @@ -40,10 +40,6 @@ function HCaptcha(_props, ref) { console.error('Challenge expired, Timeout.'); toast.error(t('toast.error.problem_completing_action')); }, - - handleVerified: () => { - toast.success(t('toast.success.success')); - }, }), [i18n.resolvedLanguage]); if (!envAPI.data?.HCAPTCHA_KEY) { @@ -56,7 +52,6 @@ function HCaptcha(_props, ref) { ref={ref} size="invisible" sitekey={envAPI.data.HCAPTCHA_KEY} - onVerify={HCaptchaHandlers.handleVerified} onError={HCaptchaHandlers.handleError} onExpire={HCaptchaHandlers.handleExpire} onChalExpired={HCaptchaHandlers.handleChalExpired} diff --git a/app/javascript/components/shared_components/utilities/SimpleSelect.jsx b/app/javascript/components/shared_components/utilities/SimpleSelect.jsx new file mode 100644 index 0000000000..7ec44282de --- /dev/null +++ b/app/javascript/components/shared_components/utilities/SimpleSelect.jsx @@ -0,0 +1,47 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import { Dropdown } from 'react-bootstrap'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { ChevronDownIcon } from '@heroicons/react/20/solid'; + +export default function SimpleSelect({ defaultValue, children }) { + // Get the currently selected option and set the dropdown toggle to that value + const defaultString = children?.filter((item) => item.props.value === defaultValue)[0]; + + return ( + + + { defaultString?.props?.children } + + + + {children} + + + ); +} + +SimpleSelect.defaultProps = { + defaultValue: '', + children: undefined, +}; + +SimpleSelect.propTypes = { + defaultValue: PropTypes.string, + children: PropTypes.arrayOf(PropTypes.element), +}; diff --git a/app/javascript/components/shared_components/utilities/Title.jsx b/app/javascript/components/shared_components/utilities/Title.jsx new file mode 100644 index 0000000000..b087934d3a --- /dev/null +++ b/app/javascript/components/shared_components/utilities/Title.jsx @@ -0,0 +1,32 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Helmet } from 'react-helmet'; + +export default function Title({ children: title }) { + return ( + + {title} + + + ); +} + +Title.propTypes = { + children: PropTypes.node.isRequired, +}; diff --git a/app/javascript/components/users/authentication/Signup.jsx b/app/javascript/components/users/authentication/Signup.jsx index 90670b3956..b5306175dc 100644 --- a/app/javascript/components/users/authentication/Signup.jsx +++ b/app/javascript/components/users/authentication/Signup.jsx @@ -22,18 +22,29 @@ import { toast } from 'react-toastify'; import SignupForm from './forms/SignupForm'; import Logo from '../../shared_components/Logo'; import useSiteSetting from '../../../hooks/queries/site_settings/useSiteSetting'; +import useEnv from '../../../hooks/queries/env/useEnv'; export default function Signup() { const { t } = useTranslation(); const [searchParams] = useSearchParams(); const inviteToken = searchParams.get('inviteToken'); - const { data: registrationMethod } = useSiteSetting('RegistrationMethod'); + const registrationMethodSettingAPI = useSiteSetting('RegistrationMethod'); + const envAPI = useEnv(); + const isLoading = envAPI.isLoading || registrationMethodSettingAPI.isLoading; - if (registrationMethod === 'invite' && !inviteToken) { + if (envAPI.data?.OPENID_CONNECT) { + return ; + } + + if (registrationMethodSettingAPI.data === 'invite' && !inviteToken) { toast.error(t('toast.error.users.invalid_invite')); return ; } + if (isLoading) { + return null; + } + return (
diff --git a/app/javascript/components/users/authentication/forms/SigninForm.jsx b/app/javascript/components/users/authentication/forms/SigninForm.jsx index da7ed08851..b0ac6baaee 100644 --- a/app/javascript/components/users/authentication/forms/SigninForm.jsx +++ b/app/javascript/components/users/authentication/forms/SigninForm.jsx @@ -29,12 +29,14 @@ import useCreateSession from '../../../../hooks/mutations/sessions/useCreateSess import useSignInForm from '../../../../hooks/forms/users/authentication/useSignInForm'; import HCaptcha from '../../../shared_components/utilities/HCaptcha'; import FormCheckBox from '../../../shared_components/forms/controls/FormCheckBox'; +import useEnv from '../../../../hooks/queries/env/useEnv'; export default function SigninForm() { const { t } = useTranslation(); const { methods, fields } = useSignInForm(); const createSessionAPI = useCreateSession(); const captchaRef = useRef(null); + const { data: env } = useEnv(); const handleSubmit = useCallback(async (session) => { const results = await captchaRef.current?.execute({ async: true }); @@ -52,7 +54,11 @@ export default function SigninForm() { - {t('authentication.forgot_password')} + { + env?.SMTP_ENABLED && ( + {t('authentication.forgot_password')} + ) + } diff --git a/app/javascript/components/users/password_management/ForgetPassword.jsx b/app/javascript/components/users/password_management/ForgetPassword.jsx index e7a2f38e66..4ced34ea2f 100644 --- a/app/javascript/components/users/password_management/ForgetPassword.jsx +++ b/app/javascript/components/users/password_management/ForgetPassword.jsx @@ -16,13 +16,21 @@ import React from 'react'; import Card from 'react-bootstrap/Card'; -import { Link } from 'react-router-dom'; +import { Link, Navigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import ForgetPwdForm from './forms/ForgetPwdForm'; import Logo from '../../shared_components/Logo'; +import useEnv from '../../../hooks/queries/env/useEnv'; export default function ForgetPassword() { const { t } = useTranslation(); + const { data: env, isLoading } = useEnv(); + + if (isLoading) return null; + + if (!env?.SMTP_ENABLED) { + return ; + } return (
diff --git a/app/javascript/components/users/user/forms/UpdateUserForm.jsx b/app/javascript/components/users/user/forms/UpdateUserForm.jsx index c6aefd2b7a..7d524695b8 100644 --- a/app/javascript/components/users/user/forms/UpdateUserForm.jsx +++ b/app/javascript/components/users/user/forms/UpdateUserForm.jsx @@ -36,9 +36,8 @@ export default function UpdateUserForm({ user }) { const { t } = useTranslation(); const currentUser = useAuth(); - // Remove the display of role input field if the user is a super admin trying to update their own role - const isSuperAdminEditOwnRole = user === currentUser && currentUser.isSuperAdmin; - const canUpdateRole = PermissionChecker.hasManageUsers(currentUser) && !isSuperAdminEditOwnRole; + // User with ManageUsers permission can update any user except themselves + const canUpdateRole = PermissionChecker.hasManageUsers(currentUser) && currentUser.id !== user.id; const { data: roles } = useRoles({ enabled: canUpdateRole }); const { data: locales } = useLocales(); diff --git a/app/javascript/helpers/FileValidationHelper.jsx b/app/javascript/helpers/FileValidationHelper.jsx index e1847d2a04..948b497903 100644 --- a/app/javascript/helpers/FileValidationHelper.jsx +++ b/app/javascript/helpers/FileValidationHelper.jsx @@ -14,32 +14,47 @@ // You should have received a copy of the GNU Lesser General Public License along // with Greenlight; if not, see . -export const fileValidation = (file, type) => { - const IMAGE_MAX_FILE_SIZE = 3_000_000; - const IMAGE_SUPPORTED_FORMATS = ['image/jpeg', 'image/png', 'image/svg+xml']; +const IMAGE_SUPPORTED_FORMATS = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.svg': 'image/svg+xml', +}; + +const PRESENTATION_SUPPORTED_FORMATS = { + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.ppt': 'application/vnd.ms-powerpoint', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.pdf': 'application/pdf', + '.xls': 'application/vnd.ms-excel', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.txt': 'text/plain', + '.rtf': 'application/rtf', + '.odt': 'application/vnd.oasis.opendocument.text', + '.ods': 'application/vnd.oasis.opendocument.spreadsheet', + '.odp': 'application/vnd.oasis.opendocument.presentation', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', +}; + +export const IMAGE_SUPPORTED_EXTENSIONS = Object.keys(IMAGE_SUPPORTED_FORMATS); +export const IMAGE_SUPPORTED_MIMES = Object.values(IMAGE_SUPPORTED_FORMATS); +export const PRESENTATION_SUPPORTED_EXTENSIONS = Object.keys(PRESENTATION_SUPPORTED_FORMATS); +export const PRESENTATION_SUPPORTED_MIMES = Object.values(PRESENTATION_SUPPORTED_FORMATS); - const PRESENTATION_MAX_FILE_SIZE = 10_000_000; - const PRESENTATION_SUPPORTED_FORMATS = [ - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.ms-powerpoint', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/pdf', - 'application/vnd.ms-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'text/plain', - 'application/rtf', - 'application/vnd.oasis.opendocument.text', - 'application/vnd.oasis.opendocument.spreadsheet', - 'application/vnd.oasis.opendocument.presentation', - 'application/vnd']; +export const IMAGE_MAX_FILE_COEFF = 3; +export const PRESENTATION_MAX_FILE_COEFF = 10; - const MAX_FILE_SIZE = type === 'image' ? IMAGE_MAX_FILE_SIZE : PRESENTATION_MAX_FILE_SIZE; - const SUPPORTED_FORMATS = type === 'image' ? IMAGE_SUPPORTED_FORMATS : PRESENTATION_SUPPORTED_FORMATS; +export const fileValidation = (file, type) => { + const MEBIBYTE = 1024 * 1024; + const MAX_FILE_SIZE = type === 'image' ? IMAGE_MAX_FILE_COEFF * MEBIBYTE : PRESENTATION_MAX_FILE_COEFF * MEBIBYTE; + const SUPPORTED_MIMES = type === 'image' ? IMAGE_SUPPORTED_MIMES : PRESENTATION_SUPPORTED_MIMES; if (file.size > MAX_FILE_SIZE) { throw new Error('fileSizeTooLarge'); - } else if (!SUPPORTED_FORMATS.includes(file.type)) { + } else if (!SUPPORTED_MIMES.includes(file.type)) { throw new Error('fileTypeNotSupported'); } }; diff --git a/app/javascript/helpers/forms/JoinFormHelpers.jsx b/app/javascript/helpers/forms/JoinFormHelpers.jsx deleted file mode 100644 index 1eb73aa6db..0000000000 --- a/app/javascript/helpers/forms/JoinFormHelpers.jsx +++ /dev/null @@ -1,64 +0,0 @@ -// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. -// -// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). -// -// This program is free software; you can redistribute it and/or modify it under the -// terms of the GNU Lesser General Public License as published by the Free Software -// Foundation; either version 3.0 of the License, or (at your option) any later -// version. -// -// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY -// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License along -// with Greenlight; if not, see . - -import * as yup from 'yup'; -import { yupResolver } from '@hookform/resolvers/yup'; - -const validationSchema = yup.object({ - // TODO: amir - Revisit validations. - name: yup.string().required('Please enter your full name.'), - access_code: yup.string(), - consent: yup.boolean().oneOf([true], ''), -}); - -export const joinFormConfig = { - mode: 'onSubmit', - criteriaMode: 'firstError', - defaultValues: { - name: '', - access_code: '', - }, - resolver: yupResolver(validationSchema), -}; - -export const joinFormFields = { - name: { - label: 'Name', - placeHolder: 'Enter your name', - controlId: 'joinFormName', - hookForm: { - id: 'name', - }, - }, - accessCode: { - label: 'Access Code', - placeHolder: 'Enter the access code', - controlId: 'joinFormCode', - hookForm: { - id: 'access_code', - }, - }, - recordingConsent: { - label: 'I acknowledge that this session may be recorded. This may include my voice and video if enabled.', - controlId: 'consentCheck', - hookForm: { - id: 'consent', - validations: { - shouldUnregister: true, - }, - }, - }, -}; diff --git a/app/javascript/hooks/forms/rooms/useRoomJoinForm.js b/app/javascript/hooks/forms/rooms/useRoomJoinForm.js new file mode 100644 index 0000000000..241a58a2a2 --- /dev/null +++ b/app/javascript/hooks/forms/rooms/useRoomJoinForm.js @@ -0,0 +1,87 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import * as yup from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useTranslation } from 'react-i18next'; +import { useForm } from 'react-hook-form'; +import { useCallback, useMemo } from 'react'; + +export function useRoomJoinFormValidation() { + const { t } = useTranslation(); + return useMemo(() => (yup.object({ + name: yup.string().required(t('forms.validations.room_join.name.required')), + access_code: yup.string(), + consent: yup.boolean().oneOf([true], ''), + })), [t]); +} + +export default function useRoomJoinForm({ defaultValues: _defaultValues, ..._config } = {}) { + const { t, i18n } = useTranslation(); + + const fields = useMemo(() => ({ + name: { + label: t('forms.room_join.fields.name.label'), + placeHolder: t('forms.room_join.fields.name.placeholder'), + controlId: 'joinFormName', + hookForm: { + id: 'name', + }, + }, + accessCode: { + label: t('forms.room_join.fields.access_code.label'), + placeHolder: t('forms.room_join.fields.access_code.placeholder'), + controlId: 'joinFormCode', + hookForm: { + id: 'access_code', + }, + }, + recordingConsent: { + label: t('forms.room_join.fields.recording_consent.label'), + controlId: 'consentCheck', + hookForm: { + id: 'consent', + validations: { + shouldUnregister: true, + }, + }, + }, + }), [i18n.resolvedLanguage]); + + const validationSchema = useRoomJoinFormValidation(); + + const config = useMemo(() => ({ + ...{ + mode: 'onSubmit', + criteriaMode: 'firstError', + defaultValues: { + ...{ + name: '', + access_code: '', + }, + ..._defaultValues, + }, + resolver: yupResolver(validationSchema), + }, + ..._config, + }), [validationSchema, _defaultValues]); + + const methods = useForm(config); + + const reset = useCallback(() => methods.reset(config.defaultValues), [methods.reset, config.defaultValues]); + + return { methods, fields, reset }; +} diff --git a/app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx b/app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx new file mode 100644 index 0000000000..b15ca62083 --- /dev/null +++ b/app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx @@ -0,0 +1,37 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import { useMutation } from 'react-query'; +import { toast } from 'react-toastify'; +import { useTranslation } from 'react-i18next'; +import axios from '../../../helpers/Axios'; + +export default function useCopyRecordingUrl() { + const { t } = useTranslation(); + + return useMutation( + (data) => axios.post('/recordings/recording_url.json', { id: data.record_id }) + .then((resp) => resp.data), + { + onSuccess: (url) => { + navigator.clipboard.writeText(url?.join('\n')).then(() => toast.success(t('toast.success.recording.copied_urls'))); + }, + onError: () => { + toast.error(t('toast.error.problem_completing_action')); + }, + }, + ); +} diff --git a/app/javascript/hooks/mutations/recordings/useRedirectRecordingUrl.jsx b/app/javascript/hooks/mutations/recordings/useRedirectRecordingUrl.jsx new file mode 100644 index 0000000000..df48ff1c5e --- /dev/null +++ b/app/javascript/hooks/mutations/recordings/useRedirectRecordingUrl.jsx @@ -0,0 +1,37 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import { useMutation } from 'react-query'; +import { toast } from 'react-toastify'; +import { useTranslation } from 'react-i18next'; +import axios from '../../../helpers/Axios'; + +export default function useRedirectRecordingUrl() { + const { t } = useTranslation(); + + return useMutation( + (data) => axios.post('/recordings/recording_url.json', { id: data.record_id, recording_format: data.format }) + .then((resp) => resp.data.data), + { + onSuccess: (url) => { + window.open(url, '_blank'); + }, + onError: () => { + toast.error(t('toast.error.problem_completing_action')); + }, + }, + ); +} diff --git a/app/javascript/hooks/mutations/recordings/useUpdateRecordingVisibility.jsx b/app/javascript/hooks/mutations/recordings/useUpdateRecordingVisibility.jsx index 2351410257..f648844767 100644 --- a/app/javascript/hooks/mutations/recordings/useUpdateRecordingVisibility.jsx +++ b/app/javascript/hooks/mutations/recordings/useUpdateRecordingVisibility.jsx @@ -28,6 +28,8 @@ export default function useUpdateRecordingVisibility() { { onSuccess: () => { queryClient.invalidateQueries(['getRecordings']); + queryClient.invalidateQueries(['getRoomRecordings']); + queryClient.invalidateQueries(['getServerRecordings']); toast.success(t('toast.success.recording.recording_visibility_updated')); }, onError: () => { diff --git a/app/javascript/hooks/queries/recordings/usePublicRecordings.jsx b/app/javascript/hooks/queries/recordings/usePublicRecordings.jsx new file mode 100644 index 0000000000..a8e93900b7 --- /dev/null +++ b/app/javascript/hooks/queries/recordings/usePublicRecordings.jsx @@ -0,0 +1,37 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import { useQuery } from 'react-query'; +import { useSearchParams } from 'react-router-dom'; +import axios from '../../../helpers/Axios'; + +export default function usePublicRecordings({ friendlyId, search, page }) { + const [searchParams] = useSearchParams(); + const params = { + 'sort[column]': searchParams.get('sort[column]'), + 'sort[direction]': searchParams.get('sort[direction]'), + search, + page, + }; + + return useQuery( + ['getPublicRecordings', { ...params }], + () => axios.get(`/rooms/${friendlyId}/public_recordings.json`, { params }).then((resp) => resp.data), + { + keepPreviousData: true, + }, + ); +} diff --git a/app/javascript/hooks/queries/rooms/useRoomConfigValue.jsx b/app/javascript/hooks/queries/rooms/useRoomConfigValue.jsx new file mode 100644 index 0000000000..6a4c038838 --- /dev/null +++ b/app/javascript/hooks/queries/rooms/useRoomConfigValue.jsx @@ -0,0 +1,25 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import { useQuery } from 'react-query'; +import axios from '../../../helpers/Axios'; + +export default function useRoomConfigValue(name) { + return useQuery( + ['getRoomsConfigValue', name], + () => axios.get(`/rooms_configurations/${name}.json`).then((resp) => resp.data.data), + ); +} diff --git a/app/javascript/main.jsx b/app/javascript/main.jsx index e3c39ea5a9..aa132f73e2 100644 --- a/app/javascript/main.jsx +++ b/app/javascript/main.jsx @@ -51,6 +51,7 @@ import PendingRegistration from './components/users/registration/PendingRegistra import RootBoundary from './RootBoundary'; import Tenants from './components/admin/tenants/Tenants'; import RoomIdRouter from './routes/RoomIdRouter'; +import PublicRecordings from './components/rooms/room/public_recordings/PublicRecordings'; const queryClientConfig = { defaultOptions: { @@ -101,6 +102,7 @@ const router = createBrowserRouter( } /> + } /> } /> , ), diff --git a/app/models/recording.rb b/app/models/recording.rb index 2de6320088..95b59ef658 100644 --- a/app/models/recording.rb +++ b/app/models/recording.rb @@ -17,6 +17,14 @@ # frozen_string_literal: true class Recording < ApplicationRecord + VISIBILITIES = { + published: 'Published', + unpublished: 'Unpublished', + protected: 'Protected', + public: 'Public', + public_protected: 'Public/Protected' + }.freeze + belongs_to :room has_one :user, through: :room has_many :formats, dependent: :destroy @@ -26,9 +34,12 @@ class Recording < ApplicationRecord validates :visibility, presence: true validates :length, presence: true validates :participants, presence: true + validates :visibility, inclusion: VISIBILITIES.values scope :with_provider, ->(current_provider) { where(user: { provider: current_provider }) } + after_destroy :destroy_bbb_recording + def self.search(input) if input return joins(:formats).where('recordings.name ILIKE :input OR recordings.visibility ILIKE :input OR formats.recording_type ILIKE :input', @@ -37,4 +48,32 @@ def self.search(input) all.includes(:formats) end + + def self.public_search(input) + if input + return joins(:formats).where('recordings.name ILIKE :input OR formats.recording_type ILIKE :input', + input: "%#{input}%").includes(:formats) + end + + all.includes(:formats) + end + + def self.server_search(input) + if input + return joins(:formats) + .where('recordings.name ILIKE :input OR ' \ + 'recordings.visibility ILIKE :input OR ' \ + 'formats.recording_type ILIKE :input OR ' \ + '"user"."name" ILIKE :input', input: "%#{input}%") + .includes(:formats) + end + + all.includes(:formats) + end + + private + + def destroy_bbb_recording + BigBlueButtonApi.new(provider: user.provider).delete_recordings(record_ids: record_id) + end end diff --git a/app/models/room.rb b/app/models/room.rb index 380a18e666..cad804f6a2 100644 --- a/app/models/room.rb +++ b/app/models/room.rb @@ -32,8 +32,8 @@ class Room < ApplicationRecord validates :meeting_id, presence: true, uniqueness: true validates :voice_bridge, uniqueness: true validates :presentation, - content_type: %i[.doc .docx .ppt .pptx .pdf .xls .xlsx .txt .rtf .odt .ods .odp .odg .odc .odi .jpg .jpeg .png], - size: { less_than: 30.megabytes } + content_type: Rails.configuration.uploads[:presentations][:formats], + size: { less_than: Rails.configuration.uploads[:presentations][:max_size] } validates :name, length: { minimum: 2, maximum: 255 } validates :recordings_processing, numericality: { only_integer: true, greater_than_or_equal_to: 0 } @@ -76,6 +76,10 @@ def create_meeting_options end end + def public_recordings + recordings.where(visibility: [Recording::VISIBILITIES[:public], Recording::VISIBILITIES[:public_protected]]) + end + private def set_friendly_id diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 4d133c3164..a4c5923abd 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -17,14 +17,18 @@ # frozen_string_literal: true class SiteSetting < ApplicationRecord - belongs_to :setting - - validates :provider, presence: true - has_one_attached :image - REGISTRATION_METHODS = { open: 'open', invite: 'invite', approval: 'approval' }.freeze + + belongs_to :setting + + has_one_attached :image + + validates :provider, presence: true + validates :image, + content_type: Rails.configuration.uploads[:images][:formats], + size: { less_than: Rails.configuration.uploads[:images][:max_size] } end diff --git a/app/models/user.rb b/app/models/user.rb index 68d1e909f3..3a28eb2da8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -17,7 +17,6 @@ # frozen_string_literal: true class User < ApplicationRecord - MAX_AVATAR_SIZE = 3_000_000 # Reset token max validity period. # It's advised to not increase this to more than 1 hour. RESET_TOKEN_VALIDITY_PERIOD = 1.hour @@ -60,8 +59,8 @@ class User < ApplicationRecord validates :avatar, dimension: { width: 300, height: 300 }, - content_type: %i[png jpg jpeg svg], - size: { less_than: 3.megabytes } + content_type: Rails.configuration.uploads[:images][:formats], + size: { less_than: Rails.configuration.uploads[:images][:max_size] } validates :reset_digest, uniqueness: true, if: :reset_digest? validates :verification_digest, uniqueness: true, if: :verification_digest? diff --git a/app/serializers/public_recording_serializer.rb b/app/serializers/public_recording_serializer.rb new file mode 100644 index 0000000000..bd76270f93 --- /dev/null +++ b/app/serializers/public_recording_serializer.rb @@ -0,0 +1,27 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# frozen_string_literal: true + +class PublicRecordingSerializer < ApplicationSerializer + attributes :id, :record_id, :name, :length, :recorded_at + + has_many :formats + + def formats + object.formats.filter { |format| format.recording_type != 'statistics' } + end +end diff --git a/app/serializers/server_recording_serializer.rb b/app/serializers/server_recording_serializer.rb new file mode 100644 index 0000000000..d30a102d4b --- /dev/null +++ b/app/serializers/server_recording_serializer.rb @@ -0,0 +1,25 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# frozen_string_literal: true + +class ServerRecordingSerializer < RecordingSerializer + attributes :user_name + + def user_name + object.user.name + end +end diff --git a/app/services/big_blue_button_api.rb b/app/services/big_blue_button_api.rb index 76a2cbe967..89a3038421 100644 --- a/app/services/big_blue_button_api.rb +++ b/app/services/big_blue_button_api.rb @@ -95,7 +95,7 @@ def update_recordings(record_id:, meta_hash:) # Decodes the JWT using the BBB secret as key (Used in Recording Ready Callback) def decode_jwt(token) - JWT.decode token, Rails.configuration.bigbluebutton_secret, true, { algorithm: 'HS256' } + JWT.decode token, @secret, true, { algorithm: 'HS256' } end private diff --git a/app/services/permissions_checker.rb b/app/services/permissions_checker.rb index 39f8783305..e213688d3f 100644 --- a/app/services/permissions_checker.rb +++ b/app/services/permissions_checker.rb @@ -16,6 +16,7 @@ # frozen_string_literal: true +# rubocop:disable Metrics/CyclomaticComplexity class PermissionsChecker def initialize(current_user:, permission_names:, current_provider:, user_id: nil, friendly_id: nil, record_id: nil) @current_user = current_user @@ -26,20 +27,29 @@ def initialize(current_user:, permission_names:, current_provider:, user_id: nil @current_provider = current_provider end + # The permission checker checks if: + # 1. A current_user exists in the context: + # 1.1. Checks if the current_user is a SuperAdmin and allow it without further checks. + # 1.2. Ensures that the current_user provider and the target resource provider matches. + # 1.3. Ensures that the the current_user is active. + # 1.4. Checks if the current_user is allowed to access a whole class of resources AKA admin (has at least one permission enabled for its role). + # 2. Checks if the current_user is not an admin but allowed to access that specific target resource. def call - # check to see if current_user has SuperAdmin role - return true if @current_user.role == Role.find_by(name: 'SuperAdmin', provider: 'bn') - - # checking if user is trying to access users/rooms/recordings from different provider - return false unless current_provider_check - # Make sure the user is not banned or pending - return false unless @current_user.active? - - return true if RolePermission.joins(:permission).exists?( - role_id: @current_user.role_id, - permission: { name: @permission_names }, - value: 'true' - ) + if @current_user + # check to see if current_user has SuperAdmin role + return true if @current_user.role == Role.find_by(name: 'SuperAdmin', provider: 'bn') + + # checking if user is trying to access users/rooms/recordings from different provider + return false unless current_provider_check + # Make sure the user is not banned or pending + return false unless @current_user.active? + + return true if RolePermission.joins(:permission).exists?( + role_id: @current_user.role_id, + permission: { name: @permission_names }, + value: 'true' + ) + end # convert to array if only checking for 1 permission (for simplicity) Array(@permission_names).each do |permission| @@ -54,6 +64,8 @@ def call return true if authorize_manage_recordings when 'RoomLimit' return true if authorize_room_limit + when 'PublicRecordings' + return true if authorize_public_recordings end end @@ -62,31 +74,53 @@ def call private + # Ensures that the current_user is requesting access to manage its account specifically. def authorize_manage_user + return false if @current_user.blank? return false if @user_id.blank? @current_user.id.to_s == @user_id.to_s end + # Ensures that the current_user is requesting access to manage one of its rooms specifically. def authorize_manage_rooms + return false if @current_user.blank? return false if @friendly_id.blank? @current_user.rooms.find_by(friendly_id: @friendly_id).present? end + # Ensures that the current_user is requesting access to manage a shared room specifically. def authorize_shared_room + return false if @current_user.blank? return false if @friendly_id.blank? && @record_id.blank? @current_user.shared_rooms.exists?(friendly_id: @friendly_id) || @current_user.shared_rooms.exists?(id: Recording.find_by(record_id: @record_id)&.room_id) end + # Ensures that the current_user is requesting access to manage its rooms recordings specifically. def authorize_manage_recordings + return false if @current_user.blank? return false if @record_id.blank? @current_user.recordings.find_by(record_id: @record_id).present? end + # Ensures that the request is to access and manage a publicly accessible recording. + def authorize_public_recordings + return false if @record_id.blank? + return false if @current_provider.blank? + + recording = Recording.find_by(record_id: @record_id) + return false unless recording + return false if recording.user.provider != @current_provider + return false unless [Recording::VISIBILITIES[:public], Recording::VISIBILITIES[:public_protected]].include? recording.visibility + + true + end + + # Checks if the target user has reached its role room limit. def authorize_room_limit return false if @user_id.blank? @@ -99,7 +133,10 @@ def authorize_room_limit true end + # Checks if the current_user is requsting to access a resource of the same provider. def current_provider_check + return false if @current_user.blank? || @current_user.provider.blank? || @current_provider.blank? + # check to see if current user is trying to access another provider return false if @current_user.provider != @current_provider @@ -115,3 +152,4 @@ def current_provider_check true end end +# rubocop:enable Metrics/CyclomaticComplexity diff --git a/app/services/provider_credentials.rb b/app/services/provider_credentials.rb index 95d5804828..6607005db9 100644 --- a/app/services/provider_credentials.rb +++ b/app/services/provider_credentials.rb @@ -30,7 +30,7 @@ def call # Cache the response for an hour # fetch will return the value if already cached, if not, it will compute the value, cache it, then return it - Rails.cache.fetch("#{@provider}/#{@route}", expires_in: 1.hour) do + Rails.cache.fetch("v3/#{@provider}/#{@route}", expires_in: 1.hour) do url = URI.parse("#{@endpoint}#{@route}?#{encoded_params}&checksum=#{checksum}") res = Net::HTTP.get_response(url) diff --git a/app/services/recording_creator.rb b/app/services/recording_creator.rb index b3cf2bb974..cde51b1376 100644 --- a/app/services/recording_creator.rb +++ b/app/services/recording_creator.rb @@ -23,7 +23,10 @@ def initialize(recording:) def call meeting_id = @recording[:metadata][:meetingId] || @recording[:meetingID] - room_id = Room.find_by(meeting_id:).id + room_id = Room.find_by(meeting_id:)&.id + + raise ActiveRecord::RecordNotFound if room_id.nil? + visibility = get_recording_visibility(recording: @recording) # Get length of presentation format(s) @@ -49,11 +52,15 @@ def call # Returns the visibility of the recording (published, unpublished or protected) def get_recording_visibility(recording:) - return 'Protected' if recording[:protected].to_s == 'true' + list = recording[:metadata][:'gl-listed'].to_s == 'true' + protect = recording[:protected].to_s == 'true' + publish = recording[:published].to_s == 'true' + + visibility = visibility_for(publish:, protect:, list:) - return 'Published' if recording[:published].to_s == 'true' + return visibility unless visibility.nil? - 'Unpublished' + Recording::VISIBILITIES[:unpublished] end # Returns the length of presentation recording for the recording given @@ -80,4 +87,17 @@ def create_formats(recording:, new_recording:) url: recording[:playback][:format][:url]) end end + + # Visibilitiy Map + def visibility_for(publish:, protect:, list:) + params_for = { + { publish: false, protect: false, list: false } => Recording::VISIBILITIES[:unpublished], + { publish: true, protect: false, list: false } => Recording::VISIBILITIES[:published], + { publish: true, protect: false, list: true } => Recording::VISIBILITIES[:public], + { publish: true, protect: true, list: false } => Recording::VISIBILITIES[:protected], + { publish: true, protect: true, list: true } => Recording::VISIBILITIES[:public_protected] + } + + params_for[{ publish:, protect:, list: }] + end end diff --git a/app/services/running_meeting_checker.rb b/app/services/running_meeting_checker.rb index 67902d8ed7..57c18b8d85 100644 --- a/app/services/running_meeting_checker.rb +++ b/app/services/running_meeting_checker.rb @@ -16,21 +16,23 @@ # frozen_string_literal: true -# Pass the room(s) to the service and it will confirm if the meeting is online or not and will return the # of participants +# Pass the online rooms to the service and it will confirm if the meeting is running or not and return the # of participants class RunningMeetingChecker - def initialize(rooms:, provider:) + def initialize(rooms:) @rooms = rooms - @provider = provider end def call - online_rooms = Array(@rooms).select { |room| room.online == true } + Array(@rooms).each do |room| + next unless room.online - online_rooms.each do |online_room| - bbb_meeting = BigBlueButtonApi.new(provider: @provider).get_meeting_info(meeting_id: online_room.meeting_id) - online_room.participants = bbb_meeting[:participantCount] - rescue BigBlueButton::BigBlueButtonException - online_room.update(online: false) + begin + bbb_meeting = BigBlueButtonApi.new(provider: room.user.provider).get_meeting_info(meeting_id: room.meeting_id) + room.participants = bbb_meeting[:participantCount] + rescue BigBlueButton::BigBlueButtonException + room.update(online: false) + next + end end end end diff --git a/app/services/tenant_setup.rb b/app/services/tenant_setup.rb new file mode 100644 index 0000000000..d1754bae91 --- /dev/null +++ b/app/services/tenant_setup.rb @@ -0,0 +1,119 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# frozen_string_literal: true + +# Creates the roles, site settings, and room config options for the given tenant. +class TenantSetup + def initialize(provider) + @provider = provider + end + + def call + create_roles + create_site_settings + create_rooms_configs_options + create_role_permissions + end + + def create_roles + Role.create! [ + { name: 'Administrator', provider: @provider }, + { name: 'User', provider: @provider }, + { name: 'Guest', provider: @provider } + ] + end + + def create_site_settings + SiteSetting.create! [ + { setting: Setting.find_by(name: 'PrimaryColor'), value: '#467fcf', provider: @provider }, + { setting: Setting.find_by(name: 'PrimaryColorLight'), value: '#e8eff9', provider: @provider }, + { setting: Setting.find_by(name: 'PrimaryColorDark'), value: '#316cbe', provider: @provider }, + { setting: Setting.find_by(name: 'BrandingImage'), + value: ActionController::Base.helpers.image_path('bbb_logo.png'), + provider: @provider }, + { setting: Setting.find_by(name: 'Terms'), value: '', provider: @provider }, + { setting: Setting.find_by(name: 'PrivacyPolicy'), value: '', provider: @provider }, + { setting: Setting.find_by(name: 'RegistrationMethod'), value: SiteSetting::REGISTRATION_METHODS[:open], + provider: @provider }, + { setting: Setting.find_by(name: 'ShareRooms'), value: 'true', provider: @provider }, + { setting: Setting.find_by(name: 'PreuploadPresentation'), value: 'true', provider: @provider }, + { setting: Setting.find_by(name: 'RoleMapping'), value: '', provider: @provider }, + { setting: Setting.find_by(name: 'DefaultRole'), provider: @provider, value: 'User' } + ] + end + + def create_rooms_configs_options + RoomsConfiguration.create! [ + { meeting_option: MeetingOption.find_by(name: 'record'), value: 'default_enabled', provider: @provider }, + { meeting_option: MeetingOption.find_by(name: 'muteOnStart'), value: 'optional', provider: @provider }, + { meeting_option: MeetingOption.find_by(name: 'guestPolicy'), value: 'optional', provider: @provider }, + { meeting_option: MeetingOption.find_by(name: 'glAnyoneCanStart'), value: 'optional', provider: @provider }, + { meeting_option: MeetingOption.find_by(name: 'glAnyoneJoinAsModerator'), value: 'optional', provider: @provider }, + { meeting_option: MeetingOption.find_by(name: 'glRequireAuthentication'), value: 'optional', provider: @provider }, + { meeting_option: MeetingOption.find_by(name: 'glViewerAccessCode'), value: 'optional', provider: @provider }, + { meeting_option: MeetingOption.find_by(name: 'glModeratorAccessCode'), value: 'optional', provider: @provider } + ] + end + + def create_role_permissions + admin = Role.find_by(name: 'Administrator', provider: @provider) + user = Role.find_by(name: 'User', provider: @provider) + guest = Role.find_by(name: 'Guest', provider: @provider) + + create_room = Permission.find_by(name: 'CreateRoom') + manage_users = Permission.find_by(name: 'ManageUsers') + manage_rooms = Permission.find_by(name: 'ManageRooms') + manage_recordings = Permission.find_by(name: 'ManageRecordings') + manage_site_settings = Permission.find_by(name: 'ManageSiteSettings') + manage_roles = Permission.find_by(name: 'ManageRoles') + shared_list = Permission.find_by(name: 'SharedList') + can_record = Permission.find_by(name: 'CanRecord') + room_limit = Permission.find_by(name: 'RoomLimit') + + RolePermission.create! [ + { role: admin, permission: create_room, value: 'true' }, + { role: admin, permission: manage_users, value: 'true' }, + { role: admin, permission: manage_rooms, value: 'true' }, + { role: admin, permission: manage_recordings, value: 'true' }, + { role: admin, permission: manage_site_settings, value: 'true' }, + { role: admin, permission: manage_roles, value: 'true' }, + { role: admin, permission: shared_list, value: 'true' }, + { role: admin, permission: can_record, value: 'true' }, + { role: admin, permission: room_limit, value: '100' }, + + { role: user, permission: create_room, value: 'true' }, + { role: user, permission: manage_users, value: 'false' }, + { role: user, permission: manage_rooms, value: 'false' }, + { role: user, permission: manage_recordings, value: 'false' }, + { role: user, permission: manage_site_settings, value: 'false' }, + { role: user, permission: manage_roles, value: 'false' }, + { role: user, permission: shared_list, value: 'true' }, + { role: user, permission: can_record, value: 'true' }, + { role: user, permission: room_limit, value: '100' }, + + { role: guest, permission: create_room, value: 'false' }, + { role: guest, permission: manage_users, value: 'false' }, + { role: guest, permission: manage_rooms, value: 'false' }, + { role: guest, permission: manage_recordings, value: 'false' }, + { role: guest, permission: manage_site_settings, value: 'false' }, + { role: guest, permission: manage_roles, value: 'false' }, + { role: guest, permission: shared_list, value: 'true' }, + { role: guest, permission: can_record, value: 'true' }, + { role: guest, permission: room_limit, value: '100' } + ] + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 472c057dfc..4da8e188c4 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -20,8 +20,8 @@ BigBlueButton - - + + <%= csrf_meta_tags %> diff --git a/bin/config.env b/bin/config.env new file mode 100644 index 0000000000..1b34376923 --- /dev/null +++ b/bin/config.env @@ -0,0 +1,17 @@ +# Set default port if PORT is not set +PORT="${PORT:=3000}" + +# Parse Rails DATABASE and REDIS urls to get host and port +TXADDR=${DATABASE_URL/*:\/\/} +TXADDR=${TXADDR/*@/} +TXADDR=${TXADDR/\/*/} +IFS=: TXADDR=($TXADDR) IFS=' ' +PGHOST=${TXADDR[0]} +PGPORT=${TXADDR[1]:-5432} + +TXADDR=${REDIS_URL/*:\/\/} +TXADDR=${TXADDR/*@/} +TXADDR=${TXADDR/\/*/} +IFS=: TXADDR=($TXADDR) IFS=' ' +RDHOST=${TXADDR[0]} +RDPORT=${TXADDR[1]:-6379} diff --git a/bin/start b/bin/start index ea22a6f3a0..4e9ef9d0c0 100755 --- a/bin/start +++ b/bin/start @@ -1,29 +1,14 @@ #!/usr/bin/env bash -PORT="${PORT:=3000}" - -# Parse Rails DATABASE and REDIS urls to get host and port -TXADDR=${DATABASE_URL/*:\/\/} -TXADDR=${TXADDR/*@/} -TXADDR=${TXADDR/\/*/} -IFS=: TXADDR=($TXADDR) IFS=' ' -PGHOST=${TXADDR[0]} -PGPORT=${TXADDR[1]:-5432} - -TXADDR=${REDIS_URL/*:\/\/} -TXADDR=${TXADDR/*@/} -TXADDR=${TXADDR/\/*/} -IFS=: TXADDR=($TXADDR) IFS=' ' -RDHOST=${TXADDR[0]} -RDPORT=${TXADDR[1]:-6379} +source config.env echo "Greenlight-v3 starting on port: $PORT" -echo $PGHOST -echo $PGPORT +echo "Postgres host: $PGHOST" +echo "Postgres port: $PGPORT" -echo $RDHOST -echo $RDPORT +echo "Redis host: $RDHOST" +echo "Redis port: $RDPORT" if [ "$RAILS_ENV" = "production" ]; then while ! nc -zw3 $PGHOST $PGPORT diff --git a/config/application.rb b/config/application.rb index f9e15cb3d6..9f876b403c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -50,7 +50,20 @@ class Application < Rails::Application room_limit: 'RoomLimitError', pending_user: 'PendingUser', banned_user: 'BannedUser', - unverified_user: 'UnverifiedUser' + unverified_user: 'UnverifiedUser', + external_signup_error: 'SignupError', + unauthorized: 'Unauthorized' + } + + config.uploads = { + images: { + max_size: 3.megabytes, + formats: %i[png jpg jpeg svg] + }, + presentations: { + max_size: 30.megabytes, + formats: %i[.doc .docx .ppt .pptx .pdf .xls .xlsx .txt .rtf .odt .ods .odp .odg .odc .odi .jpg .jpeg .png] + } } ActiveModelSerializers.config.adapter = :json diff --git a/config/environments/development.rb b/config/environments/development.rb index c4f401ba61..0f1121f34b 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -52,7 +52,15 @@ end # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local + config.active_storage.service = if ENV['S3_ACCESS_KEY_ID'].present? && ENV['S3_ENDPOINT'].present? + :s3 + elsif ENV['S3_ACCESS_KEY_ID'].present? + :amazon + elsif ENV['GCS_PROJECT'].present? + :google + else + :local + end if ENV['SMTP_SERVER'].present? config.action_mailer.perform_deliveries = true diff --git a/config/environments/production.rb b/config/environments/production.rb index caa5f45454..9e785e5097 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -60,6 +60,8 @@ :s3 elsif ENV['S3_ACCESS_KEY_ID'].present? :amazon + elsif ENV['GCS_PROJECT'].present? + :google else :local end @@ -103,7 +105,7 @@ # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). - config.log_level = :info + config.log_level = ENV['LOG_LEVEL'] || :info # Prepend all log lines with the following tags. config.log_tags = [:request_id] @@ -134,11 +136,31 @@ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") if ENV['RAILS_LOG_TO_STDOUT'].present? + $stdout.sync = true logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end + if ENV['RAILS_LOG_REMOTE_NAME'] && ENV['RAILS_LOG_REMOTE_PORT'] + require 'remote_syslog_logger' + logger_program = ENV['RAILS_LOG_REMOTE_TAG'] || "greenlight-v3-#{ENV.fetch('RAILS_ENV', nil)}" + logger = RemoteSyslogLogger.new(ENV['RAILS_LOG_REMOTE_NAME'], ENV['RAILS_LOG_REMOTE_PORT'], program: logger_program) + end + + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + + # Use Lograge for logging + config.lograge.enabled = true + config.lograge.custom_options = lambda do |event| + { time: Time.zone.now, host: event.payload[:host] } + end + + config.lograge.ignore_actions = ['HealthChecksController#check', + 'ApplicationCable::Connection#connect', 'RoomsChannel#subscribe', + 'ApplicationCable::Connection#disconnect', 'RoomsChannel#unsubscribe'] + # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false end diff --git a/config/initializers/actioncable.rb b/config/initializers/actioncable.rb new file mode 100644 index 0000000000..c4be484aae --- /dev/null +++ b/config/initializers/actioncable.rb @@ -0,0 +1,24 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# frozen_string_literal: true + +require 'action_cable/subscription_adapter/redis' + +ActionCable::SubscriptionAdapter::Redis.redis_connector = lambda { |config| + config[:id] = nil + Redis.new(config.except(:adapter, :channel_prefix)) +} diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 00d13d6ba7..9567c50278 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -18,8 +18,31 @@ Rails.application.config.middleware.use OmniAuth::Builder do issuer = ENV.fetch('OPENID_CONNECT_ISSUER', '') + lb = ENV.fetch('LOADBALANCER_ENDPOINT', '') - if issuer.present? + if lb.present? + provider :openid_connect, setup: lambda { |env| + request = Rack::Request.new(env) + current_provider = request.params['current_provider'] || request.host&.split('.')&.first + secret = Tenant.find_by(name: current_provider)&.client_secret + issuer_url = File.join issuer.to_s, "/#{current_provider}" + + env['omniauth.strategy'].options[:issuer] = issuer_url + env['omniauth.strategy'].options[:scope] = %i[openid email profile] + env['omniauth.strategy'].options[:uid_field] = ENV.fetch('OPENID_CONNECT_UID_FIELD', 'preferred_username') + env['omniauth.strategy'].options[:discovery] = true + env['omniauth.strategy'].options[:client_options].identifier = ENV.fetch('OPENID_CONNECT_CLIENT_ID') + env['omniauth.strategy'].options[:client_options].secret = secret + env['omniauth.strategy'].options[:client_options].redirect_uri = File.join( + File.join('https://', "#{current_provider}.#{ENV.fetch('OPENID_CONNECT_REDIRECT', '')}", 'auth', 'openid_connect', 'callback') + ) + env['omniauth.strategy'].options[:client_options].authorization_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'auth') + env['omniauth.strategy'].options[:client_options].token_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'token') + env['omniauth.strategy'].options[:client_options].userinfo_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'userinfo') + env['omniauth.strategy'].options[:client_options].jwks_uri = File.join(issuer_url, 'protocol', 'openid-connect', 'certs') + env['omniauth.strategy'].options[:client_options].end_session_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'logout') + } + elsif issuer.present? provider :openid_connect, issuer:, scope: %i[openid email profile], diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 15e7fe96ad..a2d57909d9 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -72,3 +72,5 @@ ar: reset_password: إعادة تعيين كلمة المرور link_expires: ستنتهي صلاحية الرابط خلال ساعة واحدة. ignore_request: إذا لم تقدم طلبًا لتغيير كلمة المرور الخاصة بك ، فيرجى تجاهل هذا البريد الإلكتروني. + room: + new_room_name: "%{username} غرفة" diff --git a/config/locales/de.yml b/config/locales/de.yml index a7c343cf20..15d8af432c 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -47,28 +47,28 @@ de: opengraph: - description: "Erfahren Sie mehr über BigBlueButton, die bewährte Open-Source-Lösung für Webkonferenzen, die eine nahtlose virtuelle Zusammenarbeit und Online-Lernerfahrungen ermöglicht." + description: "Erfahre mehr über BigBlueButton, die bewährte Open-Source-Lösung für Webkonferenzen, die eine nahtlose virtuelle Zusammenarbeit und Online-Lernerfahrungen ermöglicht." meeting: - moderator_message: "Um jemanden zur Konferenz einzuladen, schicken Sie ihm diesen Link:" + moderator_message: "Um jemanden zur Konferenz einzuladen, verschicke diesen Link:" access_code: "Zugangscode: %{code}" email: activation: account_activation: Kontoaktivierung welcome_to_bbb: Willkommen in BigBlueButton! - get_started: "Um loszulegen, aktivieren Sie bitte Ihr Konto, indem Sie auf die Schaltfläche unten klicken." + get_started: Klicke auf die Schaltfläche um das Konto zu aktivieren. activate_account: Konto aktivieren link_expires: Der Link wird in 24 Stunden ungültig. - if_link_expires: "Wenn der Link abläuft, melden Sie sich bitte an und fordern Sie eine neue E-Mail zur Aktivierung an." + if_link_expires: "Sollte der Link abgelaufen sein, dann melde dich bitte an und fordere eine neue Aktivierungs-EMail an." invitation: invitation_to_join: BigBlueButton-Einladung - you_have_been_invited: "Sie wurden von %{name} eingeladen, ein BigBlueButton-Konto zu erstellen." - get_started: "Um sich zu registrieren, klicken Sie bitte auf die Schaltfläche unten und folgen Sie den Schritten." - valid_invitation: Die Einladung ist 48 Stunden gültig. + you_have_been_invited: "Du wurdest von %{name} eingeladen, ein BigBlueButton-Konto zu erstellen." + get_started: "Um dich zu registrieren, klicken bitte auf diese Schaltfläche." + valid_invitation: Die Einladung ist 24 Stunden gültig. sign_up: Registrieren reset: password_reset: Passwort zurücksetzen password_reset_requested: "Eine Passwortrücksetzung wurde für %{email} beantragt." - password_reset_confirmation: "Um Ihr Passwort zurückzusetzen, klicken Sie bitte auf die Schaltfläche unten." + password_reset_confirmation: "Um das Passwort zurückzusetzen, klicke bitte auf die Schaltfläche unten." reset_password: Passwort zurücksetzen link_expires: Der Link wird in 1 Stunde ungültig. - ignore_request: "Wenn Sie keinen Antrag zur Änderung Ihres Passworts gestellt haben, ignorieren Sie bitte diese E-Mail." + ignore_request: "Wenn du dein Passwort nicht ändern wolltest, dann ignoriere bitte diese E-Mail." diff --git a/config/locales/el.yml b/config/locales/el.yml index b5ea98015b..3c5b7a7db4 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -72,3 +72,5 @@ el: reset_password: Επαναφορά κωδικού πρόσβασης link_expires: Ο σύνδεσμος θα λήξει σε 1 ώρα. ignore_request: "Εάν δεν υποβάλατε αίτημα αλλαγής κωδικού πρόσβασης, παρακαλούμε αγνοήστε αυτό το email." + room: + new_room_name: "Αίθουσα του %{username}" diff --git a/config/locales/el_GR.yml b/config/locales/el_GR.yml new file mode 100644 index 0000000000..70e1f3671f --- /dev/null +++ b/config/locales/el_GR.yml @@ -0,0 +1,76 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# "true": "foo" +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +el_GR: + opengraph: + description: "Εκπαιδευτείτε με χρήση του BigBlueButton, την αξιόπιστη λύση διαδικτυακής διάσκεψης ανοιχτού κώδικα που επιτρέπει την απρόσκοπτη εικονική συνεργασία και τη διαδικτυακή εμπειρία εκπαίδευσης." + meeting: + moderator_message: "Για να προσκαλέσετε κάποιον στη διάσκεψη, στείλτε τον σύνδεσμο:" + access_code: "Κωδικός πρόσβασης: %{code}" + email: + activation: + account_activation: Ενεργοποίηση λογαριασμού + welcome_to_bbb: Καλώς ορίσατε στο BigBlueButton! + get_started: "Για να ξεκινήσετε, παρακαλούμε ενεργοποιήστε τον λογαριασμό σας κάνοντας κλικ στο κουμπί παρακάτω." + activate_account: Ενεργοποίηση λογαριασμού + link_expires: Ο σύνδεσμος θα λήξει σε 24 ώρες. + if_link_expires: "Εάν έληξε ο σύνδεσμος, συνδεθείτε και ζητήστε νέο email ενεργοποίησης." + invitation: + invitation_to_join: Πρόσκληση BigBlueButton + you_have_been_invited: "Έχετε προσκληθεί από τον/την %{name}, να δημιουργήσετε έναν λογαριασμό BigBlueButton." + get_started: "Για να εγγραφείτε, παρακαλούμε κάντε κλικ στο κουμπί παρακάτω και ακολουθήστε τα βήματα." + valid_invitation: Η πρόσκληση ισχύει για 24 ώρες. + sign_up: Εγγραφή + reset: + password_reset: Επαναφορά κωδικού πρόσβασης + password_reset_requested: "Αίτημα επαναφοράς κωδικού πρόσβασης για %{email}." + password_reset_confirmation: "Για επαναφορά κωδικού πρόσβασης, παρακαλούμε κάντε κλικ στο παρακάτω κουμπί." + reset_password: Επαναφορά κωδικού πρόσβασης + link_expires: Ο σύνδεσμος θα λήξει σε 1 ώρα. + ignore_request: "Εάν δεν υποβάλατε αίτημα αλλαγής κωδικού πρόσβασης, παρακαλούμε αγνοήστε αυτό το email." + room: + new_room_name: "Αίθουσα του %{username}" diff --git a/config/locales/en.yml b/config/locales/en.yml index 4720a1f455..983931086f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -72,3 +72,5 @@ en: reset_password: Reset Password link_expires: The link will expire in 1 hour. ignore_request: If you did not make a request to change your password, please ignore this email. + room: + new_room_name: "%{username}'s Room" diff --git a/config/locales/et.yml b/config/locales/et.yml new file mode 100644 index 0000000000..ac5b20b100 --- /dev/null +++ b/config/locales/et.yml @@ -0,0 +1,74 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# "true": "foo" +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +et: + opengraph: + description: "Õppige kasutades BigBlueButtonit, usaldusväärset avatud lähtekoodiga veebikonverentsilahendust, mis võimaldab sujuvat virtuaalset koostööd ja veebipõhiseid õppimiskogemusi." + meeting: + moderator_message: "Selleks, et keegi koosolekule kutsuda, saatke neile see link:" + access_code: "Ligipääsukood: %{code}" + email: + activation: + account_activation: Konto aktiveerimine + welcome_to_bbb: Tere tulemast BigBlueButtonisse! + get_started: Alustamiseks vajuta all olevate nupule ning aktiveeri oma konto. + activate_account: Aktiveeri konto + link_expires: Link kaotab kehtivuse 24 tunni pärast. + if_link_expires: "Kui link aegub, logige sisse ja taotlege uut aktiveerimismeili." + invitation: + invitation_to_join: BigBlueButtoni kutse + you_have_been_invited: "Oled %{name} poolt kutsutud BigBlueButtoni kasutamiseks kontot looma." + get_started: Registreerimiseks vajuta all olevat nuppu ning järgi juhiseid. + valid_invitation: Kutse kehtib 24 tundi. + sign_up: Registreeri + reset: + password_reset: Lähtesta parool + password_reset_requested: "%{email} soovis parooli lähtestada." + password_reset_confirmation: Parooli lähtestamiseks vajuta all olevat nuppu. + reset_password: Lähtesta parool + link_expires: Link aegub 1 tunni pärast. + ignore_request: "Kui sa ei soovinud oma parooli muuta, siis palun eira seda e-kirja." diff --git a/config/locales/fa.yml b/config/locales/fa.yml index 84f48d2364..5a87f558f1 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -63,12 +63,14 @@ fa: invitation_to_join: دعوت به پیوستن به BigBlueButton you_have_been_invited: "شما توسط %{name} برای ایجاد حساب کاربری در BigBlueButton دعوت شده‌اید." get_started: برای ثبت نام لطفا روی دکمه زیر کلیک کنید و مراحل را دنبال کنید. - valid_invitation: دعوتنامه فقط ۴۸ ساعت اعتبار دارد. + valid_invitation: این پیوند ۲۴ ساعت دیگر منقضی خواهد شد. sign_up: ثبت نام reset: - password_reset: بازنشانی رمز عبور + password_reset: بازنشانی گذرواژه password_reset_requested: "بازنشانی رمز عبور برای %{email} درخواست شده است." password_reset_confirmation: برای بازنشانی رمز عبور، لطفا روی دکمه زیر کلیک کنید. - reset_password: بازنشانی رمز عبور + reset_password: بازنشانی گذرواژه link_expires: این پیوند ۱ ساعت دیگر منقضی خواهد شد. ignore_request: اگر درخواستی برای تغییر رمز عبور خود نکرده‌اید، لطفا این ایمیل را نادیده بگیرید. + room: + new_room_name: "اتاق %{username}" diff --git a/config/locales/fa_IR.yml b/config/locales/fa_IR.yml new file mode 100644 index 0000000000..69c2aa46b6 --- /dev/null +++ b/config/locales/fa_IR.yml @@ -0,0 +1,76 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# "true": "foo" +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +fa_IR: + opengraph: + description: با استفاده از BigBlueButton بیاموزید؛ راه‌حل متن‌باز و قابل اعتماد کنفرانس تحت وب که همکاری مجازی یکپارچه و تجربیات یادگیری آنلاین را امکان‌پذیر می‌کند. + meeting: + moderator_message: "برای دعوت از افراد دیگر به این جلسه، این پیوند را برای آنها ارسال کنید:" + access_code: "کد دسترسی: %{code}" + email: + activation: + account_activation: فعال‌سازی حساب کاربری + welcome_to_bbb: به BigBlueButton خوش آمدید. + get_started: برای شروع، لطفا با کلیک بر روی دکمه زیر حساب کاربری خود را فعال کنید. + activate_account: فعال کردن حساب کاربری + link_expires: این پیوند ۲۴ ساعت دیگر منقضی خواهد شد. + if_link_expires: در صورت منقضی شدن پیوند، لطفا در سیستم وارد شوید و درخواست فعال سازی ایمیل، جدید کنید. + invitation: + invitation_to_join: دعوت‌نامه BigBlueButton + you_have_been_invited: "شما توسط %{name} برای ایجاد حساب کاربری در BigBlueButton دعوت شده‌اید." + get_started: برای ثبت نام لطفا روی دکمه زیر کلیک کنید و مراحل را دنبال کنید. + valid_invitation: دعوت‌نامه فقط ۴۸ ساعت اعتبار دارد. + sign_up: ثبت نام + reset: + password_reset: بازنشانی گذرواژه + password_reset_requested: "بازنشانی گذرواژه برای %{email} درخواست شده‌است." + password_reset_confirmation: برای بازنشانی گذرواژه، لطفا روی دکمه زیر کلیک کنید. + reset_password: بازنشانی گذرواژه + link_expires: این پیوند ۱ ساعت دیگر منقضی خواهد شد. + ignore_request: اگر درخواستی برای تغییر رمز عبور خود نکرده‌اید، لطفا این ایمیل را نادیده بگیرید. + room: + new_room_name: "اتاق %{username}" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 8d2ab79aef..4bf19f3fde 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -72,3 +72,5 @@ ja: reset_password: パスワード再設定 link_expires: リンクは1時間で無効となります。 ignore_request: もしパスワード再設定の要請をした覚えがないようでしたら、このメールは読み捨ててください。 + room: + new_room_name: "%{username}の会議室" diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 35edd9147f..a5ace66307 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -47,28 +47,30 @@ tr: opengraph: - description: "Sorunsuz sanal işbirliği ve çevrimiçi öğrenme deneyimleri sağlayan, güvenilir ve açık kaynaklı web üzerinden görüşme çözümü BigBlueButton uygulamasını kullanarak öğrenin." + description: "Sorunsuz sanal işbirliği ve çevrimiçi öğrenme deneyimi sağlayan, güvenilir açık kaynaklı web konferansı çözümü BigBlueButton uygulamasını kullanarak öğrenin." meeting: - moderator_message: "Toplantıya katılmasını istediğiniz kişilere bu bağlantıyı gönderin: " + moderator_message: "Toplantıya çağırmak istediğiniz kişilere bu bağlantıyı gönderin:" access_code: "Erişim kodu: %{code}" email: activation: account_activation: Hesap etkinleştirme welcome_to_bbb: BigBlueButton uygulamasına hoş geldiniz! - get_started: "Başlamak için, lütfen aşağıdaki düğmeye tıklayarak hesabınızı etkinleştirin." + get_started: Lütfen başlamak için aşağıdaki düğmeye tıklayarak hesabınızı etkinleştirin. activate_account: Hesabı etkinleştir - link_expires: Bağlantı 24 saat sonra geçersiz olacak. - if_link_expires: Bağlantının süresi geçerse oturum açarak yeni bir etkinleştirme e-postası alın. + link_expires: Bu bağlantı 24 saat sonra geçersiz olacak. + if_link_expires: "Bağlantının süresi geçerse, yeni bir etkinleştirme e-postası almak için oturum açın." invitation: invitation_to_join: BigBlueButton çağrısı you_have_been_invited: "%{name} tarafından bir BigBlueButton hesabı açmaya çağrıldınız." get_started: Hesap açmak için aşağıdaki düğmeye tıklayın ve yönergeleri izleyin. - valid_invitation: Bu çağrı 24 saat süreyle geçerlidir. + valid_invitation: Çağrı 24 saat boyunca geçerlidir. sign_up: Hesap aç reset: password_reset: Parolayı sıfırla - password_reset_requested: "%{email} için bir parola sıfırlama isteğinde bulunuldu." + password_reset_requested: "%{email} e-posta adresi için parola sıfırlama isteğinde bulunuldu." password_reset_confirmation: Parolanızı sıfırlamak için aşağıdaki düğmeye tıklayın. reset_password: Parolayı sıfırla link_expires: Bağlantı 1 saat sonra geçersiz olacak. - ignore_request: Parola değiştirme isteğinde bulunmadıysanız bu e-postayı yok sayabilirsiniz. + ignore_request: Parolanızı değiştirme isteğinde bulunmadıysanız bu e-postayı yok sayabilirsiniz. + room: + new_room_name: "%{username} kullanıcısının odası" diff --git a/config/locales/uk.yml b/config/locales/uk.yml index b5b38ffaba..5548bcf6d7 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -63,7 +63,7 @@ uk: invitation_to_join: Запрошення доєднатись до BigBlueButton you_have_been_invited: "Вас було запрошено створити обліковий запис в BigBlueButton користувачем %{name}" get_started: "Щоб зареєструватись, натисніть кнопку знизу та слідуйте подальшим крокам." - valid_invitation: Запрошення дійсне протягом 48 годин. + valid_invitation: Запрошення дійсне протягом 24 годин. sign_up: Реєстрація reset: password_reset: Запит на новий пароль @@ -72,3 +72,5 @@ uk: reset_password: Скидування паролю link_expires: Посилання залишатиметься дійсним протягом 1 години. ignore_request: "Якщо ви не робили запит на зміну паролю, проігноруйте цей лист." + room: + new_room_name: "Кімната користувача %{username}" diff --git a/config/locales/zh_TW.yml b/config/locales/zh_TW.yml new file mode 100644 index 0000000000..c57be2de33 --- /dev/null +++ b/config/locales/zh_TW.yml @@ -0,0 +1,74 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# "true": "foo" +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +zh_TW: + opengraph: + description: 使用 BigBlueButton 學習,這是一個值得信賴的開源網絡會議解決方案,提供無縫的虛擬協作和線上學習體驗。 + meeting: + moderator_message: "若要邀請某人參加會議,請發送此連結:" + access_code: "連結代碼:%{code}" + email: + activation: + account_activation: 啟用帳號 + welcome_to_bbb: 歡迎使用 BigBlueButton! + get_started: 要開始使用,請點擊下方按鈕啟用您的帳號。 + activate_account: 啟用帳號 + link_expires: 連結將在 24 小時後失效。 + if_link_expires: 如果連結已過期,請登入並申請新的啟用郵件。 + invitation: + invitation_to_join: BigBlueButton 邀請 + you_have_been_invited: "您已獲得 %{name} 邀請建立 BigBlueButton 帳號。" + get_started: 要註冊,請點擊下方按鈕並按照步驟進行。 + valid_invitation: 邀請有效期為 24 小時。 + sign_up: 註冊 + reset: + password_reset: 重設密碼 + password_reset_requested: "已對 %{email} 提出重設密碼的請求。" + password_reset_confirmation: 要重設密碼,請點擊下方按鈕。 + reset_password: 重設密碼 + link_expires: 連結將在 1 小時後失效。 + ignore_request: 如果您沒有提出更改密碼的請求,請忽略此郵件。 diff --git a/config/routes.rb b/config/routes.rb index 25e5abb567..6851e54b9c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -25,6 +25,9 @@ get '/meeting_ended', to: 'external#meeting_ended' post '/recording_ready', to: 'external#recording_ready' + # Health checks + get '/health_check', to: 'health_checks#check' + # All the Api endpoints must be under /api/v1 and must have an extension .json. namespace :api do namespace :v1 do @@ -42,6 +45,7 @@ resources :rooms, param: :friendly_id do member do get '/recordings', to: 'rooms#recordings' + get '/public_recordings', to: 'rooms#public_recordings' get '/recordings_processing', to: 'rooms#recordings_processing' get '/public', to: 'rooms#public_show' delete :purge_presentation @@ -59,6 +63,7 @@ collection do post '/update_visibility', to: 'recordings#update_visibility' get '/recordings_count', to: 'recordings#recordings_count' + post '/recording_url', to: 'recordings#recording_url' end end resources :shared_accesses, only: %i[create show destroy], param: :friendly_id do @@ -78,7 +83,7 @@ post '/activate', to: 'verify_account#activate', on: :collection end resources :site_settings, only: :index - resources :rooms_configurations, only: :index + resources :rooms_configurations, only: %i[index show], param: :name resources :locales, only: %i[index show], param: :name namespace :admin do diff --git a/config/storage.yml b/config/storage.yml index 0c67dd2125..68112a74ec 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -36,13 +36,24 @@ s3: secret_access_key: <%= ENV['S3_SECRET_ACCESS_KEY'] %> region: <%= ENV['S3_REGION'] %> bucket: <%= ENV['S3_BUCKET'] %> + force_path_style: <%= ENV.fetch("S3_FORCE_PATH_STYLE", false) %> # Remember not to checkin your GCS keyfile to a repository -# google: -# service: GCS -# project: your_project -# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> -# bucket: your_own_bucket-<%= Rails.env %> +google: + service: GCS + project: "<%= ENV['GCS_PROJECT'] %>" + bucket: "<%= ENV['GCS_BUCKET'] %>" + credentials: + type: 'service_account' + project_id: "<%= ENV['GCS_PROJECT_ID'] %>" + private_key_id: "<%= ENV['GCS_PRIVATE_KEY_ID'] %>" + private_key: "<%= ENV['GCS_PRIVATE_KEY']&.lines&.join("\\n") %>" + client_email: "<%= ENV['GCS_CLIENT_EMAIL'] %>" + client_id: "<%= ENV['GCS_CLIENT_ID'] %>" + auth_uri: 'https://accounts.google.com/o/oauth2/auth' + token_uri: 'https://accounts.google.com/o/oauth2/token' + auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs' + client_x509_cert_url: "<%= ENV['GCS_CLIENT_CERT'] %>" # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: diff --git a/db/migrate/20230705183745_add_service_name_to_active_storage_blobs.active_storage.rb b/db/migrate/20230705183745_add_service_name_to_active_storage_blobs.active_storage.rb new file mode 100644 index 0000000000..2002bd25f5 --- /dev/null +++ b/db/migrate/20230705183745_add_service_name_to_active_storage_blobs.active_storage.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# This migration comes from active_storage (originally 20190112182829) +class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] + def up + return unless table_exists?(:active_storage_blobs) + + return if column_exists?(:active_storage_blobs, :service_name) + + add_column :active_storage_blobs, :service_name, :string + + if (configured_service = ActiveStorage::Blob.service.name) + ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) # rubocop:disable Rails/SkipsModelValidations + end + + change_column :active_storage_blobs, :service_name, :string, null: false + end + + def down + return unless table_exists?(:active_storage_blobs) + + remove_column :active_storage_blobs, :service_name + end +end diff --git a/db/migrate/20230705183746_create_active_storage_variant_records.active_storage.rb b/db/migrate/20230705183746_create_active_storage_variant_records.active_storage.rb new file mode 100644 index 0000000000..91e341db2a --- /dev/null +++ b/db/migrate/20230705183746_create_active_storage_variant_records.active_storage.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# This migration comes from active_storage (originally 20191206030411) +class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] + def change + return unless table_exists?(:active_storage_blobs) + + # Use Active Record's configured type for primary key + + create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| # rubocop:disable Rails/CreateTableWithTimestamps + t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type + t.string :variation_digest, null: false + + t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + + def primary_key_type + config = Rails.configuration.generators + config.options[config.orm][:primary_key_type] || :primary_key + end + + def blobs_primary_key_type + pkey_name = connection.primary_key(:active_storage_blobs) + pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } + pkey_column.bigint? ? :bigint : pkey_column.type + end +end diff --git a/db/migrate/20230705183747_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb b/db/migrate/20230705183747_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb new file mode 100644 index 0000000000..6f3182210c --- /dev/null +++ b/db/migrate/20230705183747_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# This migration comes from active_storage (originally 20211119233751) +class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] + def change + return unless table_exists?(:active_storage_blobs) + + change_column_null(:active_storage_blobs, :checksum, true) + end +end diff --git a/db/schema.rb b/db/schema.rb index c67d9653c0..28b87151d9 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[7.0].define(version: 2023_03_07_184209) do +ActiveRecord::Schema[7.0].define(version: 2023_07_05_183747) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" diff --git a/esbuild.dev.mjs b/esbuild.dev.mjs index 83a11e9c47..1bd3839ca4 100644 --- a/esbuild.dev.mjs +++ b/esbuild.dev.mjs @@ -20,6 +20,7 @@ await esbuild.build({ }, define: { 'process.env.RELATIVE_URL_ROOT': `"${relativeUrlRoot}"`, + 'process.env.OMNIAUTH_PATH': `"${relativeUrlRoot}/auth/openid_connect"`, }, }); diff --git a/esbuild.mjs b/esbuild.mjs index 8a65895920..6330cbc21d 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -14,6 +14,7 @@ await esbuild.build({ }, define: { 'process.env.RELATIVE_URL_ROOT': `"${relativeUrlRoot}"`, + 'process.env.OMNIAUTH_PATH': `"${relativeUrlRoot}/auth/openid_connect"`, }, }); diff --git a/greenlight-v3.nginx b/greenlight-v3.nginx index c0331915fb..86fb01d34d 100644 --- a/greenlight-v3.nginx +++ b/greenlight-v3.nginx @@ -3,43 +3,67 @@ location /cable { proxy_pass http://127.0.0.1:5050; proxy_redirect off; + proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - proxy_http_version 1.1; - proxy_read_timeout 6h; - proxy_send_timeout 6h; - client_body_timeout 6h; - send_timeout 6h; + proxy_set_header Connection "upgrade"; + proxy_set_header Upgrade $http_upgrade; } - location @bbb-fe { proxy_pass http://127.0.0.1:5050; + proxy_redirect off; + proxy_http_version 1.1; - proxy_read_timeout 60s; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; +} + +location ~ '/api/v1/rooms/\w{3}-\w{3}-\w{3}-\w{3}.json$' { + proxy_pass http://127.0.0.1:5050; proxy_redirect off; + proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Cookie "$http_cookie; ip=$remote_addr"; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + + client_max_body_size 31m; +} + +location ~ '/api/v1/users/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}.json$' { + proxy_pass http://127.0.0.1:5050; + proxy_redirect off; proxy_http_version 1.1; - proxy_headers_hash_max_size 512; - proxy_headers_hash_bucket_size 128; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + + client_max_body_size 4m; +} - proxy_buffer_size 128k; - proxy_buffers 4 256k; - proxy_busy_buffers_size 256k; +location ~ /api/v1/admin/site_settings/BrandingImage.json$ { + proxy_pass http://127.0.0.1:5050; + proxy_redirect off; + proxy_http_version 1.1; - client_max_body_size 30m; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; - rewrite ~/(.*)$ /$1 break; + client_max_body_size 4m; } diff --git a/lib/tasks/add_tenant.rake b/lib/tasks/add_tenant.rake new file mode 100644 index 0000000000..977bbd80f4 --- /dev/null +++ b/lib/tasks/add_tenant.rake @@ -0,0 +1,34 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# frozen_string_literal: true + +require_relative 'task_helpers' + +desc 'Add tenant' +task :add_tenant, %i[provider secret] => :environment do |_t, args| + err 'Missing provider' unless args.provider + err 'Missing secret' unless args.secret + + tenant = Tenant.new(name: args.provider, client_secret: args.secret) + + if tenant.save + TenantSetup.new(args.provider).call + success 'Tenant created successfully.' + else + err "Tenant not created. Errors: #{tenant.errors.to_a}" + end +end diff --git a/lib/tasks/admin.rake b/lib/tasks/admin.rake index 66903cb7bc..c9679a06a9 100644 --- a/lib/tasks/admin.rake +++ b/lib/tasks/admin.rake @@ -32,4 +32,33 @@ namespace :admin do exit 0 end + + desc 'Create an super admin account' + task :super_admin, %i[name email password] => :environment do |_task, args| + super_admin_email = "superadmin-#{args[:email]}" + + user = User.new( + name: args[:name], + email: super_admin_email, + password: args[:password], + role: Role.find_by(name: 'SuperAdmin', provider: 'bn'), + provider: 'bn', + verified: true, + status: :active, + language: I18n.default_locale + ) + + if user.save + success 'User account was created successfully!' + info " Name: #{user.name}" + info " Email: #{user.email}" + info " Password: #{user.password}" + info " Role: #{user.role.name}" + else + warning 'There was an error creating this user' + err " Error: #{user.errors.full_messages}" + end + + exit 0 + end end diff --git a/lib/tasks/configuration.rake b/lib/tasks/configuration.rake index 83fab74da8..09af5a6112 100644 --- a/lib/tasks/configuration.rake +++ b/lib/tasks/configuration.rake @@ -32,9 +32,7 @@ namespace :configuration do info 'Checking connection to Postgres Database:' begin - ActiveRecord::Base.establish_connection # Establishes connection - ActiveRecord::Base.connection # Calls connection object - failed('Unable to connect to Database') unless ActiveRecord::Base.connected? + failed('Unable to connect to Database') unless ActiveRecord::Base.connection.active? rescue StandardError => e failed("Unable to connect to Database - #{e}") end diff --git a/lib/tasks/poller.rake b/lib/tasks/poller.rake new file mode 100644 index 0000000000..a4a3070d79 --- /dev/null +++ b/lib/tasks/poller.rake @@ -0,0 +1,82 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# frozen_string_literal: true + +namespace :poller do + desc 'Runs all pollers' + task :run_all, %i[interval] => :environment do |_task, args| + args.with_defaults(interval: 30) + interval = args[:interval].to_i.minutes # set the interval in minutes + + poller_tasks = %w[poller:meetings_poller poller:recordings_poller] + + info "Running poller with interval #{interval}" + loop do + poller_tasks.each do |poller_task| + info "Running #{poller_task} at #{Time.zone.now}" + Rake::Task[poller_task].invoke(interval) + rescue StandardError => e + err "An error occurred in #{poller_task}: #{e.message}. Continuing..." + end + + sleep interval + + poller_tasks.each do |poller_task| + Rake::Task[poller_task].reenable + end + end + end + + desc 'Polls meetings to check if they are still online' + task meetings_poller: :environment do + online_meetings = Room.includes(:user).where(online: true) + + RunningMeetingChecker.new(rooms: online_meetings).call + + rescue StandardError => e + err "Unable to poll meetings. Error: #{e}" + end + + desc 'Polls recordings to check if they have been created in GL' + task :recordings_poller, %i[interval] => :environment do |_task, args| + # Returns the providers which have recordings disabled + disabled_recordings = RoomsConfiguration.joins(:meeting_option).where(meeting_option: { name: 'record' }, value: 'false').pluck(:provider) + + # Returns the rooms which have been online recently and have not been recorded yet + recent_meeting_interval = args[:interval] * 2 + recent_meetings = Room.includes(:user) + .where(last_session: recent_meeting_interval.ago..Time.zone.now, online: false) + .where.not(user: { provider: disabled_recordings }) + + recent_meetings.each do |meeting| + recordings = BigBlueButtonApi.new(provider: meeting.user.provider).get_recordings(meeting_ids: meeting.meeting_id) + recordings[:recordings].each do |recording| + next if Recording.exists?(record_id: recording[:recordID]) + + unless meeting.recordings_processing.zero? + meeting.update(recordings_processing: meeting.recordings_processing - 1) # cond. in case both callbacks fail + end + + RecordingCreator.new(recording:).call + + rescue StandardError => e + err "Unable to poll Recording:\nRecordID: #{recording[:recordID]}\nError: #{e}" + next + end + end + end +end diff --git a/lib/tasks/server_recordings_sync.rake b/lib/tasks/server_recordings_sync.rake index bcee11b88b..c8621a3f3d 100644 --- a/lib/tasks/server_recordings_sync.rake +++ b/lib/tasks/server_recordings_sync.rake @@ -18,15 +18,19 @@ desc 'Server Recordings sync with BBB server' -task server_recordings_sync: :environment do - Recording.destroy_all +task :server_recordings_sync, %i[provider] => :environment do |_task, args| + args.with_defaults(provider: 'greenlight') - Room.select(:id, :meeting_id).in_batches(of: 25) do |rooms| + Room.includes(:user).select(:id, :meeting_id).with_provider(args[:provider]).in_batches(of: 25) do |rooms| meeting_ids = rooms.pluck(:meeting_id) - recordings = BigBlueButtonApi.new(provider: 'greenlight').get_recordings(meeting_ids:) + recordings = BigBlueButtonApi.new(provider: args[:provider]).get_recordings(meeting_ids:) recordings[:recordings].each do |recording| RecordingCreator.new(recording:).call + success 'Successfully migrated Recording:' + info "RecordID: #{recording[:recordID]}" + rescue StandardError => e + err "Unable to migrate Recording:\nRecordID: #{recording[:recordID]}\nError: #{e}" end end end diff --git a/package-lock.json b/package-lock.json index 600c0a5e96..da2c89be62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "react-bootstrap": "^2.2.0", "react-colorful": "^5.6.1", "react-dom": "^17.0.2", + "react-helmet": "^6.1.0", "react-hook-form": "^7.28.0", "react-i18next": "^11.18.5", "react-query": "^3.34.16", @@ -2086,9 +2087,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2230,9 +2231,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2289,9 +2290,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -5821,6 +5822,20 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-hook-form": { "version": "7.43.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.1.tgz", @@ -5948,6 +5963,14 @@ "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-test-renderer": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", @@ -6245,9 +6268,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -6770,9 +6793,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -8212,9 +8235,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -8293,9 +8316,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -8335,9 +8358,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -10733,6 +10756,17 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + } + }, "react-hook-form": { "version": "7.43.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.1.tgz", @@ -10803,6 +10837,12 @@ "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" } }, + "react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "requires": {} + }, "react-test-renderer": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", @@ -11020,9 +11060,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" }, "shallowequal": { "version": "1.1.0", @@ -11409,9 +11449,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true }, "wrappy": { diff --git a/package.json b/package.json index dd490ec7ee..6c4eca4db6 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "react-bootstrap": "^2.2.0", "react-colorful": "^5.6.1", "react-dom": "^17.0.2", + "react-helmet": "^6.1.0", "react-hook-form": "^7.28.0", "react-i18next": "^11.18.5", "react-query": "^3.34.16", diff --git a/sample.env b/sample.env index 055a827970..4be5e5c388 100644 --- a/sample.env +++ b/sample.env @@ -56,6 +56,17 @@ REDIS_URL= #S3_REGION= #S3_BUCKET= #S3_ENDPOINT= +#S3_FORCE_PATH_STYLE= + +# Set these environment variables if you are using Google Cloud Storage +#GCS_PROJECT= +#GCS_BUCKET= +#GCS_PROJECT_ID= +#GCS_PRIVATE_KEY_ID= +#GCS_PRIVATE_KEY= +#GCS_CLIENT_EMAIL= +#GCS_CLIENT_ID= +#GCS_CLIENT_CERT= # Define the default locale language code (i.e. 'en' for English) from the following list: # [en, ar, fr, es] @@ -68,3 +79,13 @@ REDIS_URL= # Set this if you like to deploy Greenlight on a relative root path other than / #RELATIVE_URL_ROOT=/gl + +## Define log level in production. +# [debug|info|warn|error|fatal] +# Default 'warn'. +LOG_LEVEL=info + +## Use to send logs to external repository (Optional) +# RAILS_LOG_REMOTE_NAME=xxx.papertrailapp.com +# RAILS_LOG_REMOTE_PORT=99999 +# RAILS_LOG_REMOTE_TAG=greenlight-v3 diff --git a/spec/controllers/admin/server_rooms_controller_spec.rb b/spec/controllers/admin/server_rooms_controller_spec.rb index d8591c485d..ec691cfc54 100644 --- a/spec/controllers/admin/server_rooms_controller_spec.rb +++ b/spec/controllers/admin/server_rooms_controller_spec.rb @@ -36,7 +36,6 @@ create_list(:room, 2, user_id: user_one.id) create_list(:room, 2, user_id: user_two.id) - allow_any_instance_of(BigBlueButtonApi).to receive(:active_meetings).and_return([]) get :index expect(JSON.parse(response.body)['data'].pluck('friendly_id')) .to match_array(Room.all.pluck(:friendly_id)) @@ -56,39 +55,12 @@ create_list(:room, 2, user_id: user_one.id) create_list(:room, 2, user_id: user_two.id) - allow_any_instance_of(BigBlueButtonApi).to receive(:active_meetings).and_return([]) get :index expect(JSON.parse(response.body)['data'].pluck('friendly_id')) .to match_array(Room.all.pluck(:friendly_id)) end end - it 'returns the server room status as online if the meeting has started' do - active_server_room = create(:room) - active_server_room.update(meeting_id: 'hulsdzwvitlk1dbekzxdprshsxmvycvar0jeaszc') - - allow_any_instance_of(BigBlueButtonApi).to receive(:active_meetings).and_return(bbb_meetings) - get :index - expect(JSON.parse(response.body)['data'][0]['online']).to be(true) - end - - it 'returns the number of participants in an online server room' do - active_server_room = create(:room) - active_server_room.update(meeting_id: 'hulsdzwvitlk1dbekzxdprshsxmvycvar0jeaszc') - - allow_any_instance_of(BigBlueButtonApi).to receive(:active_meetings).and_return(bbb_meetings) - get :index - expect(JSON.parse(response.body)['data'][0]['participants']).to be(1) - end - - it 'returns the server status as not running if BBB server does not return any online meetings' do - create(:room) - - allow_any_instance_of(BigBlueButtonApi).to receive(:active_meetings).and_return([]) - get :index - expect(JSON.parse(response.body)['data'][0]['online']).to be(false) - end - it 'excludes rooms whose owners have a different provider' do user1 = create(:user, provider: 'greenlight') role_with_provider_test = create(:role, provider: 'test') @@ -96,8 +68,6 @@ rooms = create_list(:room, 2, user: user1) create_list(:room, 2, user: user2) - allow_any_instance_of(BigBlueButtonApi).to receive(:active_meetings).and_return([]) - get :index expect(JSON.parse(response.body)['data'].pluck('id')).to match_array(rooms.pluck(:id)) end diff --git a/spec/controllers/admin/site_settings_controller_spec.rb b/spec/controllers/admin/site_settings_controller_spec.rb index 730c79268a..67d122b514 100644 --- a/spec/controllers/admin/site_settings_controller_spec.rb +++ b/spec/controllers/admin/site_settings_controller_spec.rb @@ -58,18 +58,51 @@ it 'updates the value of SiteSetting' do setting = create(:setting, name: 'ShareRooms') site_setting = create(:site_setting, setting:, value: false) - get :update, params: { name: 'ShareRooms', site_setting: { value: true } } + patch :update, params: { name: 'ShareRooms', site_setting: { value: true } } expect(response).to have_http_status(:ok) expect(site_setting.reload.value).to eq('true') end + context 'Branding image upload' do + describe 'valid image' do + it 'returns :ok and attach the file' do + setting = create(:setting, name: 'BrandingImage') + site_setting = create(:site_setting, setting:, value: false) + + patch :update, + params: { name: 'BrandingImage', site_setting: { + value: fixture_file_upload(file_fixture('default-avatar.png'), 'image/png') + } } + + expect(site_setting.reload.image).to be_attached + expect(site_setting.image.blob.filename).to eq('default-avatar.png') + expect(response).to have_http_status(:ok) + end + end + + describe 'invalid image' do + it 'returns :bad_request and does NOT attach the file' do + setting = create(:setting, name: 'BrandingImage') + site_setting = create(:site_setting, setting:, value: false) + + patch :update, + params: { name: 'BrandingImage', site_setting: { + value: fixture_file_upload(file_fixture('default-pdf.pdf'), 'application/pdf') + } } + + expect(site_setting.reload.image).not_to be_attached + expect(response).to have_http_status(:bad_request) + end + end + end + context 'user without ManageSiteSettings permission' do before do sign_in_user(user) end it 'cant update the value of SiteSetting' do - get :update, params: { name: 'ShareRooms', site_setting: { value: true } } + patch :update, params: { name: 'ShareRooms', site_setting: { value: true } } expect(response).to have_http_status(:forbidden) end end diff --git a/spec/controllers/admin/tenants_controller_spec.rb b/spec/controllers/admin/tenants_controller_spec.rb index 699e47b9ec..2563f83736 100644 --- a/spec/controllers/admin/tenants_controller_spec.rb +++ b/spec/controllers/admin/tenants_controller_spec.rb @@ -19,7 +19,7 @@ require 'rails_helper' RSpec.describe Api::V1::Admin::TenantsController, type: :controller do - let(:user) { create(:user) } + let(:user) { create(:user, :with_super_admin) } let(:valid_tenant_params) do { name: 'new_provider', diff --git a/spec/controllers/external_controller_spec.rb b/spec/controllers/external_controller_spec.rb index d9b7a9abf3..0f28fd71de 100644 --- a/spec/controllers/external_controller_spec.rb +++ b/spec/controllers/external_controller_spec.rb @@ -54,7 +54,7 @@ get :create_user, params: { provider: 'openid_connect' } expect(session[:session_token]).to eq(User.find_by(email: OmniAuth.config.mock_auth[:openid_connect][:info][:email]).session_token) - expect(response).to redirect_to('/') + expect(response).to redirect_to(root_path) end it 'assigns the User role to the user' do @@ -73,8 +73,28 @@ expect(User.find_by(email: OmniAuth.config.mock_auth[:openid_connect][:info][:email]).verified?).to be true end + it 'looks the user up based on external id' do + request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:openid_connect] + + create(:user, external_id: request.env['omniauth.auth']['uid']) + + expect do + get :create_user, params: { provider: 'openid_connect' } + end.to change(User, :count).by(0) + end + + it 'looks the user up based on email' do + request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:openid_connect] + + create(:user, email: request.env['omniauth.auth']['info']['email']) + + expect do + get :create_user, params: { provider: 'openid_connect' } + end.to change(User, :count).by(0) + end + context 'redirect' do - it 'redirects to the location cookie if the format is valid' do + it 'redirects to the location cookie if a relative redirection 1' do request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:openid_connect] cookies[:location] = { @@ -86,19 +106,43 @@ expect(response).to redirect_to('/rooms/o5g-hvb-s44-p5t/join') end - it 'doesnt redirect if it doesnt match a room joins format' do + it 'redirects to the location cookie if its a legacy url (3 sections in uid)' do request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:openid_connect] cookies[:location] = { - value: 'https://google.com', + value: '/rooms/o5g-hvb-s44/join', path: '/' } get :create_user, params: { provider: 'openid_connect' } - expect(response).to redirect_to('/') + expect(response).to redirect_to('/rooms/o5g-hvb-s44/join') end - it 'doesnt redirect if it doesnt match a room joins format check 2' do + it 'redirects to the location cookie if a relative redirection 2' do + request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:openid_connect] + + cookies[:location] = { + value: '/a/b/c/d/rooms/o5g-hvb-s44-p5t/join', + path: '/' + } + get :create_user, params: { provider: 'openid_connect' } + + expect(response).to redirect_to('/a/b/c/d/rooms/o5g-hvb-s44-p5t/join') + end + + it 'doesnt redirect if NOT a relative redirection check 1' do + request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:openid_connect] + + cookies[:location] = { + value: Faker::Internet.url, + path: '/' + } + get :create_user, params: { provider: 'openid_connect' } + + expect(response).to redirect_to(root_path) + end + + it 'doesnt redirect if it NOT a relative redirection format check 2' do request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:openid_connect] cookies[:location] = { @@ -107,10 +151,10 @@ } get :create_user, params: { provider: 'openid_connect' } - expect(response).to redirect_to('/') + expect(response).to redirect_to(root_path) end - it 'doesnt redirect if it doesnt match a room joins format check 3' do + it 'doesnt redirect if it NOT a relative redirection format check 3' do request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:openid_connect] cookies[:location] = { @@ -119,7 +163,31 @@ } get :create_user, params: { provider: 'openid_connect' } - expect(response).to redirect_to('/') + expect(response).to redirect_to(root_path) + end + + it 'doesnt redirect if NOT a relative redirection check 4' do + request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:openid_connect] + + cookies[:location] = { + value: Faker::Internet.url(path: '/rooms/o5g-hvb-s44-p5t/join'), + path: '/' + } + get :create_user, params: { provider: 'openid_connect' } + + expect(response).to redirect_to(root_path) + end + + it 'doesnt redirect if NOT a valid room join link check 5' do + request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:openid_connect] + + cookies[:location] = { + value: '/romios/o5g-hvb-s44-p5t/join', + path: '/' + } + get :create_user, params: { provider: 'openid_connect' } + + expect(response).to redirect_to(root_path) end it 'deletes the cookie after reading' do @@ -215,7 +283,7 @@ create(:user, external_id: OmniAuth.config.mock_auth[:openid_connect][:uid]) expect { get :create_user, params: { provider: 'openid_connect' } }.not_to raise_error - expect(response).to redirect_to('/') + expect(response).to redirect_to(root_path) end it 'returns an InviteInvalid error if no invite is passed' do @@ -223,7 +291,7 @@ get :create_user, params: { provider: 'openid_connect' } - expect(response).to redirect_to('/?error=InviteInvalid') + expect(response).to redirect_to(root_path(error: Rails.configuration.custom_error_msgs[:invite_token_invalid])) end it 'returns an InviteInvalid error if the token is wrong' do @@ -235,7 +303,7 @@ get :create_user, params: { provider: 'openid_connect' } - expect(response).to redirect_to('/?error=InviteInvalid') + expect(response).to redirect_to(root_path(error: Rails.configuration.custom_error_msgs[:invite_token_invalid])) end end @@ -252,6 +320,7 @@ expect { get :create_user, params: { provider: 'openid_connect' } }.to change(User, :count).by(1) expect(User.find_by(email: OmniAuth.config.mock_auth[:openid_connect][:info][:email])).to be_pending + expect(response).to redirect_to(controller.pending_path) end end end @@ -312,18 +381,45 @@ end describe '#meeting_ended' do - let(:room) { create(:room) } + let(:room) { create(:room, online: true) } + + context 'Recorded session' do + it 'sets online to false' do + get :meeting_ended, params: { meetingID: room.meeting_id, recordingmarks: 'true' } + + expect(room.reload.online).to be(false) + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq({}) + end + + it 'increments a rooms recordings processing value if the meeting was recorded' do + get :meeting_ended, params: { meetingID: room.meeting_id, recordingmarks: 'true' } + expect(room.reload.recordings_processing).to eq(1) - it 'increments a rooms recordings processing value if the meeting was recorded' do - get :meeting_ended, params: { meetingID: room.meeting_id, recordingmarks: 'true' } - expect(room.reload.recordings_processing).to eq(1) - get :meeting_ended, params: { meetingID: room.meeting_id, recordingmarks: 'true' } - expect(room.reload.recordings_processing).to eq(2) + get :meeting_ended, params: { meetingID: room.meeting_id, recordingmarks: 'true' } + expect(room.reload.recordings_processing).to eq(2) + end end - it 'does not increment a rooms recordings processing value if the meeting was not recorded' do - get :meeting_ended, params: { meetingID: room.meeting_id, recordingmarks: 'false' } - expect(room.reload.recordings_processing).to eq(0) + context 'Unrecorded session' do + it 'sets online to false without incrementing a rooms recordings processing' do + expect do + get :meeting_ended, params: { meetingID: room.meeting_id, recordingmarks: 'false' } + end.not_to(change { room.reload.recordings_processing }) + + expect(room.online).to be(false) + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq({}) + end + end + + context 'Inexistent room' do + it 'silently fail' do + get :meeting_ended, params: { meetingID: '404', recordingmarks: 'false' } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq({}) + end end end diff --git a/spec/controllers/health_checks_controller_spec.rb b/spec/controllers/health_checks_controller_spec.rb new file mode 100644 index 0000000000..9bb6ec69d1 --- /dev/null +++ b/spec/controllers/health_checks_controller_spec.rb @@ -0,0 +1,106 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe HealthChecksController, type: :controller do + describe '#check' do + # Disable all checks initially + before do + allow(ENV).to receive(:fetch).with('DATABASE_HEALTH_CHECK_DISABLED', false).and_return('true') + allow(ENV).to receive(:fetch).with('REDIS_HEALTH_CHECK_DISABLED', false).and_return('true') + allow(ENV).to receive(:fetch).with('SMTP_HEALTH_CHECK_DISABLED', false).and_return('true') + allow(ENV).to receive(:fetch).with('BBB_HEALTH_CHECK_DISABLED', false).and_return('true') + end + + context 'when all services are disabled' do + it 'returns success' do + get :check + expect(response.body).to eq('success') + expect(response).to have_http_status(:ok) + end + end + + context 'when database check is enabled' do + before do + allow(ENV).to receive(:fetch).with('DATABASE_HEALTH_CHECK_DISABLED', false).and_return(false) + end + + it 'returns success' do + allow(ActiveRecord::Base).to receive(:connected?).and_return(true) + get :check + expect(response.body).to eq('success') + expect(response).to have_http_status(:ok) + end + + it 'returns failure message' do + allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false) + get :check + expect(response.body).to include('Unable to connect to Database') + expect(response).to have_http_status(:internal_server_error) + end + end + + context 'when redis check is enabled' do + before do + allow(ENV).to receive(:fetch).with('REDIS_HEALTH_CHECK_DISABLED', false).and_return(false) + end + + it 'returns success' do + allow_any_instance_of(Redis).to receive(:ping) + get :check + expect(response.body).to eq('success') + expect(response).to have_http_status(:ok) + end + + it 'returns failure message' do + allow(Redis).to receive(:new).and_raise(StandardError.new('Redis connection error')) + get :check + expect(response.body).to include('Unable to connect to Redis') + expect(response).to have_http_status(:internal_server_error) + end + end + + context 'when smtp check is enabled' do + before do + allow(ENV).to receive(:fetch).with('SMTP_SENDER_EMAIL', nil).and_return('test@test.test') + allow(ENV).to receive(:fetch).with('SMTP_HEALTH_CHECK_DISABLED', false).and_return(false) + end + + it 'returns failure message' do + allow(Net::SMTP).to receive(:new).and_raise(StandardError.new('SMTP error')) + get :check + expect(response.body).to include('Unable to connect to SMTP Server') + expect(response).to have_http_status(:internal_server_error) + end + end + + context 'when big_blue_button check is enabled' do + before do + allow(ENV).to receive(:fetch).with('BBB_HEALTH_CHECK_DISABLED', false).and_return(false) + end + + it 'returns failure message' do + allow(Net::HTTP).to receive(:get).and_raise(StandardError.new('BBB error')) + get :check + expect(response.body).to include('Unable to connect to BigBlueButton') + expect(response).to have_http_status(:internal_server_error) + end + end + end +end diff --git a/spec/controllers/migrations/external_controller_spec.rb b/spec/controllers/migrations/external_controller_spec.rb index 97eb213277..e2cf4c2229 100644 --- a/spec/controllers/migrations/external_controller_spec.rb +++ b/spec/controllers/migrations/external_controller_spec.rb @@ -28,7 +28,7 @@ context 'when decryption passes' do describe 'when decrypted params encapsulation is conform and data is valid' do it 'returns :created and creates a role' do - encrypted_params = encrypt_params({ role: { name: 'CrazyRole', role_permissions: {} } }, expires_in: 10.seconds) + encrypted_params = encrypt_params({ role: { name: 'CrazyRole', provider: 'greenlight', role_permissions: {} } }, expires_in: 10.seconds) expect { post :create_role, params: { v2: { encrypted_params: } } }.to change(Role, :count).from(0).to(1) role = Role.take expect(role.name).to eq('CrazyRole') @@ -39,7 +39,7 @@ describe 'when decrypted params data are invalid' do it 'returns :bad_request without creating a role' do - encrypted_params = encrypt_params({ role: { name: '', role_permissions: {} } }, expires_in: 10.seconds) + encrypted_params = encrypt_params({ role: { name: '', provider: 'greenlight', role_permissions: {} } }, expires_in: 10.seconds) expect { post :create_role, params: { v2: { encrypted_params: } } }.not_to change(Role, :count) expect(response).to have_http_status(:bad_request) end @@ -57,11 +57,45 @@ let(:role) { create(:role, provider: 'greenlight', name: 'OnlyOne') } it 'returns :created without creating a role' do - encrypted_params = encrypt_params({ role: { name: role.name, role_permissions: {} } }, expires_in: 10.seconds) + encrypted_params = encrypt_params({ role: { name: role.name, provider: 'greenlight', role_permissions: {} } }, expires_in: 10.seconds) expect { post :create_role, params: { v2: { encrypted_params: } } }.not_to change(Role, :count) expect(response).to have_http_status(:created) end end + + describe 'when role already exists and role permissions are not default values' do + let!(:role) { create(:role) } + let!(:not_greenlight_role) { create(:role, provider: 'not_greenlight') } + let!(:create_room_role_permission) { create(:role_permission, role:, permission: create(:permission, name: 'ManageUsers'), value: 'true') } + let!(:not_greenlight_create_room_role_permission) do + create(:role_permission, + role: not_greenlight_role, + permission: create(:permission, name: 'ManageUsers'), + value: 'true') + end + + it 'creates role role_permissions with the given value' do + role_permissions = { + ManageUsers: 'false' + } + + encrypted_params = encrypt_params({ role: { name: role.name, provider: role.provider, role_permissions: } }, expires_in: 10.seconds) + post :create_role, params: { v2: { encrypted_params: } } + expect(create_room_role_permission.reload.value).to eq(role_permissions[:ManageUsers]) + expect(response).to have_http_status(:created) + end + + it 'does not create other providers role role_permissions' do + role_permissions = { + ManageUsers: 'false' + } + + encrypted_params = encrypt_params({ role: { name: role.name, provider: role.provider, role_permissions: } }, expires_in: 10.seconds) + post :create_role, params: { v2: { encrypted_params: } } + expect(not_greenlight_create_room_role_permission.reload.value).not_to eq(role_permissions[:ManageUsers]) + expect(response).to have_http_status(:created) + end + end end context 'when decryption failes' do @@ -115,6 +149,7 @@ { name: 'user', email: 'user@users.com', + provider: 'greenlight', language: 'language', role: valid_user_role.name } @@ -155,6 +190,28 @@ end end + context 'when the provider does not exists' do + before { valid_user_params[:provider] = 'not_a_provider' } + + it 'returns :bad_request without creating a user' do + encrypted_params = encrypt_params({ user: valid_user_params }, expires_in: 10.seconds) + expect { post :create_user, params: { v2: { encrypted_params: } } }.not_to change(User, :count) + expect(response).to have_http_status(:bad_request) + end + end + + context 'when the provider is ldap' do + before { valid_user_params[:provider] = 'ldap' } + + it 'creates a user with the greenlight provider' do + encrypted_params = encrypt_params({ user: valid_user_params }, expires_in: 10.seconds) + expect { post :create_user, params: { v2: { encrypted_params: } } }.to change(User, :count).from(0).to(1) + user = User.take + expect(user.provider).to eq('greenlight') + expect(response).to have_http_status(:created) + end + end + context 'when external_id is present' do before { valid_user_params[:external_id] = 'EXTERNAL' } @@ -218,7 +275,7 @@ end context 'when providing a :provider or a :password' do - before { valid_user_params.merge!(provider: 'lightgreen', password: 'Password1!') } + before { valid_user_params.merge!(password: 'Password1!') } it 'returns :created and creates a user while ignoring the extra values' do encrypted_params = encrypt_params({ user: valid_user_params }, expires_in: 10.seconds) @@ -230,7 +287,7 @@ expect(user.language).to eq(valid_user_params[:language]) expect(user.role).to eq(valid_user_role) expect(user.session_token).to be_present - expect(user.provider).to eq('greenlight') + expect(user.provider).to eq(valid_user_params[:provider]) expect(response).to have_http_status(:created) expect(user.authenticate('Password1!')).to be_falsy end @@ -393,6 +450,7 @@ meeting_id: 'kzukaw3xk7ql5kefbfpsruud61pztf00jzltgafs', last_session: Time.zone.now.to_datetime, owner_email: user.email, + provider: 'greenlight', room_settings: {}, shared_users_emails: [] } @@ -410,6 +468,7 @@ expect(room.friendly_id).to eq(valid_room_params[:friendly_id]) expect(room.meeting_id).to eq(valid_room_params[:meeting_id]) expect(room.last_session).to eq(valid_room_params[:last_session]) + expect(room.user.provider).to eq(valid_room_params[:provider]) expect(room.user).to eq(user) expect(response).to have_http_status(:created) end @@ -420,6 +479,12 @@ encrypted_params = encrypt_params({ room: valid_room_params }, expires_in: 10.seconds) expect { post :create_room, params: { v2: { encrypted_params: } } }.not_to change(Room, :count) end + + it 'does not create a new room if the room owner does not have same provider has the room data' do + valid_room_params[:provider] = 'random_provider' + encrypted_params = encrypt_params({ room: valid_room_params }, expires_in: 10.seconds) + expect { post :create_room, params: { v2: { encrypted_params: } } }.not_to change(Room, :count) + end end describe 'when decrypted params data are invalid' do @@ -491,6 +556,94 @@ end end + describe '#create_settings' do + let(:primary_color_setting) { create(:setting, name: 'PrimaryColor') } + let(:terms_setting) { create(:setting, name: 'Terms') } + let(:registration_method_setting) { create(:setting, name: 'RegistrationMethod') } + + let!(:site_setting_a) { create(:site_setting, setting: primary_color_setting, value: 'valueA') } + let!(:site_setting_b) { create(:site_setting, setting: terms_setting, value: 'valueB') } + let!(:site_setting_c) { create(:site_setting, setting: registration_method_setting, value: 'valueC') } + + let!(:site_setting_d) do + create(:site_setting, setting: primary_color_setting, value: 'valueA', provider: 'not_greenlight') + end + let!(:site_setting_e) do + create(:site_setting, setting: terms_setting, value: 'valueB', provider: 'not_greenlight') + end + let!(:site_setting_f) do + create(:site_setting, setting: registration_method_setting, value: 'valueC', provider: 'not_greenlight') + end + + let(:record_meeting_option) { create(:meeting_option, name: 'record') } + let(:mute_on_start_meeting_option) { create(:meeting_option, name: 'muteOnStart') } + let(:guest_policy_meeting_option) { create(:meeting_option, name: 'guestPolicy') } + + let!(:rooms_config_a) { create(:rooms_configuration, meeting_option: record_meeting_option, value: 'true') } + let!(:rooms_config_b) { create(:rooms_configuration, meeting_option: mute_on_start_meeting_option, value: 'true') } + let!(:rooms_config_c) { create(:rooms_configuration, meeting_option: guest_policy_meeting_option, value: 'true') } + + let!(:rooms_config_d) do + create(:rooms_configuration, meeting_option: record_meeting_option, value: 'true', provider: 'not_greenlight') + end + let!(:rooms_config_e) do + create(:rooms_configuration, meeting_option: mute_on_start_meeting_option, value: 'true', provider: 'not_greenlight') + end + let!(:rooms_config_f) do + create(:rooms_configuration, meeting_option: guest_policy_meeting_option, value: 'true', provider: 'not_greenlight') + end + + let(:valid_settings_params) do + { + provider: 'greenlight', + site_settings: { + PrimaryColor: 'new_valueA', + Terms: 'new_valueB', + RegistrationMethod: 'new_valueC' + }, + rooms_configurations: { + record: 'false', + muteOnStart: 'false', + guestPolicy: 'false' + } + } + end + + before { clear_enqueued_jobs } + + it 'updates the site settings' do + encrypted_params = encrypt_params({ settings: valid_settings_params }, expires_in: 10.seconds) + post :create_settings, params: { v2: { encrypted_params: } } + expect(site_setting_a.reload.value).to eq(valid_settings_params[:site_settings][:PrimaryColor]) + expect(site_setting_b.reload.value).to eq(valid_settings_params[:site_settings][:Terms]) + expect(site_setting_c.reload.value).to eq(valid_settings_params[:site_settings][:RegistrationMethod]) + end + + it 'does not update the site settings for other providers' do + encrypted_params = encrypt_params({ settings: valid_settings_params }, expires_in: 10.seconds) + post :create_settings, params: { v2: { encrypted_params: } } + expect(site_setting_d.reload.value).to eq('valueA') + expect(site_setting_e.reload.value).to eq('valueB') + expect(site_setting_f.reload.value).to eq('valueC') + end + + it 'updates the room configs' do + encrypted_params = encrypt_params({ settings: valid_settings_params }, expires_in: 10.seconds) + post :create_settings, params: { v2: { encrypted_params: } } + expect(rooms_config_a.reload.value).to eq(valid_settings_params[:rooms_configurations][:record]) + expect(rooms_config_b.reload.value).to eq(valid_settings_params[:rooms_configurations][:muteOnStart]) + expect(rooms_config_c.reload.value).to eq(valid_settings_params[:rooms_configurations][:guestPolicy]) + end + + it 'does not update the room configs for other providers' do + encrypted_params = encrypt_params({ settings: valid_settings_params }, expires_in: 10.seconds) + post :create_settings, params: { v2: { encrypted_params: } } + expect(rooms_config_d.reload.value).to eq('true') + expect(rooms_config_e.reload.value).to eq('true') + expect(rooms_config_f.reload.value).to eq('true') + end + end + private def encrypt_params(params, key: nil, expires_at: nil, expires_in: nil, purpose: nil) diff --git a/spec/controllers/recordings_controller_spec.rb b/spec/controllers/recordings_controller_spec.rb index f4dd50494c..fd8a53c358 100644 --- a/spec/controllers/recordings_controller_spec.rb +++ b/spec/controllers/recordings_controller_spec.rb @@ -194,66 +194,88 @@ describe '#update_visibility' do let(:room) { create(:room, user:) } - let(:published_recording) { create(:recording, room:, visibility: 'Published') } - let(:unpublished_recording) { create(:recording, room:, visibility: 'Unpublished') } - let(:protected_recording) { create(:recording, room:, visibility: 'Protected') } + let(:recording) { create(:recording, room:) } - it 'changes the recording from Published to Unpublished on the bbb server' do - expect_any_instance_of(BigBlueButtonApi).to receive(:publish_recordings).with(record_ids: published_recording.record_id, publish: false) - expect_any_instance_of(BigBlueButtonApi).not_to receive(:update_recordings) - post :update_visibility, params: { visibility: 'Unpublished', id: published_recording.record_id } + def expect_to_update_recording_props_to(publish:, protect:, list:, visibility:) + expect_any_instance_of(BigBlueButtonApi).to receive(:publish_recordings).with(record_ids: recording.record_id, publish:) + expect_any_instance_of(BigBlueButtonApi).to receive(:update_recordings).with(record_id: recording.record_id, + meta_hash: { + protect:, 'meta_gl-listed': list + }) + + post :update_visibility, params: { visibility:, id: recording.record_id } + + expect(recording.reload.visibility).to eq(visibility) + expect(response).to have_http_status(:ok) end - it 'changes the recording from Published to Protected on the bbb server' do - expect_any_instance_of(BigBlueButtonApi).to receive(:update_recordings).with(record_id: published_recording.record_id, - meta_hash: { protect: true }) - expect_any_instance_of(BigBlueButtonApi).not_to receive(:publish_recordings) - post :update_visibility, params: { visibility: 'Protected', id: published_recording.record_id } + it 'changes the recording visibility to "Published"' do + expect_to_update_recording_props_to(publish: true, protect: false, list: false, visibility: Recording::VISIBILITIES[:published]) end - it 'changes the recording from Unpublished to Protected on the bbb server' do - expect_any_instance_of(BigBlueButtonApi).to receive(:publish_recordings).with(record_ids: unpublished_recording.record_id, publish: true) - expect_any_instance_of(BigBlueButtonApi).to receive(:update_recordings).with(record_id: unpublished_recording.record_id, - meta_hash: { protect: true }) - post :update_visibility, params: { visibility: 'Protected', id: unpublished_recording.record_id } + it 'changes the recording visibility to "Unpublished"' do + expect_to_update_recording_props_to(publish: false, protect: false, list: false, visibility: Recording::VISIBILITIES[:unpublished]) end - it 'changes the recording from Unpublished to Published on the bbb server' do - expect_any_instance_of(BigBlueButtonApi).to receive(:publish_recordings).with(record_ids: unpublished_recording.record_id, publish: true) - expect_any_instance_of(BigBlueButtonApi).not_to receive(:update_recordings) - post :update_visibility, params: { visibility: 'Published', id: unpublished_recording.record_id } + it 'changes the recording visibility to "Protected"' do + expect_to_update_recording_props_to(publish: true, protect: true, list: false, visibility: Recording::VISIBILITIES[:protected]) end - it 'changes the recording from Protected to Published on the bbb server' do - expect_any_instance_of(BigBlueButtonApi).not_to receive(:publish_recordings) - expect_any_instance_of(BigBlueButtonApi).to receive(:update_recordings).with(record_id: protected_recording.record_id, - meta_hash: { protect: false }) - post :update_visibility, params: { visibility: 'Published', id: protected_recording.record_id } + it 'changes the recording visibility to "Public"' do + expect_to_update_recording_props_to(publish: true, protect: false, list: true, visibility: Recording::VISIBILITIES[:public]) end - it 'changes the recording from Protected to Unpublished on the bbb server' do - expect_any_instance_of(BigBlueButtonApi).to receive(:publish_recordings).with(record_ids: protected_recording.record_id, publish: false) - expect_any_instance_of(BigBlueButtonApi).to receive(:update_recordings).with(record_id: protected_recording.record_id, - meta_hash: { protect: false }) - post :update_visibility, params: { visibility: 'Unpublished', id: protected_recording.record_id } + it 'changes the recording visibility to "Public/Protected"' do + expect_to_update_recording_props_to(publish: true, protect: true, list: true, visibility: Recording::VISIBILITIES[:public_protected]) end - it 'changes the local recording visibility' do - allow_any_instance_of(BigBlueButtonApi).to receive(:publish_recordings) - post :update_visibility, params: { visibility: 'Unpublished', id: unpublished_recording.record_id } - expect(unpublished_recording.reload.visibility).to eq('Unpublished') + context 'Unkown visibility' do + it 'returns :bad_request and does not update the recording' do + expect_any_instance_of(BigBlueButtonApi).not_to receive(:publish_recordings) + expect_any_instance_of(BigBlueButtonApi).not_to receive(:update_recordings) + + expect do + post :update_visibility, params: { visibility: '404', id: recording.record_id } + end.not_to(change { recording.reload.visibility }) + + expect(response).to have_http_status(:bad_request) + end end - it 'allows a shared user to update a recording visibility' do - shared_user = create(:user) - create(:shared_access, user_id: shared_user.id, room_id: room.id) - sign_in_user(shared_user) + context 'shared access' do + let(:signed_in_user) { create(:user) } + let(:recording) { create(:recording, room:, visibility: Recording::VISIBILITIES[:published]) } - expect_any_instance_of(BigBlueButtonApi).to receive(:publish_recordings).with(record_ids: published_recording.record_id, publish: false) - expect_any_instance_of(BigBlueButtonApi).not_to receive(:update_recordings) - expect do - post :update_visibility, params: { visibility: 'Unpublished', id: published_recording.record_id } - end.to(change { published_recording.reload.visibility }) + before do + sign_in_user(signed_in_user) + end + + it 'allows a shared user to update a recording visibility' do + create(:shared_access, user_id: signed_in_user.id, room_id: room.id) + + expect_any_instance_of(BigBlueButtonApi).to receive(:publish_recordings).with(record_ids: recording.record_id, publish: false) + expect_any_instance_of(BigBlueButtonApi).to receive(:update_recordings).with(record_id: recording.record_id, + meta_hash: { + protect: false, 'meta_gl-listed': false + }) + + expect do + post :update_visibility, params: { visibility: Recording::VISIBILITIES[:unpublished], id: recording.record_id } + end.to(change { recording.reload.visibility }) + + expect(response).to have_http_status(:ok) + end + + it 'disallows a none shared user to update a recording visibility' do + expect_any_instance_of(BigBlueButtonApi).not_to receive(:publish_recordings) + expect_any_instance_of(BigBlueButtonApi).not_to receive(:update_recordings) + + expect do + post :update_visibility, params: { visibility: Recording::VISIBILITIES[:unpublished], id: recording.record_id } + end.not_to(change { recording.reload.visibility }) + + expect(response).to have_http_status(:forbidden) + end end # TODO: samuel - add tests for user_with_manage_recordings_permission @@ -268,6 +290,216 @@ expect(JSON.parse(response.body)['data']).to be(5) end end + + describe '#recording_url' do + let(:room) { create(:room, user:) } + + before do + allow_any_instance_of(BigBlueButtonApi).to receive(:get_recording).and_return( + playback: { format: [{ type: 'screenshare', url: 'https://test.com/screenshare' }, { type: 'video', url: 'https://test.com/video' }] } + ) + end + + context 'format not passed' do + context 'Protected recording' do + let(:recording) do + create(:recording, visibility: [ + Recording::VISIBILITIES[:protected], + Recording::VISIBILITIES[:public_protected] + ].sample, + room:) + end + + it 'makes a call to BBB and returns the URLs' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to match_array ['https://test.com/screenshare', 'https://test.com/video'] + end + + context 'Single playback format' do + before do + allow_any_instance_of(BigBlueButtonApi).to receive(:get_recording).and_return( + playback: { format: { type: 'screenshare', url: 'https://test.com/screenshare' } } + ) + end + + it 'makes a call to BBB and returns the URL' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to eq ['https://test.com/screenshare'] + end + end + end + + context 'Unprotected recording' do + let(:recording) do + create(:recording, visibility: [ + Recording::VISIBILITIES[:published], + Recording::VISIBILITIES[:unpublished], + Recording::VISIBILITIES[:public] + ].sample, + room:) + end + + before { create_list(:format, Faker::Number.within(range: 1..3), recording:) } + + it 'returns the recording record formats URLs' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)).to match_array recording.formats.pluck(:url) + end + end + end + + context 'format is passed' do + context 'Protected recording' do + let(:recording) do + create(:recording, visibility: [ + Recording::VISIBILITIES[:protected], + Recording::VISIBILITIES[:public_protected] + ].sample, + room:) + end + + before { create(:format, recording:, recording_type: 'screenshare', url: 'https://invalid.com/screenshare') } + + it 'makes a call to BBB and returns the URL' do + post :recording_url, params: { id: recording.record_id, recording_format: 'screenshare' } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']).to eq 'https://test.com/screenshare' + end + + context 'Single playback format' do + before do + allow_any_instance_of(BigBlueButtonApi).to receive(:get_recording).and_return( + playback: { format: { type: 'screenshare', url: 'https://test.com/screenshare/solo' } } + ) + end + + it 'makes a call to BBB and returns the URL' do + post :recording_url, params: { id: recording.record_id, recording_format: 'screenshare' } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']).to eq 'https://test.com/screenshare/solo' + end + end + + context 'Inexistent format' do + it 'returns :not_found' do + post :recording_url, params: { id: recording.record_id, recording_format: '404' } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['data']).to be_blank + end + end + end + + context 'Unprotected recording' do + let(:recording) do + create(:recording, visibility: [ + Recording::VISIBILITIES[:published], + Recording::VISIBILITIES[:unpublished], + Recording::VISIBILITIES[:public] + ].sample, + room:) + end + + let(:target_format) { create(:format, recording:, recording_type: 'screenshare') } + + before { create_list(:format, 2, recording:) } + + it 'returns the formats URL' do + post :recording_url, params: { id: recording.record_id, recording_format: target_format.recording_type } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']).to eq target_format.url + end + + context 'Inexistent format' do + it 'returns :not_found' do + post :recording_url, params: { id: recording.record_id, recording_format: '404' } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['data']).to be_blank + end + end + end + end + + context 'Other users' do + let(:recording) { create(:recording, room:) } + let(:signed_in_user) { create(:user) } + + before { sign_in_user(signed_in_user) } + + it 'returns :forbidden' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:forbidden) + end + + context 'shared room' do + before do + create(:shared_access, user_id: signed_in_user.id, room_id: room.id) + end + + it 'returns :ok' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + end + end + + context 'Recordings Manager' do + before { sign_in_user(user_with_manage_recordings_permission) } + + it 'returns :ok' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + end + end + end + + context 'Unauthenticated' do + before { sign_out_user } + + describe 'Public recordings' do + let(:recording) { create(:recording, visibility: [Recording::VISIBILITIES[:public], Recording::VISIBILITIES[:public_protected]].sample) } + + it 'returns :ok' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:ok) + end + end + + describe 'Private recordings' do + let(:recording) do + create(:recording, + visibility: [Recording::VISIBILITIES[:protected], Recording::VISIBILITIES[:published], Recording::VISIBILITIES[:unpublished]].sample) + end + + it 'returns :forbidden' do + post :recording_url, params: { id: recording.record_id } + + expect(response).to have_http_status(:forbidden) + end + end + end + + context 'Inexistent recording' do + it 'returns :not_found' do + post :recording_url, params: { id: '404' } + + expect(response).to have_http_status(:not_found) + end + end + end end def http_ok_response diff --git a/spec/controllers/rooms_configurations_controller_spec.rb b/spec/controllers/rooms_configurations_controller_spec.rb index 4aab1b6eb4..9d38dd8cb6 100644 --- a/spec/controllers/rooms_configurations_controller_spec.rb +++ b/spec/controllers/rooms_configurations_controller_spec.rb @@ -46,4 +46,24 @@ expect(response).to have_http_status(:ok) end end + + describe 'rooms_configurations#show' do + before do + create(:rooms_configuration, meeting_option: create(:meeting_option, name: 'record'), value: 'false') + end + + it 'returns the correct configuration value' do + get :show, params: { name: 'record' } + + expect(JSON.parse(response.body)['data']).to eq('false') + + expect(response).to have_http_status(:ok) + end + + it 'returns :not_found if the configuration :name passed does not exist' do + get :show, params: { name: 'nonexistent' } + + expect(response).to have_http_status(:not_found) + end + end end diff --git a/spec/controllers/rooms_controller_spec.rb b/spec/controllers/rooms_controller_spec.rb index d86c9a3fdf..747ab0b840 100644 --- a/spec/controllers/rooms_controller_spec.rb +++ b/spec/controllers/rooms_controller_spec.rb @@ -152,6 +152,8 @@ end it 'deletes the recordings associated with the room' do + allow_any_instance_of(BigBlueButtonApi).to receive(:delete_recordings).and_return(true) + room = create(:room, user:) create_list(:recording, 10, room:) expect { delete :destroy, params: { friendly_id: room.friendly_id } }.to change(Recording, :count).by(-10) @@ -327,4 +329,218 @@ expect(recording_ids).to match_array(recordings.pluck(:id)) end end + + describe '#public_recordings' do + let(:room) { create(:room) } + + before { sign_out_user } + + context 'Filtration' do + let!(:public_recording) { create(:recording, room:, visibility: Recording::VISIBILITIES[:public]) } + let!(:public_protected_recording) { create(:recording, room:, visibility: Recording::VISIBILITIES[:public_protected]) } + + before do + create(:recording, room:, visibility: Recording::VISIBILITIES[:unpublished]) + create(:recording, room:, visibility: Recording::VISIBILITIES[:published]) + create(:recording, room:, visibility: Recording::VISIBILITIES[:protected]) + end + + it 'returns :ok with a list of the room public recordings only' do + expected_response = JSON.parse( + [PublicRecordingSerializer.new(public_recording), PublicRecordingSerializer.new(public_protected_recording)].to_json + ) + + get :public_recordings, params: { friendly_id: room.friendly_id } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data']).to match_array(expected_response) + end + end + + context 'Pagination' do + # The order of creation and the matching of :recorded_at value impacts a page recordings list. + # Thus fixing those values ensures the determinism of these examples. + let!(:first_page_recordings) do + create_list(:recording, Pagy::DEFAULT[:items], room:, recorded_at: Time.zone.at(1_686_943_664), visibility: Recording::VISIBILITIES[:public]) + end + let!(:second_page_recordings) do + create_list(:recording, Pagy::DEFAULT[:items], room:, recorded_at: Time.zone.at(1_686_943_664), + visibility: Recording::VISIBILITIES[:public_protected]) + end + + def expect_response_to_have(page:, pages:, recordings:) + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data'].pluck('id')).to match_array(recordings.pluck('id')) + expect(JSON.parse(response.body)['meta']['pages']).to eq(pages) + expect(JSON.parse(response.body)['meta']['page']).to eq(page) + end + + context 'First page' do + it 'returns :ok with a list of the first page room public recordings' do + get :public_recordings, params: { friendly_id: room.friendly_id, page: 1 } + + expect_response_to_have page: 1, pages: 2, recordings: first_page_recordings + end + end + + context 'Second page' do + it 'returns :ok with a list of the first second page room public recordings' do + get :public_recordings, params: { friendly_id: room.friendly_id, page: 2 } + + expect_response_to_have page: 2, pages: 2, recordings: second_page_recordings + end + end + + context 'No page' do + it 'returns :ok with a list of the first page room public recordings' do + get :public_recordings, params: { friendly_id: room.friendly_id } + + expect_response_to_have page: 1, pages: 2, recordings: first_page_recordings + end + end + + context 'Overflowing page' do + it 'returns :ok with a list of the last page room public recordings' do + get :public_recordings, params: { friendly_id: room.friendly_id, page: 3 } + + expect_response_to_have page: 2, pages: 2, recordings: second_page_recordings + end + end + end + + context 'Sorting' do + # The order of creation and the choice of :recorded_at, :name and :length is not RANDOM and was carefully crafted. + # It was meant to have a scenario with high entropy. + # We decrease inter-records correlation for those values (especially respective to :created_at). + let!(:third_recorded_named_c_length_one) do + create(:recording, room:, name: 'C', recorded_at: Time.zone.at(1_686_943_800), length: 1, visibility: Recording::VISIBILITIES[:public]) + end + let!(:second_recorded_named_a_length_three) do + create(:recording, room:, name: 'A', recorded_at: Time.zone.at(1_686_943_700), length: 3, visibility: Recording::VISIBILITIES[:public]) + end + let!(:first_recorded_named_b_length_two) do + create(:recording, room:, name: 'B', recorded_at: Time.zone.at(1_686_943_600), length: 2, visibility: Recording::VISIBILITIES[:public]) + end + let!(:last_recorded_named_c_length_one) do + create(:recording, room:, name: 'C', recorded_at: Time.zone.at(1_686_943_900), length: 1, visibility: Recording::VISIBILITIES[:public]) + end + + def expect_response_to_have_ordered(recordings:) + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data'].pluck('id')).to eq(recordings.pluck('id')) + end + + describe 'Sort by name' do + context 'ASC' do + it 'returns :ok with the list of the room public recordings sorted by name' do + get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'name', direction: 'ASC' } } + + expect_response_to_have_ordered recordings: [second_recorded_named_a_length_three, first_recorded_named_b_length_two, + last_recorded_named_c_length_one, third_recorded_named_c_length_one] + end + end + + context 'DESC' do + it 'returns :ok with the list of the room public recordings sorted by name' do + get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'name', direction: 'DESC' } } + + expect_response_to_have_ordered recordings: [last_recorded_named_c_length_one, third_recorded_named_c_length_one, + first_recorded_named_b_length_two, second_recorded_named_a_length_three] + end + end + end + + describe 'Sort by length' do + context 'ASC' do + it 'returns :ok with the list of the room public recordings sorted by length' do + get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'length', direction: 'ASC' } } + + expect_response_to_have_ordered recordings: [last_recorded_named_c_length_one, third_recorded_named_c_length_one, + first_recorded_named_b_length_two, second_recorded_named_a_length_three] + end + end + + context 'DESC' do + it 'returns :ok with the list of the room public recordings sorted by length' do + get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'length', direction: 'DESC' } } + + expect_response_to_have_ordered recordings: [second_recorded_named_a_length_three, first_recorded_named_b_length_two, + last_recorded_named_c_length_one, third_recorded_named_c_length_one] + end + end + end + + describe 'No sort by' do + it 'returns :ok with the list of the room public recordings sorted by recorded_at DESC' do + get :public_recordings, params: { friendly_id: room.friendly_id } + + expect_response_to_have_ordered recordings: [last_recorded_named_c_length_one, third_recorded_named_c_length_one, + second_recorded_named_a_length_three, first_recorded_named_b_length_two] + end + end + end + + context 'Search' do + let!(:applied_math) do + create(:recording, room:, name: 'Applied mathematics', visibility: Recording::VISIBILITIES[:public]) + end + let!(:advanced_math) do + create(:recording, room:, name: 'Advanced MaTHematics', visibility: Recording::VISIBILITIES[:public_protected]) + end + let!(:thermodynamics) do + create(:recording, room:, name: 'Thermodynamics', visibility: Recording::VISIBILITIES[:public_protected]) + end + + def expect_response_to_match(recordings:) + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['data'].pluck('id')).to match_array(recordings.pluck('id')) + end + + describe 'Matched by name' do + it 'returns :ok with the list of the room public recordings having matching name case insensitive' do + get :public_recordings, params: { friendly_id: room.friendly_id, search: 'math' } + + expect_response_to_match recordings: [applied_math, advanced_math] + end + end + + describe 'Matched by format type' do + before do + create(:format, recording: applied_math, recording_type: 'podcast') + end + + it 'returns :ok with the list of the room public recordings having matching visibility case insensitive' do + get :public_recordings, params: { friendly_id: room.friendly_id, search: 'podcast' } + + expect_response_to_match recordings: [applied_math] + end + end + + describe 'No match' do + it 'returns :ok with an empty list' do + get :public_recordings, + params: { friendly_id: room.friendly_id, search: [Recording::VISIBILITIES[:public_protected], Recording::VISIBILITIES[:public]].sample } + + expect_response_to_match recordings: [] + end + end + + describe 'No Search' do + it 'returns :ok with the list of the room public recordings' do + get :public_recordings, params: { friendly_id: room.friendly_id } + + expect_response_to_match recordings: [applied_math, advanced_math, thermodynamics] + end + end + end + + context 'Inexistent room' do + it 'returns :not_found' do + get :public_recordings, params: { friendly_id: '404' } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['data']).to be_blank + end + end + end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 471507ced1..7249637a56 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -25,6 +25,7 @@ before do ENV['SMTP_SERVER'] = 'test.com' + allow(controller).to receive(:external_authn_enabled?).and_return(false) request.headers['ACCEPT'] = 'application/json' end @@ -35,7 +36,7 @@ name: Faker::Name.name, email: Faker::Internet.email, password: 'Password123+', - language: 'language' + language: 'en' } } end @@ -68,7 +69,7 @@ context 'User language' do it 'Persists the user language in the user record' do post :create, params: user_params - expect(User.find_by(email: user_params[:user][:email]).language).to eq('language') + expect(User.find_by(email: user_params[:user][:email]).language).to eq('en') end it 'defaults user language to default_locale if the language isn\'t specified' do @@ -112,28 +113,49 @@ user = User.find_by email: user_params[:user][:email] expect(user).to be_verified expect(session[:session_token]).to eq(user.session_token) + expect(ActionMailer::MailDeliveryJob).not_to have_been_enqueued end end end - context 'Admin creation' do - before { sign_in_user(user) } + context 'Authenticated request' do + context 'Not admin creation' do + let(:signed_in_user) { user } - it 'sends activation email to but does NOT signin the created user' do - expect { post :create, params: user_params }.to change(User, :count).by(1) - expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.at(:no_wait).exactly(:once).with('UserMailer', 'activate_account_email', - 'deliver_now', Hash) - expect(response).to have_http_status(:created) - expect(session[:session_token]).to eql(user.session_token) + before { sign_in_user(signed_in_user) } + + it 'returns :forbidden and does NOT create the user' do + expect { post :create, params: user_params }.not_to change(User, :count) + expect(ActionMailer::MailDeliveryJob).not_to have_been_enqueued + + expect(response).to have_http_status(:forbidden) + expect(session[:session_token]).to eql(signed_in_user.session_token) + end end - context 'User language' do - it 'defaults user language to admin language if the language isn\'t specified' do - user.update! language: 'language' + context 'Admin creation' do + let(:signed_in_user) { user_with_manage_users_permission } - user_params[:user][:language] = nil - post :create, params: user_params - expect(User.find_by(email: user_params[:user][:email]).language).to eq('language') + before { sign_in_user(signed_in_user) } + + it 'sends activation email to but does NOT signin the created user' do + expect { post :create, params: user_params }.to change(User, :count).by(1) + expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.at(:no_wait).exactly(:once).with('UserMailer', 'activate_account_email', + 'deliver_now', Hash) + expect(response).to have_http_status(:created) + expect(session[:session_token]).to eql(signed_in_user.session_token) + end + + context 'User language' do + it 'defaults user language to admin language if the language isn\'t specified' do + signed_in_user.update! language: 'en' + + user_params[:user][:language] = nil + post :create, params: user_params + expect(User.find_by(email: user_params[:user][:email]).language).to eq('en') + expect(response).to have_http_status(:created) + expect(session[:session_token]).to eql(signed_in_user.session_token) + end end end end @@ -180,7 +202,7 @@ name: 'Optimus Prime', email: 'optimus@autobots.cybertron', password: 'Autobots1!', - language: 'teletraan' + language: 'en' } expect { post :create, params: { user: user_params } }.to change(User, :count).from(0).to(1) @@ -255,6 +277,20 @@ end end end + + context 'External AuthN enabled' do + before do + allow(controller).to receive(:external_authn_enabled?).and_return(true) + end + + it 'returns :forbidden without creating the user account' do + expect { post :create, params: user_params }.not_to change(User, :count) + + expect(response).to have_http_status(:forbidden) + expect(JSON.parse(response.body)['data']).to be_blank + expect(JSON.parse(response.body)['errors']).not_to be_nil + end + end end describe '#show' do @@ -277,7 +313,7 @@ it 'updates the users attributes' do updated_params = { name: 'New Name', - language: 'gl' + language: 'fr' } patch :update, params: { id: user.id, user: updated_params } expect(response).to have_http_status(:ok) @@ -317,15 +353,32 @@ expect(user.role_id).not_to eq(updated_params[:role_id]) end - it 'allows a user with ManageUser permissions to edit their own role' do + it 'allows a user to change their own name' do + updated_params = { + name: 'New Awesome Name' + } + + patch :update, params: { id: user.id, user: updated_params } + + user.reload + + expect(user.name).to eq(updated_params[:name]) + end + + it 'doesnt allow a user with ManageUser permissions to edit their own role' do sign_in_user(user_with_manage_users_permission) + + old_role_id = user_with_manage_users_permission.role_id updated_params = { role_id: create(:role, name: 'New Role').id } + patch :update, params: { id: user_with_manage_users_permission.id, user: updated_params } user_with_manage_users_permission.reload - expect(user_with_manage_users_permission.role_id).to eq(updated_params[:role_id]) + expect(response).to have_http_status(:forbidden) + expect(user_with_manage_users_permission.role_id).to eq(old_role_id) + expect(user_with_manage_users_permission.role_id).not_to eq(updated_params[:role_id]) end it 'allows a user with ManageUser permissions to edit another users role' do @@ -417,4 +470,32 @@ expect(response).to have_http_status(:forbidden) end end + + context 'private methods' do + describe '#external_authn_enabled?' do + before do + allow(controller).to receive(:external_authn_enabled?).and_call_original + end + + context 'OPENID_CONNECT_ISSUER is present?' do + before do + ENV['OPENID_CONNECT_ISSUER'] = 'issuer' + end + + it 'returns true' do + expect(controller).to be_external_authn_enabled + end + end + + context 'OPENID_CONNECT_ISSUER is NOT present?' do + before do + ENV['OPENID_CONNECT_ISSUER'] = '' + end + + it 'returns false' do + expect(controller).not_to be_external_authn_enabled + end + end + end + end end diff --git a/spec/factories/recording_factory.rb b/spec/factories/recording_factory.rb index 7c1655bea2..c90e7668cc 100644 --- a/spec/factories/recording_factory.rb +++ b/spec/factories/recording_factory.rb @@ -21,7 +21,7 @@ room name { Faker::Educator.course_name } record_id { Faker::Internet.uuid } - visibility { 'Unpublished' } + visibility { Recording::VISIBILITIES[:unpublished] } length { Faker::Number.within(range: 1..60) } participants { Faker::Number.within(range: 1..100) } recorded_at { Faker::Time.between(from: 2.days.ago, to: Time.zone.now) } diff --git a/spec/factories/user_factory.rb b/spec/factories/user_factory.rb index d07008f8d4..e59a4f63b8 100644 --- a/spec/factories/user_factory.rb +++ b/spec/factories/user_factory.rb @@ -29,6 +29,14 @@ language { %w[en fr es ar].sample } verified { true } + trait :with_super_admin do + after(:create) do |user| + user.provider = 'bn' + user.role = create(:role, :with_super_admin) + user.save + end + end + trait :with_manage_users_permission do after(:create) do |user| create(:role_permission, role: user.role, permission: create(:permission, name: 'ManageUsers'), value: 'true') diff --git a/spec/fixtures/files/default-avatar.jpg b/spec/fixtures/files/default-avatar.jpg new file mode 100644 index 0000000000..069a677d96 Binary files /dev/null and b/spec/fixtures/files/default-avatar.jpg differ diff --git a/spec/fixtures/files/default-avatar.svg b/spec/fixtures/files/default-avatar.svg new file mode 100644 index 0000000000..817fa0597a --- /dev/null +++ b/spec/fixtures/files/default-avatar.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/helpers.rb b/spec/helpers.rb index 05f6b8f674..6e679ec4a0 100644 --- a/spec/helpers.rb +++ b/spec/helpers.rb @@ -20,4 +20,8 @@ module Helpers def sign_in_user(user) session[:session_token] = user.session_token end + + def sign_out_user + session[:session_token] = nil + end end diff --git a/spec/models/recording_spec.rb b/spec/models/recording_spec.rb index 7d074b7de0..6f9b974523 100644 --- a/spec/models/recording_spec.rb +++ b/spec/models/recording_spec.rb @@ -19,6 +19,20 @@ require 'rails_helper' RSpec.describe Recording, type: :model do + describe 'Constants' do + context 'VISIBILITES' do + it 'matches certain map' do + expect(Recording::VISIBILITIES).to eq({ + published: 'Published', + unpublished: 'Unpublished', + protected: 'Protected', + public: 'Public', + public_protected: 'Public/Protected' + }) + end + end + end + describe 'validations' do subject { create(:recording) } @@ -29,6 +43,7 @@ it { is_expected.to validate_presence_of(:visibility) } it { is_expected.to validate_presence_of(:length) } it { is_expected.to validate_presence_of(:participants) } + it { is_expected.to validate_inclusion_of(:visibility).in_array(Recording::VISIBILITIES.values) } end describe 'scopes' do @@ -62,4 +77,51 @@ expect(described_class.all.search('').pluck(:id)).to match_array(described_class.all.pluck(:id)) end end + + describe '#public_search' do + let(:recording1) { create(:recording, name: 'Greenlight 101', visibility: Recording::VISIBILITIES[:public]) } + let(:recording2) { create(:recording, name: 'Greenlight 201', visibility: Recording::VISIBILITIES[:public]) } + let(:recording3) { create(:recording, name: 'Bluelight 301', visibility: Recording::VISIBILITIES[:public]) } + + before do + create_list(:recording, 5) + create(:format, recording: recording3, recording_type: 'podcast') + end + + context 'Matching name' do + it 'returns the searched recordings' do + expect(described_class.public_search('greenlight')).to match_array([recording1, recording2]) + end + end + + context 'Matching format type' do + it 'returns the searched recordings' do + expect(described_class.public_search('podcast')).to match_array([recording3]) + end + end + + context 'Matching visibility' do + it 'returns an empty list' do + expect(described_class.public_search('public')).to be_empty + end + end + + context 'No match' do + it 'returns an empty list' do + expect(described_class.public_search('404')).to be_empty + end + end + + it 'returns all recordings if input is empty' do + expect(described_class.all.search('')).to match_array(described_class.all) + end + end + + describe 'after_destroy' do + it 'makes a call to BBB to delete the recording' do + expect_any_instance_of(BigBlueButtonApi).to receive(:delete_recordings).and_return(true) + + create(:recording).destroy + end + end end diff --git a/spec/models/room_spec.rb b/spec/models/room_spec.rb index 721c682184..fca0f4f397 100644 --- a/spec/models/room_spec.rb +++ b/spec/models/room_spec.rb @@ -164,5 +164,24 @@ expect(room.get_setting(name: '404')).to be_nil end end + + describe '#public_recordings' do + let(:public_recordings) do + [ + create(:recording, room:, visibility: Recording::VISIBILITIES[:public]), + create(:recording, room:, visibility: Recording::VISIBILITIES[:public_protected]) + ] + end + + before do + [Recording::VISIBILITIES[:unpublished], Recording::VISIBILITIES[:published], Recording::VISIBILITIES[:protected]].each do |visibility| + create(:recording, room:, visibility:) + end + end + + it 'retuns filters out the room public recordings' do + expect(room.public_recordings).to match_array(public_recordings) + end + end end end diff --git a/spec/models/site_setting_spec.rb b/spec/models/site_setting_spec.rb index 01e1567c51..0328dc8b8f 100644 --- a/spec/models/site_setting_spec.rb +++ b/spec/models/site_setting_spec.rb @@ -22,5 +22,34 @@ describe 'validations' do it { is_expected.to belong_to(:setting) } it { is_expected.to validate_presence_of(:provider) } + + context 'image validations' do + it 'passes if the attachement is a png' do + site_setting = build(:site_setting, image: fixture_file_upload(file_fixture('default-avatar.png'), 'image/png')) + expect(site_setting).to be_valid + end + + it 'passes if the attachement is a jpg' do + site_setting = build(:site_setting, image: fixture_file_upload(file_fixture('default-avatar.jpg'), 'image/jpeg')) + expect(site_setting).to be_valid + end + + it 'passes if the attachement is a svg' do + site_setting = build(:site_setting, image: fixture_file_upload(file_fixture('default-avatar.svg'), 'image/svg+xml')) + expect(site_setting).to be_valid + end + + it 'fails if the attachement isn\'t of image type' do + site_setting = build(:site_setting, image: fixture_file_upload(file_fixture('default-pdf.pdf'), 'application/pdf')) + expect(site_setting).to be_invalid + expect(site_setting.errors).to be_of_kind(:image, :content_type_invalid) + end + + it 'fails if the attachement is too large' do + site_setting = build(:site_setting, image: fixture_file_upload(file_fixture('large-avatar.jpg'), 'image/jpeg')) + expect(site_setting).to be_invalid + expect(site_setting.errors).to be_of_kind(:image, :file_size_out_of_range) + end + end end end diff --git a/spec/services/permissions_checker_spec.rb b/spec/services/permissions_checker_spec.rb index 339f61ebb3..99a0d07939 100644 --- a/spec/services/permissions_checker_spec.rb +++ b/spec/services/permissions_checker_spec.rb @@ -170,5 +170,143 @@ end end end + + context 'Public recordings' do + let(:public_recording) { create(:recording, visibility: [Recording::VISIBILITIES[:public], Recording::VISIBILITIES[:public_protected]].sample) } + + let(:private_recording) do + create(:recording, + visibility: [Recording::VISIBILITIES[:published], Recording::VISIBILITIES[:protected], Recording::VISIBILITIES[:unpublished]].sample) + end + + it 'returns true if the recording is public' do + expect(described_class.new( + current_user: nil, + permission_names: 'PublicRecordings', + user_id: '', + friendly_id: '', + record_id: public_recording.record_id, + current_provider: public_recording.user.provider + ).call).to be(true) + end + + it 'returns false if the recording is private' do + expect(described_class.new( + current_user: nil, + permission_names: 'PublicRecordings', + user_id: '', + friendly_id: '', + record_id: private_recording.record_id, + current_provider: private_recording.user.provider + ).call).to be(false) + end + + describe 'Differnt provider' do + it 'returns false' do + expect(described_class.new( + current_user: nil, + permission_names: 'PublicRecordings', + user_id: '', + friendly_id: '', + record_id: public_recording.record_id, + current_provider: "NOT_#{public_recording.user.provider}" + ).call).to be(false) + end + end + + describe 'Inexistent recording' do + it 'returns false' do + expect(described_class.new( + current_user: nil, + permission_names: 'PublicRecordings', + user_id: '', + friendly_id: '', + record_id: '404', + current_provider: public_recording.user.provider + ).call).to be(false) + end + end + end + + context 'ManageRecordings' do + let(:current_user) { create(:user) } + + describe 'Recording owned by current user' do + let(:room) { create(:room, user: current_user) } + let(:recording) { create(:recording, room:) } + + it 'returns true' do + expect(described_class.new( + current_user:, + permission_names: 'ManageRecordings', + user_id: '', + friendly_id: '', + record_id: recording.record_id, + current_provider: current_user.provider + ).call).to be(true) + end + end + + describe 'Recording not owned by current user' do + let(:recording) { create(:recording) } + + it 'returns false' do + expect(described_class.new( + current_user:, + permission_names: 'ManageRecordings', + user_id: '', + friendly_id: '', + record_id: recording.record_id, + current_provider: current_user.provider + ).call).to be(false) + end + + context 'User with ManageRecordings permission' do + let(:current_user) { create(:user, :with_manage_recordings_permission) } + + describe 'Same provider' do + it 'returns true' do + expect(described_class.new( + current_user:, + permission_names: 'ManageRecordings', + user_id: '', + friendly_id: '', + record_id: recording.record_id, + current_provider: current_user.provider + ).call).to be(true) + end + end + + describe 'Different provider' do + it 'returns true' do + expect(described_class.new( + current_user:, + permission_names: 'ManageRecordings', + user_id: '', + friendly_id: '', + record_id: recording.record_id, + current_provider: "NOT_#{recording.user.provider}" + ).call).to be(false) + end + end + end + end + + describe 'Unauthenticated user' do + let(:current_user) { nil } + let(:recording) { create(:recording) } + + it 'returns false' do + expect(described_class.new( + current_user:, + permission_names: 'ManageRecordings', + user_id: '', + friendly_id: '', + record_id: recording.record_id, + current_provider: recording.room.user.provider + ).call).to be(false) + end + end + end end end diff --git a/spec/services/provider_credentials_spec.rb b/spec/services/provider_credentials_spec.rb index c910e0e485..2f3062b5d5 100644 --- a/spec/services/provider_credentials_spec.rb +++ b/spec/services/provider_credentials_spec.rb @@ -61,7 +61,7 @@ service.call expect(service.call).to eq(['https://test.com', 'secret']) - expect(Rails.cache.read('bbb/getUser')).to eq(['https://test.com', 'secret']) + expect(Rails.cache.read('v3/bbb/getUser')).to eq(['https://test.com', 'secret']) end end end diff --git a/spec/services/recording_creator_spec.rb b/spec/services/recording_creator_spec.rb index 05556dea5c..1c697d0ea1 100644 --- a/spec/services/recording_creator_spec.rb +++ b/spec/services/recording_creator_spec.rb @@ -21,54 +21,230 @@ describe RecordingCreator, type: :service do let(:room) { create(:room) } + let(:bbb_recording) { single_format_recording } - describe '#call' do - it 'creates single recording and format based on response' do - room.update(meeting_id: 'random-1291479') + before { room.update(meeting_id: single_format_recording[:meetingID]) } - described_class.new(recording: single_format_recording).call + describe '#call' do + it 'creates recording if not found on GL based on BBB response' do + expect do + described_class.new(recording: bbb_recording).call + end.to change(Recording, :count).from(0).to(1) - expect(room.recordings.first.record_id).to eq('f0e2be4518868febb0f381ebe7d46ae61364ef1e-1652287428125') - expect(room.recordings.first.formats.count).to eq(1) - expect(room.recordings.first.formats.first.recording_type).to eq('presentation') + expect(room.recordings.first.record_id).to eq(bbb_recording[:recordID]) + expect(room.recordings.first.participants).to eq(bbb_recording[:participants].to_i) + expect(room.recordings.first.recorded_at.to_i).to eq(bbb_recording[:startTime].to_i) end - it 'creates multiple formats based on response' do - room.update(meeting_id: 'random-5678484') + it 'updates recording data if found on GL based on BBB response' do + create(:recording, room:, record_id: bbb_recording[:recordID]) - described_class.new(recording: multiple_formats_recording).call + expect do + described_class.new(recording: bbb_recording).call + end.not_to change(Recording, :count) - expect(room.recordings.first.formats.count).to eq(2) - expect(room.recordings.first.formats.first.recording_type).to eq('presentation') - expect(room.recordings.first.formats.second.recording_type).to eq('podcast') + expect(room.recordings.first.record_id).to eq(bbb_recording[:recordID]) + expect(room.recordings.first.participants).to eq(bbb_recording[:participants].to_i) + expect(room.recordings.first.recorded_at.to_i).to eq(bbb_recording[:startTime].to_i) end it 'does not create duplicate recordings if called more than once' do - room.update(meeting_id: 'random-1291479') + expect do + described_class.new(recording: bbb_recording).call + end.to change(Recording, :count).from(0).to(1) - described_class.new(recording: single_format_recording).call - described_class.new(recording: single_format_recording).call - described_class.new(recording: single_format_recording).call + expect do + described_class.new(recording: bbb_recording).call + end.not_to change(Recording, :count) - expect(room.recordings.count).to eq(1) + expect do + described_class.new(recording: bbb_recording).call + end.not_to change(Recording, :count) end - it 'returns recording protectable attribute as true if the bbb server protected feature is enabled' do - room.update(meeting_id: 'random-1291479') - described_class.new(recording: protected_recording).call - expect(room.recordings.first.protectable).to be(true) + context 'Formats' do + describe 'Single format' do + let(:bbb_recording) { single_format_recording } + + it 'creates single recording and format based on response' do + expect do + described_class.new(recording: bbb_recording).call + end.to change(Recording, :count).from(0).to(1) + + expect(room.recordings.first.formats.count).to eq(1) + expect(room.recordings.first.formats.first.recording_type).to eq('presentation') + expect(room.recordings.first.length).to eq(bbb_recording[:playback][:format][:length]) + end + end + + describe 'Multiple formats' do + let(:bbb_recording) { multiple_formats_recording } + + it 'creates multiple formats based on response' do + expect do + described_class.new(recording: bbb_recording).call + end.to change(Recording, :count).from(0).to(1) + + expect(room.recordings.first.formats.count).to eq(2) + expect(room.recordings.first.formats.first.recording_type).to eq('presentation') + expect(room.recordings.first.formats.second.recording_type).to eq('podcast') + expect(room.recordings.first.length).to eq(bbb_recording[:playback][:format][0][:length]) + end + end end - it 'returns recording protectable attribute as false if the bbb server protected feature is not enabled' do - room.update(meeting_id: 'random-1291479') - described_class.new(recording: single_format_recording).call - expect(room.recordings.first.protectable).to be(false) + context 'Meeting ID' do + describe 'When meta Meeting ID is NOT returned' do + let(:bbb_recording) { without_meta_meeting_id_recording(meeting_id: room.meeting_id) } + + it 'Finds recording room by the response meeting ID' do + expect do + described_class.new(recording: bbb_recording).call + end.not_to raise_error + + expect(room.recordings.first.record_id).to eq(bbb_recording[:recordID]) + expect(room.meeting_id).to eq(bbb_recording[:meetingID]) + expect(room.meeting_id).not_to eq(bbb_recording[:metadata][:meetingId]) + end + end + + describe 'When meta Meeting ID is returned' do + let(:bbb_recording) { with_meta_meeting_id_recording(meeting_id: room.meeting_id) } + + it 'Finds recording room by the response metadata meeting ID' do + expect do + described_class.new(recording: bbb_recording).call + end.not_to raise_error + + expect(room.recordings.first.record_id).to eq(bbb_recording[:recordID]) + expect(room.meeting_id).to eq(bbb_recording[:metadata][:meetingId]) + expect(room.meeting_id).not_to eq(bbb_recording[:meetingID]) + end + end + + describe 'Inexsitent room for the given meeting ID' do + let(:bbb_recording) { without_meta_meeting_id_recording(meeting_id: '404') } + + it 'Fails without upserting recording' do + expect do + described_class.new(recording: bbb_recording).call + end.to raise_error(ActiveRecord::RecordNotFound) + + expect(room.recordings.count).to eq(0) + end + end + end + + context 'Name' do + describe 'When meta name is NOT returned' do + let(:bbb_recording) { without_meta_name_recording } + + it 'sets recording name to response name' do + described_class.new(recording: bbb_recording).call + + expect(room.recordings.first.name).not_to eq(bbb_recording[:metadata][:name]) + expect(room.recordings.first.name).to eq(bbb_recording[:name]) + end + end + + describe 'When meta name is returned' do + let(:bbb_recording) { with_meta_name_recording } + + it 'sets recording name to response metadata name' do + described_class.new(recording: bbb_recording).call + + expect(room.recordings.first.name).not_to eq(bbb_recording[:name]) + expect(room.recordings.first.name).to eq(bbb_recording[:metadata][:name]) + end + end + end + + context 'Protectable' do + describe 'When BBB server protected feature is enabled' do + let(:bbb_recording) { protected_recording } + + it 'returns recording protectable attribute as true' do + described_class.new(recording: bbb_recording).call + expect(room.recordings.first.protectable).to be(true) + end + end + + describe 'When BBB server protected feature is NOT enabled' do + let(:bbb_recording) { single_format_recording } + + it 'returns recording protectable attribute as false if the bbb server protected feature is not enabled' do + described_class.new(recording: bbb_recording).call + expect(room.recordings.first.protectable).to be(false) + end + end + end + + context 'Visibility' do + describe 'Published' do + let(:bbb_recording) { published_recording } + + it 'sets a BBB published recording visibility to "Published"' do + described_class.new(recording: bbb_recording).call + + expect(room.recordings.first.visibility).to eq(Recording::VISIBILITIES[:published]) + end + end + + describe 'Protected' do + let(:bbb_recording) { protected_recording } + + it 'sets a BBB published recording visibility to "Protected"' do + described_class.new(recording: bbb_recording).call + + expect(room.recordings.first.visibility).to eq(Recording::VISIBILITIES[:protected]) + end + end + + describe 'Unpublished' do + let(:bbb_recording) { unpublished_recording } + + it 'sets a BBB published recording visibility to "Unpublished"' do + described_class.new(recording: bbb_recording).call + + expect(room.recordings.first.visibility).to eq(Recording::VISIBILITIES[:unpublished]) + end + end + + describe 'Public' do + let(:bbb_recording) { public_recording } + + it 'sets a BBB public recording visibility to "Public"' do + described_class.new(recording: bbb_recording).call + + expect(room.recordings.first.visibility).to eq(Recording::VISIBILITIES[:public]) + end + end + + describe 'Public/Protected' do + let(:bbb_recording) { public_protected_recording } + + it 'sets a BBB Public/Protected recording visibility to "Public/Protected"' do + described_class.new(recording: bbb_recording).call + + expect(room.recordings.first.visibility).to eq(Recording::VISIBILITIES[:public_protected]) + end + end + + describe 'Unkown cases' do + let(:bbb_recording) { unkown_visibility_recording } + + it 'sets a BBB with unkown recording visibility to "Unpublished"' do + described_class.new(recording: bbb_recording).call + + expect(room.recordings.first.visibility).to eq(Recording::VISIBILITIES[:unpublished]) + end + end end end private - def single_format_recording + def dummy_recording(**args) { recordID: 'f0e2be4518868febb0f381ebe7d46ae61364ef1e-1652287428125', meetingID: 'random-1291479', @@ -77,86 +253,89 @@ def single_format_recording isBreakout: 'false', published: true, state: 'published', - startTime: 'Wed, 11 May 2022 12:43:48 -0400'.to_datetime, - endTime: 'Wed, 11 May 2022 12:44:20 -0400'.to_datetime, - participants: '1', + startTime: Faker::Time.between(from: 2.days.ago, to: Time.zone.now).to_datetime, + endTime: Faker::Time.between(from: 2.days.ago, to: Time.zone.now).to_datetime, + participants: Faker::Number.within(range: 1..100).to_s, rawSize: '977816', - metadata: { isBreakout: 'false', meetingId: 'random-1291479', meetingName: 'random-1291479' }, + metadata: { isBreakout: 'false' }, size: '305475', playback: { format: { type: 'presentation', url: 'https://test24.bigbluebutton.org/playback/presentation/2.3/f0e2be4518868febb0f381ebe7d46ae61364ef1e-1652287428125', processingTime: '6386', - length: 0, + length: Faker::Number.within(range: 1..60), size: '305475' } }, data: {} - } + }.merge(args) + end + + def single_format_recording + dummy_recording end def multiple_formats_recording - { - recordID: '955458f326d02d78ef8d27f4fbf5fafb7c2f666a-1652296432321', - meetingID: 'random-5678484', - internalMeetingID: '955458f326d02d78ef8d27f4fbf5fafb7c2f666a-1652296432321', - name: 'random-5678484', - isBreakout: 'false', - published: true, - state: 'published', - startTime: 'Wed, 11 May 2022 15:13:52 -0400'.to_datetime, - endTime: 'Wed, 11 May 2022 15:14:19 -0400'.to_datetime, - participants: '1', - rawSize: '960565', - metadata: { isBreakout: 'false', meetingId: 'random-5678484', meetingName: 'random-5678484' }, - size: '272997', - playback: { - format: [{ - type: 'presentation', - url: 'https://test24.bigbluebutton.org/playback/presentation/2.3/955458f326d02d78ef8d27f4fbf5fafb7c2f666a-1652296432321', - processingTime: '5780', - length: 0, - size: '211880' - }, - { - type: 'podcast', - url: 'https://test24.bigbluebutton.org/podcast/955458f326d02d78ef8d27f4fbf5fafb7c2f666a-1652296432321/audio.ogg', - processingTime: '0', - length: 0, - size: '61117' - }] + dummy_recording playback: { + format: [{ + type: 'presentation', + url: 'https://test24.bigbluebutton.org/playback/presentation/2.3/955458f326d02d78ef8d27f4fbf5fafb7c2f666a-1652296432321', + processingTime: '5780', + length: 0, + size: '211880' }, - data: {} + { + type: 'podcast', + url: 'https://test24.bigbluebutton.org/podcast/955458f326d02d78ef8d27f4fbf5fafb7c2f666a-1652296432321/audio.ogg', + processingTime: '0', + length: 0, + size: '61117' + }] } end + def without_meta_meeting_id_recording(meeting_id:) + dummy_recording meetingID: meeting_id + end + + def with_meta_meeting_id_recording(meeting_id:) + dummy_recording meetingID: "NOT_#{meeting_id}", metadata: { isBreakout: 'false', meetingId: meeting_id } + end + + def without_meta_name_recording + name = Faker::Name.name + + dummy_recording name:, metadata: { isBreakout: 'false' } + end + + def with_meta_name_recording + name = Faker::Name.name + + dummy_recording name: "WRONG_#{name}", metadata: { isBreakout: 'false', name: } + end + def protected_recording - { - recordID: 'f0e2be4518868febb0f381ebe7d46ae61364ef1e-1652287428125', - meetingID: 'random-1291479', - internalMeetingID: 'f0e2be4518868febb0f381ebe7d46ae61364ef1e-1652287428125', - name: 'random-1291479', - isBreakout: 'false', - published: true, - protected: true, - state: 'published', - startTime: 'Wed, 11 May 2022 12:43:48 -0400'.to_datetime, - endTime: 'Wed, 11 May 2022 12:44:20 -0400'.to_datetime, - participants: '1', - rawSize: '977816', - metadata: { isBreakout: 'false', meetingId: 'random-1291479', meetingName: 'random-1291479' }, - size: '305475', - playback: { - format: { - type: 'presentation', - url: 'https://test24.bigbluebutton.org/playback/presentation/2.3/f0e2be4518868febb0f381ebe7d46ae61364ef1e-1652287428125', - processingTime: '6386', - length: 0, - size: '305475' - } - }, - data: {} - } + dummy_recording published: true, protected: true, metadata: { isBreakout: 'false', 'gl-listed': [false, nil].sample } + end + + def published_recording + dummy_recording published: true, protected: false, metadata: { isBreakout: 'false', 'gl-listed': [false, nil].sample } + end + + def unpublished_recording + dummy_recording published: false, protected: false, metadata: { isBreakout: 'false', 'gl-listed': [false, nil].sample } + end + + def public_recording + dummy_recording published: true, protected: false, metadata: { isBreakout: 'false', 'gl-listed': true } + end + + def public_protected_recording + dummy_recording published: true, protected: true, metadata: { isBreakout: 'false', 'gl-listed': true } + end + + def unkown_visibility_recording + dummy_recording published: false, protected: [true, false].sample, metadata: { isBreakout: 'false', 'gl-listed': [true, false].sample } end end diff --git a/spec/services/recordings_sync_spec.rb b/spec/services/recordings_sync_spec.rb index 6d9d378e42..92a11caea8 100644 --- a/spec/services/recordings_sync_spec.rb +++ b/spec/services/recordings_sync_spec.rb @@ -23,6 +23,10 @@ let(:room) { create(:room, user:, recordings_processing: 5) } let(:service) { described_class.new(room:, provider: 'greenlight') } + before do + allow_any_instance_of(BigBlueButtonApi).to receive(:delete_recordings).and_return(true) + end + describe '#call' do let(:fake_recording_creator) { instance_double(RecordingCreator) } let(:other_recordings) { create_list(:recording, 2) } @@ -61,7 +65,7 @@ expect(RecordingCreator).to receive(:new).with(recording: multiple_recordings_response[:recordings][1]).and_call_original service.call - expect(Recording.where(id: other_recordings.pluck(:id))).to eq(other_recordings) + expect(Recording.where(id: other_recordings.pluck(:id))).to match_array(other_recordings) end it 'resets the recordings processing value for the room' do diff --git a/spec/services/running_meeting_checker_spec.rb b/spec/services/running_meeting_checker_spec.rb new file mode 100644 index 0000000000..fa0ed5f102 --- /dev/null +++ b/spec/services/running_meeting_checker_spec.rb @@ -0,0 +1,83 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RunningMeetingChecker, type: :service do + let!(:room) { create(:room, user: create(:user, provider: 'greenlight'), online: true) } + let!(:rooms) { create_list(:room, 3, user: create(:user, provider: 'greenlight'), online: true) } + + describe '#call' do + it 'retrieves the room participants for an online room' do + allow_any_instance_of(BigBlueButtonApi).to receive(:get_meeting_info).with(meeting_id: room.meeting_id).and_return(meeting_info) + + described_class.new(rooms: room).call + + expect(room.participants).to eq(5) + end + + it 'retrieves the room participants for multiple online room' do + allow_any_instance_of(BigBlueButtonApi).to receive(:get_meeting_info).and_return(meeting_info) + + described_class.new(rooms:).call + + rooms.each do |room| + expect(room.participants).to eq(5) + end + end + + it 'handles BigBlueButtonException and sets room online status to false' do + allow_any_instance_of(BigBlueButtonApi).to receive(:get_meeting_info).and_raise(BigBlueButton::BigBlueButtonException) + + described_class.new(rooms: room).call + + expect(room.online).to be(false) + end + end + + def meeting_info + { + returncode: 'SUCCESS', + meetingName: 'random-671854', + meetingID: 'random-671854', + internalMeetingID: 'ed055e2011ec6a76e39347808259a42a56f270d6-1690571458817', + createTime: '1690571458817', + createDate: 'Fri Jul 28 19:10:58 UTC 2023', + voiceBridge: '79474', + dialNumber: '343-633-0064', + attendeePW: 'PvioX208', + moderatorPW: 'b9WMdCtJ', + running: 'false', + duration: '0', + hasUserJoined: 'false', + recording: 'false', + hasBeenForciblyEnded: 'false', + startTime: '1690571458841', + endTime: '0', + participantCount: 5, # Also as integer + listenerCount: '0', + voiceParticipantCount: '0', + videoCount: '0', + maxUsers: '0', + moderatorCount: '0', + attendees: '', + metadata: '', + isBreakout: 'false' + } + end +end diff --git a/yarn.lock b/yarn.lock index 6febbe5231..62bd7e2c9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,19 +5,16 @@ "@babel/code-frame@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== dependencies: "@babel/highlight" "^7.18.6" "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5": version "7.20.14" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.14.tgz" - integrity sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw== "@babel/generator@^7.20.7": version "7.20.14" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz" - integrity sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg== dependencies: "@babel/types" "^7.20.7" "@jridgewell/gen-mapping" "^0.3.2" @@ -26,14 +23,12 @@ "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== dependencies: "@babel/types" "^7.18.6" "@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": version "7.18.9" resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz" - integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== dependencies: "@babel/helper-explode-assignable-expression" "^7.18.6" "@babel/types" "^7.18.9" @@ -41,7 +36,6 @@ "@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.20.7": version "7.20.7" resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz" - integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== dependencies: "@babel/compat-data" "^7.20.5" "@babel/helper-validator-option" "^7.18.6" @@ -52,7 +46,6 @@ "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.5", "@babel/helper-create-class-features-plugin@^7.20.7": version "7.20.12" resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz" - integrity sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-environment-visitor" "^7.18.9" @@ -66,7 +59,6 @@ "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": version "7.20.5" resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz" - integrity sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" regexpu-core "^5.2.1" @@ -74,7 +66,6 @@ "@babel/helper-define-polyfill-provider@^0.3.3": version "0.3.3" resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz" - integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== dependencies: "@babel/helper-compilation-targets" "^7.17.7" "@babel/helper-plugin-utils" "^7.16.7" @@ -86,19 +77,16 @@ "@babel/helper-environment-visitor@^7.18.9": version "7.18.9" resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== "@babel/helper-explode-assignable-expression@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz" - integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== dependencies: "@babel/types" "^7.18.6" "@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0": version "7.19.0" resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== dependencies: "@babel/template" "^7.18.10" "@babel/types" "^7.19.0" @@ -106,28 +94,24 @@ "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== dependencies: "@babel/types" "^7.18.6" "@babel/helper-member-expression-to-functions@^7.20.7": version "7.20.7" resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz" - integrity sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw== dependencies: "@babel/types" "^7.20.7" "@babel/helper-module-imports@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz" - integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== dependencies: "@babel/types" "^7.18.6" "@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11": version "7.20.11" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz" - integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== dependencies: "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-module-imports" "^7.18.6" @@ -141,19 +125,16 @@ "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz" - integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== dependencies: "@babel/types" "^7.18.6" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.20.2" resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz" - integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-environment-visitor" "^7.18.9" @@ -163,7 +144,6 @@ "@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7": version "7.20.7" resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz" - integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== dependencies: "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-member-expression-to-functions" "^7.20.7" @@ -175,43 +155,36 @@ "@babel/helper-simple-access@^7.20.2": version "7.20.2" resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== dependencies: "@babel/types" "^7.20.2" "@babel/helper-skip-transparent-expression-wrappers@^7.20.0": version "7.20.0" resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz" - integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== dependencies: "@babel/types" "^7.20.0" "@babel/helper-split-export-declaration@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== dependencies: "@babel/types" "^7.18.6" "@babel/helper-string-parser@^7.19.4": version "7.19.4" resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz" - integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== "@babel/helper-wrap-function@^7.18.9": version "7.20.5" resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz" - integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== dependencies: "@babel/helper-function-name" "^7.19.0" "@babel/template" "^7.18.10" @@ -221,7 +194,6 @@ "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== dependencies: "@babel/helper-validator-identifier" "^7.18.6" chalk "^2.0.0" @@ -230,19 +202,16 @@ "@babel/parser@^7.20.13", "@babel/parser@^7.20.7": version "7.20.15" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.20.15.tgz" - integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz" - integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz" - integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" @@ -251,7 +220,6 @@ "@babel/plugin-proposal-async-generator-functions@^7.20.1": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz" - integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== dependencies: "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-plugin-utils" "^7.20.2" @@ -261,7 +229,6 @@ "@babel/plugin-proposal-class-properties@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== dependencies: "@babel/helper-create-class-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" @@ -269,7 +236,6 @@ "@babel/plugin-proposal-class-static-block@^7.18.6": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.20.7.tgz" - integrity sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ== dependencies: "@babel/helper-create-class-features-plugin" "^7.20.7" "@babel/helper-plugin-utils" "^7.20.2" @@ -278,7 +244,6 @@ "@babel/plugin-proposal-dynamic-import@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz" - integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-dynamic-import" "^7.8.3" @@ -286,7 +251,6 @@ "@babel/plugin-proposal-export-namespace-from@^7.18.9": version "7.18.9" resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz" - integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== dependencies: "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" @@ -294,7 +258,6 @@ "@babel/plugin-proposal-json-strings@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz" - integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-json-strings" "^7.8.3" @@ -302,7 +265,6 @@ "@babel/plugin-proposal-logical-assignment-operators@^7.18.9": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz" - integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" @@ -310,7 +272,6 @@ "@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz" - integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" @@ -318,7 +279,6 @@ "@babel/plugin-proposal-numeric-separator@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz" - integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-numeric-separator" "^7.10.4" @@ -326,7 +286,6 @@ "@babel/plugin-proposal-object-rest-spread@^7.20.2": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz" - integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== dependencies: "@babel/compat-data" "^7.20.5" "@babel/helper-compilation-targets" "^7.20.7" @@ -337,7 +296,6 @@ "@babel/plugin-proposal-optional-catch-binding@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz" - integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" @@ -345,7 +303,6 @@ "@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz" - integrity sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" @@ -354,7 +311,6 @@ "@babel/plugin-proposal-private-methods@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz" - integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== dependencies: "@babel/helper-create-class-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" @@ -362,7 +318,6 @@ "@babel/plugin-proposal-private-property-in-object@^7.18.6": version "7.20.5" resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz" - integrity sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-create-class-features-plugin" "^7.20.5" @@ -372,7 +327,6 @@ "@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz" - integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" @@ -380,126 +334,108 @@ "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-class-properties@^7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" "@babel/plugin-syntax-class-static-block@^7.14.5": version "7.14.5" resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-export-namespace-from@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== dependencies: "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-import-assertions@^7.20.0": version "7.20.0" resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz" - integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ== dependencies: "@babel/helper-plugin-utils" "^7.19.0" "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-jsx@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz" - integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-numeric-separator@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-private-property-in-object@^7.14.5": version "7.14.5" resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-top-level-await@^7.14.5": version "7.14.5" resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-transform-arrow-functions@^7.18.6": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz" - integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-async-to-generator@^7.18.6": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz" - integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q== dependencies: "@babel/helper-module-imports" "^7.18.6" "@babel/helper-plugin-utils" "^7.20.2" @@ -508,21 +444,18 @@ "@babel/plugin-transform-block-scoped-functions@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz" - integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-block-scoping@^7.20.2": version "7.20.15" resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.15.tgz" - integrity sha512-Vv4DMZ6MiNOhu/LdaZsT/bsLRxgL94d269Mv4R/9sp6+Mp++X/JqypZYypJXLlM4mlL352/Egzbzr98iABH1CA== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-classes@^7.20.2": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz" - integrity sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-compilation-targets" "^7.20.7" @@ -537,7 +470,6 @@ "@babel/plugin-transform-computed-properties@^7.18.9": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz" - integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/template" "^7.20.7" @@ -545,14 +477,12 @@ "@babel/plugin-transform-destructuring@^7.20.2": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz" - integrity sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz" - integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" @@ -560,14 +490,12 @@ "@babel/plugin-transform-duplicate-keys@^7.18.9": version "7.18.9" resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz" - integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== dependencies: "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-transform-exponentiation-operator@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz" - integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== dependencies: "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" @@ -575,14 +503,12 @@ "@babel/plugin-transform-for-of@^7.18.8": version "7.18.8" resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz" - integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-function-name@^7.18.9": version "7.18.9" resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz" - integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== dependencies: "@babel/helper-compilation-targets" "^7.18.9" "@babel/helper-function-name" "^7.18.9" @@ -591,21 +517,18 @@ "@babel/plugin-transform-literals@^7.18.9": version "7.18.9" resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz" - integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== dependencies: "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-transform-member-expression-literals@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz" - integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-modules-amd@^7.19.6": version "7.20.11" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz" - integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g== dependencies: "@babel/helper-module-transforms" "^7.20.11" "@babel/helper-plugin-utils" "^7.20.2" @@ -613,7 +536,6 @@ "@babel/plugin-transform-modules-commonjs@^7.19.6": version "7.20.11" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.20.11.tgz" - integrity sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw== dependencies: "@babel/helper-module-transforms" "^7.20.11" "@babel/helper-plugin-utils" "^7.20.2" @@ -622,7 +544,6 @@ "@babel/plugin-transform-modules-systemjs@^7.19.6": version "7.20.11" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz" - integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw== dependencies: "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-module-transforms" "^7.20.11" @@ -632,7 +553,6 @@ "@babel/plugin-transform-modules-umd@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz" - integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== dependencies: "@babel/helper-module-transforms" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" @@ -640,7 +560,6 @@ "@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": version "7.20.5" resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz" - integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.20.5" "@babel/helper-plugin-utils" "^7.20.2" @@ -648,14 +567,12 @@ "@babel/plugin-transform-new-target@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz" - integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-object-super@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz" - integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/helper-replace-supers" "^7.18.6" @@ -663,35 +580,30 @@ "@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz" - integrity sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/plugin-transform-property-literals@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz" - integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-react-display-name@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz" - integrity sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-react-jsx-development@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz" - integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA== dependencies: "@babel/plugin-transform-react-jsx" "^7.18.6" "@babel/plugin-transform-react-jsx@^7.18.6": version "7.20.13" resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.20.13.tgz" - integrity sha512-MmTZx/bkUrfJhhYAYt3Urjm+h8DQGrPrnKQ94jLo7NLuOU+T89a7IByhKmrb8SKhrIYIQ0FN0CHMbnFRen4qNw== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-module-imports" "^7.18.6" @@ -702,7 +614,6 @@ "@babel/plugin-transform-react-pure-annotations@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz" - integrity sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" @@ -710,7 +621,6 @@ "@babel/plugin-transform-regenerator@^7.18.6": version "7.20.5" resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz" - integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== dependencies: "@babel/helper-plugin-utils" "^7.20.2" regenerator-transform "^0.15.1" @@ -718,14 +628,12 @@ "@babel/plugin-transform-reserved-words@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz" - integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-runtime@^7.12.1": version "7.19.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz" - integrity sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw== dependencies: "@babel/helper-module-imports" "^7.18.6" "@babel/helper-plugin-utils" "^7.19.0" @@ -737,14 +645,12 @@ "@babel/plugin-transform-shorthand-properties@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz" - integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-spread@^7.19.0": version "7.20.7" resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz" - integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw== dependencies: "@babel/helper-plugin-utils" "^7.20.2" "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" @@ -752,35 +658,30 @@ "@babel/plugin-transform-sticky-regex@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz" - integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-template-literals@^7.18.9": version "7.18.9" resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz" - integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== dependencies: "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-transform-typeof-symbol@^7.18.9": version "7.18.9" resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz" - integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== dependencies: "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-transform-unicode-escapes@^7.18.10": version "7.18.10" resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== dependencies: "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-transform-unicode-regex@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz" - integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" @@ -788,7 +689,6 @@ "@babel/preset-env@^7.16.11": version "7.20.2" resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz" - integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== dependencies: "@babel/compat-data" "^7.20.1" "@babel/helper-compilation-targets" "^7.20.0" @@ -869,7 +769,6 @@ "@babel/preset-modules@^0.1.5": version "0.1.5" resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" @@ -880,7 +779,6 @@ "@babel/preset-react@^7.16.7": version "7.18.6" resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz" - integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg== dependencies: "@babel/helper-plugin-utils" "^7.18.6" "@babel/helper-validator-option" "^7.18.6" @@ -892,19 +790,16 @@ "@babel/regjsgen@^0.8.0": version "0.8.0" resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.9", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.20.13" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz" - integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== dependencies: regenerator-runtime "^0.13.11" "@babel/template@^7.18.10", "@babel/template@^7.20.7": version "7.20.7" resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== dependencies: "@babel/code-frame" "^7.18.6" "@babel/parser" "^7.20.7" @@ -913,7 +808,6 @@ "@babel/traverse@^7.20.10", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7": version "7.20.13" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz" - integrity sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ== dependencies: "@babel/code-frame" "^7.18.6" "@babel/generator" "^7.20.7" @@ -929,7 +823,6 @@ "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.4.4": version "7.20.7" resolved "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz" - integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== dependencies: "@babel/helper-string-parser" "^7.19.4" "@babel/helper-validator-identifier" "^7.19.1" @@ -938,22 +831,18 @@ "@esbuild/android-arm@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80" - integrity sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw== "@esbuild/linux-loong64@0.14.54": version "0.14.54" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" - integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== "@esbuild/linux-loong64@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239" - integrity sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ== "@eslint/eslintrc@^1.4.1": version "1.4.1" resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz" - integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -968,14 +857,12 @@ "@fluentui/react-component-event-listener@~0.63.0": version "0.63.1" resolved "https://registry.npmjs.org/@fluentui/react-component-event-listener/-/react-component-event-listener-0.63.1.tgz" - integrity sha512-gSMdOh6tI3IJKZFqxfQwbTpskpME0CvxdxGM2tdglmf6ZPVDi0L4+KKIm+2dN8nzb8Ya1A8ZT+Ddq0KmZtwVQg== dependencies: "@babel/runtime" "^7.10.4" "@fluentui/react-component-ref@~0.63.0": version "0.63.1" resolved "https://registry.npmjs.org/@fluentui/react-component-ref/-/react-component-ref-0.63.1.tgz" - integrity sha512-8MkXX4+R3i80msdbD4rFpEB4WWq2UDvGwG386g3ckIWbekdvN9z2kWAd9OXhRGqB7QeOsoAGWocp6gAMCivRlw== dependencies: "@babel/runtime" "^7.10.4" react-is "^16.6.3" @@ -983,24 +870,20 @@ "@hcaptcha/react-hcaptcha@^1.3.0": version "1.4.4" resolved "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.4.4.tgz" - integrity sha512-Aen217LDnf5ywbPSwBG5CsoqBLIHIAS9lhj3zQjXJuO13doQ6/ubkCWNuY8jmwYLefoFt3V3MrZmCdKDaFoTuQ== dependencies: "@babel/runtime" "^7.17.9" "@heroicons/react@^2.0.11": version "2.0.15" resolved "https://registry.npmjs.org/@heroicons/react/-/react-2.0.15.tgz" - integrity sha512-CZ2dGWgWG3/z5LEoD5D3MEr1syn45JM/OB2aDpw531Ryecgkz2V7TWQ808P0lva7zP003PVW6WlwbofsYyga3A== "@hookform/resolvers@^2.8.8": version "2.9.11" resolved "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.11.tgz" - integrity sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ== "@humanwhocodes/config-array@^0.11.8": version "0.11.8" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz" - integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -1009,17 +892,14 @@ "@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/object-schema@^1.2.1": version "1.2.1" resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@jridgewell/gen-mapping@^0.3.2": version "0.3.2" resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== dependencies: "@jridgewell/set-array" "^1.0.1" "@jridgewell/sourcemap-codec" "^1.4.10" @@ -1028,22 +908,18 @@ "@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== dependencies: "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" @@ -1051,7 +927,6 @@ "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" @@ -1059,12 +934,10 @@ "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" @@ -1072,36 +945,30 @@ "@popperjs/core@^2.11.5", "@popperjs/core@^2.11.6", "@popperjs/core@^2.6.0": version "2.11.6" resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz" - integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== "@rails/actioncable@^7.0.2": version "7.0.4" resolved "https://registry.npmjs.org/@rails/actioncable/-/actioncable-7.0.4.tgz" - integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ== "@react-aria/ssr@^3.4.1": version "3.4.1" resolved "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.4.1.tgz" - integrity sha512-NmhoilMDyIfQiOSdQgxpVH2tC2u85Y0mVijtBNbI9kcDYLEiW/r6vKYVKtkyU+C4qobXhGMPfZ70PTc0lysSVA== dependencies: "@swc/helpers" "^0.4.14" "@remix-run/router@1.3.2": version "1.3.2" resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz" - integrity sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA== "@restart/hooks@^0.4.6", "@restart/hooks@^0.4.7": version "0.4.8" resolved "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.8.tgz" - integrity sha512-Ivvp1FZ0Lja80iUTYAhbzy+stxwO7FbPHP95ypCtIh0wyOLiayQywXhVJ2ZYP5S1AjW2GmKHeRU4UglMwTG2sA== dependencies: dequal "^2.0.2" "@restart/ui@^1.4.1": version "1.5.4" resolved "https://registry.npmjs.org/@restart/ui/-/ui-1.5.4.tgz" - integrity sha512-ziNtXY2PrjXrRUfR1D/ry1v1i5IF+kfMcH9d1kIcU2lOELfmDsVb+fzbyEDz3yKvKuqkphTunVDuLdYRJ0YsAg== dependencies: "@babel/runtime" "^7.20.7" "@popperjs/core" "^2.11.6" @@ -1116,7 +983,6 @@ "@semantic-ui-react/event-stack@^3.1.3": version "3.1.3" resolved "https://registry.npmjs.org/@semantic-ui-react/event-stack/-/event-stack-3.1.3.tgz" - integrity sha512-FdTmJyWvJaYinHrKRsMLDrz4tTMGdFfds299Qory53hBugiDvGC0tEJf+cHsi5igDwWb/CLOgOiChInHwq8URQ== dependencies: exenv "^1.2.2" prop-types "^15.6.2" @@ -1124,41 +990,34 @@ "@swc/helpers@^0.4.14": version "0.4.14" resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz" - integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw== dependencies: tslib "^2.4.0" "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" - integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/lodash@^4.14.175": version "4.14.191" resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz" - integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== "@types/prop-types@*": version "15.7.5" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" - integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== "@types/react-transition-group@^4.4.4": version "4.4.5" resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz" - integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== dependencies: "@types/react" "*" "@types/react@*", "@types/react@>=16.9.11": version "18.0.27" resolved "https://registry.npmjs.org/@types/react/-/react-18.0.27.tgz" - integrity sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -1167,22 +1026,18 @@ "@types/scheduler@*": version "0.16.2" resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" - integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz" - integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== "@types/warning@^3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz" - integrity sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA== "@typescript-eslint/eslint-plugin@^5.14.0": version "5.51.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz" - integrity sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ== dependencies: "@typescript-eslint/scope-manager" "5.51.0" "@typescript-eslint/type-utils" "5.51.0" @@ -1198,7 +1053,6 @@ "@typescript-eslint/parser@^5.14.0": version "5.51.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.51.0.tgz" - integrity sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA== dependencies: "@typescript-eslint/scope-manager" "5.51.0" "@typescript-eslint/types" "5.51.0" @@ -1208,7 +1062,6 @@ "@typescript-eslint/scope-manager@5.51.0": version "5.51.0" resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz" - integrity sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ== dependencies: "@typescript-eslint/types" "5.51.0" "@typescript-eslint/visitor-keys" "5.51.0" @@ -1216,7 +1069,6 @@ "@typescript-eslint/type-utils@5.51.0": version "5.51.0" resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz" - integrity sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ== dependencies: "@typescript-eslint/typescript-estree" "5.51.0" "@typescript-eslint/utils" "5.51.0" @@ -1226,12 +1078,10 @@ "@typescript-eslint/types@5.51.0": version "5.51.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.51.0.tgz" - integrity sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw== "@typescript-eslint/typescript-estree@5.51.0": version "5.51.0" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz" - integrity sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA== dependencies: "@typescript-eslint/types" "5.51.0" "@typescript-eslint/visitor-keys" "5.51.0" @@ -1244,7 +1094,6 @@ "@typescript-eslint/utils@5.51.0": version "5.51.0" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.51.0.tgz" - integrity sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" @@ -1258,7 +1107,6 @@ "@typescript-eslint/visitor-keys@5.51.0": version "5.51.0" resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz" - integrity sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ== dependencies: "@typescript-eslint/types" "5.51.0" eslint-visitor-keys "^3.3.0" @@ -1266,17 +1114,14 @@ 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.8.0: version "8.8.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1286,26 +1131,22 @@ ajv@^6.10.0, ajv@^6.12.4: ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" anymatch@~3.1.2: version "3.1.3" resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -1313,19 +1154,16 @@ anymatch@~3.1.2: argparse@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== aria-query@^5.1.3: version "5.1.3" resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== dependencies: deep-equal "^2.0.5" array-includes@^3.1.5, array-includes@^3.1.6: version "3.1.6" resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz" - integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -1336,12 +1174,10 @@ array-includes@^3.1.5, array-includes@^3.1.6: array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== array.prototype.flat@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz" - integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -1351,7 +1187,6 @@ array.prototype.flat@^1.3.1: array.prototype.flatmap@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz" - integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -1361,7 +1196,6 @@ array.prototype.flatmap@^1.3.1: array.prototype.tosorted@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz" - integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -1372,36 +1206,30 @@ array.prototype.tosorted@^1.1.1: ast-types-flow@^0.0.7: version "0.0.7" resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" - integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz" - integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== axe-core@^4.6.2: version "4.6.3" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.6.3.tgz" - integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== axios@^0.26.1: version "0.26.1" resolved "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz" - integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== dependencies: follow-redirects "^1.14.8" axobject-query@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz" - integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== dependencies: deep-equal "^2.0.5" babel-plugin-polyfill-corejs2@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz" - integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== dependencies: "@babel/compat-data" "^7.17.7" "@babel/helper-define-polyfill-provider" "^0.3.3" @@ -1410,7 +1238,6 @@ babel-plugin-polyfill-corejs2@^0.3.3: babel-plugin-polyfill-corejs3@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz" - integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== dependencies: "@babel/helper-define-polyfill-provider" "^0.3.3" core-js-compat "^3.25.1" @@ -1418,39 +1245,32 @@ babel-plugin-polyfill-corejs3@^0.6.0: babel-plugin-polyfill-regenerator@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz" - integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== dependencies: "@babel/helper-define-polyfill-provider" "^0.3.3" balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== big-integer@^1.6.16: version "1.6.51" resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== bootstrap-icons@^1.8.3: version "1.10.3" resolved "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz" - integrity sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw== bootstrap@5.1.3: version "5.1.3" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.3.tgz#ba081b0c130f810fa70900acbc1c6d3c28fa8f34" - integrity sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q== + resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz" brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -1458,14 +1278,12 @@ brace-expansion@^1.1.7: braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" broadcast-channel@^3.4.1: version "3.7.0" resolved "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz" - integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg== dependencies: "@babel/runtime" "^7.7.2" detect-node "^2.1.0" @@ -1479,7 +1297,6 @@ broadcast-channel@^3.4.1: browserslist@^4.21.3, browserslist@^4.21.4: version "4.21.5" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== dependencies: caniuse-lite "^1.0.30001449" electron-to-chromium "^1.4.284" @@ -1489,7 +1306,6 @@ browserslist@^4.21.3, browserslist@^4.21.4: call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== dependencies: function-bind "^1.1.1" get-intrinsic "^1.0.2" @@ -1497,17 +1313,14 @@ call-bind@^1.0.0, call-bind@^1.0.2: callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== caniuse-lite@^1.0.30001449: version "1.0.30001451" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001451.tgz" - integrity sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w== chalk@^2.0.0: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" @@ -1516,7 +1329,6 @@ chalk@^2.0.0: chalk@^4.0.0: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -1524,7 +1336,6 @@ chalk@^4.0.0: "chokidar@>=3.0.0 <4.0.0": version "3.5.3" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -1539,65 +1350,54 @@ chalk@^4.0.0: classnames@^2.3.1: version "2.3.2" resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== clsx@^1.1.1: version "1.2.1" resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== color-convert@^1.9.0: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== confusing-browser-globals@^1.0.10: version "1.0.11" resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" - integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== core-js-compat@^3.25.1: version "3.27.2" resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.27.2.tgz" - integrity sha512-welaYuF7ZtbYKGrIy7y3eb40d37rG1FvzEOfe7hSLd2iD6duMDqUhRfSvCGyC46HhR6Y8JXXdZ2lnRUMkPBpvg== dependencies: browserslist "^4.21.4" cross-fetch@3.1.5: version "3.1.5" resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== dependencies: node-fetch "2.6.7" cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -1606,31 +1406,26 @@ cross-spawn@^7.0.2: csstype@^3.0.2: version "3.1.1" resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" - integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" - integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" deep-equal@^2.0.5: version "2.2.0" resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz" - integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== dependencies: call-bind "^1.0.2" es-get-iterator "^1.1.2" @@ -1653,12 +1448,10 @@ deep-equal@^2.0.5: 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== define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz" - integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== dependencies: has-property-descriptors "^1.0.0" object-keys "^1.1.1" @@ -1666,38 +1459,32 @@ define-properties@^1.1.3, define-properties@^1.1.4: dequal@^2.0.2, dequal@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== detect-node@^2.0.4, detect-node@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: path-type "^4.0.0" doctrine@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== dependencies: esutils "^2.0.2" doctrine@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: esutils "^2.0.2" dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz" - integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== dependencies: "@babel/runtime" "^7.8.7" csstype "^3.0.2" @@ -1705,17 +1492,14 @@ dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: electron-to-chromium@^1.4.284: version "1.4.292" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.292.tgz" - integrity sha512-ESWOSyJy5odDlE8wvh5NNAMORv4r6assPwIPGHEMWrWD0SONXcG/xT+9aD9CQyeRwyYDPo6dJT4Bbeg5uevVQQ== emoji-regex@^9.2.2: version "9.2.2" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== es-abstract@^1.19.0, es-abstract@^1.20.4: version "1.21.1" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz" - integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== dependencies: available-typed-arrays "^1.0.5" call-bind "^1.0.2" @@ -1754,7 +1538,6 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: es-get-iterator@^1.1.2: version "1.1.3" resolved "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== dependencies: call-bind "^1.0.2" get-intrinsic "^1.1.3" @@ -1769,7 +1552,6 @@ es-get-iterator@^1.1.2: es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz" - integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== dependencies: get-intrinsic "^1.1.3" has "^1.0.3" @@ -1778,14 +1560,12 @@ es-set-tostringtag@^2.0.1: es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== dependencies: has "^1.0.3" es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== dependencies: is-callable "^1.1.4" is-date-object "^1.0.1" @@ -1794,174 +1574,140 @@ es-to-primitive@^1.2.1: esbuild-android-64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" - integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== esbuild-android-64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz#20a7ae1416c8eaade917fb2453c1259302c637a5" - integrity sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA== esbuild-android-arm64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" - integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== esbuild-android-arm64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz#9cc0ec60581d6ad267568f29cf4895ffdd9f2f04" - integrity sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ== esbuild-darwin-64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" - integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== esbuild-darwin-64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz#428e1730ea819d500808f220fbc5207aea6d4410" - integrity sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg== esbuild-darwin-arm64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" - integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== esbuild-darwin-arm64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz#b6dfc7799115a2917f35970bfbc93ae50256b337" - integrity sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA== esbuild-freebsd-64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" - integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== esbuild-freebsd-64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz#4e190d9c2d1e67164619ae30a438be87d5eedaf2" - integrity sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA== esbuild-freebsd-arm64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" - integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== esbuild-freebsd-arm64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz#18a4c0344ee23bd5a6d06d18c76e2fd6d3f91635" - integrity sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA== esbuild-linux-32@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" - integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== esbuild-linux-32@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz#9a329731ee079b12262b793fb84eea762e82e0ce" - integrity sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg== esbuild-linux-64@0.14.54: version "0.14.54" resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz" - integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== esbuild-linux-64@0.15.18: version "0.15.18" resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz" - integrity sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw== esbuild-linux-arm64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" - integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== esbuild-linux-arm64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz#5372e7993ac2da8f06b2ba313710d722b7a86e5d" - integrity sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug== esbuild-linux-arm@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" - integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== esbuild-linux-arm@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz#e734aaf259a2e3d109d4886c9e81ec0f2fd9a9cc" - integrity sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA== esbuild-linux-mips64le@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" - integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== esbuild-linux-mips64le@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz#c0487c14a9371a84eb08fab0e1d7b045a77105eb" - integrity sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ== esbuild-linux-ppc64le@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" - integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== esbuild-linux-ppc64le@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz#af048ad94eed0ce32f6d5a873f7abe9115012507" - integrity sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w== esbuild-linux-riscv64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" - integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== esbuild-linux-riscv64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz#423ed4e5927bd77f842bd566972178f424d455e6" - integrity sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg== esbuild-linux-s390x@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" - integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== esbuild-linux-s390x@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz#21d21eaa962a183bfb76312e5a01cc5ae48ce8eb" - integrity sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ== esbuild-netbsd-64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" - integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== esbuild-netbsd-64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz#ae75682f60d08560b1fe9482bfe0173e5110b998" - integrity sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg== esbuild-openbsd-64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" - integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== esbuild-openbsd-64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz#79591a90aa3b03e4863f93beec0d2bab2853d0a8" - integrity sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ== esbuild-plugin-import-glob@^0.1.1: version "0.1.1" resolved "https://registry.npmjs.org/esbuild-plugin-import-glob/-/esbuild-plugin-import-glob-0.1.1.tgz" - integrity sha512-yAFH+9AoIcsQkODSx0KUPRv1FeJUN6Tef8vkPQMcuVkc2vXYneYKsHhOiFS/yIsg5bQ70HHtAlXVA1uTjgoJXg== dependencies: fast-glob "^3.2.5" esbuild-sass-plugin@^2.2.6: version "2.4.5" resolved "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.4.5.tgz" - integrity sha512-di2hLaIwhRXe513uaPPxv+5bjynxAgrS8R+u38lbBfvp1g1xOki4ACXV2aXip2CRPGTbAVDySSxujd9iArFV0w== dependencies: esbuild "^0.15.17" resolve "^1.22.1" @@ -1970,47 +1716,38 @@ esbuild-sass-plugin@^2.2.6: esbuild-sunos-64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" - integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== esbuild-sunos-64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz#fd528aa5da5374b7e1e93d36ef9b07c3dfed2971" - integrity sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw== esbuild-windows-32@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" - integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== esbuild-windows-32@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz#0e92b66ecdf5435a76813c4bc5ccda0696f4efc3" - integrity sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ== esbuild-windows-64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" - integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== esbuild-windows-64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz#0fc761d785414284fc408e7914226d33f82420d0" - integrity sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw== esbuild-windows-arm64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" - integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== esbuild-windows-arm64@0.15.18: version "0.15.18" resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz#5b5bdc56d341d0922ee94965c89ee120a6a86eb7" - integrity sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ== esbuild@^0.14.42: version "0.14.54" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz" - integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== optionalDependencies: "@esbuild/linux-loong64" "0.14.54" esbuild-android-64 "0.14.54" @@ -2037,7 +1774,6 @@ esbuild@^0.14.42: esbuild@^0.15.17: version "0.15.18" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz" - integrity sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q== optionalDependencies: "@esbuild/android-arm" "0.15.18" "@esbuild/linux-loong64" "0.15.18" @@ -2065,22 +1801,18 @@ esbuild@^0.15.17: escalade@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== 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-config-airbnb-base@^15.0.0: version "15.0.0" resolved "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz" - integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig== dependencies: confusing-browser-globals "^1.0.10" object.assign "^4.1.2" @@ -2090,7 +1822,6 @@ eslint-config-airbnb-base@^15.0.0: eslint-config-airbnb@^19.0.4: version "19.0.4" resolved "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz" - integrity sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew== dependencies: eslint-config-airbnb-base "^15.0.0" object.assign "^4.1.2" @@ -2099,7 +1830,6 @@ eslint-config-airbnb@^19.0.4: eslint-import-resolver-node@^0.3.7: version "0.3.7" resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz" - integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== dependencies: debug "^3.2.7" is-core-module "^2.11.0" @@ -2108,14 +1838,12 @@ eslint-import-resolver-node@^0.3.7: eslint-module-utils@^2.7.4: version "2.7.4" resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz" - integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== dependencies: debug "^3.2.7" eslint-plugin-import@^2.25.4: version "2.27.5" resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz" - integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== dependencies: array-includes "^3.1.6" array.prototype.flat "^1.3.1" @@ -2136,7 +1864,6 @@ eslint-plugin-import@^2.25.4: eslint-plugin-jsx-a11y@^6.5.1: version "6.7.1" resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz" - integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA== dependencies: "@babel/runtime" "^7.20.7" aria-query "^5.1.3" @@ -2158,12 +1885,10 @@ eslint-plugin-jsx-a11y@^6.5.1: eslint-plugin-react-hooks@^4.4.0: version "4.6.0" resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz" - integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== eslint-plugin-react@^7.29.4: version "7.32.2" resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz" - integrity sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg== dependencies: array-includes "^3.1.6" array.prototype.flatmap "^1.3.1" @@ -2184,7 +1909,6 @@ eslint-plugin-react@^7.29.4: eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: esrecurse "^4.3.0" estraverse "^4.1.1" @@ -2192,7 +1916,6 @@ eslint-scope@^5.1.1: eslint-scope@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" @@ -2200,24 +1923,20 @@ eslint-scope@^7.1.1: eslint-utils@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== dependencies: eslint-visitor-keys "^2.0.0" eslint-visitor-keys@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== eslint-visitor-keys@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== eslint@^8.12.0: version "8.33.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz" - integrity sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA== dependencies: "@eslint/eslintrc" "^1.4.1" "@humanwhocodes/config-array" "^0.11.8" @@ -2262,7 +1981,6 @@ eslint@^8.12.0: espree@^9.4.0: version "9.4.1" resolved "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz" - integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== dependencies: acorn "^8.8.0" acorn-jsx "^5.3.2" @@ -2271,46 +1989,38 @@ espree@^9.4.0: esquery@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== 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@^4.1.1: version "4.3.0" resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== 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== exenv@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz" - integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw== 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-glob@^3.2.5, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -2321,38 +2031,32 @@ fast-glob@^3.2.5, fast-glob@^3.2.9: 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== fastq@^1.6.0: version "1.15.0" resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== dependencies: reusify "^1.0.4" file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: flat-cache "^3.0.4" fill-range@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 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" @@ -2360,7 +2064,6 @@ find-up@^5.0.0: flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== dependencies: flatted "^3.1.0" rimraf "^3.0.2" @@ -2368,39 +2071,32 @@ flat-cache@^3.0.4: flatted@^3.1.0: version "3.2.7" resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== follow-redirects@^1.14.8: version "1.15.2" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== for-each@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== dependencies: is-callable "^1.1.3" fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== function-bind@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz" - integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" @@ -2410,12 +2106,10 @@ function.prototype.name@^1.1.5: functions-have-names@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: version "1.2.0" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== dependencies: function-bind "^1.1.1" has "^1.0.3" @@ -2424,7 +2118,6 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== dependencies: call-bind "^1.0.2" get-intrinsic "^1.1.1" @@ -2432,21 +2125,18 @@ get-symbol-description@^1.0.0: glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" 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" glob@^7.1.3: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -2458,26 +2148,22 @@ glob@^7.1.3: globals@^11.1.0: version "11.12.0" resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0: version "13.20.0" resolved "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz" - integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== dependencies: type-fest "^0.20.2" globalthis@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== dependencies: define-properties "^1.1.3" globby@^11.1.0: version "11.1.0" resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" @@ -2489,96 +2175,80 @@ globby@^11.1.0: gopd@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== dependencies: get-intrinsic "^1.1.3" grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== dependencies: get-intrinsic "^1.1.1" has-proto@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== dependencies: has-symbols "^1.0.2" has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: function-bind "^1.1.1" html-parse-stringify@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz" - integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== dependencies: void-elements "3.1.0" i18next-http-backend@^1.4.1: version "1.4.5" resolved "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.5.tgz" - integrity sha512-tLuHWuLWl6CmS07o+UB6EcQCaUjrZ1yhdseIN7sfq0u7phsMePJ8pqlGhIAdRDPF/q7ooyo5MID5DRFBCH+x5w== dependencies: cross-fetch "3.1.5" i18next@^21.9.1: version "21.10.0" resolved "https://registry.npmjs.org/i18next/-/i18next-21.10.0.tgz" - integrity sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg== dependencies: "@babel/runtime" "^7.17.2" ignore@^5.2.0: version "5.2.4" resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== immutable@^4.0.0: version "4.2.4" resolved "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz" - integrity sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -2586,12 +2256,10 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" @@ -2599,12 +2267,10 @@ inflight@^1.0.4: inherits@2: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== internal-slot@^1.0.3, internal-slot@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz" - integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== dependencies: get-intrinsic "^1.1.3" has "^1.0.3" @@ -2613,14 +2279,12 @@ internal-slot@^1.0.3, internal-slot@^1.0.4: invariant@^2.2.4: version "2.2.4" resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== dependencies: loose-envify "^1.0.0" is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== dependencies: call-bind "^1.0.2" has-tostringtag "^1.0.0" @@ -2628,7 +2292,6 @@ is-arguments@^1.1.1: is-array-buffer@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz" - integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== dependencies: call-bind "^1.0.2" get-intrinsic "^1.1.3" @@ -2637,21 +2300,18 @@ is-array-buffer@^3.0.1: is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== dependencies: has-bigints "^1.0.1" is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== dependencies: binary-extensions "^2.0.0" is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== dependencies: call-bind "^1.0.2" has-tostringtag "^1.0.0" @@ -2659,65 +2319,54 @@ is-boolean-object@^1.1.0: is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== is-core-module@^2.11.0, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== dependencies: has "^1.0.3" is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== dependencies: has-tostringtag "^1.0.0" 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.1, is-glob@^4.0.3, is-glob@~4.0.1: 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-map@^2.0.1, is-map@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz" - integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== is-number-object@^1.0.4: version "1.0.7" resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== dependencies: has-tostringtag "^1.0.0" 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== is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-regex@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== dependencies: call-bind "^1.0.2" has-tostringtag "^1.0.0" @@ -2725,33 +2374,28 @@ is-regex@^1.1.4: is-set@^2.0.1, is-set@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz" - integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== is-shared-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz" - integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== dependencies: call-bind "^1.0.2" is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== dependencies: has-tostringtag "^1.0.0" is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== dependencies: has-symbols "^1.0.2" is-typed-array@^1.1.10, is-typed-array@^1.1.9: version "1.1.10" resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz" - integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== dependencies: available-typed-arrays "^1.0.5" call-bind "^1.0.2" @@ -2762,19 +2406,16 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9: is-weakmap@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== dependencies: call-bind "^1.0.2" is-weakset@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz" - integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== dependencies: call-bind "^1.0.2" get-intrinsic "^1.1.1" @@ -2782,71 +2423,58 @@ is-weakset@^2.0.1: isarray@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== jquery@x.*: version "3.6.3" resolved "https://registry.npmjs.org/jquery/-/jquery-3.6.3.tgz" - integrity sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg== js-sdsl@^4.1.4: version "4.3.0" resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz" - integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== js-sha3@0.8.0: version "0.8.0" resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz" - integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== jsesc@~0.5.0: version "0.5.0" resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== 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== json5@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: version "3.3.3" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" - integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== dependencies: array-includes "^3.1.5" object.assign "^4.1.3" @@ -2854,24 +2482,20 @@ json5@^1.0.1: keyboard-key@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.1.0.tgz" - integrity sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ== language-subtag-registry@~0.3.2: version "0.3.22" resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" - integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== language-tags@=1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz" - integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== dependencies: language-subtag-registry "~0.3.2" 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" @@ -2879,55 +2503,46 @@ levn@^0.4.1: 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" lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== dependencies: js-tokens "^3.0.0 || ^4.0.0" lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: yallist "^3.0.2" lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" match-sorter@^6.0.2: version "6.3.1" resolved "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz" - integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw== dependencies: "@babel/runtime" "^7.12.5" remove-accents "0.4.2" @@ -2935,12 +2550,10 @@ match-sorter@^6.0.2: merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== micromatch@^4.0.4: version "4.0.5" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: braces "^3.0.2" picomatch "^2.3.1" @@ -2948,78 +2561,64 @@ micromatch@^4.0.4: microseconds@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz" - integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" minimist@^1.2.0, minimist@^1.2.6: version "1.2.7" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== ms@2.1.2, ms@^2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== nano-time@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz" - integrity sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA== dependencies: big-integer "^1.6.16" nanoclone@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz" - integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" - integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== 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-fetch@2.6.7: version "2.6.7" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" node-releases@^2.0.8: version "2.0.10" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-inspect@^1.12.2, object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== object-is@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" @@ -3027,12 +2626,10 @@ object-is@^1.1.5: object-keys@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object.assign@^4.1.2, object.assign@^4.1.3, object.assign@^4.1.4: version "4.1.4" resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -3042,7 +2639,6 @@ object.assign@^4.1.2, object.assign@^4.1.3, object.assign@^4.1.4: object.entries@^1.1.5, object.entries@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz" - integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -3051,7 +2647,6 @@ object.entries@^1.1.5, object.entries@^1.1.6: object.fromentries@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz" - integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -3060,7 +2655,6 @@ object.fromentries@^2.0.6: object.hasown@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz" - integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw== dependencies: define-properties "^1.1.4" es-abstract "^1.20.4" @@ -3068,7 +2662,6 @@ object.hasown@^1.1.2: object.values@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz" - integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -3077,19 +2670,16 @@ object.values@^1.1.6: oblivious-set@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz" - integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== once@^1.3.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" optionator@^0.9.1: version "0.9.1" resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== dependencies: deep-is "^0.1.3" fast-levenshtein "^2.0.6" @@ -3101,68 +2691,56 @@ optionator@^0.9.1: 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" parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" 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-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== 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== path-parse@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== picocolors@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 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== prop-types-extra@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz" - integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== dependencies: react-is "^16.3.2" warning "^4.0.0" @@ -3170,7 +2748,6 @@ prop-types-extra@^1.1.0: prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== dependencies: loose-envify "^1.4.0" object-assign "^4.1.1" @@ -3179,22 +2756,18 @@ prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: property-expr@^2.0.4: version "2.0.5" resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz" - integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== punycode@^2.1.0: version "2.3.0" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== react-avatar-editor@^13.0.0: version "13.0.0" resolved "https://registry.npmjs.org/react-avatar-editor/-/react-avatar-editor-13.0.0.tgz" - integrity sha512-0xw63MbRRQdDy7YI1IXU9+7tTFxYEFLV8CABvryYOGjZmXRTH2/UA0mafe57ns62uaEFX181kA4XlGlxCaeXKA== dependencies: "@babel/plugin-transform-runtime" "^7.12.1" "@babel/runtime" "^7.12.5" @@ -3203,7 +2776,6 @@ react-avatar-editor@^13.0.0: react-bootstrap@^2.2.0: version "2.7.0" resolved "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.7.0.tgz" - integrity sha512-Jcrn6aUuRVBeSB6dzKODKZU1TONOdhAxu0IDm4Sv74SJUm98dMdhSotF2SNvFEADANoR+stV+7TK6SNX1wWu5w== dependencies: "@babel/runtime" "^7.17.2" "@restart/hooks" "^0.4.6" @@ -3221,31 +2793,35 @@ react-bootstrap@^2.2.0: react-colorful@^5.6.1: version "5.6.1" resolved "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz" - integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== react-dom@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" scheduler "^0.20.2" -react-fast-compare@^3.0.1: +react-fast-compare@^3.0.1, react-fast-compare@^3.1.1: version "3.2.0" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz" - integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + +react-helmet@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz" + dependencies: + object-assign "^4.1.1" + prop-types "^15.7.2" + react-fast-compare "^3.1.1" + react-side-effect "^2.1.0" react-hook-form@^7.28.0: version "7.43.1" resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.1.tgz" - integrity sha512-+s3+s8LLytRMriwwuSqeLStVjRXFGxgjjx2jED7Z+wz1J/88vpxieRQGvJVvzrzVxshZ0BRuocFERb779m2kNg== react-i18next@^11.18.5: version "11.18.6" resolved "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz" - integrity sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA== dependencies: "@babel/runtime" "^7.14.5" html-parse-stringify "^3.0.1" @@ -3253,22 +2829,18 @@ react-i18next@^11.18.5: "react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^16.13.1, react-is@^16.3.2, react-is@^16.6.3, "react-is@^16.8.6 || ^17.0.0 || ^18.0.0": version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== react-is@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== react-popper@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz" - integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q== dependencies: react-fast-compare "^3.0.1" warning "^4.0.2" @@ -3276,7 +2848,6 @@ react-popper@^2.3.0: react-query@^3.34.16: version "3.39.3" resolved "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz" - integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g== dependencies: "@babel/runtime" "^7.5.5" broadcast-channel "^3.4.1" @@ -3285,7 +2856,6 @@ react-query@^3.34.16: react-router-dom@^6.4.3: version "6.8.1" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz" - integrity sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ== dependencies: "@remix-run/router" "1.3.2" react-router "6.8.1" @@ -3293,22 +2863,23 @@ react-router-dom@^6.4.3: react-router@6.8.1: version "6.8.1" resolved "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz" - integrity sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg== dependencies: "@remix-run/router" "1.3.2" react-shallow-renderer@^16.13.1: version "16.15.0" resolved "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz" - integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== dependencies: object-assign "^4.1.1" react-is "^16.12.0 || ^17.0.0 || ^18.0.0" +react-side-effect@^2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz" + react-test-renderer@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz" - integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== dependencies: object-assign "^4.1.1" react-is "^17.0.2" @@ -3318,14 +2889,12 @@ react-test-renderer@^17.0.2: react-toastify@^9.1.1: version "9.1.1" resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz" - integrity sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw== dependencies: clsx "^1.1.1" react-transition-group@^4.4.2: version "4.4.5" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" - integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== dependencies: "@babel/runtime" "^7.5.5" dom-helpers "^5.0.1" @@ -3335,7 +2904,6 @@ react-transition-group@^4.4.2: react@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -3343,38 +2911,32 @@ react@^17.0.2: readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" regenerate-unicode-properties@^10.1.0: version "10.1.0" resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== dependencies: regenerate "^1.4.2" regenerate@^1.4.2: version "1.4.2" resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== regenerator-runtime@^0.13.11: version "0.13.11" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== regenerator-transform@^0.15.1: version "0.15.1" resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz" - integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== dependencies: "@babel/runtime" "^7.8.4" regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz" - integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" @@ -3383,12 +2945,10 @@ regexp.prototype.flags@^1.4.3: regexpp@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== regexpu-core@^5.2.1: version "5.3.0" resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.0.tgz" - integrity sha512-ZdhUQlng0RoscyW7jADnUZ25F5eVtHdMyXSb2PiwafvteRAOJUjFoUPEYZSIfP99fBIs3maLIRfpEddT78wAAQ== dependencies: "@babel/regjsgen" "^0.8.0" regenerate "^1.4.2" @@ -3400,24 +2960,20 @@ regexpu-core@^5.2.1: regjsparser@^0.9.1: version "0.9.1" resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== dependencies: jsesc "~0.5.0" remove-accents@0.4.2: version "0.4.2" resolved "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz" - integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve@^1.14.2, resolve@^1.22.1: version "1.22.1" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== dependencies: is-core-module "^2.9.0" path-parse "^1.0.7" @@ -3426,7 +2982,6 @@ resolve@^1.14.2, resolve@^1.22.1: resolve@^2.0.0-next.4: version "2.0.0-next.4" resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz" - integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== dependencies: is-core-module "^2.9.0" path-parse "^1.0.7" @@ -3435,26 +2990,22 @@ resolve@^2.0.0-next.4: reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== rimraf@3.0.2, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: queue-microtask "^1.2.2" safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz" - integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== dependencies: call-bind "^1.0.2" get-intrinsic "^1.1.3" @@ -3463,7 +3014,6 @@ safe-regex-test@^1.0.0: sass@^1.52.1, sass@^1.56.1: version "1.58.0" resolved "https://registry.npmjs.org/sass/-/sass-1.58.0.tgz" - integrity sha512-PiMJcP33DdKtZ/1jSjjqVIKihoDc6yWmYr9K/4r3fVVIEDAluD0q7XZiRKrNJcPK3qkLRF/79DND1H5q1LBjgg== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -3472,7 +3022,6 @@ sass@^1.52.1, sass@^1.56.1: scheduler@^0.20.2: version "0.20.2" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -3480,14 +3029,12 @@ scheduler@^0.20.2: semantic-ui-css@^2.4.1: version "2.5.0" resolved "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.5.0.tgz" - integrity sha512-jIWn3WXXE2uSaWCcB+gVJVRG3masIKtTMNEP2X8Aw909H2rHpXGneYOxzO3hT8TpyvB5/dEEo9mBFCitGwoj1A== dependencies: jquery x.* semantic-ui-react@^2.1.3: version "2.1.4" resolved "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-2.1.4.tgz" - integrity sha512-7CxjBoFUfH7fUvtn+SPkkIocthUD9kV3niF1mUMa9TbeyPAf2brtRCZBlT2OpHaXmkscFzGjEfhbJo9gKfotzQ== dependencies: "@babel/runtime" "^7.10.5" "@fluentui/react-component-event-listener" "~0.63.0" @@ -3504,38 +3051,32 @@ semantic-ui-react@^2.1.3: shallowequal "^1.1.0" semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" semver@^7.3.7: - version "7.3.8" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" dependencies: lru-cache "^6.0.0" shallowequal@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" - integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== 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== side-channel@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== dependencies: call-bind "^1.0.0" get-intrinsic "^1.0.2" @@ -3544,24 +3085,20 @@ side-channel@^1.0.4: slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== "source-map-js@>=0.6.2 <2.0.0": version "1.0.2" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== stop-iteration-iterator@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== dependencies: internal-slot "^1.0.4" string.prototype.matchall@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz" - integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -3575,7 +3112,6 @@ string.prototype.matchall@^4.0.8: string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz" - integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -3584,7 +3120,6 @@ string.prototype.trimend@^1.0.6: string.prototype.trimstart@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz" - integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" @@ -3593,75 +3128,62 @@ string.prototype.trimstart@^1.0.6: strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" supports-color@^7.1.0: version "7.2.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== text-table@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== tinycolor2@^1.4.2: version "1.6.0" resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz" - integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== 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" toposort@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz" - integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== tr46@~0.0.3: version "0.0.3" resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" - integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.1" @@ -3671,36 +3193,30 @@ tsconfig-paths@^3.14.1: tslib@^1.8.1: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.4.0: version "2.5.0" resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" 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" type-fest@^0.20.2: version "0.20.2" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz" - integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== dependencies: call-bind "^1.0.2" for-each "^0.3.3" @@ -3709,7 +3225,6 @@ typed-array-length@^1.0.4: unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== dependencies: call-bind "^1.0.2" has-bigints "^1.0.2" @@ -3719,7 +3234,6 @@ unbox-primitive@^1.0.2: uncontrollable@^7.2.1: version "7.2.1" resolved "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz" - integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== dependencies: "@babel/runtime" "^7.6.3" "@types/react" ">=16.9.11" @@ -3729,12 +3243,10 @@ uncontrollable@^7.2.1: unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== unicode-match-property-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== dependencies: unicode-canonical-property-names-ecmascript "^2.0.0" unicode-property-aliases-ecmascript "^2.0.0" @@ -3742,17 +3254,14 @@ unicode-match-property-ecmascript@^2.0.0: unicode-match-property-value-ecmascript@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== unicode-property-aliases-ecmascript@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== unload@2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz" - integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== dependencies: "@babel/runtime" "^7.6.2" detect-node "^2.0.4" @@ -3760,7 +3269,6 @@ unload@2.2.0: update-browserslist-db@^1.0.10: version "1.0.10" resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -3768,31 +3276,26 @@ update-browserslist-db@^1.0.10: 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" void-elements@3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz" - integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== warning@^4.0.0, warning@^4.0.2, warning@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== dependencies: loose-envify "^1.0.0" webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== dependencies: tr46 "~0.0.3" webidl-conversions "^3.0.0" @@ -3800,7 +3303,6 @@ whatwg-url@^5.0.0: which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== dependencies: is-bigint "^1.0.1" is-boolean-object "^1.1.0" @@ -3811,7 +3313,6 @@ which-boxed-primitive@^1.0.2: which-collection@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== dependencies: is-map "^2.0.1" is-set "^2.0.1" @@ -3821,7 +3322,6 @@ which-collection@^1.0.1: which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz" - integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== dependencies: available-typed-arrays "^1.0.5" call-bind "^1.0.2" @@ -3833,39 +3333,32 @@ which-typed-array@^1.1.9: 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.3: - version "1.2.3" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== yallist@^3.0.2: version "3.1.1" resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 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== yup@^0.32.11: version "0.32.11" resolved "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz" - integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== dependencies: "@babel/runtime" "^7.15.4" "@types/lodash" "^4.14.175"