diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index d8487220..f9ffa6fb 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -44,20 +44,14 @@ jobs: env: RAILS_ENV: test run: bundle exec bin/rails db:schema:load - - name: Set up GPG - env: - GNUPGHOME: tmp/fauna_fake_gpg_home - run: mkdir tmp/fauna_fake_gpg_home && gpg -K && gpg --gen-key --batch spec/support/test_gpg_key_generation.batch - name: Set up test user env: RAILS_ENV: test - GNUPGHOME: tmp/fauna_fake_gpg_home run: bundle exec bin/setup -u admin -e admin@example.com -n admin -p foobarbaz - name: Build and test with rspec env: - GNUPGHOME: tmp/fauna_fake_gpg_home RAILS_ENV: test run: bundle exec rspec diff --git a/.gitignore b/.gitignore index ac000aad..59056db5 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,10 @@ # Ignore Ruby installations /vendor/bundle/ruby/ + +# Ignore precompiled assets +/public/assets/ + +# Ignore key files for decrypting credentials and more. +/config/*.key + diff --git a/Gemfile b/Gemfile index 4e6060fc..c4acb7a4 100644 --- a/Gemfile +++ b/Gemfile @@ -2,12 +2,12 @@ source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem "rails", "~> 7.0.8" +gem "rails", "~> 8.1.1" gem "bootsnap" # Use SCSS for stylesheets -gem "sass-rails" gem "sprockets" +gem 'dartsass-sprockets' # Use Uglifier as compressor for JavaScript assets gem "uglifier" @@ -42,14 +42,17 @@ gem "phony_rails" gem "slim-rails" # Use the Bootstrap CSS framework and the FA icon font -gem "bootstrap-sass" -gem "font-awesome-sass", "~> 4.0" +gem "bootstrap" +gem "font-awesome-sass", :github => 'sunbirddcim/font-awesome-sass' # Gravatar helper gem "gravatar-ultimate" # Add Ruby 2.4.0 support gem "xmlrpc" +# SpaceApi +gem 'ostruct' + # Use simple form for form building gem "simple_form" gem "cocoon" @@ -58,12 +61,8 @@ gem "cocoon" gem "kaminari" # Use rolify and pundit for authorization -gem "rolify" gem "pundit" -# GPG Signing -gem "mail-gpg" - # Asynchronous job execution gem "delayed_job" gem "delayed_job_active_record" @@ -86,30 +85,6 @@ gem "matrix" gem "rack-cors" group :development do - # Use Capistrano for deployment - gem "capistrano" - gem "capistrano-rails" - gem "capistrano-bundler" - gem "capistrano-rbenv" - gem "net-ssh" - - gem "pry" - gem "pry-rails" - gem "pry-doc" - - # Continuous testing with Guard - gem "guard-rspec" - - # Deploy to a puma server - gem "capistrano3-puma", "~> 5" - - # Goodies for prettier printing of records in the console - gem "awesome_print" - gem "hirb" - - gem "better_errors" - gem "binding_of_caller" - gem "web-console" end @@ -155,4 +130,6 @@ group :production do gem "pg" gem "puma" + + gem "sd_notify" end diff --git a/Gemfile.lock b/Gemfile.lock index e92e62a2..9762a67b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,130 +1,121 @@ +GIT + remote: https://github.com/sunbirddcim/font-awesome-sass.git + revision: d23c4173812366b719768bd071d19dd7f1dfed6d + specs: + font-awesome-sass (6.5.2.pre.sunbird.pre.1) + GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) + action_text-trix (2.1.18) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.0.8.1) - actionpack (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activesupport (= 7.0.8.1) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp - rails-dom-testing (~> 2.0) - actionpack (7.0.8.1) - actionview (= 7.0.8.1) - activesupport (= 7.0.8.1) - rack (~> 2.0, >= 2.2.4) + zeitwerk (~> 2.6) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.1) - actionpack (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.3) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.1) - activesupport (= 7.0.8.1) + actionview (8.1.3) + activesupport (= 8.1.3) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8.1) - activesupport (= 7.0.8.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.3) + activesupport (= 8.1.3) globalid (>= 0.3.6) - activemodel (7.0.8.1) - activesupport (= 7.0.8.1) - activerecord (7.0.8.1) - activemodel (= 7.0.8.1) - activesupport (= 7.0.8.1) - activestorage (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activesupport (= 7.0.8.1) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) + timeout (>= 0.4.0) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.8.1) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) - airbrussh (1.5.1) - sshkit (>= 1.6.1, != 1.7.0) - ast (2.4.2) - autoprefixer-rails (10.4.16.0) - execjs (~> 2) - awesome_print (1.9.2) - bcrypt (3.1.20) - better_errors (2.10.1) - erubi (>= 1.0.0) - rack (>= 0.9.0) - rouge (>= 1.0.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + base64 (0.3.0) + bcrypt (3.1.22) + benchmark (0.5.0) + bigdecimal (4.1.1) bindex (0.8.1) - binding_of_caller (1.0.0) - debug_inspector (>= 0.0.1) - bootsnap (1.17.1) + bootsnap (1.23.0) msgpack (~> 1.2) - bootstrap-sass (3.4.1) - autoprefixer-rails (>= 5.2.1) - sassc (>= 2.0.0) - brakeman (6.1.1) + bootstrap (5.3.8) + popper_js (>= 2.11.8, < 3) + brakeman (8.0.4) racc - builder (3.2.4) - bundler-audit (0.9.1) - bundler (>= 1.2.0, < 3) + builder (3.3.0) + bundler-audit (0.9.3) + bundler (>= 1.2.0) thor (~> 1.0) - byebug (11.1.3) - capistrano (3.18.0) - airbrussh (>= 1.0.0) - i18n - rake (>= 10.0.0) - sshkit (>= 1.9.0) - capistrano-bundler (2.1.0) - capistrano (~> 3.1) - capistrano-rails (1.6.3) - capistrano (~> 3.1) - capistrano-bundler (>= 1.1, < 3) - capistrano-rbenv (2.2.0) - capistrano (~> 3.1) - sshkit (~> 1.3) - capistrano3-puma (5.2.0) - capistrano (~> 3.7) - capistrano-bundler - puma (>= 4.0, < 6.0) - capybara (3.39.2) + byebug (13.0.0) + reline (>= 0.6.0) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - capybara-screenshot (1.0.26) + capybara-screenshot (1.0.27) capybara (>= 1.0, < 4) launchy + childprocess (5.1.0) + logger (~> 1.5) cocoon (1.2.15) - coderay (1.1.3) coffee-rails (5.0.0) coffee-script (>= 2.2.0) railties (>= 5.2.0) @@ -132,76 +123,75 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) crass (1.0.6) daemons (1.4.1) - date (3.4.1) - debug_inspector (1.2.0) - delayed_job (4.1.11) - activesupport (>= 3.0, < 8.0) - delayed_job_active_record (4.1.8) - activerecord (>= 3.0, < 8.0) + dartsass-sprockets (3.2.1) + railties (>= 4.0.0) + sassc-embedded (~> 1.80.1) + sprockets (> 3.0) + sprockets-rails + tilt + date (3.5.1) + delayed_job (4.2.0) + activesupport (>= 3.0, < 9.0) + benchmark + logger + delayed_job_active_record (4.1.11) + activerecord (>= 3.0, < 9.0) delayed_job (>= 3.0, < 5) - devise (4.9.3) + devise (5.0.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0) + railties (>= 7.0) responders warden (~> 1.2.3) - devise-i18n (1.12.0) - devise (>= 4.9.0) - diff-lcs (1.5.0) - docile (1.4.0) - doorkeeper (5.6.8) + devise-i18n (1.16.0) + devise (>= 5.0.0) + rails-i18n + diff-lcs (1.6.2) + docile (1.4.1) + doorkeeper (5.9.0) railties (>= 5) - doorkeeper-i18n (5.2.7) + doorkeeper-i18n (5.2.8) doorkeeper (>= 5.2) - erubi (1.12.0) - execjs (2.9.1) - factory_bot (6.4.5) - activesupport (>= 5.0.0) - factory_bot_rails (6.4.3) - factory_bot (~> 6.4) - railties (>= 5.0.0) - fakefs (2.5.0) - faker (3.2.3) + drb (2.2.3) + erb (6.0.2) + erubi (1.13.1) + execjs (2.10.1) + factory_bot (6.5.6) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + fakefs (3.2.1) + faker (3.6.1) i18n (>= 1.8.11, < 2) - ffi (1.16.3) - font-awesome-sass (4.7.0) - sass (>= 3.2) - formatador (1.1.0) - globalid (1.2.1) + globalid (1.3.0) activesupport (>= 6.1) - gpgme (2.0.23) - mini_portile2 (~> 2.7) + google-protobuf (4.34.1-x86_64-linux-gnu) + bigdecimal + rake (~> 13.3) gravatar-ultimate (2.0.0) activesupport (>= 2.3.14) rack - guard (2.18.1) - formatador (>= 0.2.4) - listen (>= 2.7, < 4.0) - lumberjack (>= 1.0.12, < 2.0) - nenv (~> 0.1) - notiffany (~> 0.0) - pry (>= 0.13.0) - shellany (~> 0.0) - thor (>= 0.18.1) - guard-compat (1.2.1) - guard-rspec (4.7.3) - guard (~> 2.1) - guard-compat (~> 1.1) - rspec (>= 2.99.0, < 4.0) - hirb (0.7.3) - i18n (1.14.4) + i18n (1.14.8) concurrent-ruby (~> 1.0) - jbuilder (2.11.5) - actionview (>= 5.0.0) - activesupport (>= 5.0.0) - jquery-rails (4.6.0) + io-console (0.8.2) + irb (1.17.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.14.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + jquery-rails (4.6.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.7.1) + json (2.19.3) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -214,256 +204,240 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - language_server-protocol (3.17.0.3) - launchy (2.5.2) + language_server-protocol (3.17.0.5) + launchy (3.1.1) addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) lint_roller (1.1.0) - listen (3.8.0) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.22.0) + logger (1.7.0) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) - lumberjack (1.2.10) - mail (2.8.1) + mail (2.9.0) + logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp - mail-gpg (0.4.4) - gpgme (~> 2.0, >= 2.0.2) - mail (~> 2.5, >= 2.5.3) - marcel (1.0.4) - matrix (0.4.2) - method_source (1.0.0) + marcel (1.1.0) + matrix (0.4.3) mini_mime (1.1.5) - mini_portile2 (2.8.5) - minitest (5.22.3) - msgpack (1.7.2) - mutex_m (0.2.0) - nenv (0.3.0) - net-imap (0.4.20) + minitest (6.0.3) + drb (~> 2.0) + prism (~> 1.5) + msgpack (1.8.0) + net-imap (0.6.3) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-scp (4.0.0) - net-ssh (>= 2.6.5, < 8.0.0) - net-sftp (4.0.0) - net-ssh (>= 5.0.0, < 8.0.0) - net-smtp (0.4.0.1) + net-smtp (0.5.1) net-protocol - net-ssh (7.2.1) - nio4r (2.7.4) - nokogiri (1.16.3-x86_64-linux) + nio4r (2.7.5) + nokogiri (1.19.2-x86_64-linux-gnu) racc (~> 1.4) - notiffany (0.1.3) - nenv (~> 0.1) - shellany (~> 0.0) orm_adapter (0.5.0) - parallel (1.24.0) - parser (3.3.0.4) + ostruct (0.6.3) + parallel (1.28.0) + parser (3.3.11.1) ast (~> 2.4.1) racc - pg (1.5.4) - phony (2.20.11) + pg (1.6.3-x86_64-linux) + phony (3.0.5) phony_rails (0.15.0) activesupport (>= 3.0) phony (>= 2.18.12) - pry (0.14.2) - coderay (~> 1.1) - method_source (~> 1.0) - pry-doc (1.5.0) - pry (~> 0.11) - yard (~> 0.9.11) - pry-rails (0.3.9) - pry (>= 0.10.4) - psych (5.1.2) + popper_js (2.11.8) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + psych (5.3.1) + date stringio - public_suffix (5.0.4) - puma (5.6.9) + public_suffix (7.0.5) + puma (7.2.0) nio4r (~> 2.0) - pundit (2.3.1) + pundit (2.5.2) activesupport (>= 3.0.0) - racc (1.7.3) - rack (2.2.9) - rack-cors (2.0.2) - rack (>= 2.0.0) - rack-test (2.1.0) + racc (1.8.1) + rack (3.2.6) + rack-cors (3.0.0) + logger + rack (>= 3.0.14) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) rack (>= 1.3) - rails (7.0.8.1) - actioncable (= 7.0.8.1) - actionmailbox (= 7.0.8.1) - actionmailer (= 7.0.8.1) - actionpack (= 7.0.8.1) - actiontext (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activemodel (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + rackup (2.3.1) + rack (>= 3) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) bundler (>= 1.15.0) - railties (= 7.0.8.1) - rails-dom-testing (2.2.0) + railties (= 8.1.3) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - rails-i18n (7.0.8) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + rails-i18n (8.1.0) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 8) - railties (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) - method_source + railties (>= 8.0.0, < 9) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + irb (~> 1.13) + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.1.0) - rb-fsevent (0.11.2) - rb-inotify (0.10.1) - ffi (~> 1.0) - rdoc (6.6.3.1) + rake (13.3.1) + rdoc (7.2.0) + erb psych (>= 4.0.0) - regexp_parser (2.9.0) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) - rexml (3.3.9) - rolify (6.0.1) - rouge (4.2.0) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + tsort + regexp_parser (2.12.0) + reline (0.6.3) + io-console (~> 0.5) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) + rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-rails (6.1.0) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) - rspec-core (~> 3.12) - rspec-expectations (~> 3.12) - rspec-mocks (~> 3.12) - rspec-support (~> 3.12) - rspec-support (3.12.1) - rubocop (1.59.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) + rspec-support (3.13.7) + rubocop (1.84.2) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) - parser (>= 3.2.2.4) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.30.0, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.30.0) - parser (>= 3.2.1.0) - rubocop-performance (1.20.2) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.30.0, < 2.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) ruby-progressbar (1.13.0) - rubyzip (2.3.2) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (6.0.0) - sassc-rails (~> 2.1, >= 2.1.1) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt - sdoc (2.6.1) + rubyzip (3.2.2) + sass-embedded (1.99.0-x86_64-linux-gnu) + google-protobuf (~> 4.31) + sassc-embedded (1.80.8) + sass-embedded (~> 1.80) + sd_notify (0.1.1) + sdoc (2.6.5) rdoc (>= 5.0) - selenium-webdriver (4.16.0) + securerandom (0.4.1) + selenium-webdriver (4.41.0) + base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) + rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) - shellany (0.0.1) - simple_form (5.3.0) - actionpack (>= 5.2) - activemodel (>= 5.2) + simple_form (5.4.1) + actionpack (>= 7.0) + activemodel (>= 7.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) - slim (5.2.0) + slim (5.2.1) temple (~> 0.10.0) tilt (>= 2.1.0) - slim-rails (3.6.3) + slim-rails (4.0.0) actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) - snmp (1.3.2) - spring (4.1.3) + snmp (1.3.4) + spring (4.4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - sprockets (4.2.1) + sprockets (4.2.2) concurrent-ruby (~> 1.0) + logger rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - sqlite3 (1.7.0-x86_64-linux) - sshkit (1.22.0) - mutex_m - net-scp (>= 1.1.2) - net-sftp (>= 2.1.2) - net-ssh (>= 2.8.0) - standard (1.33.0) + sqlite3 (2.9.2-x86_64-linux-gnu) + standard (1.54.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.59.0) + rubocop (~> 1.84.0) standard-custom (~> 1.0.0) - standard-performance (~> 1.3) + standard-performance (~> 1.8) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.3.1) + standard-performance (1.9.0) lint_roller (~> 1.1) - rubocop-performance (~> 1.20.2) - stringio (3.1.0) - temple (0.10.3) - thor (1.3.1) - tilt (2.3.0) - timeout (0.4.3) + rubocop-performance (~> 1.26.0) + stringio (3.2.0) + temple (0.10.4) + thor (1.5.0) + tilt (2.7.0) + timeout (0.6.1) + tsort (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - uglifier (4.2.0) + uglifier (4.2.1) execjs (>= 0.3.0, < 3) - unicode-display_width (2.5.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) - web-console (4.2.1) - actionview (>= 6.0.0) - activemodel (>= 6.0.0) + web-console (4.3.0) + actionview (>= 8.0.0) bindex (>= 0.4.0) - railties (>= 6.0.0) - webrick (1.8.2) - websocket (1.2.10) - websocket-driver (0.7.6) + railties (>= 8.0.0) + webrick (1.9.2) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xmlrpc (0.3.3) @@ -473,31 +447,23 @@ GEM yaml_db (0.7.0) rails (>= 3.0) rake (>= 0.8.7) - yard (0.9.36) - zeitwerk (2.6.13) + zeitwerk (2.7.5) PLATFORMS x86_64-linux DEPENDENCIES - awesome_print - better_errors - binding_of_caller bootsnap - bootstrap-sass + bootstrap brakeman bundler-audit byebug - capistrano - capistrano-bundler - capistrano-rails - capistrano-rbenv - capistrano3-puma (~> 5) capybara capybara-screenshot cocoon coffee-rails daemons + dartsass-sprockets delayed_job delayed_job_active_record devise @@ -507,30 +473,23 @@ DEPENDENCIES factory_bot_rails fakefs faker - font-awesome-sass (~> 4.0) + font-awesome-sass! gravatar-ultimate - guard-rspec - hirb jbuilder jquery-rails kaminari - mail-gpg matrix net-smtp - net-ssh + ostruct pg phony_rails - pry - pry-doc - pry-rails puma pundit rack-cors - rails (~> 7.0.8) + rails (~> 8.1.1) rails-i18n - rolify rspec-rails - sass-rails + sd_notify sdoc selenium-webdriver simple_form diff --git a/app/assets/images/initlab-logo.svg b/app/assets/images/initlab-logo.svg index 61d6ea1f..ff17f188 100644 --- a/app/assets/images/initlab-logo.svg +++ b/app/assets/images/initlab-logo.svg @@ -1,384 +1,11 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 645a1a5d..2534c3aa 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,10 +12,8 @@ // //= require jquery3 //= require rails-ujs -//= require bootstrap/dropdown -//= require bootstrap/collapse -//= require bootstrap/alert -//= require bootstrap/transition +//= require popper +//= require bootstrap //= require cocoon //= require_tree . diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 85a4b22f..f2dd4f46 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,53 +1,17 @@ -@import 'font-awesome-sprockets'; -@import 'font-awesome'; -@import 'paper_variables'; -@import 'bootstrap-sprockets'; - -// Core variables and mixins -@import "bootstrap/variables"; -@import "bootstrap/mixins"; - -// Reset and dependencies -@import "bootstrap/normalize"; +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; -// Core CSS -@import "bootstrap/scaffolding"; -@import "bootstrap/type"; -@import "bootstrap/code"; -@import "bootstrap/grid"; -@import "bootstrap/tables"; -@import "bootstrap/forms"; -@import "bootstrap/buttons"; - -// Components -@import "bootstrap/component-animations"; -@import "bootstrap/dropdowns"; -@import "bootstrap/button-groups"; -@import "bootstrap/input-groups"; -@import "bootstrap/navs"; -@import "bootstrap/navbar"; -@import "bootstrap/pagination"; -@import "bootstrap/labels"; -@import "bootstrap/badges"; -@import "bootstrap/thumbnails"; -@import "bootstrap/alerts"; -@import "bootstrap/panels"; -@import "bootstrap/close"; - -// Utility classes -@import "bootstrap/utilities"; -@import "bootstrap/responsive-utilities"; +@import 'font-awesome'; -@import 'paper'; @import 'roboto'; @import 'open_sans'; @import 'components/flash_message'; @import 'components/footer'; -@import 'components/login'; @import 'components/nested_fields'; @import 'components/network_devices'; -@import 'components/role_assignments'; @import 'components/large_buttons'; @import 'fauna/user_list'; @import 'components/header'; diff --git a/app/assets/stylesheets/components/_flash_message.scss b/app/assets/stylesheets/components/_flash_message.scss index 0a367b3b..9e79b2f5 100644 --- a/app/assets/stylesheets/components/_flash_message.scss +++ b/app/assets/stylesheets/components/_flash_message.scss @@ -1,6 +1,11 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .flash_message { @extend .col-md-4; - @extend .col-md-offset-4; + @extend .offset-md-4; @extend .col-sm-6; - @extend .col-sm-offset-3; + @extend .offset-sm-3; } \ No newline at end of file diff --git a/app/assets/stylesheets/components/_footer.scss b/app/assets/stylesheets/components/_footer.scss index f2fb6053..ee60207d 100644 --- a/app/assets/stylesheets/components/_footer.scss +++ b/app/assets/stylesheets/components/_footer.scss @@ -1,37 +1,17 @@ -html { - position: relative; - min-height: 100%; -} - -body { - margin-bottom: 64px; - - @media (max-width: $screen-xs-max) { - margin-bottom: 128px; - } -} - -.footer { - position: absolute; - bottom: 0; - width: 100%; - height: 64px; - background-color: #f5f5f5; - - @media (max-width: $screen-xs-max) { - height: 128px; - } +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap/grid'; +footer { p { - margin: 20px 0; - @extend .text-muted; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(sm) { text-align: center; } } p.right { - @media (min-width: $screen-xs-max) { + @include media-breakpoint-up(sm) { text-align: right; } } diff --git a/app/assets/stylesheets/components/_login.scss b/app/assets/stylesheets/components/_forms.scss similarity index 69% rename from app/assets/stylesheets/components/_login.scss rename to app/assets/stylesheets/components/_forms.scss index fb578c44..b9da22e4 100644 --- a/app/assets/stylesheets/components/_login.scss +++ b/app/assets/stylesheets/components/_forms.scss @@ -1,28 +1,33 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .centered_form { @extend .col-md-4; - @extend .col-md-offset-4; + @extend .offset-md-4; @extend .col-sm-6; - @extend .col-sm-offset-3; + @extend .offset-sm-3; } input[type=submit] { @extend .btn; @extend .btn-primary; @extend .btn-lg; - @extend .btn-block; + //@extend .btn-block; } .account_form { @extend .col-md-10; - @extend .col-md-offset-1; + @extend .offset-md-1; @extend .col-lg-8; - @extend .col-lg-offset-2; + @extend .offset-lg-2; } .form-horizontal { div.boolean { padding: 10px; - @extend .col-sm-offset-3; + @extend .offset-sm-3; } } @@ -32,7 +37,7 @@ input[type=submit] { } .edit_gravatar { - @extend .img-thumbnail; + //@extend .img-thumbnail; position: relative; diff --git a/app/assets/stylesheets/components/_header.scss b/app/assets/stylesheets/components/_header.scss index 4b896ad5..ececd2ab 100644 --- a/app/assets/stylesheets/components/_header.scss +++ b/app/assets/stylesheets/components/_header.scss @@ -1,16 +1,19 @@ -.navbar-brand { - margin: 0 !important; - padding: 0; - - img { - padding: 5px 0px; - @media (max-width: $screen-sm-min) { - padding: 5px; - } - - } - - &.active { - background-color: #0c7cd5; - } -} +//@import 'bootstrap/functions'; +//@import 'bootstrap/variables'; +//@import 'bootstrap/mixins'; +// +//.navbar-brand { +// margin: 0 !important; +// padding: 0; +// +// img { +// padding: 5px 0; +// @include media-breakpoint-up(sm) { +// padding: 5px; +// } +// } +// +// &.active { +// background-color: #0c7cd5; +// } +//} diff --git a/app/assets/stylesheets/components/_large_buttons.scss b/app/assets/stylesheets/components/_large_buttons.scss index 3ab6c1cd..03b11e94 100644 --- a/app/assets/stylesheets/components/_large_buttons.scss +++ b/app/assets/stylesheets/components/_large_buttons.scss @@ -1,5 +1,11 @@ +@use 'sass:map'; +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; + + .current_status_indicator { - @extend .thumbnail; + //@extend .thumbnail; @extend .text-center; width: 350px; font-size: 2em; @@ -7,16 +13,16 @@ margin: 20px auto 0 auto; i.fa { font-size: 72pt; } - @media (max-width: $screen-sm-min) { + @include media-breakpoint-only(xs) { width: 250px; } - @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { + @include media-breakpoint-only(sm) { width: 350px; i.fa { font-size: 36pt; } } - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { width: 200px; height: 200px; i.fa { font-size: 72pt; } @@ -29,7 +35,7 @@ margin: 0 auto; padding: 20px; - @media (min-width: $screen-md-max) { + @include media-breakpoint-up(lg) { form { display: inline-block; } } } diff --git a/app/assets/stylesheets/components/_nested_fields.scss b/app/assets/stylesheets/components/_nested_fields.scss index dc1b2329..2ed15256 100644 --- a/app/assets/stylesheets/components/_nested_fields.scss +++ b/app/assets/stylesheets/components/_nested_fields.scss @@ -1,10 +1,9 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .add_nested_fields_container { @extend .text-center; } -.form-horizontal .add_fields { - @extend .col-sm-offset-3; - @extend .btn; - @extend .btn-primary; - @extend .btn-sm; -} diff --git a/app/assets/stylesheets/components/_network_devices.scss b/app/assets/stylesheets/components/_network_devices.scss index c3fd4f45..053a1f3a 100644 --- a/app/assets/stylesheets/components/_network_devices.scss +++ b/app/assets/stylesheets/components/_network_devices.scss @@ -1,3 +1,8 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .new_network_device_container, .network_device_container { @extend .col-sm-4; diff --git a/app/assets/stylesheets/components/_page_actions.scss b/app/assets/stylesheets/components/_page_actions.scss index b1576fce..8e775605 100644 --- a/app/assets/stylesheets/components/_page_actions.scss +++ b/app/assets/stylesheets/components/_page_actions.scss @@ -1,13 +1,18 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .page-actions { float: right; - @extend .hidden-xs; + @extend .d-none; + @extend .d-sm-block; } .mobile-page-actions { - @extend .hidden-sm; - @extend .visible-xs; + @extend .d-block; + @extend .d-sm-none; a { @extend .btn-lg; - @extend .btn-block; } } \ No newline at end of file diff --git a/app/assets/stylesheets/components/_record_actions.scss b/app/assets/stylesheets/components/_record_actions.scss index 9bb389c6..6ec074ba 100644 --- a/app/assets/stylesheets/components/_record_actions.scss +++ b/app/assets/stylesheets/components/_record_actions.scss @@ -1,3 +1,8 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .record-actions { @extend .btn-group; a { @extend .btn; } @@ -7,7 +12,7 @@ } td .record-actions { - @extend .hidden-print; + @extend .d-print-none; justify-content: flex-end; display: flex; } diff --git a/app/assets/stylesheets/components/_record_table.scss b/app/assets/stylesheets/components/_record_table.scss index 4e0a47bf..703b1f86 100644 --- a/app/assets/stylesheets/components/_record_table.scss +++ b/app/assets/stylesheets/components/_record_table.scss @@ -1,3 +1,8 @@ +@import 'bootstrap/functions'; +@import 'bootstrap/variables'; +@import 'bootstrap/mixins'; +@import 'bootstrap'; + .record-table { @extend .table; @extend .table-striped; diff --git a/app/assets/stylesheets/components/_role_assignments.scss b/app/assets/stylesheets/components/_role_assignments.scss deleted file mode 100644 index 13e0216b..00000000 --- a/app/assets/stylesheets/components/_role_assignments.scss +++ /dev/null @@ -1,8 +0,0 @@ -.role-assignments { - @extend .btn-group; - white-space: nowrap; - .btn { - float: none; - display: inline-block; - } -} \ No newline at end of file diff --git a/app/controllers/api/phone_access_controller.rb b/app/controllers/api/phone_access_controller.rb index 0808347b..737ad3f6 100644 --- a/app/controllers/api/phone_access_controller.rb +++ b/app/controllers/api/phone_access_controller.rb @@ -5,15 +5,15 @@ class Api::PhoneAccessController < Api::ApplicationController before_action -> { doorkeeper_authorize! :account_data_read }, only: [:verify_pin] def phone_number_token - secret = Rails.application.secrets.PHONE_TOKEN_SECRET - return head :internal_server_error if !secret + secret = Rails.application.credentials.phone.token_secret + return head :internal_server_error unless secret - phone_app_id = Rails.application.secrets.PHONE_OAUTH_APP_ID - return head :internal_server_error if !phone_app_id + phone_app_id = Rails.application.credentials.phone.app_id + return head :internal_server_error unless phone_app_id - return head :bad_request if !params[:phone_number] + return head :bad_request unless params[:phone_number] - return head :forbidden if !ActiveSupport::SecurityUtils.secure_compare(params[:secret], secret) + return head :forbidden unless ActiveSupport::SecurityUtils.secure_compare(params[:secret], secret) # TODO: the default country code should be taken from the model @user = User.joins(:phone_numbers).find_by phone_numbers: {phone_number: PhonyRails.normalize_number(params[:phone_number], default_country_code: "BG")} @@ -29,7 +29,7 @@ def phone_number_token end def verify_pin - return head :bad_request if !params[:pin] + return head :bad_request unless params[:pin] @valid = if current_resource_owner.encrypted_pin? (BCrypt::Password.new(current_resource_owner.encrypted_pin) == params[:pin]) ? "valid" : "invalid" diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb index d37363b7..42c19654 100644 --- a/app/controllers/api/users_controller.rb +++ b/app/controllers/api/users_controller.rb @@ -1,8 +1,10 @@ -class Api::UsersController < Api::ApplicationController +module Api + class UsersController < Api::ApplicationController wrap_parameters format: [:json] include Api::PublicApiExposingController def present @users = Presence.present_users end + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6f91d878..cc327e84 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -32,12 +32,11 @@ def set_locale def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :username, :email]) - devise_parameter_sanitizer.permit(:sign_in, keys: [:username]) + devise_parameter_sanitizer.permit(:sign_in, keys: [:username, :email]) devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name, :username, :email, :url, :locale, :twitter, :announce_my_presence, :github, :jabber, - :gpg_fingerprint, :pin, :pin_confirmation, phone_numbers_attributes: [ :_destroy, diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index e1192e4d..36f3a2f0 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,2 +1,5 @@ class DashboardController < ApplicationController + def index + redirect_to 'https://space.initlab.org', allow_other_host: true + end end diff --git a/app/controllers/fauna/role_assignments_controller.rb b/app/controllers/fauna/role_assignments_controller.rb index b29f45cc..af87ce46 100644 --- a/app/controllers/fauna/role_assignments_controller.rb +++ b/app/controllers/fauna/role_assignments_controller.rb @@ -1,36 +1,39 @@ +# frozen_string_literal: true module Fauna class RoleAssignmentsController < ApplicationController before_action :authenticate_user! before_action :assign_user - def create + def index authorize :role_assignment + end - respond_to do |format| - format.js do - if @user.add_role(role_params[:name]).persisted? - render :refresh, status: :created - else - head :unprocessable_entity - end - end + def create + authorize :role_assignment + role = Role.find(role_params[:role]) + start_time = role_params[:start_time] + end_time = role_params[:end_time] + + if @user.add_role(role, start_time, end_time) + redirect_to fauna_user_role_assignments_path + else + render :new, status: :unprocessable_content end + rescue Pundit::NotAuthorizedError head :forbidden end def destroy authorize :role_assignment + role = Role.find(params[:id]) + start_time = params[:start_time] - respond_to do |format| - format.js do - if @user.remove_role(params[:role_name]).empty? - head :unprocessable_entity - else - render :refresh - end - end + if @user.remove_role(role, start_time) + redirect_to fauna_user_role_assignments_path + else + redirect_back fallback_location: fauna_user_role_assignments_path, status: :unprocessable_entity end rescue Pundit::NotAuthorizedError head :forbidden @@ -43,7 +46,7 @@ def assign_user end def role_params - params.require(:role).permit(:name) + params.require(:user_role).permit(%i[role start_time end_time]) end end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 655a4d05..000e6ceb 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -22,4 +22,13 @@ def create respond_with resource end end + + def after_sign_up_path_for(_resource) + user_network_devices_path + end + + def after_inactive_sign_up_path_for(_resource) + new_user_session_path + end + end diff --git a/app/helpers/bootstrap_flash_helper.rb b/app/helpers/bootstrap_flash_helper.rb index 012c155c..f036e5d9 100644 --- a/app/helpers/bootstrap_flash_helper.rb +++ b/app/helpers/bootstrap_flash_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module BootstrapFlashHelper ALERT_TYPES_MAP = { notice: :success, @@ -5,21 +7,24 @@ module BootstrapFlashHelper error: :danger, info: :info, warning: :warning - } + }.freeze def bootstrap_flash safe_join(flash.each_with_object([]) do |(type, message), messages| next if message.blank? || !message.respond_to?(:to_str) + type = ALERT_TYPES_MAP.fetch(type.to_sym, type) messages << flash_container(type, message) end, "\n").presence end def flash_container(type, message) - content_tag :div, class: "alert alert-#{type} alert-dismissable" do - button_tag type: "button", class: "close", data: {dismiss: "alert"} do - "×".html_safe - end.safe_concat(message) + content_tag :div, class: "card border-#{type}" do + content_tag :div, class: 'card-body' do + content_tag :div, class: 'card-text' do + safe_concat(message) + end + end end end end diff --git a/app/mailers/authentication_mailer.rb b/app/mailers/authentication_mailer.rb index 378aa32a..1283a3bb 100644 --- a/app/mailers/authentication_mailer.rb +++ b/app/mailers/authentication_mailer.rb @@ -1,5 +1,2 @@ class AuthenticationMailer < Devise::Mailer - def mail(headers = {}, &block) - super(headers.merge(gpg: {sign: true}), &block) - end end diff --git a/app/models/rad_who.rb b/app/models/rad_who.rb index e010c0d2..f6b73071 100644 --- a/app/models/rad_who.rb +++ b/app/models/rad_who.rb @@ -8,7 +8,7 @@ def initialize end def self.radwho - if Rails.env.production? + if Rails.env.production? and File.file?('/var/log/freeradius/radutmp') `radwho -i -r -F /var/log/freeradius/radutmp`.gsub(RAW_ENTRY_REGEXP, '\k-\k') else "" diff --git a/app/models/role.rb b/app/models/role.rb index 5583a595..215b7c26 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -1,12 +1,8 @@ class Role < ApplicationRecord - PREDEFINED_ROLES = [:board_member, :trusted_member, :member, :infra, :tenant, :landlord].freeze + PREDEFINED_ROLES = %i[board_member trusted_member member infra tenant landlord].freeze - has_and_belongs_to_many :users, join_table: :users_roles - belongs_to :resource, polymorphic: true, optional: true - - validates :resource_type, - inclusion: {in: Rolify.resource_types}, - allow_nil: true + has_many :user_roles + has_many :users, through: :user_roles validates :name, inclusion: {in: Role::PREDEFINED_ROLES.map(&:to_s)} @@ -17,6 +13,4 @@ def localized_name def self.predefined PREDEFINED_ROLES.map { |name| Role.find_or_initialize_by name: name } end - - scopify end diff --git a/app/models/user.rb b/app/models/user.rb index 7750056f..11c9b786 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,62 +1,77 @@ -require "digest/md5" +# frozen_string_literal: true -class User < ApplicationRecord - rolify +require 'digest/md5' +class User < ApplicationRecord # Include default devise modules. Others available are: # :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :confirmable, :lockable, - :recoverable, :rememberable, :trackable, :validatable, :doorkeeper, - authentication_keys: [:login] + :recoverable, :rememberable, :trackable, :validatable, :doorkeeper, + authentication_keys: [:login] + + has_many :user_roles + has_many :roles, through: :user_roles has_many :network_devices, foreign_key: :owner_id, dependent: :destroy has_many :phone_numbers, foreign_key: :owner_id, dependent: :destroy - has_many :oauth_applications, class_name: "Doorkeeper::Application", as: :owner + has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner - validates :username, uniqueness: {case_sensitive: false}, presence: true - validates :twitter, format: {with: /\A[A-Za-z0-9_]{1,15}\z/}, allow_blank: true - validates :url, format: {with: /\A(http|https):\/\/[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z-]{2,63}(:[0-9]{1,5})?(\/.*)?\z/ix}, allow_blank: true + validates :username, uniqueness: { case_sensitive: false }, presence: true + validates :twitter, format: { with: /\A[A-Za-z0-9_]{1,15}\z/ }, allow_blank: true + validates :url, + format: { with: %r{\A(http|https)://[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z-]{2,63}(:[0-9]{1,5})?(/.*)?\z}ix }, + allow_blank: true validates :first_name, presence: true validates :last_name, presence: true - validates :github, format: {with: /\A[a-z0-9][a-z0-9-]{,38}\z/i}, allow_blank: true - validates :jabber, format: {with: /\A[^@]+@[^@]+\z/}, allow_blank: true - validates :gpg_fingerprint, format: {with: /\A[0-9a-f]{4}( ?)([0-9a-f]{4}\1){4}\1{0,2}([0-9a-f]{4}\1){4}[0-9a-f]{4}\z/i}, allow_blank: true - validates :pin, numericality: true, length: {minimum: 6}, allow_blank: true, confirmation: true - validates :locale, presence: true, inclusion: {in: I18n.available_locales.map(&:to_s)} + validates :github, format: { with: /\A[a-z0-9][a-z0-9-]{,38}\z/i }, allow_blank: true + validates :jabber, format: { with: /\A[^@]+@[^@]+\z/ }, allow_blank: true + validates :pin, numericality: true, length: { minimum: 6 }, allow_blank: true, confirmation: true + validates :locale, presence: true, inclusion: { in: I18n.available_locales.map(&:to_s) } accepts_nested_attributes_for :phone_numbers, update_only: true, allow_destroy: true, reject_if: :all_blank attr_accessor :login - after_validation :normalize_gpg_fingerprint def self.find_for_database_authentication(warden_conditions) conditions = warden_conditions.dup - if (login = conditions.delete(:login)) - where(conditions).where(["lower(username) = :value OR lower(email) = :value", {value: login.downcase.strip}]).first - end + return unless (login = conditions.delete(:login)) + + where(conditions).where(['lower(username) = :value OR lower(email) = :value', + { value: login.downcase.strip }]).first + end + + def role?(role) + roles.exists?(name: role) end - def email_md5 - Digest::MD5.hexdigest email + def add_role(role, start_time = nil, end_time = nil) + user_role = UserRole.new(user_id: id, role_id: role.id, start_time: start_time, end_time: end_time) + user_role.save + end + + def remove_role(role, start_time = nil) + user_role = UserRole.unscoped.find_by(user_id: id, role_id: role.id, start_time: start_time) + user_role ||= UserRole.find_by(user_id: id, role_id: role.id) + user_role&.deactivate end def name - [first_name, last_name].compact.join(" ").strip + [first_name, last_name].compact.join(' ').strip end def twitter=(handle) - write_attribute :twitter, handle.gsub(/\A@/, "") if handle + write_attribute :twitter, handle.gsub(/\A@/, '') if handle end def picture(size = 128) - Gravatar.new(email).image_url ssl: true, s: size, d: "retro" + Gravatar.new(email).image_url ssl: true, s: size, d: 'retro' end def pin=(pin) - if pin.present? - @pin = pin - self.encrypted_pin = BCrypt::Password.create pin - end + return unless pin.present? + + @pin = pin + self.encrypted_pin = BCrypt::Password.create pin end attr_reader :pin @@ -64,10 +79,4 @@ def pin=(pin) def to_s "User(id: #{id}, email: #{email}, name: #{name})" end - - private - - def normalize_gpg_fingerprint - self.gpg_fingerprint = gpg_fingerprint.delete(" ").upcase.gsub(/([0-9a-f]{4})/i, '\1 ').strip if gpg_fingerprint.present? - end end diff --git a/app/models/user_role.rb b/app/models/user_role.rb new file mode 100644 index 00000000..05b64c8f --- /dev/null +++ b/app/models/user_role.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +class UserRole < ApplicationRecord + self.table_name = :users_roles + + belongs_to :role + belongs_to :user + + scope :expired, -> { unscope(where: %i[start_time end_time]).where('(end_time is not null and end_time < ?)', Time.current) } + scope :future, -> { unscope(where: %i[start_time end_time]).where('(start_time is not null and start_time > ? and (end_time is null or end_time > ?))', Time.current, Time.current) } + + scope :active_at, ->(time) { unscope(where: %i[start_time end_time]).where('(start_time < ?) and (end_time is null or end_time > ?)', time, time) } + + default_scope { where(start_time: ...Time.current).and(where(end_time: nil).or(where(end_time: Time.current...))) } + + def deactivate + update(end_time: Time.current) + end +end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 8980019f..afe3d192 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -9,7 +9,7 @@ def initialize(user, record) end def index? - user&.has_role?(:board_member) + user&.role?(:board_member) end def show? diff --git a/app/views/devise/registrations/_phone_number_fields.html.slim b/app/views/devise/registrations/_phone_number_fields.html.slim index a1532cae..f1073750 100644 --- a/app/views/devise/registrations/_phone_number_fields.html.slim +++ b/app/views/devise/registrations/_phone_number_fields.html.slim @@ -1,4 +1,4 @@ -.nested-fields +.nested-fields.mb-3 = f.input :phone_number do .input-group = f.input_field :phone_number, class: 'form-control', value: f.object.phone_number.try(:phony_formatted, format: :international) diff --git a/app/views/devise/registrations/edit.html.slim b/app/views/devise/registrations/edit.html.slim index f6775f4b..818f18dd 100644 --- a/app/views/devise/registrations/edit.html.slim +++ b/app/views/devise/registrations/edit.html.slim @@ -1,57 +1,95 @@ - content_for(:title) { t 'views.registrations.edit_account' } section.container - .row - .account_form - = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), wrapper: :horizontal_form, html: { method: :put, class: 'form-horizontal' }) do |f| + .row.justify-content-center + .col-12 + = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'form-horizontal' }) do |f| header legend h3 = t('views.registrations.edit_account') = f.error_notification - .form-inputs - .avatar_box - .edit_gravatar - = image_tag resource.picture(128) - span.overlay - | Avatar by - =< link_to 'Gravatar', 'https://gravatar.com' - - = f.input :first_name, required: true - = f.input :last_name, required: true - = f.input :username, required: true - = f.input :email, required: true - = f.input :locale, collection: I18n.available_locales, required: true - - if devise_mapping.confirmable? && resource.pending_reconfirmation? - p - | Currently waiting confirmation for: - =< resource.unconfirmed_email - - = f.input :password, autocomplete: "off", hint: t('views.registrations.leave_blank_if_you_do_not_want_to_change'), required: false - = f.input :password_confirmation, required: false - - = f.input :pin, as: :password, autocomplete: "off", required: false - = f.input :pin_confirmation, as: :password, required: false - - #phone_numbers + h4 = t('views.registrations.personal_data') + .row + .col-6 + .row + .col-12 + .col-6 + = f.input :first_name, required: true + .col-6 + = f.input :last_name, required: true + .col-12 + = f.input :username, required: true + .col-12 + = f.input :email, required: true + - if devise_mapping.confirmable? && resource.pending_reconfirmation? + p + | Currently waiting confirmation for: + =< resource.unconfirmed_email + .col-md-3 + .col-6.col-md-3 + .row + = f.input :locale, collection: I18n.available_locales, required: true, class: '' + .row + .row.edit_gravatar + .col.d-flex.justify-content-center + = image_tag resource.picture(192) + .row + .col.d-flex.justify-content-center + span.overlay + | Avatar by + =< link_to 'Gravatar', 'https://gravatar.com' + + .row + .col + = f.input :announce_my_presence, hint: t('views.registrations.announce_my_presence_explanation') + + .row + .col-6 + = f.input :password, autocomplete: "off", hint: t('views.registrations.leave_blank_if_you_do_not_want_to_change'), required: false + .col-6 + = f.input :password_confirmation, required: false + + hr + h4 = t('views.registrations.phones') + .row + .col-6 + .row + = f.input :pin, as: :password, autocomplete: "off", required: false + = f.input :pin_confirmation, as: :password, required: false + .col-12.d-grid + = link_to_add_association t('views.registrations.add_phone_number'), f, :phone_numbers, role: 'button', class: 'btn btn-primary', data: {'association-insertion-node' => '#phone_numbers', 'association-insertion-method' => 'append'} + + .col-6#phone_numbers = f.simple_fields_for :phone_numbers do |phone_numbers_form| = render 'phone_number_fields', f: phone_numbers_form - .add_nested_fields_container - = link_to_add_association t('views.registrations.add_phone_number'), f, :phone_numbers, role: 'button', data: {'association-insertion-node' => '#phone_numbers', 'association-insertion-method' => 'append'} + .add_nested_fields_container.mb-3 + + hr + h4 = t('views.registrations.socials') + + .row + .col-3 + = f.input :github + .col-3 + = f.input :url + .col-3 + = f.input :twitter + .col-3 + = f.input :jabber - = f.input :github - = f.input :twitter - = f.input :url - = f.input :jabber - = f.input :gpg_fingerprint - = f.input :announce_my_presence, hint: t('views.registrations.announce_my_presence_explanation') + hr - = f.input :current_password, hint: t('views.registrations.we_need_your_current_password'), required: true, wrapper_html: {class: 'has-warning'} + .row.align-items-end + .col-6 + = f.input :current_password, hint: t('views.registrations.we_need_your_current_password'), required: true, wrapper_html: {class: 'has-warning'} + .form-actions.d-grid.g-2 + = f.button :submit, t('views.registrations.update') + .col-3 + .col-3 + .row + = t('views.registrations.unhappy') + = link_to t('views.registrations.cancel_my_account'), registration_path(resource_name), data: { confirm: t('views.registrations.are_you_sure') }, method: :delete, class: 'btn btn-danger' + .clearfix - .form-actions - = f.button :submit, t('views.registrations.update') - .clearfix - h4 - = t('views.registrations.unhappy') - = link_to t('views.registrations.cancel_my_account'), registration_path(resource_name), data: { confirm: t('views.registrations.are_you_sure') }, method: :delete, class: 'btn btn-danger pull-right' diff --git a/app/views/devise/registrations/new.html.slim b/app/views/devise/registrations/new.html.slim index bbcf31ff..b6f89698 100644 --- a/app/views/devise/registrations/new.html.slim +++ b/app/views/devise/registrations/new.html.slim @@ -2,18 +2,21 @@ section.container .row .centered_form - = simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| + = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: 'form-horizontal' }) do |f| header legend h3 = t 'views.registrations.sign_up' = f.error_notification .form-inputs - = f.input :username, required: true, autofocus: true - = f.input :first_name, required: true, autofocus: true - = f.input :last_name, required: true, autofocus: true - = f.input :email, required: true, autofocus: true - = f.input :password, required: true, hint: (t("views.registrations.minimum_characters", minimum: @minimum_password_length) if @validatable) - = f.input :password_confirmation, required: true + .row + .col-6 = f.input :email, required: true, autofocus: true + .col-6 = f.input :username, required: true, autofocus: true + .row + .col-6 = f.input :first_name, required: true, autofocus: true + .col-6 = f.input :last_name, required: true, autofocus: true + .row + .col-6 = f.input :password, required: true, hint: (t("views.registrations.minimum_characters", minimum: @minimum_password_length) if @validatable) + .col-6 = f.input :password_confirmation, required: true .form-actions = f.button :submit, t('views.registrations.sign_me_up') = render "devise/shared/links" diff --git a/app/views/devise/sessions/new.html.slim b/app/views/devise/sessions/new.html.slim index 5616b329..903f40a7 100644 --- a/app/views/devise/sessions/new.html.slim +++ b/app/views/devise/sessions/new.html.slim @@ -1,6 +1,6 @@ - content_for(:title) { t 'views.sessions.sign_in_heading' } section.container - .row + .row.standalone-form .centered_form = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| header @@ -14,3 +14,4 @@ section.container .form-actions = f.button :submit, t('views.sessions.sign_in') = render "devise/shared/links" +f \ No newline at end of file diff --git a/app/views/doorkeeper/applications/_application.html.erb b/app/views/doorkeeper/applications/_application.html.erb index 0c11e088..3e9ac8b4 100644 --- a/app/views/doorkeeper/applications/_application.html.erb +++ b/app/views/doorkeeper/applications/_application.html.erb @@ -3,8 +3,8 @@ <%= application.redirect_uri %>
- <%= link_to icon(:pencil), edit_oauth_application_path(application), class: 'edit', alt: t('doorkeeper.applications.buttons.edit') %> - <%= link_to icon(:times), oauth_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.applications.confirmations.destroy')} %> + <%= link_to icon('fa-solid', :pencil), edit_oauth_application_path(application), class: 'edit', alt: t('doorkeeper.applications.buttons.edit') %> + <%= link_to icon('fa-solid', :times), oauth_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.applications.confirmations.destroy')} %>
diff --git a/app/views/doorkeeper/authorized_applications/_application.html.slim b/app/views/doorkeeper/authorized_applications/_application.html.slim index a5c14d08..c72d42de 100644 --- a/app/views/doorkeeper/authorized_applications/_application.html.slim +++ b/app/views/doorkeeper/authorized_applications/_application.html.slim @@ -3,4 +3,4 @@ tr td = l application.created_at, format: :long td.text-right .record-actions - = link_to icon(:times), oauth_authorized_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.authorized_applications.confirmations.revoke')}, title: t('doorkeeper.authorized_applications.buttons.revoke') + = link_to icon('fa-solid', :times), oauth_authorized_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.authorized_applications.confirmations.revoke')}, title: t('doorkeeper.authorized_applications.buttons.revoke') diff --git a/app/views/fauna/role_assignments/_form.html.slim b/app/views/fauna/role_assignments/_form.html.slim new file mode 100644 index 00000000..6e9f1e7a --- /dev/null +++ b/app/views/fauna/role_assignments/_form.html.slim @@ -0,0 +1,12 @@ += form_for @user.user_roles.new, url: fauna_user_role_assignments_path, html: { class: 'row m-0 p-0 row-cols-xs-auto g-3 align-items-end' } do |f| + .col-2 + = f.label :role, class: 'form-label fw-bold' + = f.select :role, Role.all.map { |r| [r.localized_name.to_s, r.id] }, {}, { required: true, selected: nil, class: 'form-select' } + .col-4 + = f.label :start_time, class: 'form-label fw-bold' + = f.datetime_field :start_time, as: :date, html5: true, class: 'form-control', value: Time.current + .col-4 + = f.label :end_time, class: 'form-label fw-bold' + = f.datetime_field :end_time, as: :date, html5: true, class: 'form-control' + .col-2.d-grid + = f.button :submit, class: 'btn btn-success' diff --git a/app/views/fauna/role_assignments/_role_assignments.html.slim b/app/views/fauna/role_assignments/_role_assignments.html.slim deleted file mode 100644 index aa0db602..00000000 --- a/app/views/fauna/role_assignments/_role_assignments.html.slim +++ /dev/null @@ -1,8 +0,0 @@ -.role-assignments id="role-assignments-#{user.id}" - - Role.predefined.each do |predefined_role| - - if user.has_role? predefined_role.name - = link_to role_assignment_fauna_user_role_assignments_path(user.id, predefined_role.name), method: :delete, remote: true, class: 'btn btn-sm btn-warning', data: {disable: true, confirm: t('views.roles.are_you_sure')} do - = icon 'check-square-o', predefined_role.localized_name - - else - = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do - = icon 'square-o', predefined_role.localized_name diff --git a/app/views/fauna/role_assignments/_temporary_roles.html.slim b/app/views/fauna/role_assignments/_temporary_roles.html.slim new file mode 100644 index 00000000..f93e85df --- /dev/null +++ b/app/views/fauna/role_assignments/_temporary_roles.html.slim @@ -0,0 +1,84 @@ +.timed-roles id="timed-roles-#{user.id}" + button.btn.btn-sm.btn-primary[type='button' data-bs-toggle='modal' data-bs-target="#modal-timed-roles-#{user.id}"] + = icon('fa-solid', 'calendar') + + .modal.modal-lg.fade[tabindex="-1" id="modal-timed-roles-#{user.id}" role="dialog" aria-hidden="true"] + .modal-dialog[role="document"] + .modal-content + .modal-header + h5.modal-title = "#{User.human_attribute_name(:roles)} - #{user.name}" + div.px-4 + button.btn.btn-success + = t('views.roles.add', model: Role.model_name.human) + button.btn-close[data-bs-dismiss="modal" aria-bs-label="Close"] + .modal-body + + + .fw-bold = t('views.roles.current') + div.d-table + div.d-table-row + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:role) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:start_time) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:end_time) + + - user.user_roles.order(end_time: :desc).each do |ur| + div.d-table-row + div.d-table-cell.px-2 + = ur.role.localized_name + div.d-table-cell.px-2 + - if !ur.start_time.nil? + = ur.start_time + div.d-table-cell.px-2 + - if !ur.end_time.nil? + = ur.end_time + div.d-table-cell.px-2 + button.btn.btn-danger.btn-sm + = icon('fa-solid', :close) + hr + .fw-bold = t('views.roles.future') + div.d-table + div.d-table-row + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:role) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:start_time) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:end_time) + + - user.user_roles.future.order(end_time: :desc).each do |ur| + div.d-table-row + div.d-table-cell.px-2 + = ur.role.localized_name + div.d-table-cell.px-2 + - if !ur.start_time.nil? + = ur.start_time + div.d-table-cell.px-2 + - if !ur.end_time.nil? + = ur.end_time + div.d-table-cell.px-2 + button.btn.btn-danger.btn-sm + = icon('fa-solid', :close) + hr + .fw-bold = t('views.roles.expired') + div.d-table + div.d-table-row + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:role) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:start_time) + div.d-table-cell.px-2.font-weight-bold + = UserRole.human_attribute_name(:end_time) + + - user.user_roles.expired.order(end_time: :desc).each do |ur| + div.d-table-row + div.d-table-cell.px-2 + = ur.role.localized_name + div.d-table-cell.px-2 + - if !ur.start_time.nil? + = ur.start_time + div.d-table-cell.px-2 + - if !ur.end_time.nil? + = ur.end_time diff --git a/app/views/fauna/role_assignments/index.html.slim b/app/views/fauna/role_assignments/index.html.slim new file mode 100644 index 00000000..fc02ca28 --- /dev/null +++ b/app/views/fauna/role_assignments/index.html.slim @@ -0,0 +1,74 @@ +- content_for(:title) { "#{User.human_attribute_name(:roles)} - #{@user.name}" } +/ - content_for(:page_actions) do +/ = link_to new_fauna_user_role_assignment_path, class: %w[btn btn-success] do +/ => icon('fa-solid', :plus) +/ = t 'views.roles.add' + +section.container + header.row + .col + header.page-header + .page-actions + = yield :page_actions + h2 = "#{User.human_attribute_name(:roles)} - #{@user.name}" + + .row.my-3 + .col.pe-0 + h4 = t 'views.roles.add' + = render partial: 'form' + .row.my-3 + .col + h4 = t 'views.roles.current' + table.table.table-hover.table-sm.m-2.table-responsive + thead + tr + th.col-2[scope='col'] = UserRole.human_attribute_name :role + th.col-4[scope='col'] = UserRole.human_attribute_name :start_time + th.col-4[scope='col'] = UserRole.human_attribute_name :end_time + th.col-2 + tbody + - @user.user_roles.order(end_time: :desc).each do |ur| + tr + td = ur.role.localized_name + td = ur.start_time.localtime + td = ur.end_time&.localtime + td + = form_tag fauna_user_role_assignment_path(id: ur.role_id), method: :delete, class: 'd-grid' do + = hidden_field_tag :start_time, value: ur.start_time + = button_tag icon('fa-solid', :close, t('views.roles.deactivate')), class: 'btn btn-danger btn-sm m-0' + hr + h4 = t 'views.roles.future' + table.table.table-hover.table-sm.m-2.table-responsive + thead + tr + th.col-2[scope='col'] = UserRole.human_attribute_name :role + th.col-4[scope='col'] = UserRole.human_attribute_name :start_time + th.col-4[scope='col'] = UserRole.human_attribute_name :end_time + th.col-2 + tbody + - @user.user_roles.future.order(end_time: :desc).each do |ur| + tr + td = ur.role.localized_name + td = ur.start_time.localtime + td = ur.end_time&.localtime + td + = form_tag fauna_user_role_assignment_path(id: ur.role_id), method: :delete, class: 'd-grid' do + = hidden_field_tag :start_time, value: ur.start_time + = button_tag icon('fa-solid', :trash, t('views.roles.cancel')), class: 'btn btn-danger btn-sm m-0' + hr + h4 = t 'views.roles.expired' + table.table.table-hover.table-sm.m-2.table-responsive + thead + tr + th.col-2[scope='col'] = UserRole.human_attribute_name :role + th.col-4[scope='col'] = UserRole.human_attribute_name :start_time + th.col-4[scope='col'] = UserRole.human_attribute_name :end_time + th.col-2 + + tbody + - @user.user_roles.expired.order(end_time: :desc).each do |ur| + tr + td = ur.role.localized_name + td = ur.start_time.localtime + td = ur.end_time&.localtime + td \ No newline at end of file diff --git a/app/views/fauna/role_assignments/new.html.slim b/app/views/fauna/role_assignments/new.html.slim new file mode 100644 index 00000000..96a05bea --- /dev/null +++ b/app/views/fauna/role_assignments/new.html.slim @@ -0,0 +1,4 @@ +- content_for(:title) { t 'views.roles.add' } +section.container + .row + = render partial: 'form' diff --git a/app/views/fauna/role_assignments/refresh.js.erb b/app/views/fauna/role_assignments/refresh.js.erb deleted file mode 100644 index a6382ed4..00000000 --- a/app/views/fauna/role_assignments/refresh.js.erb +++ /dev/null @@ -1 +0,0 @@ -$("#role-assignments-<%= @user.id %>").replaceWith("<%= j render partial: 'role_assignments', locals: {user: @user} %>"); diff --git a/app/views/fauna/users/_user.html.slim b/app/views/fauna/users/_user.html.slim index 79e51048..90247c2d 100644 --- a/app/views/fauna/users/_user.html.slim +++ b/app/views/fauna/users/_user.html.slim @@ -2,5 +2,34 @@ tr td = user.name td = user.username td = user.email - td.roles_column - = render partial: '/fauna/role_assignments/role_assignments', locals: {user: user} + td.d-flex + ul.list-group.list-group-horizontal-md.align-items-center.border-primary.small + - Role.predefined.each do |predefined_role| + - if user.role? predefined_role.name + / TODO: hidden form here + / = button_to fauna_user_role_assignments_path(user_id: user.id, id: predefined_role.id), method: :delete, remote: true, class: 'btn btn-sm btn-warning', data: {disable: true, confirm: t('views.roles.are_you_sure')} do + / button.btn.btn-sm.btn-warning.disabled + li.list-group-item.list-group-item-success.px-1 + - if user.user_roles.find_by(role: predefined_role.id)&.end_time.nil? + => icon('fa-solid', 'square-check') + - else + => icon('fa-solid', :calendar, html_options: { title: user.user_roles.find_by(role: predefined_role.id)&.end_time }) + = predefined_role.localized_name + + - elsif user.user_roles.future.find_by(role: predefined_role.id) + / = link_to fauna_user_role_assignments_path(user.id, role: {name: predefined_role.name}), method: :post, remote: true, class: 'btn btn-sm btn-default', data: {disable: true, confirm: t('views.roles.are_you_sure')} do + / button.btn.btn-sm.btn-default.disabled + li.list-group-item.list-group-item-warning.px-1 + => icon('fa-regular', :calendar, html_options: { title: user.user_roles.unscoped.future.find_by(role: predefined_role.id).end_time }) + = predefined_role.localized_name + - else + li.list-group-item.px-1 + => icon('fa-regular', :square) + = predefined_role.localized_name + .d-inline.m-2 + = link_to fauna_user_role_assignments_path(user.id), :class => 'btn btn-primary btn-sm' do + => icon('fa-solid', :pencil) + / = t 'views.navigation.view_edit' + / button.btn.btn-sm.btn-primary[type='button' data-bs-toggle='modal' data-bs-target="#modal-timed-roles-#{user.id}"] + = icon('fa-solid', :pencil) + / = render partial: '/fauna/role_assignments/temporary_roles', locals: {user: user} diff --git a/app/views/fauna/users/index.html.slim b/app/views/fauna/users/index.html.slim index 9f125a9e..7b70fbca 100644 --- a/app/views/fauna/users/index.html.slim +++ b/app/views/fauna/users/index.html.slim @@ -7,12 +7,12 @@ section.container .users .row .col-sm-12 - table + table.table.table-hover.table-sm.m-2.table-responsive thead tr - th = User.human_attribute_name(:name) - th = User.human_attribute_name(:username) - th = User.human_attribute_name(:email) - th.roles_column = User.human_attribute_name(:roles) + th.col-2[scope='col'] = User.human_attribute_name(:name) + th.col-2[scope='col'] = User.human_attribute_name(:username) + th.col-2[scope='col'] = User.human_attribute_name(:email) + th.col-6[scope='col'] = User.human_attribute_name(:roles) tbody#user_table = render @users diff --git a/app/views/layouts/_deprecated.html.slim b/app/views/layouts/_deprecated.html.slim index cc306c54..5731a0b4 100644 --- a/app/views/layouts/_deprecated.html.slim +++ b/app/views/layouts/_deprecated.html.slim @@ -1,3 +1,5 @@ -.alert.alert-info - = t 'views.deprecated.message' - =< link_to 'https://space.initlab.org/', 'https://space.initlab.org/' +.card.border-info + .card-body + .card-text + = t 'views.deprecated.message' + =< link_to 'https://space.initlab.org/', 'https://space.initlab.org/' diff --git a/app/views/layouts/_flash.html.slim b/app/views/layouts/_flash.html.slim index 0e44ab16..2d23d4f6 100644 --- a/app/views/layouts/_flash.html.slim +++ b/app/views/layouts/_flash.html.slim @@ -1,4 +1,4 @@ -.container - .row - .flash_message +- if !flash.empty? + .container + .row.my-4 = bootstrap_flash diff --git a/app/views/layouts/_footer.html.slim b/app/views/layouts/_footer.html.slim index 0c623b45..01287402 100644 --- a/app/views/layouts/_footer.html.slim +++ b/app/views/layouts/_footer.html.slim @@ -2,10 +2,10 @@ footer.footer .container .row div.col-xs-12.col-sm-6 - p + p.my-4 = t('views.footer.license') =< link_to 'MIT License', 'https://github.com/initLab/fauna/blob/master/LICENSE' div.col-xs-12.col-sm-6 - p.right + p.right.my-4 = t('views.footer.source_code') =< link_to 'Github', 'https://github.com/initLab/fauna/' diff --git a/app/views/layouts/_navigation.html.slim b/app/views/layouts/_navigation.html.slim index 0af4cebe..cfb01fa1 100644 --- a/app/views/layouts/_navigation.html.slim +++ b/app/views/layouts/_navigation.html.slim @@ -1,47 +1,43 @@ -nav class="navbar navbar-static-top #{content_for?(:nav_color) ? yield(:nav_color) : 'navbar-inverse'}" +nav class="py-0 navbar navbar-expand-lg #{content_for?(:nav_color) ? yield(:nav_color) : 'navbar-dark bg-primary'}" .container - .navbar-header - button.navbar-toggle data-target=".navbar-inverse-collapse" data-toggle="collapse" type="button" - span.icon-bar - span.icon-bar - span.icon-bar - = link_to root_url, class: "navbar-brand #{'active' if current_page?(root_path)}" do - = image_tag 'logo.svg', class: 'navbar-brand', alt: 'Fauna' - .navbar-collapse.collapse.navbar-inverse-collapse - ul.nav.navbar-nav + = link_to root_url, class: "navbar-brand", active: false do + = image_tag 'initlab-logo.svg', class: 'logo', height: '47px', alt: 'Fauna' + button.navbar-toggler data-target="#navbar-collapse" data-toggle="collapse" type="button" + span.navbar-toggler-icon + + .navbar-collapse.collapse#navbar-collapse + .navbar-nav.flex-grow-1 - if user_signed_in? and UserPolicy.new(current_user, nil).index? - li class="#{'active' if current_page?(fauna_users_path)}" - = link_to fauna_users_path do - = icon 'users', t('views.navigation.labbers') - ul.nav.navbar-nav.navbar-right + = link_to fauna_users_path, class: "nav-link #{'active' if current_page?(fauna_users_path)}" do + => icon('fa-solid', :users) + = t('views.navigation.labbers') + .navbar-nav.navbar-right - unless user_signed_in? - li.dropdown - = link_to '#', class: 'dropdown-toggle', data: {toggle: 'dropdown'} do - => icon 'flag' + .nav-item.dropdown + button.dropdown-toggle.nav-link#langdropdown type='button' data-bs-toggle='dropdown' aria-haspopup='true' aria-expanded='false' + => icon('fa-solid', :flag) = {bg: "БГ", en: "EN"}[I18n.locale] span.caret< - ul.dropdown-menu role="menu" + .dropdown-menu aria-labelledby="langdropdown" - I18n.available_locales.each do |locale| - li.text-center - = link_to({bg: "БГ", en: "EN"}[locale], "?locale=#{locale}") + = link_to({bg: "БГ", en: "EN"}[locale], "?locale=#{locale}", class: 'dropdown-item') - if user_signed_in? - li.dropdown class="#{'active' if current_page?(edit_user_registration_path) || current_page?(user_network_devices_path)}" - a.dropdown-toggle data-toggle="dropdown" href="#" - = icon 'user', t('views.navigation.account') + .nav-item.dropdown + / a.nav-link.dropdown class="#{'active' if current_page?(edit_user_registration_path) || current_page?(user_network_devices_path)}" + button.dropdown-toggle.nav-link#accountdropdown type='button' data-bs-toggle='dropdown' aria-haspopup='true' aria-expanded='false' + / a.dropdown-toggle data-toggle="dropdown" href="#" + => icon('fa-solid', :user) + = t('views.navigation.account') b.caret< - ul.dropdown-menu - li - = link_to t('views.navigation.view_edit'), edit_user_registration_path - li - = link_to t('views.navigation.network_devices'), user_network_devices_path - li - = link_to t('views.navigation.oauth_application_management'), oauth_applications_path - li - = link_to t('views.navigation.oauth_token_management'), oauth_authorized_applications_path - li.divider - li - = link_to t('views.navigation.sign_out'), destroy_user_session_path, method: :delete + .dropdown-menu aria-labelledby="accountdropdown" + = link_to t('views.navigation.view_edit'), edit_user_registration_path, class: 'dropdown-item' + = link_to t('views.navigation.network_devices'), user_network_devices_path, class: 'dropdown-item' + = link_to t('views.navigation.oauth_application_management'), oauth_applications_path, class: 'dropdown-item' + = link_to t('views.navigation.oauth_token_management'), oauth_authorized_applications_path, class: 'dropdown-item' + hr.dropdown-divider role="separator" + = link_to t('views.navigation.sign_out'), destroy_user_session_path, method: :delete, class: 'dropdown-item' - else - li class="#{'active' if controller_name == 'sessions'}" - = link_to new_user_session_path do - = icon 'sign-in', t('views.navigation.sign_in') + a.nav-link + = link_to new_user_session_path, class: "nav-link #{'active' if controller_name == 'sessions'}" do + => icon('fa-solid', 'sign-in') + = t('views.navigation.sign_in') diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 017fdb53..8b6a2174 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -16,13 +16,14 @@ html lang="#{I18n.locale}" = csrf_meta_tags = csp_meta_tag - = stylesheet_link_tag 'application', :media => 'all', 'data-turbolinks-track' => 'reload', :nonce => content_security_policy_nonce, :integrity => true - = javascript_include_tag 'application', 'data-turbolinks-track' => 'reload', :nonce => content_security_policy_nonce, :integrity => true + = stylesheet_link_tag 'application', :media => 'all', :nonce => content_security_policy_nonce, :integrity => true + = javascript_include_tag 'application', :nonce => content_security_policy_nonce, :integrity => true link rel="manifest" href="/manifest.json" body - = render partial: 'layouts/navigation' - = render partial: 'layouts/flash' - main + header + = render partial: 'layouts/navigation' + main.mt-4 + = render partial: 'layouts/flash' == yield = render partial: 'layouts/footer' diff --git a/app/views/network_devices/_network_device.html.slim b/app/views/network_devices/_network_device.html.slim index ae563ead..e4536b8f 100644 --- a/app/views/network_devices/_network_device.html.slim +++ b/app/views/network_devices/_network_device.html.slim @@ -3,5 +3,5 @@ tr td = network_device.mac_address td.text-right .record-actions - => link_to icon(:pencil), edit_user_network_device_path(network_device), class: 'edit' - = link_to icon(:times), user_network_device_path(network_device), method: :delete, class: 'delete', data: {confirm: t('views.network_devices.are_you_sure')} + => link_to icon('fa-solid', :pencil), edit_user_network_device_path(network_device), class: 'edit' + = link_to icon('fa-solid', :times), user_network_device_path(network_device), method: :delete, class: 'delete', data: {confirm: t('views.network_devices.are_you_sure')} diff --git a/app/views/network_devices/index.html.slim b/app/views/network_devices/index.html.slim index e62602ff..875cdf17 100644 --- a/app/views/network_devices/index.html.slim +++ b/app/views/network_devices/index.html.slim @@ -1,7 +1,7 @@ - content_for(:title) { t 'views.network_devices.network_devices' } - content_for(:page_actions) do = link_to new_user_network_device_path, class: ['btn', 'btn-primary'] do - => icon :plus + => icon('fa-solid', :plus) = t('.new_network_device') section.container diff --git a/app/views/oauth/applications/_application.html.slim b/app/views/oauth/applications/_application.html.slim index a96f4af1..8d7f5ce1 100644 --- a/app/views/oauth/applications/_application.html.slim +++ b/app/views/oauth/applications/_application.html.slim @@ -5,5 +5,5 @@ tr id="application_#{application.id}" = application.redirect_uri td.text-right .record-actions - => link_to icon(:pencil), edit_oauth_application_path(application), class: 'edit', title: t('doorkeeper.applications.buttons.edit') - = link_to icon(:times), oauth_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.applications.confirmations.destroy')}, title: t('doorkeeper.applications.buttons.destroy') + => link_to icon('fa-solid', :pencil), edit_oauth_application_path(application), class: 'edit', title: t('doorkeeper.applications.buttons.edit') + = link_to icon('fa-solid', :times), oauth_application_path(application), method: :delete, class: 'delete', data: {confirm: t('doorkeeper.applications.confirmations.destroy')}, title: t('doorkeeper.applications.buttons.destroy') diff --git a/app/views/oauth/applications/index.html.slim b/app/views/oauth/applications/index.html.slim index c6009d20..e6bcd142 100644 --- a/app/views/oauth/applications/index.html.slim +++ b/app/views/oauth/applications/index.html.slim @@ -1,7 +1,7 @@ - content_for(:title) { t 'doorkeeper.applications.index.title' } - content_for(:page_actions) do = link_to new_oauth_application_path, class: ['btn', 'btn-primary'] do - => icon :plus + => icon('fa-solid', :plus) = t('doorkeeper.applications.index.new') section.container diff --git a/app/views/oauth/applications/show.html.slim b/app/views/oauth/applications/show.html.slim index 9cff02fa..248d7782 100644 --- a/app/views/oauth/applications/show.html.slim +++ b/app/views/oauth/applications/show.html.slim @@ -1,10 +1,10 @@ - content_for(:title) { t('doorkeeper.applications.show.title', name: @application.name) } - content_for(:page_actions) do = link_to edit_oauth_application_path(@application), class: ['btn', 'btn-primary'] do - => icon(:pencil) + => icon('fa-solid', :pencil) = t('doorkeeper.applications.buttons.edit') = link_to oauth_application_path(@application), method: :delete, class: ['btn', 'btn-danger'], data: {confirm: t('doorkeeper.applications.confirmations.destroy')} do - => icon(:times) + => icon('fa-solid', :times) = t('doorkeeper.applications.buttons.destroy') section.container diff --git a/config/application.rb b/config/application.rb index f623aad2..c0542241 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,7 +9,7 @@ module Fauna class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.2 + config.load_defaults 8.1 # Configuration for the application, engines, and railties goes here. # @@ -25,7 +25,7 @@ class Application < Rails::Application config.i18n.enforce_available_locales = true config.i18n.default_locale = :bg - config.action_view.field_error_proc = proc do |html_tag, instance| + config.action_view.field_error_proc = proc do |html_tag, _instance| html_tag end @@ -46,5 +46,7 @@ class Application < Rails::Application end config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden + + config.sass.quiet_deps = true end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 285ee8b8..7d3837fa 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -44,7 +44,7 @@ # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true +# config.force_ssl = true # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). @@ -69,10 +69,10 @@ # SMTP is default # config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { - address: Rails.application.secrets.SMTP_ADDRESS, + address: Rails.application.credentials&.smtp&.host, port: 465, - user_name: Rails.application.secrets.SMTP_USER_NAME, - password: Rails.application.secrets.SMTP_PASSWORD, + user_name: Rails.application.credentials&.smtp&.user, + password: Rails.application.credentials&.smtp&.password, ssl: true, } diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 83ff64c1..cc5cfcae 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -41,12 +41,12 @@ # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [:email] + config.case_insensitive_keys = [:email, :username] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [:email] + config.strip_whitespace_keys = [:email, :username] # Tell if authentication through request.params is enabled. True by default. # It can be set to an array that will enable params authentication only for the diff --git a/config/initializers/rolify.rb b/config/initializers/rolify.rb deleted file mode 100644 index 4a17233e..00000000 --- a/config/initializers/rolify.rb +++ /dev/null @@ -1,7 +0,0 @@ -Rolify.configure do |config| - # By default ORM adapter is ActiveRecord. uncomment to use mongoid - # config.use_mongoid - - # Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false - # config.use_dynamic_shortcuts -end diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index 92935e95..d2687848 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -1,3 +1,11 @@ +# frozen_string_literal: true +# +# Uncomment this and change the path if necessary to include your own +# components. +# See https://github.com/heartcombo/simple_form#custom-components to know +# more about custom components. +# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } +# # Use this setup block to configure all options available in SimpleForm. SimpleForm.setup do |config| # Wrappers are used by the form builder to generate a @@ -6,7 +14,7 @@ # stack. The options given below are used to wrap the # whole input. config.wrappers :default, class: :input, - hint_class: :field_with_hint, error_class: :field_with_errors do |b| + hint_class: :field_with_hint, error_class: :field_with_errors, valid_class: :field_without_errors do |b| ## Extensions enabled by default # Any of these extensions can be disabled for a # given input by passing: `f.input EXTENSION_NAME => false`. @@ -28,8 +36,12 @@ # extensions by default, you can change `b.optional` to `b.use`. # Calculates maxlength from length validations for string inputs + # and/or database column lengths b.optional :maxlength + # Calculate minlength from length validations for string inputs + b.optional :minlength + # Calculates pattern from format validations for string inputs b.optional :pattern @@ -40,9 +52,10 @@ b.optional :readonly ## Inputs + # b.use :input, class: 'input', error_class: 'is-invalid', valid_class: 'is-valid' b.use :label_input - b.use :hint, wrap_with: {tag: :span, class: :hint} - b.use :error, wrap_with: {tag: :span, class: :error} + b.use :hint, wrap_with: { tag: :span, class: :hint } + b.use :error, wrap_with: { tag: :span, class: :error } ## full_messages_for # If you want to display the full error message for the attribute, you can @@ -61,7 +74,7 @@ config.boolean_style = :nested # Default class for buttons - config.button_class = "btn" + config.button_class = 'btn' # Method used to tidy up errors. Specify any Rails Array method. # :first lists the first message for each field. @@ -72,10 +85,7 @@ config.error_notification_tag = :div # CSS class to add for error notification helper. - config.error_notification_class = "error_notification" - - # ID to add for error notification helper. - # config.error_notification_id = nil + config.error_notification_class = 'error_notification' # Series of attempts to detect a default label method for collection. # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] @@ -90,8 +100,7 @@ # config.collection_wrapper_class = nil # You can wrap each item in a collection of radio/check boxes with a tag, - # defaulting to :span. Please note that when using :boolean_style = :nested, - # SimpleForm will force this option to be a label. + # defaulting to :span. # config.item_wrapper_tag = :span # You can define a class to use in all item wrappers. Defaulting to none. @@ -103,7 +112,7 @@ # You can define the class to use on all labels. Default is nil. # config.label_class = nil - # You can define the default class to be used on forms. Can be overriden + # You can define the default class to be used on forms. Can be overridden # with `html: { :class }`. Defaulting to none. # config.default_form_class = nil @@ -120,9 +129,6 @@ # change this configuration to true. config.browser_validations = false - # Collection of methods to detect if a file type was given. - # config.file_methods = [ :mounted_as, :file?, :public_filename ] - # Custom mappings for input types. This should be a hash containing a regexp # to match as key, and the input type that will be used when the field name # matches the regexp as value. @@ -155,7 +161,7 @@ # config.input_class = nil # Define the default class of the input wrapper of the boolean input. - config.boolean_label_class = "checkbox" + config.boolean_label_class = 'checkbox' # Defines if the default input wrapper class should be included in radio # collection wrappers. @@ -163,4 +169,8 @@ # Defines which i18n scope will be used in Simple Form. # config.i18n_scope = 'simple_form' + + # Defines validation classes to the input_field. By default it's nil. + # config.input_field_valid_class = 'is-valid' + # config.input_field_error_class = 'is-invalid' end diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb index e0fca33f..89a0b551 100644 --- a/config/initializers/simple_form_bootstrap.rb +++ b/config/initializers/simple_form_bootstrap.rb @@ -1,136 +1,372 @@ +# frozen_string_literal: true + +# These defaults are defined and maintained by the community at +# https://github.com/heartcombo/simple_form-bootstrap +# Please submit feedback, changes and tests only there. + +# Uncomment this and change the path if necessary to include your own +# components. +# See https://github.com/heartcombo/simple_form#custom-components +# to know more about custom components. +# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } + # Use this setup block to configure all options available in SimpleForm. SimpleForm.setup do |config| - config.error_notification_class = "alert alert-danger" - config.button_class = "btn btn-default" - config.boolean_label_class = nil + # Default class for buttons + config.button_class = 'btn btn-primary' + + # Define the default class of the input wrapper of the boolean input. + config.boolean_label_class = 'form-check-label' + + # How the label text should be generated altogether with the required text. + config.label_text = ->(label, required, _explicit_label) { "#{label} #{required}" } + + # Define the way to render check boxes / radio buttons with labels. + config.boolean_style = :inline + + # You can wrap each item in a collection of radio/check boxes with a tag + config.item_wrapper_tag = :div + + # Defines if the default input wrapper class should be included in radio + # collection wrappers. + config.include_default_input_wrapper_class = false + + # CSS class to add for error notification helper. + config.error_notification_class = 'card card-body border-danger text-danger md-6 mb-1' + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # :to_sentence to list all errors for each field. + config.error_method = :to_sentence + + # add validation classes to `input_field` + config.input_field_error_class = 'is-invalid' + config.input_field_valid_class = 'is-valid' - config.wrappers :vertical_form, tag: "div", class: "form-group", error_class: "has-error" do |b| + + # vertical forms + # + # vertical default_wrapper + config.wrappers :vertical_form, class: 'mb-3' do |b| b.use :html5 b.use :placeholder b.optional :maxlength + b.optional :minlength b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: "control-label" + b.use :label, class: 'form-label' + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end + + # vertical input for boolean + config.wrappers :vertical_boolean, tag: 'fieldset', class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :form_check_wrapper, class: 'form-check' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :full_error, wrap_with: { class: 'invalid-feedback' } + bb.use :hint, wrap_with: { class: 'form-text' } + end + end + + # vertical input for radio buttons and check boxes + config.wrappers :vertical_collection, item_wrapper_class: 'form-check', item_label_class: 'form-check-label', tag: 'fieldset', class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| + ba.use :label_text + end + b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + b.use :hint, wrap_with: { class: 'form-text' } + end - b.use :input, class: "form-control" - b.use :error, wrap_with: {tag: "span", class: "help-block"} - b.use :hint, wrap_with: {tag: "p", class: "help-block"} + # vertical input for inline radio buttons and check boxes + config.wrappers :vertical_collection_inline, item_wrapper_class: 'form-check form-check-inline', item_label_class: 'form-check-label', tag: 'fieldset', class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| + ba.use :label_text + end + b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + b.use :hint, wrap_with: { class: 'form-text' } end - config.wrappers :vertical_file_input, tag: "div", class: "form-group", error_class: "has-error" do |b| + # vertical file input + config.wrappers :vertical_file, class: 'mb-3' do |b| b.use :html5 b.use :placeholder b.optional :maxlength + b.optional :minlength b.optional :readonly - b.use :label, class: "control-label" - - b.use :input - b.use :error, wrap_with: {tag: "span", class: "help-block"} - b.use :hint, wrap_with: {tag: "p", class: "help-block"} + b.use :label, class: 'form-label' + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } end - config.wrappers :vertical_boolean, tag: "div", class: "form-group", error_class: "has-error" do |b| + # vertical select input + config.wrappers :vertical_select, class: 'mb-3' do |b| b.use :html5 b.optional :readonly + b.use :label, class: 'form-label' + b.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end - b.wrapper tag: "div", class: "checkbox" do |ba| - ba.use :label_input + # vertical multi select + config.wrappers :vertical_multi_select, class: 'mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'form-label' + b.wrapper class: 'd-flex flex-row justify-content-between align-items-center' do |ba| + ba.use :input, class: 'form-select mx-1', error_class: 'is-invalid', valid_class: 'is-valid' end - - b.use :error, wrap_with: {tag: "span", class: "help-block"} - b.use :hint, wrap_with: {tag: "p", class: "help-block"} + b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + b.use :hint, wrap_with: { class: 'form-text' } end - config.wrappers :vertical_radio_and_checkboxes, tag: "div", class: "form-group", error_class: "has-error" do |b| + # vertical range input + config.wrappers :vertical_range, class: 'mb-3' do |b| b.use :html5 + b.use :placeholder b.optional :readonly - b.use :label, class: "control-label" - b.use :input - b.use :error, wrap_with: {tag: "span", class: "help-block"} - b.use :hint, wrap_with: {tag: "p", class: "help-block"} + b.optional :step + b.use :label, class: 'form-label' + b.use :input, class: 'form-range', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } end - config.wrappers :horizontal_form, tag: "div", class: "form-group", error_class: "has-error" do |b| + + # horizontal forms + # + # horizontal default_wrapper + config.wrappers :horizontal_form, class: 'row mb-3' do |b| b.use :html5 b.use :placeholder b.optional :maxlength + b.optional :minlength b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: "col-sm-3 control-label" + b.use :label, class: 'col-sm-3 col-md-2 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| + ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # horizontal input for boolean + config.wrappers :horizontal_boolean, class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10 offset-sm-3' do |wr| + wr.wrapper :form_check_wrapper, class: 'form-check' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :full_error, wrap_with: { class: 'invalid-feedback' } + bb.use :hint, wrap_with: { class: 'form-text' } + end + end + end + + # horizontal input for radio buttons and check boxes + config.wrappers :horizontal_collection, item_wrapper_class: 'form-check', item_label_class: 'form-check-label', class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'col-sm-3 col-md-2 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| + ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end - b.wrapper tag: "div", class: "col-sm-9" do |ba| - ba.use :input, class: "form-control" - ba.use :error, wrap_with: {tag: "span", class: "help-block"} - ba.use :hint, wrap_with: {tag: "p", class: "help-block"} + # horizontal input for inline radio buttons and check boxes + config.wrappers :horizontal_collection_inline, item_wrapper_class: 'form-check form-check-inline', item_label_class: 'form-check-label', class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'col-sm-3 col-md-2 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| + ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + ba.use :hint, wrap_with: { class: 'form-text' } end end - config.wrappers :horizontal_file_input, tag: "div", class: "form-group", error_class: "has-error" do |b| + # horizontal file input + config.wrappers :horizontal_file, class: 'row mb-3' do |b| b.use :html5 b.use :placeholder b.optional :maxlength + b.optional :minlength b.optional :readonly - b.use :label, class: "col-sm-3 control-label" - - b.wrapper tag: "div", class: "col-sm-9" do |ba| - ba.use :input - ba.use :error, wrap_with: {tag: "span", class: "help-block"} - ba.use :hint, wrap_with: {tag: "p", class: "help-block"} + b.use :label, class: 'col-sm-3 col-md-2 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| + ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } end end - config.wrappers :horizontal_boolean, tag: "div", class: "form-group", error_class: "has-error" do |b| + # horizontal select input + config.wrappers :horizontal_select, class: 'row mb-3' do |b| b.use :html5 b.optional :readonly + b.use :label, class: 'col-sm-3 col-md-2 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| + ba.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end - b.wrapper tag: "div", class: "col-sm-offset-3 col-sm-9" do |wr| - wr.wrapper tag: "div", class: "checkbox" do |ba| - ba.use :label_input, class: "col-sm-9" + # horizontal multi select + config.wrappers :horizontal_multi_select, class: 'row mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'col-sm-3 col-md-2 col-form-label' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| + ba.wrapper class: 'd-flex flex-row justify-content-between align-items-center' do |bb| + bb.use :input, class: 'form-select mx-1', error_class: 'is-invalid', valid_class: 'is-valid' end + ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + # horizontal range input + config.wrappers :horizontal_range, class: 'row mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :readonly + b.optional :step + b.use :label, class: 'col-sm-3 col-md-2 col-form-label pt-0' + b.wrapper :grid_wrapper, class: 'col-sm-9 col-md-10' do |ba| + ba.use :input, class: 'form-range', error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } + ba.use :hint, wrap_with: { class: 'form-text' } + end + end + + + # inline forms + # + # inline default_wrapper + config.wrappers :inline_form, class: 'col-12' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'visually-hidden' - wr.use :error, wrap_with: {tag: "span", class: "help-block"} - wr.use :hint, wrap_with: {tag: "p", class: "help-block"} + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :error, wrap_with: { class: 'invalid-feedback' } + b.optional :hint, wrap_with: { class: 'form-text' } + end + + # inline input for boolean + config.wrappers :inline_boolean, class: 'col-12' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :form_check_wrapper, class: 'form-check' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :error, wrap_with: { class: 'invalid-feedback' } + bb.optional :hint, wrap_with: { class: 'form-text' } end end - config.wrappers :horizontal_radio_and_checkboxes, tag: "div", class: "form-group", error_class: "has-error" do |b| + + # bootstrap custom forms + # + # custom input switch for boolean + config.wrappers :custom_boolean_switch, class: 'mb-3' do |b| b.use :html5 b.optional :readonly + b.wrapper :form_check_wrapper, tag: 'div', class: 'form-check form-switch' do |bb| + bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' + bb.use :label, class: 'form-check-label' + bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } + bb.use :hint, wrap_with: { class: 'form-text' } + end + end - b.use :label, class: "col-sm-3 control-label" - b.wrapper tag: "div", class: "col-sm-9" do |ba| - ba.use :input - ba.use :error, wrap_with: {tag: "span", class: "help-block"} - ba.use :hint, wrap_with: {tag: "p", class: "help-block"} + # Input Group - custom component + # see example app and config at https://github.com/heartcombo/simple_form-bootstrap + config.wrappers :input_group, class: 'mb-3' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'form-label' + b.wrapper :input_group_tag, class: 'input-group' do |ba| + ba.optional :prepend + ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + ba.optional :append + ba.use :full_error, wrap_with: { class: 'invalid-feedback' } end + b.use :hint, wrap_with: { class: 'form-text' } end - config.wrappers :inline_form, tag: "div", class: "form-group", error_class: "has-error" do |b| + + # Floating Labels form + # + # floating labels default_wrapper + config.wrappers :floating_labels_form, class: 'form-floating mb-3' do |b| b.use :html5 b.use :placeholder b.optional :maxlength + b.optional :minlength b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: "sr-only" + b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :label + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } + end - b.use :input, class: "form-control" - b.use :error, wrap_with: {tag: "span", class: "help-block"} - b.use :hint, wrap_with: {tag: "p", class: "help-block"} + # custom multi select + config.wrappers :floating_labels_select, class: 'form-floating mb-3' do |b| + b.use :html5 + b.optional :readonly + b.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' + b.use :label + b.use :full_error, wrap_with: { class: 'invalid-feedback' } + b.use :hint, wrap_with: { class: 'form-text' } end - # Wrappers for forms and inputs using the Bootstrap toolkit. - # Check the Bootstrap docs (http://getbootstrap.com) - # to learn about the different styles for forms and inputs, - # buttons and other elements. + + # The default wrapper to be used by the FormBuilder. config.default_wrapper = :vertical_form + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. config.wrapper_mappings = { - check_boxes: :vertical_radio_and_checkboxes, - radio_buttons: :vertical_radio_and_checkboxes, - file: :vertical_file_input, - boolean: :vertical_boolean + boolean: :vertical_boolean, + check_boxes: :vertical_collection, + date: :vertical_multi_select, + datetime: :vertical_multi_select, + file: :vertical_file, + radio_buttons: :vertical_collection, + range: :vertical_range, + time: :vertical_multi_select, + select: :vertical_select } end diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 7c01bad3..c2168f2a 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -14,8 +14,6 @@ bg: invalid: трябва да съдържа „@“ точно веднъж github: invalid: трябва да съдържа по-малко от 40 латински букви, цифри или „-“ и да не започва с „-“ - gpg_fingerprint: - invalid: трябва да съдържа точно 40 цифри или латински букви от „A“ до „F“ network_device: attributes: mac_address: @@ -51,7 +49,6 @@ bg: password_confirmation: Потвърждение на паролата current_password: Текуща парола announce_my_presence: Показвай името ми, когато съм в Лаба и когато отварям вратата - gpg_fingerprint: Отпечатък на GPG ключ jabber: Jabber акаунт picture: Снимка github: Github акаунт @@ -66,6 +63,11 @@ bg: use_for_presence: Използвай за определяне кога съм в Лаба phone_number: phone_number: Телефонен номер + user_role: + user: Потребител + role: Роля + start_time: Начало + end_time: Край mailers: unlocks: hello: "Здравейте, %{name}!" @@ -130,6 +132,9 @@ bg: Ви свързани с устройствата пак ще се изпращат, но вместо с името ви, ще пише Someone who does not wish to be named, unlocked/locked/opened the door. и т.н. + personal_data: "Лични данни" + phones: "Достъп чрез телефон" + socials: "Социални мрежи" passwords: forgotten_password: Забравена парола send_instructions: Изпрати инструкции @@ -156,6 +161,14 @@ bg: new_network_device: Ново мрежово устройство roles: are_you_sure: 'Сигурни ли сте?' + add: 'Добави достъп' + active: 'Активна' + inactive: 'Неактивна' + expired: 'Изтекли' + current: 'Активни' + future: 'Бъдещи' + deactivate: 'Деактивирай' + cancel: 'Откажи' footer: license: 'init Lab Fauna е лицензирана под' source_code: 'Изходният код е достъпен в' diff --git a/config/locales/devise.bg.yml b/config/locales/devise.bg.yml index 3921ab7e..9c99819a 100644 --- a/config/locales/devise.bg.yml +++ b/config/locales/devise.bg.yml @@ -38,7 +38,7 @@ bg: signed_up: "Добре дошли! Вие се регистрирахте успешно." signed_up_but_inactive: "Вие се регистирахте успешно. Въпреки това, не можете да влезете в профила си, защото той все още не е потвърден." signed_up_but_locked: "Вие се регистрирахте успешно. Въпреки това, не можете да влезете в профила си, защото той е заключен." - signed_up_but_unconfirmed: "Писмо с връзка за потвърждаване на профила ви беше изпратено на вашия имейл адрес. Моля, отворете връзката, за даактивирате вашия профил." + signed_up_but_unconfirmed: "Писмо с връзка за потвърждаване на профила ви беше изпратено на вашия имейл адрес. Моля, отворете връзката, за да активирате вашия профил." update_needs_confirmation: "Вие променихте профила си успешно, но ние трябва да проверим вашия нов имейл адрес. Моля, проверете пощата си и отворете връзката за потвърждаване на новия адрес." updated: "Вие променихте профила си успешно." sessions: diff --git a/config/locales/en.yml b/config/locales/en.yml index 82f339d9..b024d806 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -14,8 +14,6 @@ en: invalid: must contain "@" exactly once github: invalid: must contain less than 40 latin letters, numbers, or "-", and not begin with a "-" - gpg_fingerprint: - invalid: must to contain exactly 40 digits or the latin leters from "A" to "F" network_device: attributes: mac_address: @@ -45,7 +43,6 @@ en: password_confirmation: Password confirmation current_password: Current password announce_my_presence: Show my name when I'm at the Lab and when I open the doors - gpg_fingerprint: GPG key fingerprint jabber: Jabber/XMPP account picture: Photo github: Github account @@ -60,6 +57,11 @@ en: use_for_presence: Use to determine when I'm in the Lab phone_number: phone_number: Phone number + user_role: + user: User + role: Role + start_time: From + end_time: Until mailers: unlocks: hello: "Hello, %{name}!" @@ -123,6 +125,9 @@ en: your actions on any of the devices will still be published, but instead of showing your username, the text will say Someone who does not wish to be named, unlocked/locked/opened the door, etc. + personal_data: "Personal Data" + phones: "Phone Access" + socials: "Social Networks" passwords: forgotten_password: Forgotten password send_instructions: Send instructions @@ -149,6 +154,8 @@ en: new_network_device: New network device roles: are_you_sure: 'Are you shnur?' + active: 'Active' + inactive: 'Inactive' footer: license: 'init Lab Fauna is licensed under' source_code: 'The source code is available in' diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml index d4754b1d..519c7dff 100644 --- a/config/locales/simple_form.bg.yml +++ b/config/locales/simple_form.bg.yml @@ -16,7 +16,6 @@ bg: twitter: потребителско име с или без „@“ url: URL, започващ с „http://“ или „https://“ jabber: Jabber акаунт във формат „username@domain.com“ - gpg_fingerprint: Отпечатък на GPG ключ с или без интервали pin: използва се за отключване на вратата и трябва да е число с поне 6 цифри; оставете празно, ако не искате да го променяте phone_numbers: phone_number: телефонен номер с или без код за държава @@ -27,6 +26,8 @@ bg: network_device: create: "Създай" update: "Промени" + user_role: + create: "Добави достъп" # Examples # labels: # defaults: diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index acfbf84b..23743833 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -1,26 +1,31 @@ en: simple_form: - 'yes': 'Yes' - 'no': 'No' + "yes": 'Yes' + "no": 'No' required: - text: 'mandatory field' + text: 'required' mark: '*' + # You can uncomment the line below if you need to overwrite the whole required html. + # When using html, text and mark won't be used. + # html: '*' error_notification: - default_message: 'Please take a look at the indicated mistakes in the form:' - hints: - user: - github: username - twitter: username with optional "@" - url: URL beginning with http:// or https:// - jabber: Jabber account in the "username@domain.com" format - gpg_fingerprint: GPG fingerprint with or without spaces - pin: it is used to lock/unlock the doors and must be a number with at least 6 digits; leave empty if you do not want to change it - phone_numbers: - phone_number: phone number with optional country code - network_device: - description: hostname or short hint - helpers: - submit: - network_device: - create: "Create" - update: "Change" + default_message: "Please review the problems below:" + # Examples + # labels: + # defaults: + # password: 'Password' + # user: + # new: + # email: 'E-mail to sign in.' + # edit: + # email: 'E-mail.' + # hints: + # defaults: + # username: 'User name to sign in.' + # password: 'No special characters, please.' + # include_blanks: + # defaults: + # age: 'Rather not say' + # prompts: + # defaults: + # age: 'Select your age' diff --git a/config/routes.rb b/config/routes.rb index f43bed41..cbc8ae52 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true Rails.application.routes.draw do use_doorkeeper do - controllers applications: "oauth/applications" + controllers applications: 'oauth/applications' end resource :user, only: [] do @@ -9,45 +10,42 @@ authenticated do devise_scope :user do - get "users/edit" => "devise/registrations#edit", :as => :user_root + get 'users/edit' => 'devise/registrations#edit', :as => :user_root end end namespace :fauna do - resources :users, only: [:index, :edit, :update, :show, :destroy] do - resources :role_assignments, only: [:create] do - collection do - delete ":role_name", action: "destroy", as: "role_assignment" - end + resources :users, only: %i[index edit update show destroy] do + resources :role_assignments, only: %i[index new create destroy] do end end end - namespace :api, defaults: {format: "json"} do + namespace :api, defaults: {format: 'json'} do resources :users, only: [] do collection do - get "present" + get 'present' end end resource :current_user, only: :show - resource :phone_access, only: [], controller: "phone_access" do + resource :phone_access, only: [], controller: 'phone_access' do collection do - post "phone_number_token" - post "verify_pin" + post 'phone_number_token' + post 'verify_pin' end end end - get "spaceapi/status", to: "space_api#status" - get "spaceapi/oauth_status", to: "space_api#oauth_status" + get 'spaceapi/status', to: 'space_api#status' + get 'spaceapi/oauth_status', to: 'space_api#oauth_status' - get "manifest", to: "web_app_manifest#manifest" + get 'manifest', to: 'web_app_manifest#manifest' - devise_for :users, controllers: {registrations: "registrations"} - get "dashboard/index" - root "dashboard#index" + devise_for :users, controllers: {registrations: 'registrations'} + get 'dashboard/index' + root 'dashboard#index' # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". diff --git a/config/space_api.yml b/config/space_api.yml index 6b160221..0efe7e4e 100644 --- a/config/space_api.yml +++ b/config/space_api.yml @@ -1,3 +1,4 @@ +--- space: init Lab logo: initlab-logo.png url: https://initlab.org diff --git a/db/migrate/20260110072625_remove_gpg_fingerprint_from_users.rb b/db/migrate/20260110072625_remove_gpg_fingerprint_from_users.rb new file mode 100644 index 00000000..af0be00a --- /dev/null +++ b/db/migrate/20260110072625_remove_gpg_fingerprint_from_users.rb @@ -0,0 +1,5 @@ +class RemoveGpgFingerprintFromUsers < ActiveRecord::Migration[7.0] + def change + remove_column :users, :gpg_fingerprint, :string + end +end diff --git a/db/migrate/20260111102636_add_times_to_users_roles.rb b/db/migrate/20260111102636_add_times_to_users_roles.rb new file mode 100644 index 00000000..78ca4209 --- /dev/null +++ b/db/migrate/20260111102636_add_times_to_users_roles.rb @@ -0,0 +1,15 @@ +class AddTimesToUsersRoles < ActiveRecord::Migration[8.1] + def up + add_column :users_roles, :start_time, :datetime, null: false, default: -> { 'CURRENT_TIMESTAMP' } + add_column :users_roles, :end_time, :datetime, null: true + + execute('ALTER TABLE users_roles ADD PRIMARY KEY (user_id, role_id, start_time)') + end + + def down + remove_column :users_roles, :start_time + remove_column :users_roles, :end_time + + execute('ALTER TABLE users_roles DROP CONSTRAINT users_roles_pkey') + end +end diff --git a/db/schema.rb b/db/schema.rb index aad0e9ea..50cf2c87 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,164 +10,166 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_01_21_190249) do +ActiveRecord::Schema[8.1].define(version: 2026_01_11_102636) do + # These are extensions that must be enabled in order to support this database + enable_extension "pg_catalog.plpgsql" + create_table "audit_log_entries", force: :cascade do |t| - t.string "type" - t.integer "user_id" - t.json "payload" t.datetime "created_at", null: false + t.json "payload" + t.string "type" t.datetime "updated_at", null: false + t.integer "user_id" t.index ["type"], name: "index_audit_log_entries_on_type" t.index ["user_id"], name: "index_audit_log_entries_on_user_id" end create_table "delayed_jobs", force: :cascade do |t| - t.integer "priority", default: 0, null: false t.integer "attempts", default: 0, null: false + t.datetime "created_at", precision: nil + t.datetime "failed_at", precision: nil t.text "handler", null: false t.text "last_error" - t.datetime "run_at", precision: nil t.datetime "locked_at", precision: nil - t.datetime "failed_at", precision: nil t.string "locked_by" + t.integer "priority", default: 0, null: false t.string "queue" - t.datetime "created_at", precision: nil + t.datetime "run_at", precision: nil t.datetime "updated_at", precision: nil t.index ["priority", "run_at"], name: "delayed_jobs_priority" end create_table "door_actions", force: :cascade do |t| - t.string "type" + t.datetime "created_at", precision: nil, null: false + t.boolean "execution_succeeded" t.integer "initiator_id" t.text "origin_information" - t.datetime "created_at", precision: nil, null: false + t.string "type" t.datetime "updated_at", precision: nil, null: false - t.boolean "execution_succeeded" t.index ["initiator_id"], name: "index_door_actions_on_initiator_id" t.index ["type"], name: "index_door_actions_on_type" end create_table "door_status_notifications", force: :cascade do |t| + t.datetime "created_at", precision: nil, null: false t.string "door" t.string "latch" - t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "log_entries", force: :cascade do |t| + t.datetime "created_at", precision: nil, null: false t.integer "loggable_id", null: false t.string "loggable_type", null: false - t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end create_table "network_devices", force: :cascade do |t| - t.integer "owner_id", null: false - t.string "mac_address" t.datetime "created_at", precision: nil, null: false + t.string "description" + t.string "mac_address" + t.integer "owner_id", null: false t.datetime "updated_at", precision: nil, null: false t.boolean "use_for_presence", default: true, null: false - t.string "description" t.index ["mac_address"], name: "index_network_devices_on_mac_address" t.index ["owner_id"], name: "index_network_devices_on_owner_id" end create_table "oauth_access_grants", force: :cascade do |t| - t.integer "resource_owner_id", null: false t.integer "application_id", null: false - t.string "token", null: false + t.string "code_challenge" + t.string "code_challenge_method" + t.datetime "created_at", precision: nil, null: false t.integer "expires_in", null: false t.text "redirect_uri", null: false - t.datetime "created_at", precision: nil, null: false + t.integer "resource_owner_id", null: false t.datetime "revoked_at", precision: nil t.string "scopes" - t.string "code_challenge" - t.string "code_challenge_method" + t.string "token", null: false t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true end create_table "oauth_access_tokens", force: :cascade do |t| - t.integer "resource_owner_id" t.integer "application_id" - t.string "token", null: false - t.string "refresh_token" + t.datetime "created_at", precision: nil, null: false t.integer "expires_in" + t.string "previous_refresh_token", default: "", null: false + t.string "refresh_token" + t.integer "resource_owner_id" t.datetime "revoked_at", precision: nil - t.datetime "created_at", precision: nil, null: false t.string "scopes" - t.string "previous_refresh_token", default: "", null: false + t.string "token", null: false t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true end create_table "oauth_applications", force: :cascade do |t| + t.boolean "confidential", default: true, null: false + t.datetime "created_at", precision: nil, null: false t.string "name", null: false - t.string "uid", null: false - t.string "secret", null: false + t.integer "owner_id" + t.string "owner_type" t.text "redirect_uri", null: false t.string "scopes", default: "", null: false - t.datetime "created_at", precision: nil, null: false + t.string "secret", null: false + t.string "uid", null: false t.datetime "updated_at", precision: nil, null: false - t.integer "owner_id" - t.string "owner_type" - t.boolean "confidential", default: true, null: false t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type" t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end create_table "phone_numbers", force: :cascade do |t| + t.datetime "created_at", precision: nil, null: false t.integer "owner_id", null: false t.string "phone_number" - t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.index ["owner_id"], name: "index_phone_numbers_on_owner_id" t.index ["phone_number"], name: "index_phone_numbers_on_phone_number" end create_table "roles", force: :cascade do |t| + t.datetime "created_at", precision: nil, null: false t.string "name" - t.string "resource_type" t.integer "resource_id" - t.datetime "created_at", precision: nil, null: false + t.string "resource_type" t.datetime "updated_at", precision: nil, null: false t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" t.index ["name"], name: "index_roles_on_name" end create_table "users", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at", precision: nil - t.datetime "remember_created_at", precision: nil - t.integer "sign_in_count", default: 0, null: false - t.datetime "current_sign_in_at", precision: nil - t.datetime "last_sign_in_at", precision: nil - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.string "first_name" - t.string "last_name" - t.string "url" - t.string "twitter" t.boolean "announce_my_presence", default: false, null: false - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false t.boolean "can_unlock_door", default: false, null: false - t.string "username" + t.datetime "confirmation_sent_at", precision: nil t.string "confirmation_token" t.datetime "confirmed_at", precision: nil - t.datetime "confirmation_sent_at", precision: nil - t.string "unconfirmed_email" + t.datetime "created_at", precision: nil, null: false + t.datetime "current_sign_in_at", precision: nil + t.string "current_sign_in_ip" + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "encrypted_pin" t.integer "failed_attempts", default: 0, null: false - t.string "unlock_token" - t.datetime "locked_at", precision: nil + t.string "first_name" t.string "github" t.string "jabber" - t.string "gpg_fingerprint" - t.string "picture" - t.string "encrypted_pin" + t.string "last_name" + t.datetime "last_sign_in_at", precision: nil + t.string "last_sign_in_ip" t.string "locale" + t.datetime "locked_at", precision: nil + t.string "picture" + t.datetime "remember_created_at", precision: nil + t.datetime "reset_password_sent_at", precision: nil + t.string "reset_password_token" + t.integer "sign_in_count", default: 0, null: false + t.string "twitter" + t.string "unconfirmed_email" + t.string "unlock_token" + t.datetime "updated_at", precision: nil, null: false + t.string "url" + t.string "username" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true @@ -175,9 +177,11 @@ t.index ["username"], name: "index_users_on_username", unique: true end - create_table "users_roles", id: false, force: :cascade do |t| - t.integer "user_id" - t.integer "role_id" + create_table "users_roles", primary_key: ["user_id", "role_id", "start_time"], force: :cascade do |t| + t.datetime "end_time" + t.integer "role_id", null: false + t.datetime "start_time", default: -> { "CURRENT_TIMESTAMP" }, null: false + t.integer "user_id", null: false t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" end diff --git a/db/seeds.rb b/db/seeds.rb index 4edb1e85..78fd80b7 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,3 +5,16 @@ # # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) # Mayor.create(name: 'Emanuel', city: cities.first) + +roles = [ + :board_member, + :trusted_member, + :member, + :tenant, + :landlord, + :infra +] + +roles.each do |role| + Role.find_or_create_by(name: role) +end diff --git a/lib/templates/slim/scaffold/_form.html.slim b/lib/templates/slim/scaffold/_form.html.slim index a2ff775a..34279286 100644 --- a/lib/templates/slim/scaffold/_form.html.slim +++ b/lib/templates/slim/scaffold/_form.html.slim @@ -1,5 +1,6 @@ = simple_form_for(@<%= singular_table_name %>) do |f| = f.error_notification + = f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? .form-inputs <%- attributes.each do |attribute| -%> diff --git a/spec/factories/users.rb b/spec/factories/users.rb index d9e8ae00..65c319d8 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -14,7 +14,6 @@ confirmed_at { Time.now } github { "foobar" } jabber { "foo@bar.com" } - gpg_fingerprint { "AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA" } factory :board_member do after(:create) { |user| user.add_role(:board_member) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 09b9fa98..fd015ad2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -196,26 +196,6 @@ end end - describe "GPG key fingerprint" do - it "can be nil" do - expect(build(:user, gpg_fingerprint: nil)).to_not have_error_on :gpg_fingerprint - end - - it "must contain 40 hex case insensitive digits separated with any number of spaces" do - expect(build(:user, gpg_fingerprint: "a" * 40)).to_not have_error_on :gpg_fingerprint - expect(build(:user, gpg_fingerprint: "A" * 40)).to_not have_error_on :gpg_fingerprint - expect(build(:user, gpg_fingerprint: "aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa")).to_not have_error_on :gpg_fingerprint - expect(build(:user, gpg_fingerprint: "AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA")).to_not have_error_on :gpg_fingerprint - - expect(build(:user, gpg_fingerprint: "z" * 40)).to have_error_on :gpg_fingerprint - expect(build(:user, gpg_fingerprint: "a" * 39)).to have_error_on :gpg_fingerprint - end - - it "stores the gpg fingerprint upcased and properly delimited with spaces" do - expect(create(:user, gpg_fingerprint: "a" * 40).gpg_fingerprint).to eq "AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA AAAA" - end - end - describe "#pin" do it "returns the pin" do expect(build(:user, pin: 123456).pin).to eq 123456