diff --git a/Gemfile b/Gemfile index 2a9aa8721d..9736ddfc20 100644 --- a/Gemfile +++ b/Gemfile @@ -90,6 +90,8 @@ gem 'devise' # An invitation strategy for Devise (https://github.com/scambra/devise_invitable) gem 'devise_invitable' +gem 'doorkeeper' + # A generalized Rack framework for multiple-provider authentication. # (https://github.com/omniauth/omniauth) gem 'omniauth' diff --git a/Gemfile.lock b/Gemfile.lock index 56db857b5c..f118b37ca9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,723 +1,731 @@ -GEM - remote: https://rubygems.org/ - specs: - actioncable (7.1.5.2) - actionpack (= 7.1.5.2) - activesupport (= 7.1.5.2) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - zeitwerk (~> 2.6) - actionmailbox (7.1.5.2) - actionpack (= 7.1.5.2) - activejob (= 7.1.5.2) - activerecord (= 7.1.5.2) - activestorage (= 7.1.5.2) - activesupport (= 7.1.5.2) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.5.2) - actionpack (= 7.1.5.2) - actionview (= 7.1.5.2) - activejob (= 7.1.5.2) - activesupport (= 7.1.5.2) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp - rails-dom-testing (~> 2.2) - actionpack (7.1.5.2) - actionview (= 7.1.5.2) - activesupport (= 7.1.5.2) - nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4) - rack-session (>= 1.0.1) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - actiontext (7.1.5.2) - actionpack (= 7.1.5.2) - activerecord (= 7.1.5.2) - activestorage (= 7.1.5.2) - activesupport (= 7.1.5.2) - globalid (>= 0.6.0) - nokogiri (>= 1.8.5) - actionview (7.1.5.2) - activesupport (= 7.1.5.2) - builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activejob (7.1.5.2) - activesupport (= 7.1.5.2) - globalid (>= 0.3.6) - activemodel (7.1.5.2) - activesupport (= 7.1.5.2) - activerecord (7.1.5.2) - activemodel (= 7.1.5.2) - activesupport (= 7.1.5.2) - timeout (>= 0.4.0) - activerecord_json_validator (3.1.0) - activerecord (>= 4.2.0, < 9) - json_schemer (~> 2.2) - activestorage (7.1.5.2) - actionpack (= 7.1.5.2) - activejob (= 7.1.5.2) - activerecord (= 7.1.5.2) - activesupport (= 7.1.5.2) - marcel (~> 1.0) - activesupport (7.1.5.2) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - logger (>= 1.4.2) - minitest (>= 5.1) - mutex_m - securerandom (>= 0.3) - tzinfo (~> 2.0) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - annotate (3.2.0) - activerecord (>= 3.2, < 8.0) - rake (>= 10.4, < 14.0) - annotate_gem (0.0.14) - bundler (>= 1.1) - api-pagination (6.0.0) - ast (2.4.3) - autoprefixer-rails (10.4.21.0) - execjs (~> 2) - base64 (0.3.0) - bcrypt (3.1.20) - benchmark (0.4.1) - better_errors (2.10.1) - erubi (>= 1.0.0) - rack (>= 0.9.0) - rouge (>= 1.0.0) - bigdecimal (3.3.0) - bindex (0.8.1) - binding_of_caller (1.0.1) - debug_inspector (>= 1.2.0) - bootsnap (1.18.6) - msgpack (~> 1.2) - brakeman (7.1.0) - racc - builder (3.3.0) - bullet (8.0.8) - activesupport (>= 3.0.0) - uniform_notifier (~> 1.11) - bundle-audit (0.1.0) - bundler-audit - bundler-audit (0.9.2) - bundler (>= 1.2.0, < 3) - thor (~> 1.0) - byebug (12.0.0) - capybara (3.40.0) - addressable - matrix - mini_mime (>= 0.1.3) - nokogiri (~> 1.11) - rack (>= 1.6.0) - rack-test (>= 0.6.3) - regexp_parser (>= 1.5, < 3.0) - xpath (~> 3.2) - cgi (0.5.0) - claide (1.1.0) - claide-plugins (0.9.2) - cork - nap - open4 (~> 1.3) - coderay (1.1.3) - colored2 (3.1.2) - concurrent-ruby (1.3.5) - connection_pool (2.5.4) - contact_us (1.2.0) - rails (>= 4.2.0) - cork (0.3.0) - colored2 (~> 3.1) - crack (1.0.0) - bigdecimal - rexml - crass (1.0.6) - cssbundling-rails (1.4.3) - railties (>= 6.0.0) - csv (3.3.5) - danger (9.5.3) - base64 (~> 0.2) - claide (~> 1.0) - claide-plugins (>= 0.9.2) - colored2 (>= 3.1, < 5) - cork (~> 0.1) - faraday (>= 0.9.0, < 3.0) - faraday-http-cache (~> 2.0) - git (>= 1.13, < 3.0) - kramdown (>= 2.5.1, < 3.0) - kramdown-parser-gfm (~> 1.0) - octokit (>= 4.0) - pstore (~> 0.1) - terminal-table (>= 1, < 5) - database_cleaner (2.1.0) - database_cleaner-active_record (>= 2, < 3) - database_cleaner-active_record (2.2.2) - activerecord (>= 5.a) - database_cleaner-core (~> 2.0) - database_cleaner-core (2.0.1) - date (3.4.1) - debug_inspector (1.2.0) - devise (4.9.4) - bcrypt (~> 3.0) - orm_adapter (~> 0.1) - railties (>= 4.1.0) - responders - warden (~> 1.2.3) - devise_invitable (2.0.11) - actionmailer (>= 5.0) - devise (>= 4.6) - diff-lcs (1.6.2) - dotenv (3.1.8) - dotenv-rails (3.1.8) - dotenv (= 3.1.8) - railties (>= 6.1) - dragonfly (1.4.1) - addressable (~> 2.3) - multi_json (~> 1.0) - ostruct (~> 0.6.1) - rack (>= 1.3) - dragonfly-s3_data_store (1.3.0) - dragonfly (~> 1.0) - fog-aws - drb (2.2.3) - erb (4.0.4) - cgi (>= 0.3.3) - erubi (1.13.1) - excon (1.3.0) - logger - execjs (2.10.0) - factory_bot (6.5.5) - activesupport (>= 6.1.0) - factory_bot_rails (6.5.1) - factory_bot (~> 6.5) - railties (>= 6.1.0) - faker (3.5.2) - i18n (>= 1.8.11, < 2) - faraday (2.14.0) - faraday-net_http (>= 2.0, < 3.5) - json - logger - faraday-http-cache (2.5.1) - faraday (>= 0.8) - faraday-net_http (3.4.1) - net-http (>= 0.5.0) - ffi (1.17.2-arm64-darwin) - ffi (1.17.2-x86_64-linux-gnu) - flag_shih_tzu (0.3.23) - fog-aws (3.33.0) - base64 (>= 0.2, < 0.4) - fog-core (~> 2.6) - fog-json (~> 1.1) - fog-xml (~> 0.1) - fog-core (2.6.0) - builder - excon (~> 1.0) - formatador (>= 0.2, < 2.0) - mime-types - fog-json (1.2.0) - fog-core - multi_json (~> 1.10) - fog-xml (0.1.5) - fog-core - nokogiri (>= 1.5.11, < 2.0.0) - formatador (1.2.1) - reline - forwardable (1.3.3) - fuubar (2.5.1) - rspec-core (~> 3.0) - ruby-progressbar (~> 1.4) - gettext (3.4.9) - erubi - locale (>= 2.0.5) - prime - racc - text (>= 1.3.0) - git (2.3.3) - activesupport (>= 5.0) - addressable (~> 2.8) - process_executer (~> 1.1) - rchardet (~> 1.8) - globalid (1.3.0) - activesupport (>= 6.1) - guard (2.19.1) - formatador (>= 0.2.4) - listen (>= 2.7, < 4.0) - logger (~> 1.6) - lumberjack (>= 1.0.12, < 2.0) - nenv (~> 0.1) - notiffany (~> 0.0) - ostruct (~> 0.6) - pry (>= 0.13.0) - shellany (~> 0.0) - thor (>= 0.18.1) - hana (1.3.7) - hashdiff (1.2.1) - hashie (5.0.0) - highline (3.1.2) - reline - htmltoword (1.1.1) - actionpack - nokogiri - rubyzip (>= 1.0) - httparty (0.23.1) - csv - mini_mime (>= 1.0.0) - multi_xml (>= 0.5.2) - i18n (1.14.7) - concurrent-ruby (~> 1.0) - io-console (0.8.1) - irb (1.15.2) - pp (>= 0.6.0) - rdoc (>= 4.0.0) - reline (>= 0.4.2) - jbuilder (2.14.1) - actionview (>= 7.0.0) - activesupport (>= 7.0.0) - jsbundling-rails (1.3.1) - railties (>= 6.0.0) - json (2.15.1) - json_schemer (2.4.0) - bigdecimal - hana (~> 1.3) - regexp_parser (~> 2.0) - simpleidn (~> 0.2) - jwt (3.1.2) - base64 - kaminari (1.2.2) - activesupport (>= 4.1.0) - kaminari-actionview (= 1.2.2) - kaminari-activerecord (= 1.2.2) - kaminari-core (= 1.2.2) - kaminari-actionview (1.2.2) - actionview - kaminari-core (= 1.2.2) - kaminari-activerecord (1.2.2) - activerecord - kaminari-core (= 1.2.2) - kaminari-core (1.2.2) - kramdown (2.5.1) - rexml (>= 3.3.9) - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - language_server-protocol (3.17.0.5) - ledermann-rails-settings (2.6.2) - activerecord (>= 6.1) - lint_roller (1.1.0) - listen (3.9.0) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - locale (2.1.4) - logger (1.7.0) - loofah (2.24.1) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - lumberjack (1.4.2) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (1.1.0) - matrix (0.4.3) - method_source (1.1.0) - mime-types (3.7.0) - logger - mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2025.0924) - mimemagic (0.4.3) - nokogiri (~> 1) - rake - mini_mime (1.1.5) - minitest (5.25.5) - mocha (2.7.1) - ruby2_keywords (>= 0.0.5) - msgpack (1.8.0) - multi_json (1.17.0) - multi_xml (0.7.1) - bigdecimal (~> 3.1) - mutex_m (0.3.0) - mysql2 (0.5.7) - bigdecimal - nap (1.1.0) - nenv (0.3.0) - net-http (0.6.0) - uri - net-imap (0.5.12) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.2) - timeout - net-smtp (0.5.1) - net-protocol - nio4r (2.7.4) - nokogiri (1.18.10-arm64-darwin) - racc (~> 1.4) - nokogiri (1.18.10-x86_64-linux-gnu) - racc (~> 1.4) - notiffany (0.1.3) - nenv (~> 0.1) - shellany (~> 0.0) - oauth2 (2.0.17) - faraday (>= 0.17.3, < 4.0) - jwt (>= 1.0, < 4.0) - logger (~> 1.2) - multi_xml (~> 0.5) - rack (>= 1.2, < 4) - snaky_hash (~> 2.0, >= 2.0.3) - version_gem (~> 1.1, >= 1.1.9) - octokit (10.0.0) - faraday (>= 1, < 3) - sawyer (~> 0.9) - omniauth (2.1.4) - hashie (>= 3.4.6) - logger - rack (>= 2.2.3) - rack-protection - omniauth-oauth2 (1.8.0) - oauth2 (>= 1.4, < 3) - omniauth (~> 2.0) - omniauth-orcid (2.1.1) - omniauth-oauth2 (~> 1.3) - ruby_dig (~> 0.0.2) - omniauth-rails_csrf_protection (1.0.2) - actionpack (>= 4.2) - omniauth (~> 2.0) - omniauth-shibboleth (1.3.0) - omniauth (>= 1.0.0) - open4 (1.3.4) - options (2.3.2) - orm_adapter (0.5.0) - ostruct (0.6.3) - parallel (1.27.0) - parser (3.3.9.0) - ast (~> 2.4.1) - racc - pg (1.6.2-arm64-darwin) - pg (1.6.2-x86_64-linux) - pp (0.6.3) - prettyprint - prettyprint (0.2.0) - prime (0.1.4) - forwardable - singleton - prism (1.5.1) - process_executer (1.3.0) - progress_bar (1.3.4) - highline (>= 1.6) - options (~> 2.3.0) - pry (0.15.2) - coderay (~> 1.1) - method_source (~> 1.0) - pstore (0.2.0) - psych (5.2.6) - date - stringio - public_suffix (6.0.2) - puma (7.0.4) - nio4r (~> 2.0) - pundit (2.5.2) - activesupport (>= 3.0.0) - pundit-matchers (4.0.0) - rspec-core (~> 3.12) - rspec-expectations (~> 3.12) - rspec-mocks (~> 3.12) - rspec-support (~> 3.12) - racc (1.8.1) - rack (3.2.2) - rack-attack (6.7.0) - rack (>= 1.0, < 4) - rack-mini-profiler (4.0.1) - rack (>= 1.2.0) - rack-protection (4.1.1) - base64 (>= 0.1.0) - logger (>= 1.6.0) - rack (>= 3.0.0, < 4) - rack-session (2.1.1) - base64 (>= 0.1.0) - rack (>= 3.0.0) - rack-test (2.2.0) - rack (>= 1.3) - rackup (2.2.1) - rack (>= 3) - rails (7.1.5.2) - actioncable (= 7.1.5.2) - actionmailbox (= 7.1.5.2) - actionmailer (= 7.1.5.2) - actionpack (= 7.1.5.2) - actiontext (= 7.1.5.2) - actionview (= 7.1.5.2) - activejob (= 7.1.5.2) - activemodel (= 7.1.5.2) - activerecord (= 7.1.5.2) - activestorage (= 7.1.5.2) - activesupport (= 7.1.5.2) - bundler (>= 1.15.0) - railties (= 7.1.5.2) - rails-controller-testing (1.0.5) - actionpack (>= 5.0.1.rc1) - actionview (>= 5.0.1.rc1) - activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.3.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) - 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) - railties (7.1.5.2) - actionpack (= 7.1.5.2) - activesupport (= 7.1.5.2) - irb - rackup (>= 1.0.0) - rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - zeitwerk (~> 2.6) - rainbow (3.1.1) - rake (13.3.0) - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) - rchardet (1.10.0) - rdoc (6.15.0) - erb - psych (>= 4.0.0) - tsort - recaptcha (5.21.1) - regexp_parser (2.11.3) - reline (0.6.2) - io-console (~> 0.5) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) - rexml (3.4.4) - rollbar (3.6.2) - rouge (4.6.1) - rspec-collection_matchers (1.2.1) - rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.13.5) - rspec-support (~> 3.13.0) - rspec-expectations (3.13.5) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-mocks (3.13.5) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-rails (7.1.1) - actionpack (>= 7.0) - activesupport (>= 7.0) - railties (>= 7.0) - rspec-core (~> 3.13) - rspec-expectations (~> 3.13) - rspec-mocks (~> 3.13) - rspec-support (~> 3.13) - rspec-support (3.13.6) - rubocop (1.81.1) - json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.47.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.47.1) - parser (>= 3.3.7.2) - prism (~> 1.4) - rubocop-i18n (3.2.3) - lint_roller (~> 1.1) - rubocop (>= 1.72.1) - rubocop-performance (1.26.0) - lint_roller (~> 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.44.0, < 2.0) - ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - ruby_dig (0.0.2) - rubyzip (2.4.1) - sawyer (0.9.2) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - securerandom (0.4.1) - selenium-webdriver (4.32.0) - base64 (~> 0.2) - logger (~> 1.4) - rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) - websocket (~> 1.0) - shellany (0.0.1) - shoulda (4.0.0) - shoulda-context (~> 2.0) - shoulda-matchers (~> 4.0) - shoulda-context (2.0.0) - shoulda-matchers (4.5.1) - activesupport (>= 4.2.0) - simpleidn (0.2.3) - singleton (0.3.0) - snaky_hash (2.0.3) - hashie (>= 0.1.0, < 6) - version_gem (>= 1.1.8, < 3) - spring (4.4.0) - spring-commands-rspec (1.0.4) - spring (>= 0.9.1) - spring-watcher-listen (2.1.0) - listen (>= 2.7, < 4.0) - spring (>= 4) - sprockets (4.2.2) - concurrent-ruby (~> 1.0) - logger - rack (>= 2.2.4, < 4) - sprockets-rails (3.5.2) - actionpack (>= 6.1) - activesupport (>= 6.1) - sprockets (>= 3.0.0) - stringio (3.1.7) - terminal-table (4.0.0) - unicode-display_width (>= 1.1.1, < 4) - text (1.3.1) - thor (1.4.0) - timeout (0.4.3) - tomparse (0.4.2) - translation (1.41) - gettext (~> 3.2, >= 3.2.5, <= 3.4.9) - tsort (0.2.0) - turbo-rails (2.0.17) - actionpack (>= 7.1.0) - railties (>= 7.1.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (3.2.0) - unicode-emoji (~> 4.1) - unicode-emoji (4.1.0) - uniform_notifier (1.18.0) - uri (1.0.4) - version_gem (1.1.9) - warden (1.2.9) - rack (>= 2.0.9) - web-console (4.2.1) - actionview (>= 6.0.0) - activemodel (>= 6.0.0) - bindex (>= 0.4.0) - railties (>= 6.0.0) - webmock (3.25.1) - addressable (>= 2.8.0) - crack (>= 0.3.2) - hashdiff (>= 0.4.0, < 2.0.0) - websocket (1.2.11) - websocket-driver (0.8.0) - base64 - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - wicked_pdf (2.8.2) - activesupport - ostruct - wkhtmltopdf-binary (0.12.6.10) - xpath (3.2.0) - nokogiri (~> 1.8) - yard (0.9.37) - yard-tomdoc (0.7.1) - tomparse (>= 0.4.0) - yard - zeitwerk (2.6.18) - -PLATFORMS - arm64-darwin-21 - arm64-darwin-22 - x86_64-linux - -DEPENDENCIES - activerecord_json_validator - annotate - annotate_gem - api-pagination - autoprefixer-rails - better_errors - binding_of_caller - bootsnap - brakeman - bullet - bundle-audit - byebug - capybara - contact_us - cssbundling-rails - danger - database_cleaner - devise - devise_invitable - dotenv-rails - dragonfly - dragonfly-s3_data_store - factory_bot_rails - faker - flag_shih_tzu - fuubar - guard - htmltoword - httparty - jbuilder - jsbundling-rails - jwt - kaminari - ledermann-rails-settings - listen - mail (= 2.7.1) - mimemagic - mocha - mysql2 - net-smtp - omniauth - omniauth-orcid - omniauth-rails_csrf_protection - omniauth-shibboleth - parallel - pg - progress_bar - puma - pundit - pundit-matchers - rack-attack (~> 6.6, >= 6.6.1) - rack-mini-profiler - rails (~> 7.1) - rails-controller-testing - recaptcha - rollbar - rspec-collection_matchers - rspec-rails - rubocop - rubocop-i18n - rubocop-performance - ruby-progressbar - selenium-webdriver - shoulda - spring - spring-commands-rspec - spring-watcher-listen - sprockets-rails - text - translation - turbo-rails - web-console - webmock - wicked_pdf - wkhtmltopdf-binary - yard - yard-tomdoc - -RUBY VERSION - ruby 3.1.4p223 - -BUNDLED WITH - 2.4.17 +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.1.5.2) + actionpack (= 7.1.5.2) + activesupport (= 7.1.5.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.1.5.2) + actionpack (= 7.1.5.2) + activejob (= 7.1.5.2) + activerecord (= 7.1.5.2) + activestorage (= 7.1.5.2) + activesupport (= 7.1.5.2) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.5.2) + actionpack (= 7.1.5.2) + actionview (= 7.1.5.2) + activejob (= 7.1.5.2) + activesupport (= 7.1.5.2) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.5.2) + actionview (= 7.1.5.2) + activesupport (= 7.1.5.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.5.2) + actionpack (= 7.1.5.2) + activerecord (= 7.1.5.2) + activestorage (= 7.1.5.2) + activesupport (= 7.1.5.2) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.1.5.2) + activesupport (= 7.1.5.2) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.5.2) + activesupport (= 7.1.5.2) + globalid (>= 0.3.6) + activemodel (7.1.5.2) + activesupport (= 7.1.5.2) + activerecord (7.1.5.2) + activemodel (= 7.1.5.2) + activesupport (= 7.1.5.2) + timeout (>= 0.4.0) + activerecord_json_validator (3.1.0) + activerecord (>= 4.2.0, < 9) + json_schemer (~> 2.2) + activestorage (7.1.5.2) + actionpack (= 7.1.5.2) + activejob (= 7.1.5.2) + activerecord (= 7.1.5.2) + activesupport (= 7.1.5.2) + marcel (~> 1.0) + activesupport (7.1.5.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + mutex_m + securerandom (>= 0.3) + tzinfo (~> 2.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) + rake (>= 10.4, < 14.0) + annotate_gem (0.0.14) + bundler (>= 1.1) + api-pagination (6.0.0) + ast (2.4.3) + autoprefixer-rails (10.4.21.0) + execjs (~> 2) + base64 (0.3.0) + bcrypt (3.1.20) + benchmark (0.4.1) + better_errors (2.10.1) + erubi (>= 1.0.0) + rack (>= 0.9.0) + rouge (>= 1.0.0) + bigdecimal (3.3.0) + bindex (0.8.1) + binding_of_caller (1.0.1) + debug_inspector (>= 1.2.0) + bootsnap (1.18.6) + msgpack (~> 1.2) + brakeman (7.1.0) + racc + builder (3.3.0) + bullet (8.0.8) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) + bundle-audit (0.1.0) + bundler-audit + bundler-audit (0.9.2) + bundler (>= 1.2.0, < 3) + thor (~> 1.0) + byebug (12.0.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + cgi (0.5.0) + claide (1.1.0) + claide-plugins (0.9.2) + cork + nap + open4 (~> 1.3) + coderay (1.1.3) + colored2 (3.1.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) + contact_us (1.2.0) + rails (>= 4.2.0) + cork (0.3.0) + colored2 (~> 3.1) + crack (1.0.0) + bigdecimal + rexml + crass (1.0.6) + cssbundling-rails (1.4.3) + railties (>= 6.0.0) + csv (3.3.5) + danger (9.5.3) + base64 (~> 0.2) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (>= 3.1, < 5) + cork (~> 0.1) + faraday (>= 0.9.0, < 3.0) + faraday-http-cache (~> 2.0) + git (>= 1.13, < 3.0) + kramdown (>= 2.5.1, < 3.0) + kramdown-parser-gfm (~> 1.0) + octokit (>= 4.0) + pstore (~> 0.1) + terminal-table (>= 1, < 5) + database_cleaner (2.1.0) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.0.1) + date (3.4.1) + debug_inspector (1.2.0) + devise (4.9.4) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + devise_invitable (2.0.11) + actionmailer (>= 5.0) + devise (>= 4.6) + diff-lcs (1.6.2) + doorkeeper (5.8.2) + railties (>= 5) + dotenv (3.1.8) + dotenv-rails (3.1.8) + dotenv (= 3.1.8) + railties (>= 6.1) + dragonfly (1.4.1) + addressable (~> 2.3) + multi_json (~> 1.0) + ostruct (~> 0.6.1) + rack (>= 1.3) + dragonfly-s3_data_store (1.3.0) + dragonfly (~> 1.0) + fog-aws + drb (2.2.3) + erb (4.0.4) + cgi (>= 0.3.3) + erubi (1.13.1) + excon (1.3.0) + logger + execjs (2.10.0) + factory_bot (6.5.5) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + faker (3.5.2) + i18n (>= 1.8.11, < 2) + faraday (2.14.0) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-http-cache (2.5.1) + faraday (>= 0.8) + faraday-net_http (3.4.1) + net-http (>= 0.5.0) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + flag_shih_tzu (0.3.23) + fog-aws (3.33.0) + base64 (>= 0.2, < 0.4) + fog-core (~> 2.6) + fog-json (~> 1.1) + fog-xml (~> 0.1) + fog-core (2.6.0) + builder + excon (~> 1.0) + formatador (>= 0.2, < 2.0) + mime-types + fog-json (1.2.0) + fog-core + multi_json (~> 1.10) + fog-xml (0.1.5) + fog-core + nokogiri (>= 1.5.11, < 2.0.0) + formatador (1.2.1) + reline + forwardable (1.3.3) + fuubar (2.5.1) + rspec-core (~> 3.0) + ruby-progressbar (~> 1.4) + gettext (3.4.9) + erubi + locale (>= 2.0.5) + prime + racc + text (>= 1.3.0) + git (2.3.3) + activesupport (>= 5.0) + addressable (~> 2.8) + process_executer (~> 1.1) + rchardet (~> 1.8) + globalid (1.3.0) + activesupport (>= 6.1) + guard (2.19.1) + formatador (>= 0.2.4) + listen (>= 2.7, < 4.0) + logger (~> 1.6) + lumberjack (>= 1.0.12, < 2.0) + nenv (~> 0.1) + notiffany (~> 0.0) + ostruct (~> 0.6) + pry (>= 0.13.0) + shellany (~> 0.0) + thor (>= 0.18.1) + hana (1.3.7) + hashdiff (1.2.1) + hashie (5.0.0) + highline (3.1.2) + reline + htmltoword (1.1.1) + actionpack + nokogiri + rubyzip (>= 1.0) + httparty (0.23.1) + csv + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + io-console (0.8.1) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.14.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) + jsbundling-rails (1.3.1) + railties (>= 6.0.0) + json (2.15.1) + json_schemer (2.4.0) + bigdecimal + hana (~> 1.3) + regexp_parser (~> 2.0) + simpleidn (~> 0.2) + jwt (3.1.2) + base64 + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + kramdown (2.5.1) + rexml (>= 3.3.9) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + language_server-protocol (3.17.0.5) + ledermann-rails-settings (2.6.2) + activerecord (>= 6.1) + lint_roller (1.1.0) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + locale (2.1.4) + logger (1.7.0) + loofah (2.24.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + lumberjack (1.4.2) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (1.1.0) + matrix (0.4.3) + method_source (1.1.0) + mime-types (3.7.0) + logger + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2025.0924) + mimemagic (0.4.3) + nokogiri (~> 1) + rake + mini_mime (1.1.5) + minitest (5.25.5) + mocha (2.7.1) + ruby2_keywords (>= 0.0.5) + msgpack (1.8.0) + multi_json (1.17.0) + multi_xml (0.7.1) + bigdecimal (~> 3.1) + mutex_m (0.3.0) + mysql2 (0.5.7) + bigdecimal + nap (1.1.0) + nenv (0.3.0) + net-http (0.6.0) + uri + net-imap (0.5.12) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.4) + nokogiri (1.18.10-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) + notiffany (0.1.3) + nenv (~> 0.1) + shellany (~> 0.0) + oauth2 (2.0.17) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0, >= 2.0.3) + version_gem (~> 1.1, >= 1.1.9) + octokit (10.0.0) + faraday (>= 1, < 3) + sawyer (~> 0.9) + omniauth (2.1.4) + hashie (>= 3.4.6) + logger + rack (>= 2.2.3) + rack-protection + omniauth-oauth2 (1.8.0) + oauth2 (>= 1.4, < 3) + omniauth (~> 2.0) + omniauth-orcid (2.1.1) + omniauth-oauth2 (~> 1.3) + ruby_dig (~> 0.0.2) + omniauth-rails_csrf_protection (1.0.2) + actionpack (>= 4.2) + omniauth (~> 2.0) + omniauth-shibboleth (1.3.0) + omniauth (>= 1.0.0) + open4 (1.3.4) + options (2.3.2) + orm_adapter (0.5.0) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.9.0) + ast (~> 2.4.1) + racc + pg (1.6.2-aarch64-linux) + pg (1.6.2-arm64-darwin) + pg (1.6.2-x86_64-linux) + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prime (0.1.4) + forwardable + singleton + prism (1.5.1) + process_executer (1.3.0) + progress_bar (1.3.4) + highline (>= 1.6) + options (~> 2.3.0) + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) + pstore (0.2.0) + psych (5.2.6) + date + stringio + public_suffix (6.0.2) + puma (7.0.4) + nio4r (~> 2.0) + pundit (2.5.2) + activesupport (>= 3.0.0) + pundit-matchers (4.0.0) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + racc (1.8.1) + rack (3.2.2) + rack-attack (6.7.0) + rack (>= 1.0, < 4) + rack-mini-profiler (4.0.1) + rack (>= 1.2.0) + rack-protection (4.1.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (7.1.5.2) + actioncable (= 7.1.5.2) + actionmailbox (= 7.1.5.2) + actionmailer (= 7.1.5.2) + actionpack (= 7.1.5.2) + actiontext (= 7.1.5.2) + actionview (= 7.1.5.2) + activejob (= 7.1.5.2) + activemodel (= 7.1.5.2) + activerecord (= 7.1.5.2) + activestorage (= 7.1.5.2) + activesupport (= 7.1.5.2) + bundler (>= 1.15.0) + railties (= 7.1.5.2) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + 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) + railties (7.1.5.2) + actionpack (= 7.1.5.2) + activesupport (= 7.1.5.2) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.0) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rchardet (1.10.0) + rdoc (6.15.0) + erb + psych (>= 4.0.0) + tsort + recaptcha (5.21.1) + regexp_parser (2.11.3) + reline (0.6.2) + io-console (~> 0.5) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) + rexml (3.4.4) + rollbar (3.6.2) + rouge (4.6.1) + rspec-collection_matchers (1.2.1) + rspec-expectations (>= 2.99.0.beta1) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (7.1.1) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.6) + rubocop (1.81.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.47.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-i18n (3.2.3) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) + rubocop-performance (1.26.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + ruby_dig (0.0.2) + rubyzip (2.4.1) + sawyer (0.9.2) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + securerandom (0.4.1) + selenium-webdriver (4.32.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + shellany (0.0.1) + shoulda (4.0.0) + shoulda-context (~> 2.0) + shoulda-matchers (~> 4.0) + shoulda-context (2.0.0) + shoulda-matchers (4.5.1) + activesupport (>= 4.2.0) + simpleidn (0.2.3) + singleton (0.3.0) + snaky_hash (2.0.3) + hashie (>= 0.1.0, < 6) + version_gem (>= 1.1.8, < 3) + spring (4.4.0) + spring-commands-rspec (1.0.4) + spring (>= 0.9.1) + spring-watcher-listen (2.1.0) + listen (>= 2.7, < 4.0) + spring (>= 4) + sprockets (4.2.2) + concurrent-ruby (~> 1.0) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + stringio (3.1.7) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + text (1.3.1) + thor (1.4.0) + timeout (0.4.3) + tomparse (0.4.2) + translation (1.41) + gettext (~> 3.2, >= 3.2.5, <= 3.4.9) + tsort (0.2.0) + turbo-rails (2.0.17) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + uniform_notifier (1.18.0) + uri (1.0.4) + version_gem (1.1.9) + warden (1.2.9) + rack (>= 2.0.9) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webmock (3.25.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + wicked_pdf (2.8.2) + activesupport + ostruct + wkhtmltopdf-binary (0.12.6.10) + xpath (3.2.0) + nokogiri (~> 1.8) + yard (0.9.37) + yard-tomdoc (0.7.1) + tomparse (>= 0.4.0) + yard + zeitwerk (2.6.18) + +PLATFORMS + aarch64-linux + arm64-darwin-21 + arm64-darwin-22 + x86_64-linux + +DEPENDENCIES + activerecord_json_validator + annotate + annotate_gem + api-pagination + autoprefixer-rails + better_errors + binding_of_caller + bootsnap + brakeman + bullet + bundle-audit + byebug + capybara + contact_us + cssbundling-rails + danger + database_cleaner + devise + devise_invitable + doorkeeper + dotenv-rails + dragonfly + dragonfly-s3_data_store + factory_bot_rails + faker + flag_shih_tzu + fuubar + guard + htmltoword + httparty + jbuilder + jsbundling-rails + jwt + kaminari + ledermann-rails-settings + listen + mail (= 2.7.1) + mimemagic + mocha + mysql2 + net-smtp + omniauth + omniauth-orcid + omniauth-rails_csrf_protection + omniauth-shibboleth + parallel + pg + progress_bar + puma + pundit + pundit-matchers + rack-attack (~> 6.6, >= 6.6.1) + rack-mini-profiler + rails (~> 7.1) + rails-controller-testing + recaptcha + rollbar + rspec-collection_matchers + rspec-rails + rubocop + rubocop-i18n + rubocop-performance + ruby-progressbar + selenium-webdriver + shoulda + spring + spring-commands-rspec + spring-watcher-listen + sprockets-rails + text + translation + turbo-rails + web-console + webmock + wicked_pdf + wkhtmltopdf-binary + yard + yard-tomdoc + +RUBY VERSION + ruby 3.1.4p223 + +BUNDLED WITH + 2.4.17 diff --git a/app/controllers/api/v2/base_api_controller.rb b/app/controllers/api/v2/base_api_controller.rb new file mode 100644 index 0000000000..f0f4f15bd0 --- /dev/null +++ b/app/controllers/api/v2/base_api_controller.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module Api + module V2 + class BaseApiController < ApplicationController # rubocop:todo Style/Documentation + # skipping the standard rails authenticity tokens passed in the UI + skip_before_action :verify_authenticity_token + + # call doorkeeper to authorize the request + before_action :doorkeeper_authorize!, except: %i[heartbeat] + # get details of server (e.g. DMPonline) and client app + before_action :base_response_content + + before_action :log_access + + # controller can respond to json format requests + respond_to :json + + # set up pages in response + before_action :pagination_params, except: %i[heartbeat] + + rescue_from Exception, with: :handle_exception + + # GET /api/v2/heartbeat + def heartbeat + render '/api/v2/heartbeat' + end + + # GET /me.json - recommended for doorkeeper gem + def me + respond_with @resource_owner + end + + private + + # define instance variable json and associated getter and setter methods + attr_accessor :json + + # rubocop:disable Metrics/AbcSize + def base_response_content + @application = ApplicationService.application_name + @caller = request.remote_ip if @client.blank? + @caller = @client.is_a?(User) ? @client.name(false) : @client.name if @client.present? + @scopes = doorkeeper_token.scopes.to_a if doorkeeper_token + return unless doorkeeper_token&.resource_owner_id + + @resource_owner = User.find(doorkeeper_token.resource_owner_id) + end + # rubocop:enable Metrics/AbcSize + + def log_access + if @client.present? + Rails.logger.info "Client (OAuth) application name: #{@client.name}" + Rails.logger.info "Client (OAuth) application uid: #{@client.uid}" + end + Rails.logger.info "Resource owner id: #{@resource_owner.id}" if @resource_owner + end + + def handle_exception(exception) + if exception.is_a?(Pundit::NotAuthorizedError) + handle_client_not_authorized + else + handle_internal_server_error(exception) + end + end + + def handle_internal_server_error(exception) + # log server errors + Rails.logger.error "Exception message: #{exception.message}" + + # inform client of server error + message = _('There was a problem in the server.') + @payload = { message: [message] } + render '/api/v2/error', status: :internal_server_error + end + + def handle_client_not_authorized + message = _('The client is not authorized to perform this action.') + @payload = { message: [message] } + render '/api/v2/error', status: :forbidden + end + + # retrieve the requested pagination params or use defaults + # only allow 100 per page as the max + def pagination_params + max_per_page = Rails.configuration.x.application.api_max_page_size + @page = params.fetch('page', 1).to_i + @per_page = params.fetch('per_page', max_per_page).to_i + @per_page = max_per_page if @per_page > max_per_page + end + + def paginate_response(results:) + results = results.page(@page).per(@per_page) + @total_items = results.total_count + results + end + end + end +end diff --git a/app/controllers/api/v2/plans_controller.rb b/app/controllers/api/v2/plans_controller.rb new file mode 100644 index 0000000000..7714e9c892 --- /dev/null +++ b/app/controllers/api/v2/plans_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Api + module V2 + class PlansController < BaseApiController # rubocop:todo Style/Documentation + respond_to :json + + # GET /api/v2/plans/:id + def show + raise Pundit::NotAuthorizedError unless @scopes.include?('read') + + @plan = Plan.find_by(id: params[:id]) + + raise Pundit::NotAuthorizedError unless @plan.present? + + plans_policy = PlansPolicy.new(@resource_owner, @plan) + raise Pundit::NotAuthorizedError unless plans_policy.show? + + @items = [@plan] + render '/api/v2/plans/index', status: :ok + end + + # GET /api/v2/plans + def index + raise Pundit::NotAuthorizedError unless @scopes.include?('read') + + @plans = PlansPolicy::Scope.new(@resource_owner).resolve + @items = paginate_response(results: @plans) + render '/api/v2/plans/index', status: :ok + end + end + end +end diff --git a/app/controllers/api/v2/templates_controller.rb b/app/controllers/api/v2/templates_controller.rb new file mode 100644 index 0000000000..5e732506dc --- /dev/null +++ b/app/controllers/api/v2/templates_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Api + module V2 + # provides a list of templates for API V2 + class TemplatesController < BaseApiController + respond_to :json + + # GET /api/v2/templates + def index + raise Pundit::NotAuthorizedError unless @scopes.include?('read') + + templates = Api::V2::TemplatesPolicy::Scope.new(@resource_owner).resolve + @items = paginate_response(results: templates) + render '/api/v2/templates/index', status: :ok + end + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1e2c29fcce..e5d2421e76 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -66,6 +66,9 @@ def store_location # rubocop:disable Metrics/AbcSize def after_sign_in_path_for(_resource) + # ensure oauth2 authorization flow is not interrupted + return session[:user_return_to] if user_is_in_oauth_flow + referer_path = URI(request.referer).path unless request.referer.nil? if from_external_domain? || referer_path.eql?(new_user_session_path) || referer_path.eql?(new_user_registration_path) || @@ -77,7 +80,10 @@ def after_sign_in_path_for(_resource) end # rubocop:enable Metrics/AbcSize - def after_sign_up_path_for(_resource) + def after_sign_up_path_for(_resource) # rubocop:todo Metrics/AbcSize + # ensure oauth2 authorization flow is not interrupted + return session[:user_return_to] if user_is_in_oauth_flow + referer_path = URI(request.referer).path unless request.referer.nil? if from_external_domain? || referer_path.eql?(new_user_session_path) || @@ -197,4 +203,8 @@ def render_respond_to_format_with_error_message(msg, url_or_path, http_status, e end end end + + def user_is_in_oauth_flow + session[:user_return_to].present? && session[:user_return_to].include?('/oauth/authorize') + end end diff --git a/app/controllers/concerns/plan_permitted_params.rb b/app/controllers/concerns/plan_permitted_params.rb new file mode 100644 index 0000000000..90363f9de8 --- /dev/null +++ b/app/controllers/concerns/plan_permitted_params.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +module PlanPermittedParams # rubocop:todo Metrics/ModuleLength, Style/Documentation + extend ActiveSupport::Concern + + def plan_permitted_params + [ + :created, + :title, + :description, + :language, + :ethical_issues_exist, + :ethical_issues_description, + :ethical_issues_report, + { dmp_ids: identifier_permitted_params }, + { contact: contributor_permitted_params }, + { contributors: contributor_permitted_params }, + { costs: cost_permitted_params }, + { project: project_permitted_params }, + { datasets: dataset_permitted_params } + ] + end + + def identifier_permitted_params + %i[ + type + identifier + ] + end + + def contributor_permitted_params + [ + :firstname, + :surname, + :mbox, + :role, + { affiliations: affiliation_permitted_params }, + { contributor_ids: identifier_permitted_params } + ] + end + + def affiliation_permitted_params + [ + :name, + :abbreviation, + { affiliation_ids: identifier_permitted_params } + ] + end + + def cost_permitted_params + %i[ + title + description + value + currency_code + ] + end + + def project_permitted_params + [ + :title, + :description, + :start_on, + :end_on, + { funding: funding_permitted_params } + ] + end + + def funding_permitted_params + [ + :name, + :funding_status, + { funder_ids: identifier_permitted_params }, + { grant_ids: identifier_permitted_params } + ] + end + + def dataset_permitted_params + [ + :title, + :doi_url, + :description, + :type, + :issued, + :language, + :personal_data, + :sensitive_data, + :keywords, + :data_quality_assurance, + :preservation_statement, + { dataset_ids: identifier_permitted_params }, + { metadata: metadatum_permitted_params }, + { security_and_privacy_statements: security_and_privacy_statement_permitted_params }, + { technical_resources: technical_resource_permitted_params }, + { distributions: distribution_permitted_params } + ] + end + + def metadatum_permitted_params + [ + :description, + :language, + { identifier: identifier_permitted_params } + ] + end + + def security_and_privacy_statement_permitted_params + %i[ + title + description + ] + end + + def technical_resource_permitted_params + [ + :description, + { identifier: identifier_permitted_params } + ] + end + + def distribution_permitted_params + [ + :title, + :description, + :format, + :byte_size, + :access_url, + :download_url, + :data_access, + :available_until, + { licenses: license_permitted_params }, + { host: host_permitted_params } + ] + end + + def license_permitted_params + %i[ + license_ref + start_date + ] + end + + def host_permitted_params + [ + :title, + :description, + :supports_versioning, + :backup_type, + :backup_frequency, + :storage_type, + :availability, + :geo_location, + :certified_with, + :pid_system, + { host_ids: identifier_permitted_params } + ] + end +end diff --git a/app/models/oauth_access_grant.rb b/app/models/oauth_access_grant.rb new file mode 100644 index 0000000000..f39c307af9 --- /dev/null +++ b/app/models/oauth_access_grant.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: oauth_access_grants +# +# id: :integer +# resource_owner_id: :integer +# application_id: :integer +# token: :string +# expires_in: :integer +# redirect_uri: :text +# scopes: :string +# created_at: :datetime +# revoked_at: :datetime + +class OauthAccessGrant < ApplicationRecord +end diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb new file mode 100644 index 0000000000..b17e824e72 --- /dev/null +++ b/app/models/oauth_access_token.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: oauth_access_tokens +# +# id: :integer +# resource_owner_id: :integer +# application_id: :integer +# token: :string +# refresh_token: :string +# expires_in: :integer +# scopes: :string +# created_at: :datetime +# revoked_at: :datetime +# previous_refresh_token: :string + +class OauthAccessToken < ApplicationRecord +end diff --git a/app/models/oauth_application.rb b/app/models/oauth_application.rb new file mode 100644 index 0000000000..4779e53cc4 --- /dev/null +++ b/app/models/oauth_application.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: oauth_applications +# +# id: :integer +# name: :string +# uid: :string +# secret: :string +# redirect_uri: :text +# scopes: :string +# confidential: :boolean +# created_at: :datetime +# updated_at: :datetime + +class OauthApplication < ApplicationRecord +end diff --git a/app/models/user.rb b/app/models/user.rb index 94d0035eec..8732c2f770 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -98,6 +98,12 @@ class User < ApplicationRecord has_and_belongs_to_many :notifications, dependent: :destroy, join_table: 'notification_acknowledgements' + has_many :access_grants, class_name: 'Doorkeeper::AccessGrant', foreign_key: :resource_owner_id, + dependent: :delete_all + + has_many :access_tokens, class_name: 'Doorkeeper::AccessToken', foreign_key: :resource_owner_id, + dependent: :delete_all + # =============== # = Validations = # =============== diff --git a/app/policies/api/v2/plans_policy.rb b/app/policies/api/v2/plans_policy.rb new file mode 100644 index 0000000000..98a22a5025 --- /dev/null +++ b/app/policies/api/v2/plans_policy.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Api + module V2 + # Security rules for API V2 Plan endpoints + class PlansPolicy < ApplicationPolicy + # overriding the initializer due to resource owner / user + # not needing to be logged in for client app to make requests + def initialize(resource_owner, plan = nil) # rubocop:todo Lint/MissingSuper + @resource_owner = resource_owner + @plan = plan + end + + def show? + @plan.roles.where(user_id: @resource_owner.id, active: true).exists? + end + + class Scope < Scope # rubocop:todo Style/Documentation + def initialize(resource_owner) # rubocop:todo Lint/MissingSuper + @resource_owner = resource_owner + end + + def resolve + Plan.joins(:roles) + .where(roles: { user_id: @resource_owner.id, active: true }) + .distinct + end + end + end + end +end diff --git a/app/policies/api/v2/templates_policy.rb b/app/policies/api/v2/templates_policy.rb new file mode 100644 index 0000000000..dacdfe48a2 --- /dev/null +++ b/app/policies/api/v2/templates_policy.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Api + module V2 + class TemplatesPolicy < ApplicationPolicy + class Scope < Scope # rubocop:todo Style/Documentation + def initialize(resource_owner) # rubocop:todo Lint/MissingSuper + @resource_owner = resource_owner + end + + def resolve + # create the sql where clause + where_clause = <<-SQL + (visibility = 0 AND org_id = ?) OR + (visibility = 1 AND customization_of IS NULL) + SQL + + # get the templates + Template + .includes(org: :identifiers) + .joins(:org) + .published + .where( + where_clause, + @resource_owner.org&.id + ) + .order(:title) + end + end + end + end +end diff --git a/app/presenters/api/v2/api_presenter.rb b/app/presenters/api/v2/api_presenter.rb new file mode 100644 index 0000000000..1ad7290ab6 --- /dev/null +++ b/app/presenters/api/v2/api_presenter.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Api + module V2 + # Generic helper methods for API V2 + class ApiPresenter + class << self + def boolean_to_yes_no_unknown(value:) + return 'unknown' unless value.present? + + value ? 'yes' : 'no' + end + end + end + end +end diff --git a/app/presenters/api/v2/contributor_presenter.rb b/app/presenters/api/v2/contributor_presenter.rb new file mode 100644 index 0000000000..77232c2838 --- /dev/null +++ b/app/presenters/api/v2/contributor_presenter.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Api + module V2 + # Helper class for the API V2 contributors views + class ContributorPresenter + class << self + # Convert the specified role into a CRediT Taxonomy URL + def role_as_uri(role:) + return nil unless role.present? + return 'other' if role.to_s.casecmp('other').zero? + + "#{Contributor::ONTOLOGY_BASE_URL}/#{role.to_s.downcase.tr('_', '-')}" + end + + def contributor_id(identifiers:) + identifiers.find { |id| id.identifier_scheme.name == 'orcid' } + end + end + end + end +end diff --git a/app/presenters/api/v2/funding_presenter.rb b/app/presenters/api/v2/funding_presenter.rb new file mode 100644 index 0000000000..c878daadb6 --- /dev/null +++ b/app/presenters/api/v2/funding_presenter.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Api + module V2 + # Helper class for the API V2 funding section + class FundingPresenter + class << self + # If the plan has a grant number then it has been awarded/granted + # otherwise it is 'planned' + def status(plan:) + return 'planned' unless plan.present? + + case plan.funding_status + when 'funded' + 'granted' + when 'denied' + 'rejected' + else + 'planned' + end + end + end + end + end +end diff --git a/app/presenters/api/v2/language_presenter.rb b/app/presenters/api/v2/language_presenter.rb new file mode 100644 index 0000000000..1b9d8aebce --- /dev/null +++ b/app/presenters/api/v2/language_presenter.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module Api + module V2 + # Helper class for the API V2 language values + class LanguagePresenter + class << self + LANGUAGE_MAP = { + aa: 'aar', ab: 'abk', af: 'afr', ak: 'aka', am: 'amh', ar: 'ara', an: 'arg', + as: 'asm', av: 'ava', ae: 'ave', ay: 'aym', az: 'aze', + + ba: 'bak', bm: 'bam', be: 'bel', bn: 'ben', bh: 'bih', bi: 'bis', bo: 'tib', + bs: 'bos', br: 'bre', bg: 'bul', + + ca: 'cat', cs: 'cze', ch: 'cha', ce: 'che', cu: 'chu', cv: 'chv', co: 'cos', + cr: 'cre', cy: 'wel', + + da: 'dan', de: 'deu', dv: 'div', dz: 'dzo', + + el: 'gre', en: 'eng', eo: 'epo', es: 'spa', et: 'est', eu: 'baq', ee: 'ewe', + + fo: 'fao', fa: 'per', fj: 'fij', fi: 'fin', fr: 'fre', fy: 'fry', ff: 'ful', + + gd: 'gla', ga: 'gle', gl: 'glg', gv: 'glv', gn: 'grn', gu: 'guj', + + ht: 'hat', ha: 'hau', he: 'heb', hz: 'her', hi: 'hin', ho: 'hmo', hr: 'hrv', + hu: 'hun', hy: 'arm', + + ig: 'ibo', io: 'ido', ii: 'iii', iu: 'iku', ie: 'ile', ia: 'ina', id: 'ind', + ik: 'ipk', is: 'ice', it: 'ita', + + jv: 'jav', ja: 'jpn', + + kl: 'kal', kn: 'kan', ks: 'kas', kr: 'kau', kk: 'kaz', km: 'khm', ki: 'kik', + ky: 'kir', kv: 'kom', kg: 'kon', ko: 'kor', kj: 'kua', ku: 'kur', ka: 'geo', + kw: 'cor', + + lo: 'lao', la: 'lat', lv: 'lav', li: 'lim', ln: 'lin', lt: 'lit', lb: 'ltz', + lu: 'lub', lg: 'lug', + + mk: 'mac', mh: 'mah', ml: 'mal', mi: 'mao', mr: 'mar', ms: 'may', mg: 'mlg', + mt: 'mlt', mn: 'mon', my: 'bur', + + na: 'nau', nv: 'nav', nr: 'nbl', nd: 'nde', ng: 'ndo', ne: 'nep', nl: 'dut', + nn: 'nno', nb: 'nob', no: 'nor', ny: 'nya', + + oc: 'oci', oj: 'oji', or: 'ori', om: 'orm', os: 'oss', + + pa: 'pan', pi: 'pli', pl: 'pol', pt: 'por', ps: 'pus', + + qu: 'que', + + rm: 'roh', ro: 'rum', rn: 'run', ru: 'rus', rw: 'kin', + + sg: 'sag', sa: 'san', si: 'sin', sk: 'slo', sl: 'slv', se: 'sme', sm: 'smo', + sn: 'sna', sd: 'snd', so: 'som', st: 'sot', sq: 'alb', sc: 'srd', sr: 'srp', + ss: 'ssw', su: 'sun', sw: 'swa', sv: 'swe', + + ty: 'tah', ta: 'tam', tt: 'tat', te: 'tel', tg: 'tgk', tl: 'tgl', th: 'tha', + ti: 'tir', to: 'ton', tn: 'tsn', ts: 'tso', tk: 'tuk', tr: 'tur', tw: 'twi', + + ug: 'uig', uk: 'ukr', ur: 'urd', uz: 'uzb', + + ve: 'ven', vi: 'vie', vo: 'vol', + + wa: 'wln', wo: 'wol', + + xh: 'xho', + + yi: 'yid', yo: 'yor', + + za: 'zha', zh: 'chi', zu: 'zul' + }.freeze + + # Convert the incoming 2 (e.g. en - ISO 639-1) or 2+region (e.g. en-UK) + # into the 3 character code (e.g. eng - ISO 639-2) + def three_char_code(lang:) + two_char_code = lang.to_s.split('-').first + LANGUAGE_MAP[two_char_code.to_sym] + end + end + end + end +end diff --git a/app/presenters/api/v2/org_presenter.rb b/app/presenters/api/v2/org_presenter.rb new file mode 100644 index 0000000000..5302acb7a7 --- /dev/null +++ b/app/presenters/api/v2/org_presenter.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Api + module V2 + # Helper class for the API V2 affiliation sections + class OrgPresenter + class << self + def affiliation_id(identifiers:) + ident = identifiers.find { |id| id.identifier_scheme&.name == 'ror' } + return ident if ident.present? + + identifiers.find { |id| id.identifier_scheme&.name == 'fundref' } + end + end + end + end +end diff --git a/app/presenters/api/v2/pagination_presenter.rb b/app/presenters/api/v2/pagination_presenter.rb new file mode 100644 index 0000000000..1b8fbc1109 --- /dev/null +++ b/app/presenters/api/v2/pagination_presenter.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Api + module V2 + # Helper class for genewric API V2 pagination + class PaginationPresenter + def initialize(current_url:, per_page:, total_items:, current_page: 1) + @url = current_url + @per_page = per_page + @total_items = total_items + @page = current_page + end + + def url_without_pagination + return nil unless @url.present? && @url.is_a?(String) + + url = @url.gsub(/per_page=\d+/, '') + .gsub(/page=\d+/, '') + .gsub(/(&)+$/, '').gsub(/\?$/, '') + + (url.include?('?') ? "#{url}&" : "#{url}?") + end + + def prev_page? + total_pages > 1 && @page != 1 + end + + def next_page? + total_pages > 1 && @page < total_pages + end + + def prev_page_link + "#{url_without_pagination}page=#{@page - 1}&per_page=#{@per_page}" + end + + def next_page_link + "#{url_without_pagination}page=#{@page + 1}&per_page=#{@per_page}" + end + + private + + def total_pages + return 1 unless @total_items.present? && @per_page.present? && + @total_items.positive? && @per_page.positive? + + (@total_items.to_f / @per_page).ceil + end + end + end +end diff --git a/app/presenters/api/v2/plan_presenter.rb b/app/presenters/api/v2/plan_presenter.rb new file mode 100644 index 0000000000..204cd68011 --- /dev/null +++ b/app/presenters/api/v2/plan_presenter.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Api + module V2 + # Helper class for the API V2 project / DMP + class PlanPresenter + attr_reader :data_contact, :contributors, :costs + + def initialize(plan:) + @contributors = [] + return unless plan.present? + + @plan = plan + + @data_contact = @plan.owner + + # Attach the first data_curation role as the data_contact, otherwise + # add the contributor to the contributors array + @plan.contributors.each do |contributor| + @data_contact = contributor if contributor.data_curation? && @data_contact.nil? + @contributors << contributor + end + + @costs = plan_costs(plan: @plan) + end + + # Extract the ARK or DOI for the DMP OR use its URL if none exists + def identifier + doi = @plan.identifiers.select do |id| + ::Plan::DMP_ID_TYPES.include?(id.identifier_format) + end + return doi.first if doi.first.present? + + # if no DOI then use the URL for the API's 'show' method + Identifier.new(value: Rails.application.routes.url_helpers.api_v2_plan_url(@plan)) + end + + private + + # Retrieve the answers that have the Budget theme + def plan_costs(plan:) + theme = Theme.where(title: 'Cost').first + return [] unless theme.present? + + # TODO: define a new 'Currency' question type that includes a float field + # any currency type selector (e.g GBP or USD) + answers = plan.answers.includes(question: :themes).select do |answer| + answer.question.themes.include?(theme) + end + + answers.map do |answer| + # TODO: Investigate whether question level guidance should be the description + { title: answer.question.text, description: nil, + currency_code: 'usd', value: answer.text } + end + end + end + end +end diff --git a/app/presenters/api/v2/research_output_presenter.rb b/app/presenters/api/v2/research_output_presenter.rb new file mode 100644 index 0000000000..fc44055b2d --- /dev/null +++ b/app/presenters/api/v2/research_output_presenter.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Api + module V2 + # Helper methods for research outputs + class ResearchOutputPresenter + attr_reader :dataset_id, :preservation_statement, :security_and_privacy, :license_start_date, + :data_quality_assurance, :distributions, :metadata, :technical_resources + + def initialize(output:) + @research_output = output + return unless output.is_a?(ResearchOutput) + + @plan = output.plan + @dataset_id = identifier + + load_narrative_content + + @license_start_date = determine_license_start_date(output: output) + end + + private + + def identifier + Identifier.new(identifiable: @research_output, value: @research_output.id) + end + + def determine_license_start_date(output:) + return nil unless output.present? + return output.release_date.to_formatted_s(:iso8601) if output.release_date.present? + + output.created_at.to_formatted_s(:iso8601) + end + + def load_narrative_content + @preservation_statement = '' + @security_and_privacy = [] + @data_quality_assurance = '' + + # Disabling rubocop here since a guard clause would make the line too long + # rubocop:disable Style/GuardClause + if Rails.configuration.x.madmp.extract_preservation_statements_from_themed_questions + @preservation_statement = fetch_q_and_a_as_single_statement(themes: %w[Preservation]) + end + if Rails.configuration.x.madmp.extract_security_privacy_statements_from_themed_questions + @security_and_privacy = fetch_q_and_a(themes: ['Ethics & privacy', 'Storage & security']) + end + if Rails.configuration.x.madmp.extract_data_quality_statements_from_themed_questions + @data_quality_assurance = fetch_q_and_a_as_single_statement(themes: ['Data Collection']) + end + # rubocop:enable Style/GuardClause + end + + def fetch_q_and_a_as_single_statement(themes:) + fetch_q_and_a(themes: themes).collect { |item| item[:description] }.join('
') + end + + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def fetch_q_and_a(themes:) + return [] unless themes.is_a?(Array) && themes.any? + + ret = themes.map do |theme| + qs = @plan.questions.select { |q| q.themes.collect(&:title).include?(theme) } + descr = qs.map do |q| + a = @plan.answers.find { |ans| ans.question_id = q.id } + next unless a.present? && !a.blank? + + "Question: #{q.text}
Answer: #{a.text}" + end + { title: theme, description: descr } + end + ret.select { |item| item[:description].present? } + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + end + end +end diff --git a/app/presenters/api/v2/template_presenter.rb b/app/presenters/api/v2/template_presenter.rb new file mode 100644 index 0000000000..2d32a45d37 --- /dev/null +++ b/app/presenters/api/v2/template_presenter.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Api + module V2 + # Helper class for the API V2 template info + class TemplatePresenter + def initialize(template:) + @template = template + end + + # If the plan has a grant number then it has been awarded/granted + # otherwise it is 'planned' + def title + return @template.title unless @template.customization_of.present? + + "#{@template.title} - with additional questions for #{@template.org.name}" + end + end + end +end diff --git a/app/services/api/v2/conversion_service.rb b/app/services/api/v2/conversion_service.rb new file mode 100644 index 0000000000..4a9a932878 --- /dev/null +++ b/app/services/api/v2/conversion_service.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Api + module V2 + # Helper service that translates to/from the RDA common standard + class ConversionService + class << self + # Converts a boolean field to [yes, no, unknown] + def boolean_to_yes_no_unknown(value) + return 'yes' if [true, 1].include?(value) + + return 'no' if [false, 0].include?(value) + + 'unknown' + end + + # Converts a [yes, no, unknown] field to boolean (or nil) + def yes_no_unknown_to_boolean(value) + return true if value&.downcase == 'yes' + + return nil if value.blank? || value&.downcase == 'unknown' + + false + end + + # Converts the context and value into an Identifier with a psuedo + # IdentifierScheme for display in JSON partials. Which will result in: + # { type: 'context', identifier: 'value' } + def to_identifier(context:, value:) + return nil unless value.present? && context.present? + + scheme = IdentifierScheme.new(name: context) + Identifier.new(value: value, identifier_scheme: scheme) + end + end + end + end +end diff --git a/app/views/api/v2/_standard_response.json.jbuilder b/app/views/api/v2/_standard_response.json.jbuilder new file mode 100644 index 0000000000..372c42cb31 --- /dev/null +++ b/app/views/api/v2/_standard_response.json.jbuilder @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# locals: response, request, total_items + +total_items ||= 0 + +paginator = Api::V2::PaginationPresenter.new(current_url: request.path, + per_page: @per_page, + total_items: total_items, + current_page: @page) + +json.prettify! +json.ignore_nil! + +json.application @application +json.source "#{request.method} #{request.path}" +json.time Time.now.to_formatted_s(:iso8601) +json.caller @caller +json.code response.status +json.message Rack::Utils::HTTP_STATUS_CODES[response.status] + +if response.status == 200 + + # Pagination Links + if total_items.positive? + json.page @page + json.per_page @per_page + json.total_items total_items + + # Prepare the base URL by removing the old pagination params + json.prev paginator.prev_page_link if paginator.prev_page? + json.next paginator.next_page_link if paginator.next_page? + else + json.total_items 0 + end + +end diff --git a/app/views/api/v2/contributors/_show.json.jbuilder b/app/views/api/v2/contributors/_show.json.jbuilder new file mode 100644 index 0000000000..267ff7e3d2 --- /dev/null +++ b/app/views/api/v2/contributors/_show.json.jbuilder @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# locals: contributor, is_contact + +is_contact ||= false + +json.name contributor.is_a?(User) ? contributor.name(false) : contributor.name +json.mbox contributor.email + +if !is_contact && contributor.selected_roles.any? + roles = contributor.selected_roles.map do |role| + Api::V2::ContributorPresenter.role_as_uri(role: role) + end + json.role roles if roles.any? +end + +if contributor.org.present? + json.affiliation do + json.partial! 'api/v2/orgs/show', org: contributor.org + end +end + +orcid = contributor.identifier_for_scheme(scheme: 'orcid') +if orcid.present? + id = Api::V2::ContributorPresenter.contributor_id( + identifiers: contributor.identifiers + ) + if is_contact + json.contact_id do + json.partial! 'api/v2/identifiers/show', identifier: id + end + else + json.contributor_id do + json.partial! 'api/v2/identifiers/show', identifier: id + end + end +end diff --git a/app/views/api/v2/datasets/_show.json.jbuilder b/app/views/api/v2/datasets/_show.json.jbuilder new file mode 100644 index 0000000000..1581dff785 --- /dev/null +++ b/app/views/api/v2/datasets/_show.json.jbuilder @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +# locals: output + +if output.is_a?(ResearchOutput) + presenter = Api::V2::ResearchOutputPresenter.new(output: output) + + json.type output.output_type + json.title output.title + json.doi_url output.doi_url + json.description output.description + json.personal_data Api::V2::ApiPresenter.boolean_to_yes_no_unknown(value: output.personal_data) + json.sensitive_data Api::V2::ApiPresenter.boolean_to_yes_no_unknown(value: output.sensitive_data) + json.issued output.release_date&.to_formatted_s(:iso8601) + + json.preservation_statement presenter.preservation_statement + json.security_and_privacy presenter.security_and_privacy + json.data_quality_assurance presenter.data_quality_assurance + + json.dataset_id do + json.partial! "api/v2/identifiers/show", identifier: presenter.dataset_id + end + + json.distribution output.repositories do |repository| + json.title "Anticipated distribution for #{output.title}" + json.byte_size output.byte_size + json.data_access output.access + + json.host do + json.title repository.name + json.description repository.description + json.url repository.homepage + + # DMPTool extensions to the RDA common metadata standard + json.dmproadmap_host_id do + json.type "url" + json.identifier repository.uri + end + end + + if output.license.present? + json.license [output.license] do |license| + json.license_ref license.uri + json.start_date presenter.license_start_date + end + end + end + + json.metadata output.metadata_standards do |metadata_standard| + website = metadata_standard.locations.find { |loc| loc["type"] == "website" } + website = { url: "" } unless website.present? + + descr_array = [metadata_standard.title, metadata_standard.description, website["url"]] + json.description descr_array.join(" - ") + + json.metadata_standard_id do + json.type "url" + json.identifier metadata_standard.uri + end + end + + json.technical_resource [] + + if output.plan.research_domain_id.present? + research_domain = ResearchDomain.find_by(id: output.plan.research_domain_id) + if research_domain.present? + combined = "#{research_domain.identifier} - #{research_domain.label}" + json.keyword [research_domain.label, combined] + end + end + +else + json.type "dataset" + json.title "Generic dataset" + json.description "No individual datasets have been defined for this DMP." + + if output.research_domain_id.present? + research_domain = ResearchDomain.find_by(id: output.research_domain_id) + if research_domain.present? + combined = "#{research_domain.identifier} - #{research_domain.label}" + json.keyword [research_domain.label, combined] + end + end +end diff --git a/app/views/api/v2/error.json.jbuilder b/app/views/api/v2/error.json.jbuilder new file mode 100644 index 0000000000..ac08f26d9f --- /dev/null +++ b/app/views/api/v2/error.json.jbuilder @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +json.partial! 'api/v2/standard_response' + +# json.items [] +json.message @payload[:message] +json.details @payload[:details] diff --git a/app/views/api/v2/heartbeat.json.jbuilder b/app/views/api/v2/heartbeat.json.jbuilder new file mode 100644 index 0000000000..70b165b95d --- /dev/null +++ b/app/views/api/v2/heartbeat.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +json.partial! 'api/v2/standard_response' + +json.items [] diff --git a/app/views/api/v2/identifiers/_show.json.jbuilder b/app/views/api/v2/identifiers/_show.json.jbuilder new file mode 100644 index 0000000000..c219222aee --- /dev/null +++ b/app/views/api/v2/identifiers/_show.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# locals: identifier + +json.type identifier&.identifier_format +json.identifier identifier&.value diff --git a/app/views/api/v2/me.json.jbuilder b/app/views/api/v2/me.json.jbuilder new file mode 100644 index 0000000000..18a1e7c114 --- /dev/null +++ b/app/views/api/v2/me.json.jbuilder @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +json.partial! 'api/v2/standard_response' + +if current_user.present? + json.items [current_user] do |user| + json.name [user.surname, user.firstname].join(', ') + json.mbox user.email + json.token user.ui_token + + if user.org.present? && ['No funder', 'Non Partner Institution'].exclude?(user.org.name) + json.affiliation do + json.partial! 'api/v2/orgs/show', org: user.org + end + end + + orcid = user.identifier_for_scheme(scheme: 'orcid') + if orcid.present? + json.user_id do + json.partial! 'api/v2/identifiers/show', identifier: orcid + end + end + end + +else + json.items [] +end diff --git a/app/views/api/v2/orgs/_show.json.jbuilder b/app/views/api/v2/orgs/_show.json.jbuilder new file mode 100644 index 0000000000..934e429a17 --- /dev/null +++ b/app/views/api/v2/orgs/_show.json.jbuilder @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# locals: org + +json.name org.name +json.abbreviation org.abbreviation +json.region org.region&.abbreviation + +if org.identifiers.any? + json.affiliation_id do + id = Api::V2::OrgPresenter.affiliation_id(identifiers: org.identifiers) + json.partial! 'api/v2/identifiers/show', identifier: id + end +end diff --git a/app/views/api/v2/plans/_cost.json.jbuilder b/app/views/api/v2/plans/_cost.json.jbuilder new file mode 100644 index 0000000000..ad36e3540e --- /dev/null +++ b/app/views/api/v2/plans/_cost.json.jbuilder @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# locals: cost + +json.title cost[:title] +json.description cost[:description] +json.currency_code cost[:currency_code] +json.value cost[:value] diff --git a/app/views/api/v2/plans/_funding.json.jbuilder b/app/views/api/v2/plans/_funding.json.jbuilder new file mode 100644 index 0000000000..35786ac2dc --- /dev/null +++ b/app/views/api/v2/plans/_funding.json.jbuilder @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# locals: plan + +json.name plan.funder&.name + +if plan.funder.present? + id = Api::V2::OrgPresenter.affiliation_id(identifiers: plan.funder.identifiers) + + if id.present? + json.funder_id do + json.partial! 'api/v2/identifiers/show', identifier: id + end + end +end + +if plan.grant_id.present? && plan.grant.present? + json.grant_id do + json.partial! 'api/v2/identifiers/show', identifier: plan.grant + end +end + +json.funding_status Api::V2::FundingPresenter.status(plan: plan) + +# DMPTool extensions to the RDA common metadata standard +# ------------------------------------------------------ + +# We collect a user entered ID on the form, so this is a way to convey it to other systems +# The ID would typically be something relevant to the funder or research organization +if plan.identifier.present? + json.dmproadmap_funding_opportunity_id do + json.partial! 'api/v2/identifiers/show', identifier: Identifier.new(identifiable: plan, + value: plan.identifier) + end +end + +# Since the Plan owner (aka contact) and contributor orgs could be different than the +# one associated with the Plan, we add it here. +json.dmproadmap_funded_affiliations [plan.org] do |funded_org| + json.partial! 'api/v2/orgs/show', org: funded_org +end diff --git a/app/views/api/v2/plans/_project.json.jbuilder b/app/views/api/v2/plans/_project.json.jbuilder new file mode 100644 index 0000000000..9bf3f97fea --- /dev/null +++ b/app/views/api/v2/plans/_project.json.jbuilder @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# locals: plan + +json.title plan.title +json.description plan.description + +start_date = plan.start_date || Time.now +json.start start_date.to_formatted_s(:iso8601) + +end_date = plan.end_date || (Time.now + 2.years) +json.end end_date&.to_formatted_s(:iso8601) + +if plan.funder.present? || plan.grant_id.present? + json.funding [plan] do + json.partial! 'api/v2/plans/funding', plan: plan + end +end diff --git a/app/views/api/v2/plans/_show.json.jbuilder b/app/views/api/v2/plans/_show.json.jbuilder new file mode 100644 index 0000000000..0de52775a3 --- /dev/null +++ b/app/views/api/v2/plans/_show.json.jbuilder @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# locals: plan + +json.schema 'https://github.com/RDA-DMP-Common/RDA-DMP-Common-Standard/tree/master/examples/JSON/JSON-schema/1.0' + +presenter = Api::V2::PlanPresenter.new(plan: plan) + +# Note the symbol of the dmproadmap json object +# nested in extensions which is the container for the json template object, etc. + +# A JSON representation of a Data Management Plan in the +# RDA Common Standard format +json.title plan.title +json.description plan.description +json.language Api::V2::LanguagePresenter.three_char_code( + lang: LocaleService.default_locale +) +json.created plan.created_at.to_formatted_s(:iso8601) +json.modified plan.updated_at.to_formatted_s(:iso8601) + +json.ethical_issues_exist Api::V2::ConversionService.boolean_to_yes_no_unknown(plan.ethical_issues) +json.ethical_issues_description plan.ethical_issues_description +json.ethical_issues_report plan.ethical_issues_report + +id = presenter.identifier +if id.present? + json.dmp_id do + json.partial! 'api/v2/identifiers/show', identifier: id + end +end + +if presenter.data_contact.present? + json.contact do + json.partial! 'api/v2/contributors/show', contributor: presenter.data_contact, + is_contact: true + end +end + +unless @minimal + if presenter.contributors.any? + json.contributor presenter.contributors do |contributor| + json.partial! 'api/v2/contributors/show', contributor: contributor, + is_contact: false + end + end + + if presenter.costs.any? + json.cost presenter.costs do |cost| + json.partial! 'api/v2/plans/cost', cost: cost + end + end + + json.project [plan] do |pln| + json.partial! 'api/v2/plans/project', plan: pln + end + + outputs = plan.research_outputs.any? ? plan.research_outputs : [plan] + + json.dataset outputs do |output| + json.partial! "api/v2/datasets/show", output: output + end + + json.extension [plan.template] do |template| + json.set! :dmproadmap do + json.template do + json.id template.id + json.title template.title + end + end + end +end diff --git a/app/views/api/v2/plans/index.json.jbuilder b/app/views/api/v2/plans/index.json.jbuilder new file mode 100644 index 0000000000..f19f41d1dc --- /dev/null +++ b/app/views/api/v2/plans/index.json.jbuilder @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +json.partial! 'api/v2/standard_response', total_items: @total_items + +json.items @items do |item| + json.dmp do + json.partial! 'api/v2/plans/show', plan: item + end +end diff --git a/app/views/api/v2/templates/index.json.jbuilder b/app/views/api/v2/templates/index.json.jbuilder new file mode 100644 index 0000000000..cdb35f54fb --- /dev/null +++ b/app/views/api/v2/templates/index.json.jbuilder @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +json.partial! 'api/v2/standard_response', total_items: @total_items + +json.items @items do |template| + presenter = Api::V2::TemplatePresenter.new(template: template) + + json.dmp_template do + json.title presenter.title + json.description template.description + json.version template.version + json.created template.created_at.to_formatted_s(:iso8601) + json.modified template.updated_at.to_formatted_s(:iso8601) + + json.affiliation do + json.partial! 'api/v2/orgs/show', org: template.org + end + + json.template_id do + identifier = Api::V2::ConversionService.to_identifier(context: @application, + value: template.id) + json.partial! 'api/v2/identifiers/show', identifier: identifier + end + end +end diff --git a/app/views/doorkeeper/applications/_delete_form.html.erb b/app/views/doorkeeper/applications/_delete_form.html.erb new file mode 100644 index 0000000000..b6377d35f5 --- /dev/null +++ b/app/views/doorkeeper/applications/_delete_form.html.erb @@ -0,0 +1,6 @@ +<%- submit_btn_css ||= 'btn btn-link' %> +<%= form_tag oauth_application_path(application), method: :delete do %> + <%= submit_tag t('doorkeeper.applications.buttons.destroy'), + onclick: "return confirm('#{ t('doorkeeper.applications.confirmations.destroy') }')", + class: submit_btn_css, style: "border-radius: 0;" %> +<% end %> \ No newline at end of file diff --git a/app/views/doorkeeper/applications/_form.html.erb b/app/views/doorkeeper/applications/_form.html.erb new file mode 100644 index 0000000000..5aeeeaf9ab --- /dev/null +++ b/app/views/doorkeeper/applications/_form.html.erb @@ -0,0 +1,42 @@ +<%= form_for application, url: doorkeeper_submit_path(application), as: :doorkeeper_application, html: { role: 'form' } do |f| %> + <% if application.errors.any? %> +

<%= t('doorkeeper.applications.form.error') %>

+ <% end %> + +
+ <%= f.label :name, class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_field :name, class: "form-control #{ 'is-invalid' if application.errors[:name].present? }", required: true, style: "border: 1px solid #ced4da;" %> + <%= doorkeeper_errors_for application, :name %> +
+
+ +
+ <%= f.label "Scope(s)", class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_field :scopes, class: "form-control #{ 'has-error' if application.errors[:scopes].present? }", style: "border: 1px solid #ced4da;" %> + <%= doorkeeper_errors_for application, :scopes %> + + <%= "Separate multiple scopes with spaces." %> + +
+
+ +
+ <%= f.label nil, 'Redirect URI(s)'.html_safe, class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_area :redirect_uri, class: "form-control #{ 'is-invalid' if application.errors[:redirect_uri].present? }", style: "border: 1px solid #ced4da;" %> + <%= doorkeeper_errors_for application, :redirect_uri %> + + <%= "Separate multiple URIs with spaces." %> + +
+
+ +
+
+ <%= f.submit t('doorkeeper.applications.buttons.submit'), class: 'btn btn-primary' %> + <%= link_to t('doorkeeper.applications.buttons.cancel'), oauth_applications_path, class: 'btn btn-secondary' %> +
+
+<% end %> \ No newline at end of file diff --git a/app/views/doorkeeper/applications/edit.html.erb b/app/views/doorkeeper/applications/edit.html.erb new file mode 100644 index 0000000000..95fb9ab2d5 --- /dev/null +++ b/app/views/doorkeeper/applications/edit.html.erb @@ -0,0 +1,5 @@ +
+

<%= t('.title') %>

+
+ +<%= render 'form', application: @application %> \ No newline at end of file diff --git a/app/views/doorkeeper/applications/index.html.erb b/app/views/doorkeeper/applications/index.html.erb new file mode 100644 index 0000000000..835e0d917f --- /dev/null +++ b/app/views/doorkeeper/applications/index.html.erb @@ -0,0 +1,34 @@ +
+

<%= t('.title') %>

+
+ +

<%= link_to t('.new'), new_oauth_application_path, class: 'btn btn-secondary', style: "border-radius: 0;" %>

+ + + + + + + + + + + + <% @applications.each do |application| %> + + + + + + + <% end %> + +
<%= t('.name') %><%= t('.callback_url') %><%= t('.actions') %>
+ <%= link_to application.name, oauth_application_path(application) %> + + <%= simple_format(application.redirect_uri) %> + + <%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %> + + <%= render 'delete_form', application: application %> +
\ No newline at end of file diff --git a/app/views/doorkeeper/applications/new.html.erb b/app/views/doorkeeper/applications/new.html.erb new file mode 100644 index 0000000000..95fb9ab2d5 --- /dev/null +++ b/app/views/doorkeeper/applications/new.html.erb @@ -0,0 +1,5 @@ +
+

<%= t('.title') %>

+
+ +<%= render 'form', application: @application %> \ No newline at end of file diff --git a/app/views/doorkeeper/applications/show.html.erb b/app/views/doorkeeper/applications/show.html.erb new file mode 100644 index 0000000000..71e06b573e --- /dev/null +++ b/app/views/doorkeeper/applications/show.html.erb @@ -0,0 +1,42 @@ +<%= fields_for @application, as: :doorkeeper_application, html: { role: 'form' } do |f| %> +
+ <%= f.label :name, class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_field :name, class: "form-control", required: true, style: "border: 1px solid #ced4da;", readonly: true %> +
+
+ +
+ <%= f.label nil, "Client ID".html_safe, class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_field :uid, class: "form-control", required: true, style: "border: 1px solid #ced4da;", readonly: true %> +
+
+ +
+ <%= f.label nil, "Client secret".html_safe, class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_field :secret, class: "form-control", required: true, style: "border: 1px solid #ced4da;", readonly: true %> +
+
+ +
+ <%= f.label "Scope(s)", class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_field :scopes, class: "form-control", style: "border: 1px solid #ced4da;", readonly: true %> +
+
+ +
+ <%= f.label nil, 'Redirect URI(s)'.html_safe, class: 'col-sm-2 col-form-label font-weight-bold' %> +
+ <%= f.text_area :redirect_uri, class: "form-control", style: "border: 1px solid #ced4da;", readonly: true %> +
+
+ +
+
+ <%= link_to "Back", oauth_applications_path, class: 'btn btn-secondary' %> +
+
+<% end %> \ No newline at end of file diff --git a/app/views/doorkeeper/authorizations/error.html.erb b/app/views/doorkeeper/authorizations/error.html.erb new file mode 100644 index 0000000000..4173caf8f2 --- /dev/null +++ b/app/views/doorkeeper/authorizations/error.html.erb @@ -0,0 +1,9 @@ +
+

<%= t('doorkeeper.authorizations.error.title') %>

+
+ +
+
+    <%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %>
+  
+
\ No newline at end of file diff --git a/app/views/doorkeeper/authorizations/form_post.html.erb b/app/views/doorkeeper/authorizations/form_post.html.erb new file mode 100644 index 0000000000..4b0df6c0c6 --- /dev/null +++ b/app/views/doorkeeper/authorizations/form_post.html.erb @@ -0,0 +1,15 @@ + + +<%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false do %> + <% auth.body.compact.each do |key, value| %> + <%= hidden_field_tag key, value %> + <% end %> +<% end %> + + \ No newline at end of file diff --git a/app/views/doorkeeper/authorizations/new.html.erb b/app/views/doorkeeper/authorizations/new.html.erb new file mode 100644 index 0000000000..c7b8d0f224 --- /dev/null +++ b/app/views/doorkeeper/authorizations/new.html.erb @@ -0,0 +1,46 @@ + + +
+

+ <%= raw t('.prompt', client_name: content_tag(:strong, class: 'text-info') { @pre_auth.client.name }) %> +

+ + <% if @pre_auth.scopes.count > 0 %> +
+

<%= t('.able_to') %>:

+ + +
+ <% end %> + +
+ <%= form_tag oauth_authorization_path, method: :post do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %> + <%= hidden_field_tag :state, @pre_auth.state, id: nil %> + <%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %> + <%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %> + <%= hidden_field_tag :scope, @pre_auth.scope, id: nil %> + <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %> + <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %> + <%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "btn btn-success mt-3 mb-3", style: "border-radius: 0;" %> + <% end %> + <%= form_tag oauth_authorization_path, method: :delete do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %> + <%= hidden_field_tag :state, @pre_auth.state, id: nil %> + <%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %> + <%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %> + <%= hidden_field_tag :scope, @pre_auth.scope, id: nil %> + <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %> + <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %> + <%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "btn btn-danger", style: "border-radius: 0;" %> + <% end %> +
+
\ No newline at end of file diff --git a/app/views/doorkeeper/authorizations/show.html.erb b/app/views/doorkeeper/authorizations/show.html.erb new file mode 100644 index 0000000000..385fc9f24b --- /dev/null +++ b/app/views/doorkeeper/authorizations/show.html.erb @@ -0,0 +1,7 @@ + + +
+ <%= params[:code] %> +
\ No newline at end of file diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.erb b/app/views/doorkeeper/authorized_applications/_delete_form.html.erb new file mode 100644 index 0000000000..b39ef93edb --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.erb @@ -0,0 +1,4 @@ +<%- submit_btn_css ||= 'btn btn-link' %> +<%= form_tag oauth_authorized_application_path(application), method: :delete do %> + <%= submit_tag t('doorkeeper.authorized_applications.buttons.revoke'), onclick: "return confirm('#{ t('doorkeeper.authorized_applications.confirmations.revoke') }')", class: submit_btn_css %> +<% end %> \ No newline at end of file diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb new file mode 100644 index 0000000000..08e8429fd2 --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/index.html.erb @@ -0,0 +1,24 @@ + + +
+ + + + + + + + + + <% @applications.each do |application| %> + + + + + + <% end %> + +
<%= t('doorkeeper.authorized_applications.index.application') %><%= t('doorkeeper.authorized_applications.index.created_at') %>
<%= application.name %><%= application.created_at.strftime(t('doorkeeper.authorized_applications.index.date_format')) %><%= render 'delete_form', application: application %>
+
\ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 882ebb0def..db4975c352 100644 --- a/config/application.rb +++ b/config/application.rb @@ -53,5 +53,12 @@ class Application < Rails::Application # Set the default host for mailer URLs config.action_mailer.default_url_options = { host: Socket.gethostname.to_s } + + # apply application styling to doorkeeper views + config.to_prepare do + Doorkeeper::ApplicationsController.layout "application" + Doorkeeper::AuthorizationsController.layout "application" + Doorkeeper::AuthorizedApplicationsController.layout "application" + end end end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 0000000000..a7da6540f0 --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +Doorkeeper.configure do # rubocop:todo Metrics/BlockLength + # set the object-relational-model (ORM) + orm :active_record + + # ensure resource owner is authenticated + resource_owner_authenticator do + if user_signed_in? + if request.path == "/oauth/authorize/native" + # the /oauth/authorize/native path is only used for mobile devices + # and so it is better to deactivate it + redirect_to root_path, alert: "You are not authorized to perform this action." + else + current_user + end + else + # preserve oauth2 request url before redirecting to login + session[:user_return_to] = request.fullpath if request.get? + + # redirect user to login page + redirect_to new_user_session_url + end + end + + # ensure only super-admins can manage oauth applications + admin_authenticator do |_routes| + if current_user + unless current_user.can_super_admin? + redirect_to root_path, alert: "You are not authorized to perform this action." + end + else + warden.authenticate!(scope: :user) + end + end + + # grant flows enabled + # Authorization Code Grant Flow (ACGF) + grant_flows %w[authorization_code client_credentials] + + # allow for redirect-uri to be blank + # (required for client_credentials apps for org-admins) + allow_blank_redirect_uri true + + # scopes enabled + default_scopes :read + + # ensure client apps cannot ask for scopes outwith those specified here + enforce_configured_scopes + + # set the token endpoint configurations + access_token_expires_in 2.hours + reuse_access_token + + # enable refresh tokens of duration 90 days + use_refresh_token expiry: 90.days + + # enable ssl requirement for redirect url + force_ssl_in_redirect_uri true +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml new file mode 100644 index 0000000000..b3c4b2c660 --- /dev/null +++ b/config/locales/doorkeeper.en.yml @@ -0,0 +1,154 @@ +en: + activerecord: + attributes: + doorkeeper/application: + name: 'Name' + redirect_uri: 'Redirect URI' + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + unspecified_scheme: 'must specify a scheme.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + forbidden_uri: 'is forbidden by the server.' + scopes: + not_match_configured: "doesn't match configured on the server." + + doorkeeper: + applications: + confirmations: + destroy: 'Are you sure?' + buttons: + edit: 'Edit' + destroy: 'Destroy' + submit: 'Submit' + cancel: 'Cancel' + authorize: 'Authorize' + form: + error: 'Whoops! Check your form for possible errors' + help: + confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.' + redirect_uri: 'Use one line per URI' + blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require redirect URI." + scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.' + edit: + title: 'Edit application' + index: + title: 'Your applications' + new: 'New Application' + name: 'Name' + callback_url: 'Callback URL' + confidential: 'Confidential?' + actions: 'Actions' + confidentiality: + 'yes': 'Yes' + 'no': 'No' + new: + title: 'New Application' + show: + title: 'Application: %{name}' + application_id: 'UID' + secret: 'Secret' + secret_hashed: 'Secret hashed' + scopes: 'Scopes' + confidential: 'Confidential' + callback_urls: 'Callback urls' + actions: 'Actions' + not_defined: 'Not defined' + + authorizations: + buttons: + authorize: 'Authorize' + deny: 'Deny' + error: + title: 'An error has occurred' + new: + title: 'Authorization required' + prompt: 'Authorize %{client_name} to use your account?' + able_to: 'This application will be able to' + show: + title: 'Authorization code' + form_post: + title: 'Submit this form' + + authorized_applications: + confirmations: + revoke: 'Are you sure?' + buttons: + revoke: 'Revoke' + index: + title: 'Your authorized applications' + application: 'Application' + created_at: 'Created At' + date_format: '%Y-%m-%d %H:%M:%S' + + pre_authorization: + status: 'Pre-authorization' + + errors: + messages: + # Common error messages + invalid_request: + unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + missing_param: 'Missing required parameter: %{value}.' + request_not_authorized: 'Request need to be authorized. Required parameter for authorizing request is missing or invalid.' + invalid_redirect_uri: "The requested redirect uri is malformed or doesn't match client redirect URI." + unauthorized_client: 'The client is not authorized to perform this request using this method.' + access_denied: 'The resource owner or authorization server denied the request.' + invalid_scope: 'The requested scope is invalid, unknown, or malformed.' + invalid_code_challenge_method: + zero: 'The authorization server does not support PKCE as there are no accepted code_challenge_method values.' + one: 'The code_challenge_method must be %{challenge_methods}.' + other: 'The code_challenge_method must be one of %{challenge_methods}.' + server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' + temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' + + # Configuration error messages + credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' + resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.' + admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + unsupported_response_mode: 'The authorization server does not support this response mode.' + + # Access token errors + invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' + invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' + unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' + + invalid_token: + revoked: "The access token was revoked" + expired: "The access token expired" + unknown: "The access token is invalid" + revoke: + unauthorized: "You are not authorized to revoke this token" + + forbidden_token: + missing_scope: 'Access to this resource requires scope "%{oauth_scopes}".' + + flash: + applications: + create: + notice: 'Application created.' + destroy: + notice: 'Application deleted.' + update: + notice: 'Application updated.' + authorized_applications: + destroy: + notice: 'Application revoked.' + + layouts: + admin: + title: 'Doorkeeper' + nav: + oauth2_provider: 'OAuth2 Provider' + applications: 'Applications' + home: 'Home' + application: + title: 'OAuth authorization required' diff --git a/config/routes.rb b/config/routes.rb index 5954d44591..152c4a327d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,7 @@ # rubocop:disable Metrics/BlockLength Rails.application.routes.draw do + use_doorkeeper # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html devise_for(:users, controllers: { @@ -203,6 +204,13 @@ resources :plans, only: %i[create show index] resources :templates, only: [:index] end + + namespace :v2 do + get :heartbeat, controller: :base_api + get :me, controller: :base_api + + resources :plans, only: %i[index show] + end end namespace :paginable do diff --git a/db/migrate/20251119131055_create_doorkeeper_tables.rb b/db/migrate/20251119131055_create_doorkeeper_tables.rb new file mode 100644 index 0000000000..c7ee41b979 --- /dev/null +++ b/db/migrate/20251119131055_create_doorkeeper_tables.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +class CreateDoorkeeperTables < ActiveRecord::Migration[7.1] + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + + # Remove `null: false` if you are planning to use grant flows + # that doesn't require redirect URI to be used during authorization + # like Client Credentials flow or Resource Owner Password. + t.text :redirect_uri, null: false + t.string :scopes, null: false, default: '' + t.boolean :confidential, null: false, default: true + t.timestamps null: false + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_grants do |t| + t.references :resource_owner, null: false + t.references :application, null: false + t.string :token, null: false + t.integer :expires_in, null: false + t.text :redirect_uri, null: false + t.string :scopes, null: false, default: '' + t.datetime :created_at, null: false + t.datetime :revoked_at + end + + add_index :oauth_access_grants, :token, unique: true + add_foreign_key( + :oauth_access_grants, + :oauth_applications, + column: :application_id + ) + + create_table :oauth_access_tokens do |t| + t.references :resource_owner, index: true + + # Remove `null: false` if you are planning to use Password + # Credentials Grant flow that doesn't require an application. + t.references :application, null: false + + # If you use a custom token generator you may need to change this column + # from string to text, so that it accepts tokens larger than 255 + # characters. More info on custom token generators in: + # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator + # + # t.text :token, null: false + t.string :token, null: false + + t.string :refresh_token + t.integer :expires_in + t.string :scopes + t.datetime :created_at, null: false + t.datetime :revoked_at + + # The authorization server MAY issue a new refresh token, in which case + # *the client MUST discard the old refresh token* and replace it with the + # new refresh token. The authorization server MAY revoke the old + # refresh token after issuing a new refresh token to the client. + # @see https://datatracker.ietf.org/doc/html/rfc6749#section-6 + # + # Doorkeeper implementation: if there is a `previous_refresh_token` column, + # refresh tokens will be revoked after a related access token is used. + # If there is no `previous_refresh_token` column, previous tokens are + # revoked as soon as a new access token is created. + # + # Comment out this line if you want refresh tokens to be instantly + # revoked after use. + t.string :previous_refresh_token, null: false, default: "" + end + + add_index :oauth_access_tokens, :token, unique: true + + # See https://github.com/doorkeeper-gem/doorkeeper/issues/1592 + if ActiveRecord::Base.connection.adapter_name == "SQLServer" + execute <<~SQL.squish + CREATE UNIQUE NONCLUSTERED INDEX index_oauth_access_tokens_on_refresh_token ON oauth_access_tokens(refresh_token) + WHERE refresh_token IS NOT NULL + SQL + else + add_index :oauth_access_tokens, :refresh_token, unique: true + end + + add_foreign_key( + :oauth_access_tokens, + :oauth_applications, + column: :application_id + ) + + # Uncomment below to ensure a valid reference to the resource owner's table + add_foreign_key :oauth_access_grants, :users, column: :resource_owner_id + add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 3b6036f2bf..f96170db11 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_01_15_102816) do +ActiveRecord::Schema[7.1].define(version: 2025_11_19_131055) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -246,6 +246,48 @@ t.boolean "enabled", default: true end + create_table "oauth_access_grants", force: :cascade do |t| + t.bigint "resource_owner_id", null: false + t.bigint "application_id", null: false + t.string "token", null: false + t.integer "expires_in", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.index ["application_id"], name: "index_oauth_access_grants_on_application_id" + t.index ["resource_owner_id"], name: "index_oauth_access_grants_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true + end + + create_table "oauth_access_tokens", force: :cascade do |t| + t.bigint "resource_owner_id" + t.bigint "application_id", null: false + t.string "token", null: false + t.string "refresh_token" + t.integer "expires_in" + t.string "scopes" + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.string "previous_refresh_token", default: "", null: false + t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" + 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.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.boolean "confidential", default: true, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true + end + create_table "org_token_permissions", id: :serial, force: :cascade do |t| t.integer "org_id" t.integer "token_permission_type_id" @@ -644,6 +686,10 @@ add_foreign_key "notes", "users" add_foreign_key "notification_acknowledgements", "notifications" add_foreign_key "notification_acknowledgements", "users" + add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_grants", "users", column: "resource_owner_id" + add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id" add_foreign_key "org_token_permissions", "orgs" add_foreign_key "org_token_permissions", "token_permission_types" add_foreign_key "orgs", "languages"