From dbedac3a9b40b57db62f014974be6a1b8b6f6a99 Mon Sep 17 00:00:00 2001 From: Alex Kriuchykhin Date: Wed, 20 Dec 2023 14:02:09 +0100 Subject: [PATCH 001/115] Add omniauth_openid_connect gem, implement basic sign-in flow [SCI-9578] (#6666) --- Gemfile | 1 + Gemfile.lock | 40 ++++++++++++ .../users/omniauth_callbacks_controller.rb | 63 ++++++++++++++++++- app/helpers/application_helper.rb | 4 ++ app/views/users/shared/_links.html.erb | 8 +++ config/initializers/extends.rb | 2 +- config/initializers/omniauth.rb | 28 +++++++++ config/locales/en.yml | 7 +++ 8 files changed, 151 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 6559d7435..4937dca18 100644 --- a/Gemfile +++ b/Gemfile @@ -24,6 +24,7 @@ gem 'omniauth', '~> 2.1' gem 'omniauth-azure-activedirectory-v2' gem 'omniauth-linkedin-oauth2' gem 'omniauth-okta', git: 'https://github.com/scinote-eln/omniauth-okta', branch: 'org_auth_server_support' +gem 'omniauth_openid_connect' gem 'omniauth-rails_csrf_protection', '~> 1.0' # Gems for API implementation diff --git a/Gemfile.lock b/Gemfile.lock index 871e20634..ac89a2868 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -141,6 +141,7 @@ GEM railties (>= 3.1) aspector (0.14.0) ast (2.4.2) + attr_required (1.0.1) auto_strip_attributes (2.6.0) activerecord (>= 4.0) awesome_print (1.9.2) @@ -474,6 +475,22 @@ GEM omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) + omniauth_openid_connect (0.7.1) + omniauth (>= 1.9, < 3) + openid_connect (~> 2.2) + openid_connect (2.2.0) + activemodel + attr_required (>= 1.0.0) + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.16) + net-smtp + rack-oauth2 (~> 2.2) + swd (~> 2.0) + tzinfo + validate_email + validate_url + webfinger (~> 2.0) orm_adapter (0.5.0) overcommit (0.60.0) childprocess (>= 0.6.3, < 5) @@ -514,6 +531,13 @@ GEM rack (>= 1.0, < 3) rack-cors (2.0.1) rack (>= 2.0.0) + rack-oauth2 (2.2.0) + activesupport + attr_required + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.11.0) + rack (>= 2.1.0) rack-protection (3.0.6) rack rack-test (2.1.0) @@ -660,6 +684,11 @@ GEM activesupport (>= 5.2) sprockets (>= 3.0.0) stream (0.5.5) + swd (2.0.2) + activesupport (>= 3) + attr_required (>= 0.0.5) + faraday (~> 2.0) + faraday-follow_redirects sys-uname (1.2.3) ffi (~> 1.1) tailwindcss-rails (2.0.29) @@ -685,6 +714,12 @@ GEM unf_ext (0.0.8.2) unicode-display_width (2.4.2) uniform_notifier (1.16.0) + validate_email (0.1.6) + activemodel (>= 3.0) + mail (>= 2.2.5) + validate_url (1.0.15) + activemodel (>= 3.0.0) + public_suffix version_gem (1.1.3) view_component (3.2.0) activesupport (>= 5.2.0, < 8.0) @@ -692,6 +727,10 @@ GEM method_source (~> 1.0) warden (1.2.9) rack (>= 2.0.9) + webfinger (2.1.2) + activesupport + faraday (~> 2.0) + faraday-follow_redirects webmock (3.18.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -776,6 +815,7 @@ DEPENDENCIES omniauth-linkedin-oauth2 omniauth-okta! omniauth-rails_csrf_protection (~> 1.0) + omniauth_openid_connect overcommit pg (~> 1.5) pg_search diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 1803b6cb0..058da755d 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -8,7 +8,7 @@ module Users skip_before_action :verify_authenticity_token before_action :sign_up_with_provider_enabled?, only: :linkedin - before_action :check_sso_status, only: %i(customazureactivedirectory okta) + before_action :check_sso_status, only: %i(customazureactivedirectory okta openid_connect) # You should configure your model like this: # devise :omniauthable, omniauth_providers: [:twitter] @@ -176,6 +176,67 @@ module Users end end + def openid_connect + auth = request.env['omniauth.auth'] + settings = ApplicationSettings.instance + provider_conf = settings.values['openid_connect'] + raise StandardError, 'No matching OpenID Connect AD provider config found' if provider_conf.blank? + + return redirect_to connected_accounts_path if current_user + + email = auth.info.email + email ||= auth.dig(:extra, :raw_info, :id_token_claims, :emails)&.first + user = User.from_omniauth(auth) + + # User found in database so just signing in + return sign_in_and_redirect(user) if user.present? + + if email.blank? + # No email in the token so can not link or create user + error_message = I18n.t('devise.openid_connect.errors.no_email') + return redirect_to after_omniauth_failure_path_for(resource_name) + end + + user = User.find_by(email: email.downcase) + + if user.blank? + # Create new user and identity + full_name = "#{auth.info.first_name} #{auth.info.last_name}" + user = User.new(full_name: full_name, + initials: generate_initials(full_name), + email: email, + password: generate_user_password) + User.transaction do + user.save! + user.user_identities.create!(provider: auth.provider, uid: auth.uid) + user.update!(confirmed_at: user.created_at) + end + + sign_in_and_redirect(user) + elsif provider_conf['auto_link_on_sign_in'] + # Link to existing local account + user.user_identities.create!(provider: auth.provider, uid: auth.uid) + user.update!(confirmed_at: user.created_at) if user.confirmed_at.blank? + sign_in_and_redirect(user) + else + # Cannot do anything with it, so just return an error + error_message = I18n.t('devise.openid_connect.errors.no_local_user_map') + redirect_to after_omniauth_failure_path_for(resource_name) + end + rescue StandardError => e + Rails.logger.error e.message + Rails.logger.error e.backtrace.join("\n") + error_message = I18n.t('devise.openid_connect.errors.failed_to_save') if e.is_a?(ActiveRecord::RecordInvalid) + error_message ||= I18n.t('devise.openid_connect.errors.generic') + redirect_to after_omniauth_failure_path_for(resource_name) + ensure + if error_message + set_flash_message(:alert, :failure, kind: I18n.t('devise.openid_connect.provider_name'), reason: error_message) + else + set_flash_message(:notice, :success, kind: I18n.t('devise.openid_connect.provider_name')) + end + end + # More info at: # https://github.com/plataformatec/devise#omniauth diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 22eb85297..79bcd1522 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -205,6 +205,10 @@ module ApplicationHelper ApplicationSettings.instance.values['azure_ad_apps'].present? end + def openid_connect_configured? + ApplicationSettings.instance.values['openid_connect'].present? + end + def wopi_enabled? ENV['WOPI_ENABLED'] == 'true' end diff --git a/app/views/users/shared/_links.html.erb b/app/views/users/shared/_links.html.erb index 0ed2a0ee0..1172c7341 100644 --- a/app/views/users/shared/_links.html.erb +++ b/app/views/users/shared/_links.html.erb @@ -47,5 +47,13 @@ <%= render partial: "users/shared/azure_sign_in_links", locals: { resource_name: resource_name } %> <% end %> + + <%- if sso_enabled? && openid_connect_configured? %> +
+ <%= form_tag user_openid_connect_omniauth_authorize_path, method: :post do %> + <%= submit_tag t('devise.sessions.new.openid_connect_submit'), class: 'btn btn-primary' %> + <% end %> +
+ <% end %> <% end %> diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index bdfbea5dd..62c703ef8 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -162,7 +162,7 @@ class Extends 'RepositoryStatusValue' => 'status', 'RepositoryStockValue' => 'stock' } - OMNIAUTH_PROVIDERS = %i(linkedin customazureactivedirectory okta) + OMNIAUTH_PROVIDERS = %i(linkedin customazureactivedirectory okta openid_connect) INITIAL_USER_OPTIONS = {} diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index b00568bd2..40dea52de 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -32,6 +32,30 @@ AZURE_SETUP_PROC = lambda do |env| env['omniauth.strategy'].options[:base_azure_url] = "#{conf_uri.scheme || 'https'}://#{conf_uri.host}" end +OPENID_CONNECT_SETUP_PROC = lambda do |env| + settings = ApplicationSettings.instance + provider_conf = settings.values['openid_connect'] + raise StandardError, 'No OpenID Connect config available for sign in' if provider_conf.blank? + + client_options = { + identifier: provider_conf['client_id'], + secret: provider_conf['client_secret'], + redirect_uri: Rails.application.routes.url_helpers.user_openid_connect_omniauth_callback_url + } + + unless provider_conf['discovery'] + client_options[:authorize_url] = provider_conf['authorize_url'] if provider_conf['authorize_url'] + client_options[:token_url] = provider_conf['token_url'] if provider_conf['token_url'] + client_options[:user_info_url] = provider_conf['authorize_url'] if provider_conf['user_info_url'] + end + + env['omniauth.strategy'].options[:name] = 'openid_connect' + env['omniauth.strategy'].options[:scope] = %i(openid email profile) + env['omniauth.strategy'].options[:issuer] = provider_conf['issuer_url'] + env['omniauth.strategy'].options[:discovery] = provider_conf['discovery'] == true + env['omniauth.strategy'].options[:client_options] = client_options +end + OKTA_SETUP_PROC = lambda do |env| settings = ApplicationSettings.instance provider_conf = settings.values['okta'] @@ -67,6 +91,10 @@ Rails.application.config.middleware.use OmniAuth::Builder do provider OmniAuth::Strategies::CustomAzureActiveDirectory, setup: AZURE_SETUP_PROC end +Rails.application.config.middleware.use OmniAuth::Builder do + provider OmniAuth::Strategies::OpenIDConnect, setup: OPENID_CONNECT_SETUP_PROC +end + Rails.application.config.middleware.use OmniAuth::Builder do provider OmniAuth::Strategies::Okta, setup: OKTA_SETUP_PROC end diff --git a/config/locales/en.yml b/config/locales/en.yml index af7fd26b6..20f8ee5f3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -86,6 +86,13 @@ en: no_local_user_map: "No local user record found" no_email: "Email is missing in auth token" failed_to_save: "Failed to create new user" + openid_connect: + provider_name: "OpenID Connect" + errors: + generic: "Failed to sign in user" + no_local_user_map: "No local user record found" + no_email: "Email is missing in auth token" + failed_to_save: "Failed to create new user" okta: provider_name: "Okta" sign_in_label: "Sign in with Okta" From 3de2f6d0f1a4719c132c17a5e0e92e4d1515069e Mon Sep 17 00:00:00 2001 From: Alex Kriuchykhin Date: Wed, 20 Dec 2023 17:52:33 +0100 Subject: [PATCH 002/115] Add generic SAML SSO strategy implementation [SCI-9579] (#6775) --- Gemfile | 1 + Gemfile.lock | 7 ++ .../users/omniauth_callbacks_controller.rb | 113 +++++++++++++----- app/helpers/application_helper.rb | 4 + app/views/users/shared/_links.html.erb | 8 ++ config/initializers/extends.rb | 2 +- config/initializers/omniauth.rb | 15 +++ config/locales/en.yml | 8 ++ 8 files changed, 125 insertions(+), 33 deletions(-) diff --git a/Gemfile b/Gemfile index 4937dca18..ef04a2e4d 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,7 @@ gem 'omniauth-linkedin-oauth2' gem 'omniauth-okta', git: 'https://github.com/scinote-eln/omniauth-okta', branch: 'org_auth_server_support' gem 'omniauth_openid_connect' gem 'omniauth-rails_csrf_protection', '~> 1.0' +gem 'omniauth-saml' # Gems for API implementation gem 'active_model_serializers', '~> 0.10.7' diff --git a/Gemfile.lock b/Gemfile.lock index ac89a2868..0c60f75a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -478,6 +478,9 @@ GEM omniauth_openid_connect (0.7.1) omniauth (>= 1.9, < 3) openid_connect (~> 2.2) + omniauth-saml (2.1.0) + omniauth (~> 2.0) + ruby-saml (~> 1.12) openid_connect (2.2.0) activemodel attr_required (>= 1.0.0) @@ -646,6 +649,9 @@ GEM rubocop (>= 1.33.0, < 2.0) ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) + ruby-saml (1.16.0) + nokogiri (>= 1.13.10) + rexml ruby-vips (2.1.4) ffi (~> 1.12) ruby2_keywords (0.0.5) @@ -815,6 +821,7 @@ DEPENDENCIES omniauth-linkedin-oauth2 omniauth-okta! omniauth-rails_csrf_protection (~> 1.0) + omniauth-saml omniauth_openid_connect overcommit pg (~> 1.5) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 058da755d..7481f06c5 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -46,17 +46,7 @@ module Users if user.blank? # Create new user and identity - full_name = "#{auth.info.first_name} #{auth.info.last_name}" - user = User.new(full_name: full_name, - initials: generate_initials(full_name), - email: email, - password: generate_user_password) - User.transaction do - user.save! - user.user_identities.create!(provider: auth.provider, uid: auth.uid) - user.update!(confirmed_at: user.created_at) - end - + user = create_user_from_auth(email, auth) sign_in_and_redirect(user) elsif provider_conf['auto_link_on_sign_in'] # Link to existing local account @@ -146,16 +136,7 @@ module Users user = User.find_by(email: auth.info.email.downcase) if user.blank? - # Create new user and identity - user = User.new(full_name: auth.info.name, - initials: generate_initials(auth.info.name), - email: auth.info.email, - password: generate_user_password) - User.transaction do - user.save! - user.user_identities.create!(provider: auth.provider, uid: auth.uid) - user.update!(confirmed_at: user.created_at) - end + user = create_user_from_auth(email, auth) else # Link to existing local account user.user_identities.create!(provider: auth.provider, uid: auth.uid) @@ -201,17 +182,7 @@ module Users if user.blank? # Create new user and identity - full_name = "#{auth.info.first_name} #{auth.info.last_name}" - user = User.new(full_name: full_name, - initials: generate_initials(full_name), - email: email, - password: generate_user_password) - User.transaction do - user.save! - user.user_identities.create!(provider: auth.provider, uid: auth.uid) - user.update!(confirmed_at: user.created_at) - end - + user = create_user_from_auth(email, auth) sign_in_and_redirect(user) elsif provider_conf['auto_link_on_sign_in'] # Link to existing local account @@ -237,6 +208,56 @@ module Users end end + def saml + auth = request.env['omniauth.auth'] + + settings = ApplicationSettings.instance + provider_conf = settings.values['saml'] + raise StandardError, 'No matching SAML provider config found' if provider_conf.blank? + + return redirect_to connected_accounts_path if current_user + + email = auth.info.email + user = User.from_omniauth(auth) + + # User found in database so just signing in + return sign_in_and_redirect(user) if user.present? + + if email.blank? + # No email in the token so can not link or create user + error_message = I18n.t('devise.saml.errors.no_email') + return redirect_to after_omniauth_failure_path_for(resource_name) + end + + user = User.find_by(email: email.downcase) + + if user.blank? + user = create_user_from_auth(email, auth) + sign_in_and_redirect(user) + elsif provider_conf['auto_link_on_sign_in'] + # Link to existing local account + user.user_identities.create!(provider: auth.provider, uid: auth.uid) + user.update!(confirmed_at: user.created_at) if user.confirmed_at.blank? + sign_in_and_redirect(user) + else + # Cannot do anything with it, so just return an error + error_message = I18n.t('devise.saml.errors.no_local_user_map') + redirect_to after_omniauth_failure_path_for(resource_name) + end + rescue StandardError => e + Rails.logger.error e.message + Rails.logger.error e.backtrace.join("\n") + error_message = I18n.t('devise.saml.errors.failed_to_save') if e.is_a?(ActiveRecord::RecordInvalid) + error_message ||= I18n.t('devise.saml.errors.generic') + redirect_to after_omniauth_failure_path_for(resource_name) + ensure + if error_message + set_flash_message(:alert, :failure, kind: I18n.t('devise.saml.provider_name'), reason: error_message) + else + set_flash_message(:notice, :success, kind: I18n.t('devise.saml.provider_name')) + end + end + # More info at: # https://github.com/plataformatec/devise#omniauth @@ -273,5 +294,33 @@ module Users initials = initials.strip.blank? ? 'PLCH' : initials[0..3] initials end + + def create_user_from_auth(email, auth) + full_name = "#{auth.info.first_name} #{auth.info.last_name}" + user = User.new(full_name: full_name, + initials: generate_initials(full_name), + email: email, + password: generate_user_password) + User.transaction do + user.save! + user.user_identities.create!(provider: auth.provider, uid: auth.uid) + user.update!(confirmed_at: user.created_at) + end + user + end + + def create_user_from_auth(email, auth) + full_name = "#{auth.info.first_name} #{auth.info.last_name}" + user = User.new(full_name: full_name, + initials: generate_initials(full_name), + email: email, + password: generate_user_password) + User.transaction do + user.save! + user.user_identities.create!(provider: auth.provider, uid: auth.uid) + user.update!(confirmed_at: user.created_at) + end + user + end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 79bcd1522..528e7718e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -209,6 +209,10 @@ module ApplicationHelper ApplicationSettings.instance.values['openid_connect'].present? end + def saml_configured? + ApplicationSettings.instance.values['saml'].present? + end + def wopi_enabled? ENV['WOPI_ENABLED'] == 'true' end diff --git a/app/views/users/shared/_links.html.erb b/app/views/users/shared/_links.html.erb index 1172c7341..2395bc310 100644 --- a/app/views/users/shared/_links.html.erb +++ b/app/views/users/shared/_links.html.erb @@ -55,5 +55,13 @@ <% end %> <% end %> + + <% if sso_enabled? && saml_configured? %> + + <% end %> <% end %> diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index 62c703ef8..0dad6a835 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -162,7 +162,7 @@ class Extends 'RepositoryStatusValue' => 'status', 'RepositoryStockValue' => 'stock' } - OMNIAUTH_PROVIDERS = %i(linkedin customazureactivedirectory okta openid_connect) + OMNIAUTH_PROVIDERS = %i(linkedin customazureactivedirectory okta openid_connect saml) INITIAL_USER_OPTIONS = {} diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 40dea52de..7beca1549 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -87,6 +87,17 @@ OKTA_SETUP_PROC = lambda do |env| env['omniauth.strategy'].options[:client_options] = client_options end +SAML_SETUP_PROC = lambda do |env| + settings = ApplicationSettings.instance + provider_conf = settings.values['saml'] + raise StandardError, 'No SAML config available for sign in' if provider_conf.blank? + + env['omniauth.strategy'].options[:idp_sso_service_url] = provider_conf['idp_sso_service_url'] + env['omniauth.strategy'].options[:idp_cert] = provider_conf['idp_cert'] + env['omniauth.strategy'].options[:sp_entity_id] = provider_conf['sp_entity_id'] + env['omniauth.strategy'].options[:uid_attribute] = 'uid' +end + Rails.application.config.middleware.use OmniAuth::Builder do provider OmniAuth::Strategies::CustomAzureActiveDirectory, setup: AZURE_SETUP_PROC end @@ -99,4 +110,8 @@ Rails.application.config.middleware.use OmniAuth::Builder do provider OmniAuth::Strategies::Okta, setup: OKTA_SETUP_PROC end +Rails.application.config.middleware.use OmniAuth::Builder do + provider OmniAuth::Strategies::SAML, setup: SAML_SETUP_PROC +end + OmniAuth.config.logger = Rails.logger diff --git a/config/locales/en.yml b/config/locales/en.yml index 20f8ee5f3..faf773a62 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -34,6 +34,7 @@ en: remember_me: "Remember me" submit: "Log in" azure_ad_submit: "Sign in with Azure AD" + saml_submit: "Sign in with SAML" 2fa: title: "Two-factor authentication" description: "Enter the one-time code found in your authenticator app to log in to SciNote." @@ -100,6 +101,13 @@ en: generic: "Failed to sign in user" no_local_user_map: "No local user record found" failed_to_save: "Failed to create new user" + saml: + provider_name: "SAML" + sign_in_label: "Sign in with SAML" + errors: + generic: "Failed to sign in user" + no_local_user_map: "No local user record found" + failed_to_save: "Failed to create new user" doorkeeper: errors: From c1453a1fe51efe6e9ed9899893b76cfbbf5d1f25 Mon Sep 17 00:00:00 2001 From: Soufiane Date: Wed, 10 Jan 2024 17:20:39 +0100 Subject: [PATCH 003/115] Add form for OpenID [SCI-9937] (#6918) --- config/locales/en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index faf773a62..c058e0798 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -34,6 +34,7 @@ en: remember_me: "Remember me" submit: "Log in" azure_ad_submit: "Sign in with Azure AD" + openid_connect_submit: "Sign in with OpenID Connect" saml_submit: "Sign in with SAML" 2fa: title: "Two-factor authentication" From 8dcd00c433dd301bd2ae1da87968281316225136 Mon Sep 17 00:00:00 2001 From: Alex Kriuchykhin Date: Thu, 1 Feb 2024 13:14:31 +0100 Subject: [PATCH 004/115] Add support of server side session store [SCI-9960] (#6974) --- Gemfile | 1 + Gemfile.lock | 9 +++++++++ config/application.rb | 2 ++ config/initializers/session_store.rb | 4 +++- db/migrate/20240118094253_add_sessions_table.rb | 14 ++++++++++++++ db/schema.rb | 11 ++++++++++- 6 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20240118094253_add_sessions_table.rb diff --git a/Gemfile b/Gemfile index ef04a2e4d..4641eff64 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ source 'http://rubygems.org' ruby '3.2.2' +gem 'activerecord-session_store' gem 'bootsnap', require: false gem 'devise', '~> 4.8.1' gem 'devise_invitable' diff --git a/Gemfile.lock b/Gemfile.lock index 92c6dd47b..24ca45a43 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -120,6 +120,13 @@ GEM activesupport (= 7.0.5.1) activerecord-import (1.4.1) activerecord (>= 4.2) + activerecord-session_store (2.1.0) + actionpack (>= 6.1) + activerecord (>= 6.1) + cgi (>= 0.3.6) + multi_json (~> 1.11, >= 1.11.2) + rack (>= 2.0.8, < 4) + railties (>= 6.1) activestorage (7.0.5.1) actionpack (= 7.0.5.1) activejob (= 7.0.5.1) @@ -219,6 +226,7 @@ GEM mail case_transform (0.2) activesupport + cgi (0.4.1) childprocess (4.1.0) chunky_png (1.4.0) coderay (1.1.3) @@ -763,6 +771,7 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10.7) activerecord-import + activerecord-session_store acts_as_list ajax-datatables-rails (~> 0.3.1) aspector diff --git a/config/application.rb b/config/application.rb index 8c24b28c3..9e09f0b9d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -82,5 +82,7 @@ module Scinote config.action_view.field_error_proc = Proc.new { |html_tag, instance| "
#{html_tag}
".html_safe } + + ActiveRecord::SessionStore::Session.serializer = :json end end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index b64e189d7..6d628d0f3 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,3 +1,5 @@ # Be sure to restart your server when you modify this file. -Rails.application.config.session_store :cookie_store, key: '_scinote_session' +session_store = ENV['SCINOTE_SERVERSIDE_SESSIONS'] == 'true' ? :active_record_store : :cookie_store + +Rails.application.config.session_store session_store, key: '_scinote_session' diff --git a/db/migrate/20240118094253_add_sessions_table.rb b/db/migrate/20240118094253_add_sessions_table.rb new file mode 100644 index 000000000..96c01f4af --- /dev/null +++ b/db/migrate/20240118094253_add_sessions_table.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AddSessionsTable < ActiveRecord::Migration[7.0] + def change + create_table :sessions do |t| + t.string :session_id, null: false + t.jsonb :data + t.timestamps + end + + add_index :sessions, :session_id, unique: true + add_index :sessions, :updated_at + end +end diff --git a/db/schema.rb b/db/schema.rb index d3da2504d..54138aaad 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_01_15_114821) do +ActiveRecord::Schema[7.0].define(version: 2024_01_18_094253) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gist" enable_extension "pg_trgm" @@ -981,6 +981,15 @@ ActiveRecord::Schema[7.0].define(version: 2024_01_15_114821) do t.index ["user_id"], name: "index_results_on_user_id" end + create_table "sessions", force: :cascade do |t| + t.string "session_id", null: false + t.jsonb "data" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["session_id"], name: "index_sessions_on_session_id", unique: true + t.index ["updated_at"], name: "index_sessions_on_updated_at" + end + create_table "settings", force: :cascade do |t| t.text "type", null: false t.jsonb "values", default: {}, null: false From 24278c287f018f674bb98e358905d3b33c0e0462 Mon Sep 17 00:00:00 2001 From: Alex Kriuchykhin Date: Fri, 2 Feb 2024 12:06:22 +0100 Subject: [PATCH 005/115] Fix sign in with OpenID Connect with disabled discovery [SCI-9578] (#7046) --- config/initializers/omniauth.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 7beca1549..da22ac831 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -44,9 +44,11 @@ OPENID_CONNECT_SETUP_PROC = lambda do |env| } unless provider_conf['discovery'] - client_options[:authorize_url] = provider_conf['authorize_url'] if provider_conf['authorize_url'] - client_options[:token_url] = provider_conf['token_url'] if provider_conf['token_url'] - client_options[:user_info_url] = provider_conf['authorize_url'] if provider_conf['user_info_url'] + client_options[:host] = provider_conf['host'] + client_options[:authorization_endpoint] = provider_conf['authorization_endpoint'] + client_options[:token_endpoint] = provider_conf['token_endpoint'] + client_options[:userinfo_endpoint] = provider_conf['userinfo_endpoint'] + client_options[:jwks_uri] = provider_conf['jwks_uri'] end env['omniauth.strategy'].options[:name] = 'openid_connect' From 57c5140267ce52fe8e92b5181f7b1f6ed2bf7dca Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 23 Feb 2024 14:08:51 +0100 Subject: [PATCH 006/115] Add quick filter flyout [SCI-10248] --- app/controllers/search_controller.rb | 16 ++ .../vue/navigation/quick_search.vue | 228 ++++++++++++++++++ app/javascript/vue/navigation/top_menu.vue | 12 +- .../vue/shared/general_dropdown.vue | 12 +- .../vue/shared/string_with_ellipsis.vue | 26 ++ .../concerns/breadcrumbs_helper.rb | 67 +++++ app/serializers/notification_serializer.rb | 59 +---- app/serializers/quick_search_serializer.rb | 28 +++ app/views/shared/navigation/_top.html.erb | 1 + config/locales/en.yml | 8 + config/routes.rb | 5 + 11 files changed, 396 insertions(+), 66 deletions(-) create mode 100644 app/javascript/vue/navigation/quick_search.vue create mode 100644 app/javascript/vue/shared/string_with_ellipsis.vue create mode 100644 app/serializers/concerns/breadcrumbs_helper.rb create mode 100644 app/serializers/quick_search_serializer.rb diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 09cfc4e51..fc2407543 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -42,6 +42,22 @@ class SearchController < ApplicationController def new end + def quick + results = [ + Project.first, + Experiment.first, + MyModule.first, + Protocol.first, + RepositoryRow.first, + Result.first, + Step.first, + Report.first, + LabelTemplate.first + ].compact + + render json: results, each_serializer: QuickSearchSerializer + end + private def load_vars diff --git a/app/javascript/vue/navigation/quick_search.vue b/app/javascript/vue/navigation/quick_search.vue new file mode 100644 index 000000000..6a363e3ab --- /dev/null +++ b/app/javascript/vue/navigation/quick_search.vue @@ -0,0 +1,228 @@ + + + diff --git a/app/javascript/vue/navigation/top_menu.vue b/app/javascript/vue/navigation/top_menu.vue index 59082ab97..395d6e154 100644 --- a/app/javascript/vue/navigation/top_menu.vue +++ b/app/javascript/vue/navigation/top_menu.vue @@ -1,9 +1,6 @@ diff --git a/app/serializers/global_search/protocol_serializer.rb b/app/serializers/global_search/protocol_serializer.rb new file mode 100644 index 000000000..feac6d3fd --- /dev/null +++ b/app/serializers/global_search/protocol_serializer.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module GlobalSearch + class ProtocolSerializer < ActiveModel::Serializer + include Rails.application.routes.url_helpers + + attributes :id, :name, :code, :created_at, :updated_at, :created_by, :team, :archived, :url + + def team + { + name: object.team.name, + url: protocols_path(team: object.team) + } + end + + def created_by + { + name: object.created_by.name, + avatar_url: avatar_path(object.created_by, :icon_small) + } + end + + def created_at + I18n.l(object.created_at, format: :full_date) + end + + def updated_at + I18n.l(object.updated_at, format: :full_date) + end + + def url + protocol_path(object) + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 3aa90769e..faf6df1c0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -457,6 +457,8 @@ en: clear_filters: "Clear filters" id: "ID" created_at: "Created on" + created_by: "Created by" + updated_at: "Updated on" team: "Team" folder: "Folder" comments: From 1f7566ae73c423b3636159a354333d2329ab599c Mon Sep 17 00:00:00 2001 From: wandji20 Date: Wed, 27 Mar 2024 11:44:52 +0100 Subject: [PATCH 011/115] Add project folders group to global search page [SCI-10470] --- .../javascripts/sitewide/constants.js.erb | 1 + app/controllers/search_controller.rb | 21 +++++++-- .../vue/global_search/groups/folders.vue | 45 ++++++++++++++++++- .../vue/global_search/groups/projects.vue | 2 +- .../vue/global_search/groups/search_mixin.js | 4 ++ .../project_folder_serializer.rb | 33 ++++++++++++++ config/initializers/constants.rb | 2 + 7 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 app/serializers/global_search/project_folder_serializer.rb diff --git a/app/assets/javascripts/sitewide/constants.js.erb b/app/assets/javascripts/sitewide/constants.js.erb index 6d3eeff90..b88b22bd5 100644 --- a/app/assets/javascripts/sitewide/constants.js.erb +++ b/app/assets/javascripts/sitewide/constants.js.erb @@ -18,4 +18,5 @@ const GLOBAL_CONSTANTS = { SLOW_STATUS_POLLING_INTERVAL: <%= Constants::SLOW_STATUS_POLLING_INTERVAL %>, ASSET_POLLING_INTERVAL: <%= Constants::ASSET_POLLING_INTERVAL %>, ASSET_SYNC_URL: '<%= Constants::ASSET_SYNC_URL %>', + GLOBAL_SEARCH_PREVIEW_LIMIT: <%= Constants::GLOBAL_SEARCH_PREVIEW_LIMIT %> }; diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 7ba3e1e47..a7d8c6cfb 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -21,12 +21,27 @@ class SearchController < ApplicationController results = @project_results.page(params[:page]).per(Constants::SEARCH_LIMIT) end - render json: results, + render json: results.includes(:team, :project_folder), each_serializer: GlobalSearch::ProjectSerializer, meta: { total: @search_count, next_page: (results.next_page if results.respond_to?(:next_page)), } + when 'project_folders' + @project_folder_search_count = fetch_cached_count ProjectFolder + search_project_folders + results = if params[:preview] == 'true' + @project_folder_results.limit(Constants::GLOBAL_SEARCH_PREVIEW_LIMIT) + else + @project_folder_results.page(params[:page]).per(Constants::SEARCH_LIMIT) + end + render json: results.includes(:team, :parent_folder), + each_serializer: GlobalSearch::ProjectFolderSerializer, + meta: { + total: @search_count, + next_page: results.try(:next_page) + } + else return when 'protocols' @protocol_search_count = fetch_cached_count(Protocol) @@ -267,13 +282,13 @@ class SearchController < ApplicationController end def search_projects - @project_results = [] + @project_results = Project.none @project_results = search_by_name(Project) if @project_search_count.positive? @search_count = @project_search_count end def search_project_folders - @project_folder_results = [] + @project_folder_results = ProjectFolder.none @project_folder_results = search_by_name(ProjectFolder) if @project_folder_search_count.positive? @search_count = @project_folder_search_count end diff --git a/app/javascript/vue/global_search/groups/folders.vue b/app/javascript/vue/global_search/groups/folders.vue index 15ea2f7f0..3b1a441c4 100644 --- a/app/javascript/vue/global_search/groups/folders.vue +++ b/app/javascript/vue/global_search/groups/folders.vue @@ -1,14 +1,55 @@ diff --git a/app/javascript/vue/global_search/groups/projects.vue b/app/javascript/vue/global_search/groups/projects.vue index f98dbf54a..e30786e65 100644 --- a/app/javascript/vue/global_search/groups/projects.vue +++ b/app/javascript/vue/global_search/groups/projects.vue @@ -1,5 +1,5 @@ diff --git a/app/models/repository_row.rb b/app/models/repository_row.rb index 0c38d7f2f..624175566 100644 --- a/app/models/repository_row.rb +++ b/app/models/repository_row.rb @@ -120,6 +120,35 @@ class RepositoryRow < ApplicationRecord where(repository: Repository.viewable_by_user(user, teams)) end + def self.search(user, + include_archived, + query = nil, + page = 1, + _current_team = nil, + options = {}) + + searchable_row_fields = [RepositoryRow::PREFIXED_ID_SQL, 'repository_rows.name', 'users.full_name'] + repositories = Repository.search(user).pluck(:id) + + new_query = + RepositoryRow + .joins(:repository, :created_by) + .where(repository_id: repositories) + .distinct + .where_attributes_like( + searchable_row_fields, query, options + ) + + new_query = new_query.active unless include_archived + + # Show all results if needed + if page == Constants::SEARCH_NO_LIMIT + new_query + else + new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT) + end + end + def self.filter_by_teams(teams = []) return self if teams.blank? diff --git a/app/serializers/global_search/repository_row_serializer.rb b/app/serializers/global_search/repository_row_serializer.rb new file mode 100644 index 000000000..49011ff90 --- /dev/null +++ b/app/serializers/global_search/repository_row_serializer.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module GlobalSearch + class RepositoryRowSerializer < ActiveModel::Serializer + include Rails.application.routes.url_helpers + + attributes :id, :name, :code, :created_at, :created_by, :team, :repository, :archived, :url + + def team + { + name: object.team.name, + url: repository_path(object.repository) + } + end + + def created_by + { + name: object.created_by.name, + avatar_url: avatar_path(object.created_by, :icon_small) + } + end + + def created_at + I18n.l(object.created_at, format: :full_date) + end + + def repository + { + name: object.repository.name, + url: repository_path(object.repository) + } + end + + def url + # switch to repository_repository_rows_path when inventory items page is implemented + repository_path(object.repository) + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 62e92ba04..e9d881f53 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -453,6 +453,7 @@ en: protocol_templates: "Protocol templates" label_templates: "Label templates" reports: "Reports" + repository: "Inventory" more_search_options: "More search options" clear_filters: "Clear filters" id: "ID" From da19a79851590e399d3a89eb89b6fb77682f8c87 Mon Sep 17 00:00:00 2001 From: sboursen-scinote Date: Wed, 27 Mar 2024 12:41:30 +0100 Subject: [PATCH 016/115] Implement global search results component for Label templates [SCI-10478] --- app/controllers/search_controller.rb | 32 ++++++++-- .../global_search/groups/label_templates.vue | 59 ++++++++++++++++++- .../vue/global_search/groups/search_mixin.js | 2 + app/models/label_template.rb | 22 +++++++ .../label_template_serializer.rb | 39 ++++++++++++ config/locales/en.yml | 1 + 6 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 app/serializers/global_search/label_template_serializer.rb diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index f7f281563..7b2ba3618 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -16,7 +16,7 @@ class SearchController < ApplicationController @project_search_count = fetch_cached_count(Project) search_projects if params[:preview] == 'true' - results = @project_results&.limit(4) || [] + results = @project_results.take(4) || [] else results = @project_results.page(params[:page]).per(Constants::SEARCH_LIMIT) end @@ -61,7 +61,7 @@ class SearchController < ApplicationController @protocol_search_count = fetch_cached_count(Protocol) search_protocols results = if params[:preview] == 'true' - @protocol_results&.limit(4) || [] + @protocol_results.take(4) || [] else @protocol_results.page(params[:page]).per(Constants::SEARCH_LIMIT) end @@ -70,8 +70,26 @@ class SearchController < ApplicationController each_serializer: GlobalSearch::ProtocolSerializer, meta: { total: @search_count, - next_page: (results.next_page if results.respond_to?(:next_page)), - } + next_page: (results.next_page if results.respond_to?(:next_page)) + } + return + when 'label_templates' + return render json: [], meta: { disabled: true }, status: :ok unless LabelTemplate.enabled? + + @label_template_search_count = fetch_cached_count(LabelTemplate) + search_label_templates + results = if params[:preview] == 'true' + @label_template_results.take(4) || [] + else + @label_template_results.page(params[:page]).per(Constants::SEARCH_LIMIT) + end + + render json: results, + each_serializer: GlobalSearch::LabelTemplateSerializer, + meta: { + total: @search_count, + next_page: (results.next_page if results.respond_to?(:next_page)) + } return end @@ -343,6 +361,12 @@ class SearchController < ApplicationController @search_count = @protocol_search_count end + def search_label_templates + @label_template_results = [] + @label_template_results = search_by_name(LabelTemplate) if @label_template_search_count.positive? + @search_count = @label_template_search_count + end + def search_steps @step_results = [] @step_results = search_by_name(Step) if @step_search_count.positive? diff --git a/app/javascript/vue/global_search/groups/label_templates.vue b/app/javascript/vue/global_search/groups/label_templates.vue index 17e71f21f..311068190 100644 --- a/app/javascript/vue/global_search/groups/label_templates.vue +++ b/app/javascript/vue/global_search/groups/label_templates.vue @@ -1,14 +1,69 @@ diff --git a/app/javascript/vue/global_search/groups/search_mixin.js b/app/javascript/vue/global_search/groups/search_mixin.js index bf638ee92..00f87e79f 100644 --- a/app/javascript/vue/global_search/groups/search_mixin.js +++ b/app/javascript/vue/global_search/groups/search_mixin.js @@ -17,6 +17,7 @@ export default { total: 0, loading: false, page: 1, + disabled: false, fullDataLoaded: false }; }, @@ -80,6 +81,7 @@ export default { if (this.selected) this.fullDataLoaded = true; this.results = this.results.concat(response.data.data); this.total = response.data.meta.total; + this.disabled = response.data.meta.disabled; this.loading = false; this.page = response.data.meta.next_page; }) diff --git a/app/models/label_template.rb b/app/models/label_template.rb index 21526f31d..bd4836ea1 100644 --- a/app/models/label_template.rb +++ b/app/models/label_template.rb @@ -7,6 +7,9 @@ class LabelTemplate < ApplicationRecord belongs_to :created_by, class_name: 'User', optional: true belongs_to :last_modified_by, class_name: 'User', optional: true + SEARCHABLE_ATTRIBUTES = ['label_templates.name', + 'label_templates.description'].freeze + enum unit: { in: 0, mm: 1 } validates :name, presence: true, length: { minimum: Constants::NAME_MIN_LENGTH, @@ -21,6 +24,25 @@ class LabelTemplate < ApplicationRecord ApplicationSettings.instance.values['label_templates_enabled'] == true end + def self.search( + user = nil, + include_archived = nil, + query = nil, + page = 1, + current_team = nil, + options = {} + ) + + new_query = LabelTemplate.where_attributes_like(SEARCHABLE_ATTRIBUTES, query, options) + + # Show all results if needed + if page == Constants::SEARCH_NO_LIMIT + new_query + else + new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT) + end + end + def icon 'zpl' end diff --git a/app/serializers/global_search/label_template_serializer.rb b/app/serializers/global_search/label_template_serializer.rb new file mode 100644 index 000000000..da33cf658 --- /dev/null +++ b/app/serializers/global_search/label_template_serializer.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module GlobalSearch + class LabelTemplateSerializer < ActiveModel::Serializer + include Rails.application.routes.url_helpers + + attributes :id, :name, :format, :created_at, :updated_at, :created_by, :team, :url + + def team + { + name: object.team.name, + url: label_templates_path(team: object.team) + } + end + + def created_by + { + name: object.created_by ? object.created_by.name : object.created_by_user, + avatar_url: object.created_by ? avatar_path(object.created_by, :icon_small) : nil + } + end + + def format + object.label_format + end + + def created_at + I18n.l(object.created_at, format: :full_date) + end + + def updated_at + I18n.l(object.updated_at, format: :full_date) + end + + def url + label_template_path(object) + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 62e92ba04..aac6f8ac8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -452,6 +452,7 @@ en: inventory_items: "Inventory items" protocol_templates: "Protocol templates" label_templates: "Label templates" + format: "Format" reports: "Reports" more_search_options: "More search options" clear_filters: "Clear filters" From 31994b643ad66ac1a6bf927f8a64b2a58cc0508c Mon Sep 17 00:00:00 2001 From: sboursen-scinote Date: Wed, 3 Apr 2024 01:04:20 +0200 Subject: [PATCH 017/115] Fix hound [SCI-10478] --- app/controllers/search_controller.rb | 18 +++++++++--------- .../global_search/groups/label_templates.vue | 2 +- app/models/label_template.rb | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 7b2ba3618..f58dc4e44 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -16,7 +16,7 @@ class SearchController < ApplicationController @project_search_count = fetch_cached_count(Project) search_projects if params[:preview] == 'true' - results = @project_results.take(4) || [] + results = @project_results.take(4) else results = @project_results.page(params[:page]).per(Constants::SEARCH_LIMIT) end @@ -61,7 +61,7 @@ class SearchController < ApplicationController @protocol_search_count = fetch_cached_count(Protocol) search_protocols results = if params[:preview] == 'true' - @protocol_results.take(4) || [] + @protocol_results.take(4) else @protocol_results.page(params[:page]).per(Constants::SEARCH_LIMIT) end @@ -69,9 +69,9 @@ class SearchController < ApplicationController render json: results, each_serializer: GlobalSearch::ProtocolSerializer, meta: { - total: @search_count, - next_page: (results.next_page if results.respond_to?(:next_page)) - } + total: @search_count, + next_page: (results.next_page if results.respond_to?(:next_page)) + } return when 'label_templates' return render json: [], meta: { disabled: true }, status: :ok unless LabelTemplate.enabled? @@ -79,7 +79,7 @@ class SearchController < ApplicationController @label_template_search_count = fetch_cached_count(LabelTemplate) search_label_templates results = if params[:preview] == 'true' - @label_template_results.take(4) || [] + @label_template_results.take(4) else @label_template_results.page(params[:page]).per(Constants::SEARCH_LIMIT) end @@ -87,9 +87,9 @@ class SearchController < ApplicationController render json: results, each_serializer: GlobalSearch::LabelTemplateSerializer, meta: { - total: @search_count, - next_page: (results.next_page if results.respond_to?(:next_page)) - } + total: @search_count, + next_page: (results.next_page if results.respond_to?(:next_page)) + } return end diff --git a/app/javascript/vue/global_search/groups/label_templates.vue b/app/javascript/vue/global_search/groups/label_templates.vue index 311068190..a8abb1983 100644 --- a/app/javascript/vue/global_search/groups/label_templates.vue +++ b/app/javascript/vue/global_search/groups/label_templates.vue @@ -52,7 +52,7 @@ export default { data() { return { group: 'label_templates', - enabled: false, + enabled: false }; }, created() { diff --git a/app/models/label_template.rb b/app/models/label_template.rb index bd4836ea1..d6584e0e5 100644 --- a/app/models/label_template.rb +++ b/app/models/label_template.rb @@ -25,11 +25,11 @@ class LabelTemplate < ApplicationRecord end def self.search( - user = nil, - include_archived = nil, + _user, + _include_archived, query = nil, page = 1, - current_team = nil, + _current_team = nil, options = {} ) From b54c4f725bc35c6ecd60b97ad0c832705d8d249e Mon Sep 17 00:00:00 2001 From: sboursen-scinote Date: Wed, 3 Apr 2024 01:23:37 +0200 Subject: [PATCH 018/115] Fix hound [SCI-10476] --- app/controllers/search_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index ceb4377c6..b04412be8 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -85,9 +85,9 @@ class SearchController < ApplicationController render json: results, each_serializer: GlobalSearch::RepositoryRowSerializer, meta: { - total: @search_count, - next_page: (results.next_page if results.respond_to?(:next_page)) - } + total: @search_count, + next_page: (results.next_page if results.respond_to?(:next_page)) + } return end From ae5cebe7d74e9763fec00b0c5dbf0a9782c1acea Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 27 Feb 2024 12:10:27 +0100 Subject: [PATCH 019/115] Implement quick search backend [SCI-10246] --- app/controllers/search_controller.rb | 32 ++++++++++++------- .../vue/navigation/quick_search.vue | 4 ++- .../concerns/searchable_by_name_model.rb | 6 ++-- app/models/label_template.rb | 12 +++++++ app/models/project_folder.rb | 6 ++++ app/serializers/quick_search_serializer.rb | 4 +-- config/initializers/constants.rb | 6 ++++ 7 files changed, 52 insertions(+), 18 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index f7f281563..936249958 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -114,23 +114,33 @@ class SearchController < ApplicationController end def quick - results = [ - Project.first, - Experiment.first, - MyModule.first, - Protocol.first, - RepositoryRow.first, - Result.first, - Step.first, - Report.first, - LabelTemplate.first - ].compact + results = if params[:filter].present? + object_quick_search(params[:filter].singularize, + search_by_id: Constants::QUICK_SEARCH_SEARCHABLE_BY_NAME + .exclude?(params[:filter].singularize)) + else + Constants::QUICK_SEARCH_SEARCHABLE_OBJECTS.filter_map do |object| + next if object == 'label_template' && !LabelTemplate.enabled? + + object_quick_search(object, search_by_id: Constants::QUICK_SEARCH_SEARCHABLE_BY_NAME.exclude?(object)) + end.flatten.sort_by(&:updated_at).reverse.take(Constants::QUICK_SEARCH_LIMIT) + end render json: results, each_serializer: QuickSearchSerializer end private + def object_quick_search(class_name, search_by_id: true) + search_method = class_name.to_s.camelize.constantize.method(search_by_id ? :search_by_name_and_id : :search_by_name) + + search_method.call(current_user, + current_team, + params[:query], + limit: Constants::QUICK_SEARCH_LIMIT) + .order(updated_at: :desc) + end + def load_vars query = (params.fetch(:q) { '' }).strip @search_category = params[:category] || '' diff --git a/app/javascript/vue/navigation/quick_search.vue b/app/javascript/vue/navigation/quick_search.vue index 6a363e3ab..d3064480c 100644 --- a/app/javascript/vue/navigation/quick_search.vue +++ b/app/javascript/vue/navigation/quick_search.vue @@ -171,7 +171,9 @@ export default { const breadcrumbs = attributes.breadcrumbs.map((breadcrumb) => breadcrumb.name); breadcrumbs.pop(); breadcrumbs.shift(); - breadcrumbs.push(`ID: ${attributes.code}`); + if (attributes.code) { + breadcrumbs.push(`ID: ${attributes.code}`); + } return breadcrumbs; }, setQuery(query) { diff --git a/app/models/concerns/searchable_by_name_model.rb b/app/models/concerns/searchable_by_name_model.rb index 7ac1143e3..6754f7691 100644 --- a/app/models/concerns/searchable_by_name_model.rb +++ b/app/models/concerns/searchable_by_name_model.rb @@ -20,10 +20,10 @@ module SearchableByNameModel sql_q = sql_q.where(id: viewable_by_user(user, teams)) - sql_q.limit(Constants::SEARCH_LIMIT) + sql_q.limit(options[:limit] || Constants::SEARCH_LIMIT) end - def self.search_by_name_and_id(user, teams = [], query = nil) + def self.search_by_name_and_id(user, teams = [], query = nil, options = {}) return if user.blank? || teams.blank? sanitized_query = ActiveRecord::Base.sanitize_sql_like(query.to_s) @@ -34,7 +34,7 @@ module SearchableByNameModel "%#{sanitized_query}%", "%#{sanitized_query}%" ) - sql_q.limit(Constants::SEARCH_LIMIT) + sql_q.limit(options[:limit] || Constants::SEARCH_LIMIT) end end # rubocop:enable Metrics/BlockLength diff --git a/app/models/label_template.rb b/app/models/label_template.rb index 21526f31d..9074364f7 100644 --- a/app/models/label_template.rb +++ b/app/models/label_template.rb @@ -2,6 +2,7 @@ class LabelTemplate < ApplicationRecord include SearchableModel + include SearchableByNameModel belongs_to :team belongs_to :created_by, class_name: 'User', optional: true @@ -17,6 +18,17 @@ class LabelTemplate < ApplicationRecord scope :default, -> { where(default: true) } + def self.viewable_by_user(user, teams) + joins("INNER JOIN user_assignments team_user_assignments + ON team_user_assignments.assignable_id = label_templates.team_id + AND team_user_assignments.assignable_type = 'Team' + AND team_user_assignments.user_id = #{user.id} + INNER JOIN user_roles team_user_roles + ON team_user_roles.id = team_user_assignments.user_role_id + AND team_user_roles.permissions @> ARRAY['#{TeamPermissions::LABEL_TEMPLATES_READ}']::varchar[]") + .where(team: teams) + end + def self.enabled? ApplicationSettings.instance.values['label_templates_enabled'] == true end diff --git a/app/models/project_folder.rb b/app/models/project_folder.rb index 5f68c351b..e52f51e21 100644 --- a/app/models/project_folder.rb +++ b/app/models/project_folder.rb @@ -33,6 +33,12 @@ class ProjectFolder < ApplicationRecord scope :top_level, -> { where(parent_folder: nil) } + def self.viewable_by_user(user, teams) + joins(team: :users) + .where(teams: { user_assignments: { user: user } }) + .where(team: teams) + end + def self.search(user, _include_archived, query = nil, page = 1, current_team = nil, options = {}) new_query = if current_team current_team.project_folders.where_attributes_like(:name, query, options) diff --git a/app/serializers/quick_search_serializer.rb b/app/serializers/quick_search_serializer.rb index bb7faa2a9..6cf804966 100644 --- a/app/serializers/quick_search_serializer.rb +++ b/app/serializers/quick_search_serializer.rb @@ -13,9 +13,7 @@ class QuickSearchSerializer < ActiveModel::Serializer end def code - @object.code - rescue StandardError - @object.id + @object.code unless @object.is_a?(ProjectFolder) || object.is_a?(Result) || object.is_a?(LabelTemplate) end def updated_at diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb index 00f966099..4d7aa80dd 100644 --- a/config/initializers/constants.rb +++ b/config/initializers/constants.rb @@ -443,6 +443,12 @@ class Constants MIN_SCINOTE_EDIT_VERSION = ENV['MIN_SCINOTE_EDIT_VERSION'].freeze MAX_SCINOTE_EDIT_VERSION = ENV['MAX_SCINOTE_EDIT_VERSION'].freeze + # quick search + QUICK_SEARCH_LIMIT = 5 + QUICK_SEARCH_SEARCHABLE_OBJECTS = %w(project experiment my_module protocol repository_row + report project_folder result label_template).freeze + QUICK_SEARCH_SEARCHABLE_BY_NAME = %w(project_folder result label_template).freeze + # ) \ / ( # /|\ )\_/( /|\ # * / | \ (/\|/\) / | \ * From c6f5997fdaa7c146e74159fe8101f0e43f429a04 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 26 Mar 2024 15:07:23 +0100 Subject: [PATCH 020/115] Add filter component to quick search [SCI-10305] --- app/assets/stylesheets/tailwind/buttons.css | 2 +- app/controllers/teams_controller.rb | 16 +- .../vue/global_search/container.vue | 33 ++- app/javascript/vue/global_search/filters.vue | 206 ++++++++++++++++++ .../vue/global_search/filters/date.vue | 132 +++++++++++ .../vue/navigation/quick_search.vue | 55 ++++- app/javascript/vue/navigation/top_menu.vue | 14 +- app/javascript/vue/shared/select_dropdown.vue | 13 +- app/serializers/team_serializer.rb | 5 + app/views/search/index.html.erb | 2 + app/views/shared/navigation/_top.html.erb | 5 +- config/locales/en.yml | 8 + config/routes.rb | 5 + 13 files changed, 473 insertions(+), 23 deletions(-) create mode 100644 app/javascript/vue/global_search/filters.vue create mode 100644 app/javascript/vue/global_search/filters/date.vue create mode 100644 app/serializers/team_serializer.rb diff --git a/app/assets/stylesheets/tailwind/buttons.css b/app/assets/stylesheets/tailwind/buttons.css index e7052435d..9f7001362 100644 --- a/app/assets/stylesheets/tailwind/buttons.css +++ b/app/assets/stylesheets/tailwind/buttons.css @@ -33,7 +33,7 @@ } .btn.btn-xs.icon-btn { - @apply px-0.5; + @apply px-0.5 w-[30px]; } .btn:hover { diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 965f079ef..36b9c2c12 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -9,9 +9,23 @@ class TeamsController < ApplicationController before_action :load_vars, only: %i(sidebar export_projects export_projects_modal disable_tasks_sharing_modal shared_tasks_toggle) before_action :load_current_folder, only: :sidebar - before_action :check_read_permissions, except: :view_type + before_action :check_read_permissions, except: %i(view_type visible_teams visible_users) before_action :check_export_projects_permissions, only: %i(export_projects_modal export_projects) + def visible_teams + teams = current_user.teams + render json: teams, each_serializer: TeamSerializer + end + + def visible_users + teams = current_user.teams + if params[:teams].present? + teams = teams.where(id: params[:teams]) + end + users = User.where(id: teams.joins(:users).select('users.id')).order(:full_name) + render json: users, each_serializer: UserSerializer, user: current_user + end + def sidebar render json: { html: render_to_string( diff --git a/app/javascript/vue/global_search/container.vue b/app/javascript/vue/global_search/container.vue index be302d1b1..48c1ebd86 100644 --- a/app/javascript/vue/global_search/container.vue +++ b/app/javascript/vue/global_search/container.vue @@ -24,10 +24,21 @@ {{ i18n.t('search.index.task_results') }} - + + + +