From 1056ffd08e543df8f40845d89d2a2792c9eb8f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 19 Oct 2023 14:29:47 +0200 Subject: [PATCH 01/39] Add optional S3 config/backend for ActiveStorage --- .env.example | 8 ++++++++ config/environments/development.rb | 7 +++++-- config/environments/production.rb | 8 +++++--- config/environments/test.rb | 8 ++++++-- config/storage.yml | 10 ++++++++++ 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index b7244d8..390fe6a 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,14 @@ SMTP_DOMAIN=example.com SMTP_AUTH_METHOD=plain SMTP_ENABLE_STARTTLS=auto +# S3_ENABLED=true +# S3_ENDPOINT=https://s3.kosmos.org +# S3_REGION=garage +# S3_BUCKET=akkounts-production +# S3_ALIAS_HOST=accounts.s3.kosmos.org +# S3_ACCESS_KEY=123456abcdefg +# S3_SECRET_KEY=123456789123456789123456789 + LDAP_HOST=localhost LDAP_PORT=389 LDAP_ADMIN_PASSWORD=passthebutter diff --git a/config/environments/development.rb b/config/environments/development.rb index eecc942..1f96c8f 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -71,6 +71,9 @@ Rails.application.configure do # Allow requests from any IP config.web_console.whiny_requests = false - # Store attachments on the local disk (in ./storage) - config.active_storage.service = :local + if ENV["S3_ENABLED"] + config.active_storage.service = :s3 + else + config.active_storage.service = :local + end end diff --git a/config/environments/production.rb b/config/environments/production.rb index bfcfecb..5adf41e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -110,9 +110,11 @@ Rails.application.configure do # Set this to true and configure the email server for immediate delivery to raise delivery errors. config.action_mailer.raise_delivery_errors = true - # TODO make configurable - # Store attachments in S3-compatible back-end - config.active_storage.service = :local + if ENV["S3_ENABLED"] + config.active_storage.service = :s3 + else + config.active_storage.service = :local + end # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). diff --git a/config/environments/test.rb b/config/environments/test.rb index 739a1ac..7d731e9 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -52,6 +52,10 @@ Rails.application.configure do config.active_job.queue_adapter = :test - # Store attachments on the local disk (in ./tmp) - config.active_storage.service = :test + if ENV["S3_ENABLED"] + config.active_storage.service = :s3 + else + # Store attachments on the local disk (in ./tmp) + config.active_storage.service = :test + end end diff --git a/config/storage.yml b/config/storage.yml index 1f93f73..6b8a50a 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -5,3 +5,13 @@ local: test: service: Disk root: <%= Rails.root.join("tmp/storage") %> + +<% if ENV["S3_ENABLED"] %> +s3: + service: S3 + endpoint: ENV["S3_ENDPOINT"] + region: ENV["S3_REGION"] + bucket: ENV["S3_BUCKET"] + access_key_id: ENV["S3_ACCESS_KEY"] + secret_access_key: ENV["S3_SECRET_KEY"] +<% end %> -- 2.25.1 From e1b7e1b2ef9fc463dc0dffea3e96c18d883f2fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 19 Oct 2023 16:17:42 +0200 Subject: [PATCH 02/39] Update dependencies, add manifique --- Gemfile | 1 + Gemfile.lock | 251 +++++++++++++++++++++++++++------------------------ 2 files changed, 136 insertions(+), 116 deletions(-) diff --git a/Gemfile b/Gemfile index b169662..2f814ba 100644 --- a/Gemfile +++ b/Gemfile @@ -59,6 +59,7 @@ gem "sentry-rails" # Services gem 'discourse_api' gem "lnurl" +gem 'manifique', git: 'https://gitea.kosmos.org/5apps/manifique.git', branch: 'master' gem 'nostr', git: 'https://gitea.kosmos.org/kosmos/nostr-gem.git', branch: 'feature/ruby_2.7_compat' group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index b869deb..954f8d7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,13 @@ +GIT + remote: https://gitea.kosmos.org/5apps/manifique.git + revision: 8d79113438ee7c3e4288f840a135622519cffd5c + branch: master + specs: + manifique (0.1.0) + faraday (~> 2.7.11) + faraday-follow_redirects (= 0.3.0) + nokogiri (~> 1.15.4) + GIT remote: https://gitea.kosmos.org/kosmos/nostr-gem.git revision: 596529d9eb50d13b3f385245636698fccf37b442 @@ -14,82 +24,84 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.5) - actionpack (= 7.0.5) - activesupport (= 7.0.5) + actioncable (7.0.8) + actionpack (= 7.0.8) + activesupport (= 7.0.8) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.5) - actionpack (= 7.0.5) - activejob (= 7.0.5) - activerecord (= 7.0.5) - activestorage (= 7.0.5) - activesupport (= 7.0.5) + actionmailbox (7.0.8) + actionpack (= 7.0.8) + activejob (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.5) - actionpack (= 7.0.5) - actionview (= 7.0.5) - activejob (= 7.0.5) - activesupport (= 7.0.5) + actionmailer (7.0.8) + actionpack (= 7.0.8) + actionview (= 7.0.8) + activejob (= 7.0.8) + activesupport (= 7.0.8) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.5) - actionview (= 7.0.5) - activesupport (= 7.0.5) + actionpack (7.0.8) + actionview (= 7.0.8) + activesupport (= 7.0.8) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.5) - actionpack (= 7.0.5) - activerecord (= 7.0.5) - activestorage (= 7.0.5) - activesupport (= 7.0.5) + actiontext (7.0.8) + actionpack (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.5) - activesupport (= 7.0.5) + actionview (7.0.8) + activesupport (= 7.0.8) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.5) - activesupport (= 7.0.5) + activejob (7.0.8) + activesupport (= 7.0.8) globalid (>= 0.3.6) - activemodel (7.0.5) - activesupport (= 7.0.5) - activerecord (7.0.5) - activemodel (= 7.0.5) - activesupport (= 7.0.5) - activestorage (7.0.5) - actionpack (= 7.0.5) - activejob (= 7.0.5) - activerecord (= 7.0.5) - activesupport (= 7.0.5) + activemodel (7.0.8) + activesupport (= 7.0.8) + activerecord (7.0.8) + activemodel (= 7.0.8) + activesupport (= 7.0.8) + activestorage (7.0.8) + actionpack (= 7.0.8) + activejob (= 7.0.8) + activerecord (= 7.0.8) + activesupport (= 7.0.8) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.5) + activesupport (7.0.8) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.4) + addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) backport (1.2.0) - bcrypt (3.1.18) - bech32 (1.3.0) + base64 (0.1.1) + bcrypt (3.1.19) + bech32 (1.4.2) thor (>= 1.1.0) benchmark (0.2.1) bindex (0.8.1) bip-schnorr (0.6.0) ecdsa_ext (~> 0.5.0) + brow (0.4.1) builder (3.2.4) byebug (11.1.3) capybara (3.39.2) @@ -107,7 +119,7 @@ GEM crack (0.4.5) rexml crass (1.0.6) - cssbundling-rails (1.1.2) + cssbundling-rails (1.3.3) railties (>= 6.0.0) database_cleaner (2.0.2) database_cleaner-active_record (>= 2, < 3) @@ -116,7 +128,7 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.3) - devise (4.9.2) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -149,9 +161,10 @@ GEM factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) - faker (3.2.0) + faker (3.2.1) i18n (>= 1.8.11, < 2) - faraday (2.7.6) + faraday (2.7.11) + base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-follow_redirects (0.3.0) @@ -159,33 +172,34 @@ GEM faraday-multipart (1.0.4) multipart-post (~> 2) faraday-net_http (3.0.2) - faye-websocket (0.11.2) + faye-websocket (0.11.3) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) - ffi (1.15.5) - flipper (0.28.0) + ffi (1.16.3) + flipper (1.0.0) + brow (~> 0.4.1) concurrent-ruby (< 2) - flipper-active_record (0.28.0) + flipper-active_record (1.0.0) activerecord (>= 4.2, < 8) - flipper (~> 0.28.0) - flipper-ui (0.28.0) + flipper (~> 1.0.0) + flipper-ui (1.0.0) erubi (>= 1.0.0, < 2.0.0) - flipper (~> 0.28.0) - rack (>= 1.4, < 3) + flipper (~> 1.0.0) + rack (>= 1.4, < 4) rack-protection (>= 1.5.3, <= 4.0.0) sanitize (< 7) fugit (1.8.1) et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) - globalid (1.1.0) - activesupport (>= 5.0) + globalid (1.2.1) + activesupport (>= 6.1) hashdiff (1.0.1) i18n (1.14.1) concurrent-ruby (~> 1.0) image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - importmap-rails (1.1.6) + importmap-rails (1.2.1) actionpack (>= 6.0.0) railties (>= 6.0.0) jaro_winkler (1.5.6) @@ -197,6 +211,7 @@ GEM rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) + language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) letter_opener (1.8.1) @@ -209,10 +224,10 @@ GEM listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - lnurl (1.0.1) + lnurl (1.1.0) bech32 (~> 1.1) - lockbox (1.2.0) - loofah (2.21.3) + lockbox (1.3.0) + loofah (2.21.4) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -224,10 +239,10 @@ GEM matrix (0.4.2) method_source (1.0.0) mini_magick (4.12.0) - mini_mime (1.1.2) - minitest (5.18.0) + mini_mime (1.1.5) + minitest (5.20.0) multipart-post (2.3.0) - net-imap (0.3.6) + net-imap (0.3.7) date net-protocol net-ldap (0.18.0) @@ -235,50 +250,51 @@ GEM net-protocol net-protocol (0.2.1) timeout - net-smtp (0.3.3) + net-smtp (0.4.0) net-protocol nio4r (2.5.9) - nokogiri (1.15.2-arm64-darwin) + nokogiri (1.15.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.15.2-x86_64-linux) + nokogiri (1.15.4-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) - pagy (6.0.4) + pagy (6.1.0) parallel (1.23.0) - parser (3.2.2.3) + parser (3.2.2.4) ast (~> 2.4.1) racc pg (1.2.3) - public_suffix (5.0.1) + public_suffix (5.0.3) puma (4.3.12) nio4r (~> 2.0) raabro (1.4.0) racc (1.7.1) - rack (2.2.7) - rack-protection (3.0.6) - rack + rack (2.2.8) + rack-protection (3.1.0) + rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.5) - actioncable (= 7.0.5) - actionmailbox (= 7.0.5) - actionmailer (= 7.0.5) - actionpack (= 7.0.5) - actiontext (= 7.0.5) - actionview (= 7.0.5) - activejob (= 7.0.5) - activemodel (= 7.0.5) - activerecord (= 7.0.5) - activestorage (= 7.0.5) - activesupport (= 7.0.5) + rails (7.0.8) + actioncable (= 7.0.8) + actionmailbox (= 7.0.8) + actionmailer (= 7.0.8) + actionpack (= 7.0.8) + actiontext (= 7.0.8) + actionview (= 7.0.8) + activejob (= 7.0.8) + activemodel (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) bundler (>= 1.15.0) - railties (= 7.0.5) + railties (= 7.0.8) 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.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) rails-html-sanitizer (1.6.0) loofah (~> 2.21) @@ -286,9 +302,9 @@ GEM rails-settings-cached (2.8.3) activerecord (>= 5.0.0) railties (>= 5.0.0) - railties (7.0.5) - actionpack (= 7.0.5) - activesupport (= 7.0.5) + railties (7.0.8) + actionpack (= 7.0.8) + activesupport (= 7.0.8) method_source rake (>= 12.2) thor (~> 1.0) @@ -300,13 +316,13 @@ GEM ffi (~> 1.0) rbs (2.8.4) redis (4.8.1) - regexp_parser (2.8.1) - responders (3.1.0) + regexp_parser (2.8.2) + responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) reverse_markdown (2.1.1) nokogiri - rexml (3.2.5) + rexml (3.2.6) rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) @@ -316,7 +332,7 @@ GEM rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.5) + rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (6.0.3) @@ -327,34 +343,36 @@ GEM rspec-expectations (~> 3.12) rspec-mocks (~> 3.12) rspec-support (~> 3.12) - rspec-support (3.12.0) - rubocop (1.52.1) + rspec-support (3.12.1) + rubocop (1.57.1) + base64 (~> 0.1.1) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.3) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.29.0) parser (>= 3.2.1.0) ruby-progressbar (1.13.0) - ruby-vips (2.1.4) + ruby-vips (2.2.0) ffi (~> 1.12) ruby2_keywords (0.0.5) rufus-scheduler (3.9.1) fugit (~> 1.1, >= 1.1.6) - sanitize (6.0.1) + sanitize (6.1.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - sentry-rails (5.9.0) + sentry-rails (5.12.0) railties (>= 5.0) - sentry-ruby (~> 5.9.0) - sentry-ruby (5.9.0) + sentry-ruby (~> 5.12.0) + sentry-ruby (5.12.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (6.5.9) + sidekiq (6.5.12) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) @@ -378,49 +396,49 @@ GEM thor (~> 1.0) tilt (~> 2.0) yard (~> 0.9, >= 0.9.24) - sprockets (4.2.0) + sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - sqlite3 (1.6.3-arm64-darwin) - sqlite3 (1.6.3-x86_64-linux) - stimulus-rails (1.2.1) + sqlite3 (1.6.7-arm64-darwin) + sqlite3 (1.6.7-x86_64-linux) + stimulus-rails (1.3.0) railties (>= 6.0.0) - thor (1.2.2) - tilt (2.2.0) - timeout (0.3.2) - turbo-rails (1.4.0) + thor (1.3.0) + tilt (2.3.0) + timeout (0.4.0) + turbo-rails (1.5.0) actionpack (>= 6.0.0) activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.4.2) - view_component (3.2.0) + unicode-display_width (2.5.0) + view_component (3.6.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) method_source (~> 1.0) warden (1.2.9) rack (>= 2.0.9) - web-console (4.2.0) + web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webmock (3.18.1) + webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - websocket-driver (0.7.5) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) yard (0.9.34) - zeitwerk (2.6.8) + zeitwerk (2.6.12) PLATFORMS arm64-darwin-22 @@ -449,6 +467,7 @@ DEPENDENCIES listen (~> 3.2) lnurl lockbox + manifique! net-ldap nostr! pagy (~> 6.0, >= 6.0.2) -- 2.25.1 From e56c9bd0d5c0f5d5580555d25873338a3dd4f69b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 19 Oct 2023 16:18:13 +0200 Subject: [PATCH 03/39] Add web app model, service to fetch metadata --- app/models/app_catalog.rb | 5 ++++ app/models/app_catalog/web_app.rb | 9 +++++++ .../app_catalog_manager/fetch_metadata.rb | 26 +++++++++++++++++++ app/services/app_catalog_manager_service.rb | 2 ++ ...31019125135_create_app_catalog_web_apps.rb | 11 ++++++++ db/schema.rb | 10 ++++++- spec/factories/app_catalog/web_apps.rb | 14 ++++++++++ spec/models/app_catalog/web_app_spec.rb | 5 ++++ 8 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 app/models/app_catalog.rb create mode 100644 app/models/app_catalog/web_app.rb create mode 100644 app/services/app_catalog_manager/fetch_metadata.rb create mode 100644 app/services/app_catalog_manager_service.rb create mode 100644 db/migrate/20231019125135_create_app_catalog_web_apps.rb create mode 100644 spec/factories/app_catalog/web_apps.rb create mode 100644 spec/models/app_catalog/web_app_spec.rb diff --git a/app/models/app_catalog.rb b/app/models/app_catalog.rb new file mode 100644 index 0000000..e6cd4c5 --- /dev/null +++ b/app/models/app_catalog.rb @@ -0,0 +1,5 @@ +module AppCatalog + def self.table_name_prefix + "app_catalog_" + end +end diff --git a/app/models/app_catalog/web_app.rb b/app/models/app_catalog/web_app.rb new file mode 100644 index 0000000..4287a11 --- /dev/null +++ b/app/models/app_catalog/web_app.rb @@ -0,0 +1,9 @@ +class AppCatalog::WebApp < ApplicationRecord + + store :metadata, coder: JSON + + validates :url, presence: true, uniqueness: true + validates :url, format: { with: URI.regexp }, + if: Proc.new { |a| a.url.present? } + +end diff --git a/app/services/app_catalog_manager/fetch_metadata.rb b/app/services/app_catalog_manager/fetch_metadata.rb new file mode 100644 index 0000000..3e6f11d --- /dev/null +++ b/app/services/app_catalog_manager/fetch_metadata.rb @@ -0,0 +1,26 @@ +require 'manifique' + +module AppCatalogManager + class FetchMetadata < AppCatalogManagerService + def initialize(app) + @app = app + end + + def call + agent = Manifique::Agent.new(url: @app.url) + metadata = agent.fetch_metadata + + @app.name = metadata.name + + [:name, :short_name, :description, :theme_color, :background_color, + :display, :start_url, :scope, :share_target].each do |prop| + @app.metadata[prop] = metadata.send(prop) if prop + end + rescue Manifique::Error => e + msg = "Fetching web app manifest failed for #{e.url}: #{e.type}" + Rails.logger.warn(msg) + Sentry.capture_message(msg) if Setting.sentry_enabled? + false + end + end +end diff --git a/app/services/app_catalog_manager_service.rb b/app/services/app_catalog_manager_service.rb new file mode 100644 index 0000000..f513907 --- /dev/null +++ b/app/services/app_catalog_manager_service.rb @@ -0,0 +1,2 @@ +class AppCatalogManagerService < ApplicationService +end diff --git a/db/migrate/20231019125135_create_app_catalog_web_apps.rb b/db/migrate/20231019125135_create_app_catalog_web_apps.rb new file mode 100644 index 0000000..451a54b --- /dev/null +++ b/db/migrate/20231019125135_create_app_catalog_web_apps.rb @@ -0,0 +1,11 @@ +class CreateAppCatalogWebApps < ActiveRecord::Migration[7.0] + def change + create_table :app_catalog_web_apps do |t| + t.string :url + t.string :name + t.text :metadata + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index bff7e78..acee874 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_09_06_073324) do +ActiveRecord::Schema[7.0].define(version: 2023_10_19_125135) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -39,6 +39,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_06_073324) do t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end + create_table "app_catalog_web_apps", force: :cascade do |t| + t.string "url" + t.string "name" + t.text "metadata" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "donations", force: :cascade do |t| t.integer "user_id" t.integer "amount_sats" diff --git a/spec/factories/app_catalog/web_apps.rb b/spec/factories/app_catalog/web_apps.rb new file mode 100644 index 0000000..b9ef31a --- /dev/null +++ b/spec/factories/app_catalog/web_apps.rb @@ -0,0 +1,14 @@ +FactoryBot.define do + factory :app_catalog_web_app, class: 'AppCatalog::WebApp' do + url { "https://myfavoritedrinks.remotestorage.io/" } + name { "My Favorite Drinks" } + short_name { "Drinks" } + description { nil } + theme_color { nil } + background_color { nil } + display { nil } + start_url { nil } + scope { nil } + share_target { nil } + end +end diff --git a/spec/models/app_catalog/web_app_spec.rb b/spec/models/app_catalog/web_app_spec.rb new file mode 100644 index 0000000..5d94508 --- /dev/null +++ b/spec/models/app_catalog/web_app_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe AppCatalog::WebApp, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end -- 2.25.1 From d4f71e98ed01ef69d9ef00fc3e74e1b9b255f624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 23 Oct 2023 15:31:22 +0200 Subject: [PATCH 04/39] Download and attach icons for web apps --- Gemfile | 2 + Gemfile.lock | 21 +++++++++ app/models/app_catalog/web_app.rb | 13 +++++- .../app_catalog_manager/fetch_metadata.rb | 26 ----------- .../app_catalog_manager/update_metadata.rb | 44 +++++++++++++++++++ config/storage.yml | 10 ++--- 6 files changed, 84 insertions(+), 32 deletions(-) delete mode 100644 app/services/app_catalog_manager/fetch_metadata.rb create mode 100644 app/services/app_catalog_manager/update_metadata.rb diff --git a/Gemfile b/Gemfile index 2f814ba..df0e515 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,8 @@ gem 'flipper-ui' # HTTP requests gem 'faraday' +gem 'down' +gem 'aws-sdk-s3', require: false # Background/scheduled jobs gem 'sidekiq', '< 7' diff --git a/Gemfile.lock b/Gemfile.lock index 954f8d7..f9c03a3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,6 +92,22 @@ GEM addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) + aws-eventstream (1.2.0) + aws-partitions (1.839.0) + aws-sdk-core (3.185.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.72.0) + aws-sdk-core (~> 3, >= 3.184.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.136.0) + aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.6) + aws-sigv4 (1.6.0) + aws-eventstream (~> 1, >= 1.0.2) backport (1.2.0) base64 (0.1.1) bcrypt (3.1.19) @@ -147,6 +163,8 @@ GEM dotenv-rails (2.8.1) dotenv (= 2.8.1) railties (>= 3.2) + down (5.4.1) + addressable (~> 2.8) e2mmap (0.1.0) ecdsa (1.2.0) ecdsa_ext (0.5.0) @@ -206,6 +224,7 @@ GEM jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) + jmespath (1.6.2) json (2.6.3) kramdown (2.4.0) rexml @@ -445,6 +464,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + aws-sdk-s3 byebug (~> 11.1) capybara cssbundling-rails @@ -453,6 +473,7 @@ DEPENDENCIES devise_ldap_authenticatable discourse_api dotenv-rails + down factory_bot_rails faker faraday diff --git a/app/models/app_catalog/web_app.rb b/app/models/app_catalog/web_app.rb index 4287a11..d2f75fd 100644 --- a/app/models/app_catalog/web_app.rb +++ b/app/models/app_catalog/web_app.rb @@ -1,9 +1,20 @@ class AppCatalog::WebApp < ApplicationRecord - store :metadata, coder: JSON + has_one_attached :icon do |attachable| + attachable.variant :medium, resize_to_limit: [128,128] + attachable.variant :large, resize_to_limit: [256,256] + end + + has_one_attached :apple_touch_icon + validates :url, presence: true, uniqueness: true validates :url, format: { with: URI.regexp }, if: Proc.new { |a| a.url.present? } + # before_create :update_metadata + + def update_metadata + AppCatalogManager::UpdateMetadata.call(self) + end end diff --git a/app/services/app_catalog_manager/fetch_metadata.rb b/app/services/app_catalog_manager/fetch_metadata.rb deleted file mode 100644 index 3e6f11d..0000000 --- a/app/services/app_catalog_manager/fetch_metadata.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'manifique' - -module AppCatalogManager - class FetchMetadata < AppCatalogManagerService - def initialize(app) - @app = app - end - - def call - agent = Manifique::Agent.new(url: @app.url) - metadata = agent.fetch_metadata - - @app.name = metadata.name - - [:name, :short_name, :description, :theme_color, :background_color, - :display, :start_url, :scope, :share_target].each do |prop| - @app.metadata[prop] = metadata.send(prop) if prop - end - rescue Manifique::Error => e - msg = "Fetching web app manifest failed for #{e.url}: #{e.type}" - Rails.logger.warn(msg) - Sentry.capture_message(msg) if Setting.sentry_enabled? - false - end - end -end diff --git a/app/services/app_catalog_manager/update_metadata.rb b/app/services/app_catalog_manager/update_metadata.rb new file mode 100644 index 0000000..32e9203 --- /dev/null +++ b/app/services/app_catalog_manager/update_metadata.rb @@ -0,0 +1,44 @@ +require "manifique" +require "down" + +module AppCatalogManager + class UpdateMetadata < AppCatalogManagerService + def initialize(app) + @app = app + end + + def call + agent = Manifique::Agent.new(url: @app.url) + metadata = agent.fetch_metadata + + @app.name = metadata.name + + [:name, :short_name, :description, :theme_color, :background_color, + :display, :start_url, :scope, :share_target, :icons].each do |prop| + @app.metadata[prop] = metadata.send(prop) if prop + end + + if icon = metadata.select_icon(sizes: "256x256") + attach_remote_image(:icon, icon) + end + if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon") + attach_remote_image(:apple_touch_icon, apple_touch_icon) + end + + byebug + rescue Manifique::Error => e + msg = "Fetching web app manifest failed for #{e.url}: #{e.type}" + Rails.logger.warn(msg) + Sentry.capture_message(msg) if Setting.sentry_enabled? + false + end + + def attach_remote_image(attachment_name, icon) + download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}" + filename = "web_apps/#{@app.id}/icons/#{attachment_name}.png" + + tempfile = Down.download(download_url) + @app.send(attachment_name).attach(io: tempfile, filename: filename) + end + end +end diff --git a/config/storage.yml b/config/storage.yml index 6b8a50a..3ddfbb8 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -9,9 +9,9 @@ test: <% if ENV["S3_ENABLED"] %> s3: service: S3 - endpoint: ENV["S3_ENDPOINT"] - region: ENV["S3_REGION"] - bucket: ENV["S3_BUCKET"] - access_key_id: ENV["S3_ACCESS_KEY"] - secret_access_key: ENV["S3_SECRET_KEY"] + endpoint: <%= ENV["S3_ENDPOINT"] %> + region: <%= ENV["S3_REGION"] %> + bucket: <%= ENV["S3_BUCKET"] %> + access_key_id: <%= ENV["S3_ACCESS_KEY"] %> + secret_access_key: <%= ENV["S3_SECRET_KEY"] %> <% end %> -- 2.25.1 From 0a69603643826554e7556a778308b4d635a7e110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 23 Oct 2023 16:07:48 +0200 Subject: [PATCH 05/39] Update web app metadata when first creating a record --- app/models/app_catalog/web_app.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/app_catalog/web_app.rb b/app/models/app_catalog/web_app.rb index d2f75fd..850a4f3 100644 --- a/app/models/app_catalog/web_app.rb +++ b/app/models/app_catalog/web_app.rb @@ -12,7 +12,7 @@ class AppCatalog::WebApp < ApplicationRecord validates :url, format: { with: URI.regexp }, if: Proc.new { |a| a.url.present? } - # before_create :update_metadata + before_create :update_metadata def update_metadata AppCatalogManager::UpdateMetadata.call(self) -- 2.25.1 From bec827acb1312ae42c69c6e435e2c334bf009ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 23 Oct 2023 16:08:23 +0200 Subject: [PATCH 06/39] Store web app icons with proper folder paths --- app/services/app_catalog_manager/update_metadata.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/services/app_catalog_manager/update_metadata.rb b/app/services/app_catalog_manager/update_metadata.rb index 32e9203..15bd326 100644 --- a/app/services/app_catalog_manager/update_metadata.rb +++ b/app/services/app_catalog_manager/update_metadata.rb @@ -35,10 +35,11 @@ module AppCatalogManager def attach_remote_image(attachment_name, icon) download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}" - filename = "web_apps/#{@app.id}/icons/#{attachment_name}.png" + filename = "#{attachment_name}.png" + key = "web_apps/#{@app.id}/icons/#{attachment_name}.png" tempfile = Down.download(download_url) - @app.send(attachment_name).attach(io: tempfile, filename: filename) + @app.send(attachment_name).attach(key: key, io: tempfile, filename: filename) end end end -- 2.25.1 From e508407df46672cca2b6e7dc503fd166746e7c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 23 Oct 2023 16:09:10 +0200 Subject: [PATCH 07/39] Remove debug statement --- app/services/app_catalog_manager/update_metadata.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/services/app_catalog_manager/update_metadata.rb b/app/services/app_catalog_manager/update_metadata.rb index 15bd326..38c1d87 100644 --- a/app/services/app_catalog_manager/update_metadata.rb +++ b/app/services/app_catalog_manager/update_metadata.rb @@ -24,8 +24,6 @@ module AppCatalogManager if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon") attach_remote_image(:apple_touch_icon, apple_touch_icon) end - - byebug rescue Manifique::Error => e msg = "Fetching web app manifest failed for #{e.url}: #{e.type}" Rails.logger.warn(msg) -- 2.25.1 From e964e7e52c9b45414afc8308f78d968345dbdc95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 23 Oct 2023 16:43:18 +0200 Subject: [PATCH 08/39] Save web app metadata explicitly --- app/models/app_catalog/web_app.rb | 2 -- app/services/app_catalog_manager/update_metadata.rb | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/app_catalog/web_app.rb b/app/models/app_catalog/web_app.rb index 850a4f3..8cc1687 100644 --- a/app/models/app_catalog/web_app.rb +++ b/app/models/app_catalog/web_app.rb @@ -12,8 +12,6 @@ class AppCatalog::WebApp < ApplicationRecord validates :url, format: { with: URI.regexp }, if: Proc.new { |a| a.url.present? } - before_create :update_metadata - def update_metadata AppCatalogManager::UpdateMetadata.call(self) end diff --git a/app/services/app_catalog_manager/update_metadata.rb b/app/services/app_catalog_manager/update_metadata.rb index 38c1d87..d19ec4c 100644 --- a/app/services/app_catalog_manager/update_metadata.rb +++ b/app/services/app_catalog_manager/update_metadata.rb @@ -24,6 +24,8 @@ module AppCatalogManager if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon") attach_remote_image(:apple_touch_icon, apple_touch_icon) end + + @app.save! rescue Manifique::Error => e msg = "Fetching web app manifest failed for #{e.url}: #{e.type}" Rails.logger.warn(msg) -- 2.25.1 From 261a782963ff3b5d94c4e8393c5dd1c7d2a2d05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 23 Oct 2023 16:43:49 +0200 Subject: [PATCH 09/39] Only complete icon URLs when given relative or absolute paths --- app/services/app_catalog_manager/update_metadata.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/services/app_catalog_manager/update_metadata.rb b/app/services/app_catalog_manager/update_metadata.rb index d19ec4c..e53e606 100644 --- a/app/services/app_catalog_manager/update_metadata.rb +++ b/app/services/app_catalog_manager/update_metadata.rb @@ -34,7 +34,11 @@ module AppCatalogManager end def attach_remote_image(attachment_name, icon) - download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}" + if icon['src'].start_with?("http") + download_url = icon['src'] + else + download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}" + end filename = "#{attachment_name}.png" key = "web_apps/#{@app.id}/icons/#{attachment_name}.png" -- 2.25.1 From fcea11f0e577f0deedffc1c48de83203817db19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 24 Oct 2023 13:08:06 +0200 Subject: [PATCH 10/39] Associate RS authorizations with web apps --- app/models/app_catalog/web_app.rb | 2 ++ app/models/remote_storage_authorization.rb | 20 +++++++++++++++++++ ...app_id_to_remote_storage_authorizations.rb | 7 +++++++ db/schema.rb | 5 ++++- spec/factories/app_catalog/web_apps.rb | 10 +--------- .../remote_storage_authorizations.rb | 3 ++- 6 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 db/migrate/20231024104909_add_web_app_id_to_remote_storage_authorizations.rb diff --git a/app/models/app_catalog/web_app.rb b/app/models/app_catalog/web_app.rb index 8cc1687..ced6b29 100644 --- a/app/models/app_catalog/web_app.rb +++ b/app/models/app_catalog/web_app.rb @@ -1,6 +1,8 @@ class AppCatalog::WebApp < ApplicationRecord store :metadata, coder: JSON + has_many :remote_storage_authorizations + has_one_attached :icon do |attachable| attachable.variant :medium, resize_to_limit: [128,128] attachable.variant :large, resize_to_limit: [256,256] diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb index 82ac744..26c3fd4 100644 --- a/app/models/remote_storage_authorization.rb +++ b/app/models/remote_storage_authorization.rb @@ -1,5 +1,6 @@ class RemoteStorageAuthorization < ApplicationRecord belongs_to :user + belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true serialize :permissions @@ -15,7 +16,9 @@ class RemoteStorageAuthorization < ApplicationRecord before_create :generate_token before_create :store_token_in_redis + before_create :find_or_create_web_app after_create :schedule_token_expiry + # after_create :notify_user before_destroy :delete_token_from_redis after_destroy :remove_token_expiry_job @@ -60,4 +63,21 @@ class RemoteStorageAuthorization < ApplicationRecord job.delete if job.display_args == [id] end end + + def find_or_create_web_app + if looks_like_hosted_origin? + web_app = AppCatalog::WebApp.find_or_create_by!(url: self.url) + self.web_app = web_app + self.app_name = web_app.name.presence || client_id + else + self.app_name = client_id + end + end + + def looks_like_hosted_origin? + uri = URI.parse self.redirect_uri + !!(uri.host =~ /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/) + rescue URI::InvalidURIError + false + end end diff --git a/db/migrate/20231024104909_add_web_app_id_to_remote_storage_authorizations.rb b/db/migrate/20231024104909_add_web_app_id_to_remote_storage_authorizations.rb new file mode 100644 index 0000000..49457b5 --- /dev/null +++ b/db/migrate/20231024104909_add_web_app_id_to_remote_storage_authorizations.rb @@ -0,0 +1,7 @@ +class AddWebAppIdToRemoteStorageAuthorizations < ActiveRecord::Migration[7.0] + def change + add_reference :remote_storage_authorizations, :web_app, foreign_key: { + to_table: :app_catalog_web_apps + } + end +end diff --git a/db/schema.rb b/db/schema.rb index acee874..f830293 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_19_125135) do +ActiveRecord::Schema[7.0].define(version: 2023_10_24_104909) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -96,8 +96,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_19_125135) do t.datetime "expire_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "web_app_id" t.index ["permissions"], name: "index_remote_storage_authorizations_on_permissions" t.index ["user_id"], name: "index_remote_storage_authorizations_on_user_id" + t.index ["web_app_id"], name: "index_remote_storage_authorizations_on_web_app_id" end create_table "settings", force: :cascade do |t| @@ -132,5 +134,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_19_125135) do add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "remote_storage_authorizations", "app_catalog_web_apps", column: "web_app_id" add_foreign_key "remote_storage_authorizations", "users" end diff --git a/spec/factories/app_catalog/web_apps.rb b/spec/factories/app_catalog/web_apps.rb index b9ef31a..c07de2e 100644 --- a/spec/factories/app_catalog/web_apps.rb +++ b/spec/factories/app_catalog/web_apps.rb @@ -1,14 +1,6 @@ FactoryBot.define do - factory :app_catalog_web_app, class: 'AppCatalog::WebApp' do + factory :web_app, class: 'AppCatalog::WebApp' do url { "https://myfavoritedrinks.remotestorage.io/" } name { "My Favorite Drinks" } - short_name { "Drinks" } - description { nil } - theme_color { nil } - background_color { nil } - display { nil } - start_url { nil } - scope { nil } - share_target { nil } end end diff --git a/spec/factories/remote_storage_authorizations.rb b/spec/factories/remote_storage_authorizations.rb index be7c810..76c93bf 100644 --- a/spec/factories/remote_storage_authorizations.rb +++ b/spec/factories/remote_storage_authorizations.rb @@ -4,6 +4,7 @@ FactoryBot.define do client_id { "some-fancy-app" } redirect_uri { "https://example.com/some-fancy-app" } app_name { "Fancy App" } - expire_at { nil } + expire_at { 1.month.from_now } + web_app end end -- 2.25.1 From 3e9a08a2668c0eceee4ac1513543ce5076da3eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 24 Oct 2023 14:08:00 +0200 Subject: [PATCH 11/39] Remove (long) obsolete edge case --- app/models/remote_storage_authorization.rb | 8 ++------ spec/factories/remote_storage_authorizations.rb | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb index 26c3fd4..ea6e2ac 100644 --- a/app/models/remote_storage_authorization.rb +++ b/app/models/remote_storage_authorization.rb @@ -23,12 +23,8 @@ class RemoteStorageAuthorization < ApplicationRecord after_destroy :remove_token_expiry_job def url - if self.redirect_uri - uri = URI.parse self.redirect_uri - "#{uri.scheme}://#{client_id}" - else - "http://#{client_id}" - end + uri = URI.parse self.redirect_uri + "#{uri.scheme}://#{client_id}" end def delete_token_from_redis diff --git a/spec/factories/remote_storage_authorizations.rb b/spec/factories/remote_storage_authorizations.rb index 76c93bf..0cb47b1 100644 --- a/spec/factories/remote_storage_authorizations.rb +++ b/spec/factories/remote_storage_authorizations.rb @@ -1,8 +1,8 @@ FactoryBot.define do factory :remote_storage_authorization do permissions { ["documents:rw"] } - client_id { "some-fancy-app" } - redirect_uri { "https://example.com/some-fancy-app" } + client_id { "app.example.com" } + redirect_uri { "https://app.example.com" } app_name { "Fancy App" } expire_at { 1.month.from_now } web_app -- 2.25.1 From 2b8bfaaca848526df9eba5316f57f1756efca944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 24 Oct 2023 17:28:50 +0200 Subject: [PATCH 12/39] Add admin page for web apps --- .../admin/app_catalog/web_apps_controller.rb | 9 ++++ .../admin/app_catalog_controller.rb | 9 ++++ app/models/remote_storage_authorization.rb | 1 + .../admin/app_catalog/web_apps/index.html.erb | 52 +++++++++++++++++++ app/views/icons/_globe.html.erb | 2 +- app/views/icons/_star.html.erb | 2 +- app/views/shared/_admin_nav.html.erb | 4 ++ .../_admin_sidenav_app_catalog.html.erb | 10 ++++ config/routes.rb | 5 ++ 9 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 app/controllers/admin/app_catalog/web_apps_controller.rb create mode 100644 app/controllers/admin/app_catalog_controller.rb create mode 100644 app/views/admin/app_catalog/web_apps/index.html.erb create mode 100644 app/views/shared/_admin_sidenav_app_catalog.html.erb diff --git a/app/controllers/admin/app_catalog/web_apps_controller.rb b/app/controllers/admin/app_catalog/web_apps_controller.rb new file mode 100644 index 0000000..052d06a --- /dev/null +++ b/app/controllers/admin/app_catalog/web_apps_controller.rb @@ -0,0 +1,9 @@ +class Admin::AppCatalog::WebAppsController < Admin::AppCatalogController + def index + @pagy, @web_apps = pagy(AppCatalog::WebApp.order('created_at desc')) + + @stats = { + known_apps: AppCatalog::WebApp.count + } + end +end diff --git a/app/controllers/admin/app_catalog_controller.rb b/app/controllers/admin/app_catalog_controller.rb new file mode 100644 index 0000000..507ea04 --- /dev/null +++ b/app/controllers/admin/app_catalog_controller.rb @@ -0,0 +1,9 @@ +class Admin::AppCatalogController < Admin::BaseController + before_action :set_current_section + + private + + def set_current_section + @current_section = :app_catalog + end +end diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb index ea6e2ac..b4fe715 100644 --- a/app/models/remote_storage_authorization.rb +++ b/app/models/remote_storage_authorization.rb @@ -23,6 +23,7 @@ class RemoteStorageAuthorization < ApplicationRecord after_destroy :remove_token_expiry_job def url + # TODO use web app scope in addition to host uri = URI.parse self.redirect_uri "#{uri.scheme}://#{client_id}" end diff --git a/app/views/admin/app_catalog/web_apps/index.html.erb b/app/views/admin/app_catalog/web_apps/index.html.erb new file mode 100644 index 0000000..aff8066 --- /dev/null +++ b/app/views/admin/app_catalog/web_apps/index.html.erb @@ -0,0 +1,52 @@ +<%= render HeaderComponent.new(title: "App Catalog") %> + +<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_app_catalog') do %> +
+ <%= render QuickstatsContainerComponent.new do %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Known Web Apps', + value: @stats[:known_apps], + ) %> + <%# <%= render QuickstatsItemComponent.new( + <%# type: :number, + <%# title: 'Accepted', + <%# value: @stats[:accepted], + <%# ) %> + <%# <%= render QuickstatsItemComponent.new( + <%# type: :number, + <%# title: 'Users with referrals', + <%# value: @stats[:users_with_referrals], + <%# meta: "/ #{User.count}" + <%# ) %> + <% end %> +
+ <% if @web_apps.any? %> +
+

Web Apps

+ + + + + + + + + + + <% @web_apps.each do |web_app| %> + + + + + + + <% end %> + +
NameURL
<%= web_app.name %><%= link_to web_app.url, web_app.url, + target: "_blank", rel: "nofollow noopener", + class: "ks-text-link" %>
+ <%== pagy_nav @pagy %> +
+ <% end %> +<% end %> diff --git a/app/views/icons/_globe.html.erb b/app/views/icons/_globe.html.erb index 0a0586d..62237bf 100644 --- a/app/views/icons/_globe.html.erb +++ b/app/views/icons/_globe.html.erb @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/views/icons/_star.html.erb b/app/views/icons/_star.html.erb index bcdc31a..5734060 100644 --- a/app/views/icons/_star.html.erb +++ b/app/views/icons/_star.html.erb @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/views/shared/_admin_nav.html.erb b/app/views/shared/_admin_nav.html.erb index 7762431..1ba5eb3 100644 --- a/app/views/shared/_admin_nav.html.erb +++ b/app/views/shared/_admin_nav.html.erb @@ -10,5 +10,9 @@ <%= link_to "Lightning", admin_lightning_path, class: main_nav_class(@current_section, :lightning) %> <% end %> +<% if Setting.remotestorage_enabled? %> + <%= link_to "Apps", admin_app_catalog_web_apps_path, + class: main_nav_class(@current_section, :app_catalog) %> +<% end %> <%= link_to "Settings", admin_settings_registrations_path, class: main_nav_class(@current_section, :settings) %> diff --git a/app/views/shared/_admin_sidenav_app_catalog.html.erb b/app/views/shared/_admin_sidenav_app_catalog.html.erb new file mode 100644 index 0000000..6fa14bc --- /dev/null +++ b/app/views/shared/_admin_sidenav_app_catalog.html.erb @@ -0,0 +1,10 @@ +<%= render SidenavLinkComponent.new( + name: "Web Apps", path: admin_app_catalog_web_apps_path, icon: "globe", + active: current_page?(admin_app_catalog_web_apps_path) +) %> +<%= render SidenavLinkComponent.new( + name: "Recommended Apps", path: "#", icon: "star", disabled: true +) %> +<%= render SidenavLinkComponent.new( + name: "OAuth Apps", path: "#", icon: "key", disabled: true +) %> diff --git a/config/routes.rb b/config/routes.rb index e7fef10..ad11ec3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -60,11 +60,16 @@ Rails.application.routes.draw do namespace :admin do root to: 'dashboard#index' + resources 'users', param: 'address', only: ['index', 'show'], constraints: { address: /.*/ } get 'invitations', to: 'invitations#index' resources :donations get 'lightning', to: 'lightning#index' + namespace :app_catalog do + resources 'web_apps', only: ['index'] + end + namespace :settings do resources 'registrations', only: ['index', 'create'] resources 'services', only: ['index', 'create'] -- 2.25.1 From 00ec7fa21c22232f7e6affe5ddc9f71fe067393c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 25 Oct 2023 22:14:07 +0200 Subject: [PATCH 13/39] WIP Add RS auths/apps to Storage dashboard --- app/components/rs_auth_component.html.erb | 25 +++++++++++++++++++ app/components/rs_auth_component.rb | 8 ++++++ .../services/remotestorage_controller.rb | 1 + .../services/remotestorage/dashboard.html.erb | 11 +++++++- config/routes.rb | 4 +++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 app/components/rs_auth_component.html.erb create mode 100644 app/components/rs_auth_component.rb diff --git a/app/components/rs_auth_component.html.erb b/app/components/rs_auth_component.html.erb new file mode 100644 index 0000000..4295bc1 --- /dev/null +++ b/app/components/rs_auth_component.html.erb @@ -0,0 +1,25 @@ +
+
+ <%= image_tag s3_image_url(@web_app.icon), class: "h-full w-full" %> +
+
+

+ <%= @web_app.name %> +

+

+ <%= @auth.client_id %> +

+
+ + + + + +
+

+ <%= link_to "#", class: "btn-md btn-outline text-red-700 relative" do %> + Revoke access + <% end %> +

+
+
diff --git a/app/components/rs_auth_component.rb b/app/components/rs_auth_component.rb new file mode 100644 index 0000000..ed588bf --- /dev/null +++ b/app/components/rs_auth_component.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class RsAuthComponent < ViewComponent::Base + def initialize(auth:) + @auth = auth + @web_app = auth.web_app + end +end diff --git a/app/controllers/services/remotestorage_controller.rb b/app/controllers/services/remotestorage_controller.rb index 5d455ba..5ed1a85 100644 --- a/app/controllers/services/remotestorage_controller.rb +++ b/app/controllers/services/remotestorage_controller.rb @@ -7,6 +7,7 @@ class Services::RemotestorageController < Services::BaseController # unless current_user.services_enabled.include?(:remotestorage) # redirect_to service_remotestorage_info_path # end + @rs_auths = current_user.remote_storage_authorizations end private diff --git a/app/views/services/remotestorage/dashboard.html.erb b/app/views/services/remotestorage/dashboard.html.erb index f6b0932..1a70995 100644 --- a/app/views/services/remotestorage/dashboard.html.erb +++ b/app/views/services/remotestorage/dashboard.html.erb @@ -2,6 +2,15 @@ <%= render MainSimpleComponent.new do %>
-

Feature enabled

+

Connected Apps

+ <% if @rs_auths.any? %> +
+ <% @rs_auths.each do |auth| %> + <%= render RsAuthComponent.new(auth: auth) %> + <% end %> +
+ <% else %> +

No apps connected yet.

+ <% end %>
<% end %> diff --git a/config/routes.rb b/config/routes.rb index ad11ec3..785b7a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -100,4 +100,8 @@ Rails.application.routes.draw do end root to: 'dashboard#index' + + direct :s3_image do |blob| + File.join(ENV['S3_ALIAS_HOST'], blob.key) + end end -- 2.25.1 From def87a1621104033242e59e87c2330de15517d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 25 Oct 2023 22:14:47 +0200 Subject: [PATCH 14/39] Remove variants from attachment --- app/models/app_catalog/web_app.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/models/app_catalog/web_app.rb b/app/models/app_catalog/web_app.rb index ced6b29..b42bea7 100644 --- a/app/models/app_catalog/web_app.rb +++ b/app/models/app_catalog/web_app.rb @@ -3,11 +3,7 @@ class AppCatalog::WebApp < ApplicationRecord has_many :remote_storage_authorizations - has_one_attached :icon do |attachable| - attachable.variant :medium, resize_to_limit: [128,128] - attachable.variant :large, resize_to_limit: [256,256] - end - + has_one_attached :icon has_one_attached :apple_touch_icon validates :url, presence: true, uniqueness: true -- 2.25.1 From 8e090daa9cfbfae283b3e8585f0bd893a9cd78a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 25 Oct 2023 22:15:03 +0200 Subject: [PATCH 15/39] Fetch web app metadata when creating RS auth --- app/models/remote_storage_authorization.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb index b4fe715..f1aee26 100644 --- a/app/models/remote_storage_authorization.rb +++ b/app/models/remote_storage_authorization.rb @@ -64,6 +64,7 @@ class RemoteStorageAuthorization < ApplicationRecord def find_or_create_web_app if looks_like_hosted_origin? web_app = AppCatalog::WebApp.find_or_create_by!(url: self.url) + web_app.update_metadata unless web_app.name.present? self.web_app = web_app self.app_name = web_app.name.presence || client_id else -- 2.25.1 From 5075fef6166be427f6b17dec5262e8e8a1e56eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 25 Oct 2023 22:15:28 +0200 Subject: [PATCH 16/39] Only show avatar when available on admin user page --- app/views/admin/users/show.html.erb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index 7f9ba87..05f8f11 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -63,10 +63,12 @@
+ <% if @avatar.present? %>

LDAP

+ <% end %>

-- 2.25.1 From 56c127ca0c607ec8321828188bbd8d44bdb5414e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 1 Nov 2023 21:46:38 +0100 Subject: [PATCH 17/39] Only allow primary domain for RS Replace user addresses with usernames in the respective URLs --- app/controllers/rs/oauth_controller.rb | 8 ++--- app/controllers/webfinger_controller.rb | 29 ++++++++------- config/routes.rb | 4 +-- docker-compose.yml | 17 +++++++++ spec/controllers/rs/oauth_controller_spec.rb | 38 +++++++++++--------- spec/features/rs/oauth_spec.rb | 8 ++--- spec/requests/webfinger_spec.rb | 4 +-- 7 files changed, 65 insertions(+), 43 deletions(-) diff --git a/app/controllers/rs/oauth_controller.rb b/app/controllers/rs/oauth_controller.rb index 67a2beb..6d9d537 100644 --- a/app/controllers/rs/oauth_controller.rb +++ b/app/controllers/rs/oauth_controller.rb @@ -3,8 +3,7 @@ class Rs::OauthController < ApplicationController before_action :authenticate_user!, only: :create def new - username, org = params[:useraddress].split("@") - @user = User.where(cn: username.downcase, ou: org).first + @user = User.where(cn: params[:username].downcase, ou: Setting.primary_domain).first @scopes = parse_scopes params[:scope] @redirect_uri = params[:redirect_uri] @client_id = params[:client_id] @@ -22,7 +21,7 @@ class Rs::OauthController < ApplicationController unless current_user == @user sign_out :user - redirect_to new_rs_oauth_url(@user.address, + redirect_to new_rs_oauth_url(@user.cn, scope: params[:scope], redirect_uri: params[:redirect_uri], client_id: params[:client_id], @@ -107,9 +106,8 @@ class Rs::OauthController < ApplicationController def require_signed_in_with_username unless user_signed_in? - username, org = params[:useraddress].split("@") session[:user_return_to] = request.url - redirect_to new_user_session_path(cn: username, ou: org) + redirect_to new_user_session_path(cn: params[:username], ou: Setting.primary_domain) end end diff --git a/app/controllers/webfinger_controller.rb b/app/controllers/webfinger_controller.rb index 5cf4012..fbc1bcb 100644 --- a/app/controllers/webfinger_controller.rb +++ b/app/controllers/webfinger_controller.rb @@ -6,15 +6,19 @@ class WebfingerController < ApplicationController def show resource = params[:resource] - if resource && resource.match(/acct:\w+/) - useraddress = resource.split(":").last - username, org = useraddress.split("@") - username.downcase! - unless User.where(cn: username, ou: org).any? + if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1) + @username, @org = @useraddress.split("@") + + unless Rails.env.development? + # Allow different domains (e.g. localhost:3000) in development only + head 404 and return unless @org == Setting.primary_domain + end + + unless User.where(cn: @username.downcase, ou: Setting.primary_domain).any? head 404 and return end - render json: webfinger(useraddress).to_json, + render json: webfinger.to_json, content_type: "application/jrd+json" else head 422 and return @@ -23,19 +27,18 @@ class WebfingerController < ApplicationController private - def webfinger(useraddress) + def webfinger links = []; - links << remotestorage_link(useraddress) if Setting.remotestorage_enabled + # TODO check if storage service is enabled for user, not just globally + links << remotestorage_link if Setting.remotestorage_enabled { "links" => links } end - def remotestorage_link(useraddress) - # TODO use when OAuth routes are available - # auth_url = new_rs_oauth_url(useraddress) - auth_url = "https://example.com/rs/oauth" - storage_url = "#{Setting.rs_storage_url}/#{useraddress}" + def remotestorage_link + auth_url = new_rs_oauth_url("#{@username}@#{Setting.primary_domain}") + storage_url = "#{Setting.rs_storage_url}/#{@username}" { "rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage", diff --git a/config/routes.rb b/config/routes.rb index 785b7a2..6e8968c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -78,8 +78,8 @@ Rails.application.routes.draw do namespace :rs do resource :oauth, only: [:new, :create], path_names: { - new: ':useraddress', create: ':useraddress' - }, controller: 'oauth', constraints: { useraddress: /[^\/]+/} + new: ':username', create: ':username' + }, controller: 'oauth' get 'oauth/token/:id/launch_app' => 'oauth#launch_app', as: :launch_app end diff --git a/docker-compose.yml b/docker-compose.yml index 3690616..1632c44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,6 +69,23 @@ services: - ldap - redis + liquor-cabinet: + image: gitea.kosmos.org/5apps/liquor-cabinet:latest + networks: + - external_network + - internal_network + ports: + - "4567:4567" + environment: + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_DB: 2 + S3_ENDPOINT: https://garage + S3_REGION: garage + S3_ACCESS_KEY: + S3_SECRET_KEY: + S3_BUCKET: + # phpldapadmin: # image: osixia/phpldapadmin:0.9.0 # ports: diff --git a/spec/controllers/rs/oauth_controller_spec.rb b/spec/controllers/rs/oauth_controller_spec.rb index 48ca14b..c80e427 100644 --- a/spec/controllers/rs/oauth_controller_spec.rb +++ b/spec/controllers/rs/oauth_controller_spec.rb @@ -3,7 +3,11 @@ require 'rails_helper' RSpec.describe Rs::OauthController, type: :controller do let(:user) { create :user } - describe "GET /rs/oauth/:useraddress" do + before do + allow_any_instance_of(AppCatalog::WebApp).to receive(:update_metadata).and_return(true) + end + + describe "GET /rs/oauth/:username" do context "when user is signed in" do before do sign_in user @@ -14,7 +18,7 @@ RSpec.describe Rs::OauthController, type: :controller do before do get :new, params: { - useraddress: other_user.address, + username: other_user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "examples" @@ -22,7 +26,7 @@ RSpec.describe Rs::OauthController, type: :controller do end it "logs out the users and repeats the request" do - url = new_rs_oauth_url other_user.address, + url = new_rs_oauth_url other_user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "examples" @@ -34,7 +38,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "when no valid token exists" do before do get :new, params: { - useraddress: user.address, + username: user.cn, redirect_uri: "https://example.com", client_id: "example.com", scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", @@ -61,7 +65,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "no redirect_uri" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", client_id: "https://example.com" } @@ -75,7 +79,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "no client_id" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com" } @@ -89,7 +93,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "different host for client_id and redirect_uri" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com/foobar", client_id: "https://google.com" @@ -116,7 +120,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "with same host for client_id and redirect_uri" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com", client_id: "https://example.com" @@ -131,7 +135,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "with different host for client_id and redirect_uri" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://app.example.com", client_id: "https://example.com" @@ -146,7 +150,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "with different redirect_uri" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com/a_new_route", client_id: "https://example.com" @@ -161,7 +165,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "with state param given" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,[photos], contacts:rw videos:r tasks/work/:r", redirect_uri: "https://example.com", client_id: "https://example.com", @@ -178,7 +182,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "no scope" do before do get :new, params: { - useraddress: user.address, + username: user.cn, redirect_uri: "https://example.com", client_id: "https://example.com", state: "foobar123" @@ -193,7 +197,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "empty scope" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "", redirect_uri: "https://example.com", client_id: "https://example.com", @@ -210,7 +214,7 @@ RSpec.describe Rs::OauthController, type: :controller do context "when user is not signed in" do it "redirects to the signin page with username pre-filled" do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "documents,photos", redirect_uri: "https://example.com" } @@ -227,7 +231,7 @@ RSpec.describe Rs::OauthController, type: :controller do describe "full" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "*:rw", redirect_uri: "https://example.com", client_id: "example.com" @@ -243,7 +247,7 @@ RSpec.describe Rs::OauthController, type: :controller do describe "read-only" do before do get :new, params: { - useraddress: user.address, + username: user.cn, scope: "*:r", redirect_uri: "https://example.com", client_id: "example.com" @@ -258,7 +262,7 @@ RSpec.describe Rs::OauthController, type: :controller do end end - describe "POST /rs/oauth/:useraddress" do + describe "POST /rs/oauth/:username" do context "when user is signed in" do before do sign_in user diff --git a/spec/features/rs/oauth_spec.rb b/spec/features/rs/oauth_spec.rb index a68556f..9e499b8 100644 --- a/spec/features/rs/oauth_spec.rb +++ b/spec/features/rs/oauth_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do context "with normal permissions" do before do - visit new_rs_oauth_path(useraddress: user.address, + visit new_rs_oauth_path(username: user.cn, redirect_uri: "http://example.com", client_id: "http://example.com", scope: "documents,[photos], contacts:r") @@ -36,7 +36,7 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do context "root access" do context "full" do before do - visit new_rs_oauth_path(useraddress: user.address, + visit new_rs_oauth_path(username: user.cn, redirect_uri: "http://example.com", client_id: "http://example.com", scope: ":rw") @@ -60,7 +60,7 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do end it "prefills the username field in the signin form" do - visit new_rs_oauth_path(useraddress: user.address, + visit new_rs_oauth_path(username: user.cn, redirect_uri: "http://example.com", client_id: "http://example.com", scope: "documents,[photos], contacts:r") @@ -69,7 +69,7 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do end it "redirects to the OAuth dialog after sign-in" do - auth_url = new_rs_oauth_url(useraddress: user.address, + auth_url = new_rs_oauth_url(username: user.cn, redirect_uri: "http://example.com", client_id: "http://example.com", scope: "documents,[photos], contacts:r") diff --git a/spec/requests/webfinger_spec.rb b/spec/requests/webfinger_spec.rb index f944a7a..9d0ae57 100644 --- a/spec/requests/webfinger_spec.rb +++ b/spec/requests/webfinger_spec.rb @@ -15,10 +15,10 @@ RSpec.describe "WebFinger", type: :request do res = JSON.parse(response.body) rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"} - expect(rs_link["href"]).to eql("https://storage.kosmos.org/tony@kosmos.org") + expect(rs_link["href"]).to eql("https://storage.kosmos.org/tony") oauth_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-4.2"] - expect(oauth_url).to eql("https://example.com/rs/oauth") + expect(oauth_url).to eql("http://www.example.com/rs/oauth/tony@kosmos.org") end end -- 2.25.1 From 92310d434a3cea033f6a2b840750ec78672c43c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 1 Nov 2023 21:48:16 +0100 Subject: [PATCH 18/39] Remove rs namespace from Redis keys Superfluous, since the whole db should be RS only --- app/models/remote_storage_authorization.rb | 4 ++-- spec/jobs/remote_storage_expire_authorization_job_spec.rb | 2 +- spec/models/remote_storage_authorization_spec.rb | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb index f1aee26..7a4078a 100644 --- a/app/models/remote_storage_authorization.rb +++ b/app/models/remote_storage_authorization.rb @@ -29,7 +29,7 @@ class RemoteStorageAuthorization < ApplicationRecord end def delete_token_from_redis - key = "rs:authorizations:#{user.address}:#{token}" + key = "authorizations:#{user.cn}:#{token}" redis.srem? key, redis.smembers(key) end @@ -44,7 +44,7 @@ class RemoteStorageAuthorization < ApplicationRecord end def store_token_in_redis - redis.sadd "rs:authorizations:#{user.address}:#{token}", permissions + redis.sadd "authorizations:#{user.cn}:#{token}", permissions end def schedule_token_expiry diff --git a/spec/jobs/remote_storage_expire_authorization_job_spec.rb b/spec/jobs/remote_storage_expire_authorization_job_spec.rb index e65662e..8142983 100644 --- a/spec/jobs/remote_storage_expire_authorization_job_spec.rb +++ b/spec/jobs/remote_storage_expire_authorization_job_spec.rb @@ -20,7 +20,7 @@ RSpec.describe RemoteStorageExpireAuthorizationJob, type: :job do } it "removes the RS authorization from redis" do - redis_key = "rs:authorizations:#{@user.address}:#{@rs_authorization.token}" + redis_key = "authorizations:#{@user.cn}:#{@rs_authorization.token}" expect(redis.keys(redis_key)).to_not be_empty perform_enqueued_jobs { job } diff --git a/spec/models/remote_storage_authorization_spec.rb b/spec/models/remote_storage_authorization_spec.rb index 3d046cf..e98625b 100644 --- a/spec/models/remote_storage_authorization_spec.rb +++ b/spec/models/remote_storage_authorization_spec.rb @@ -7,7 +7,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do describe "#create" do after(:each) { clear_enqueued_jobs } - after(:all) { redis_rs_delete_keys("rs:authorizations:*") } + after(:all) { redis_rs_delete_keys("authorizations:*") } let(:auth) do user.remote_storage_authorizations.create!( @@ -22,7 +22,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do end it "stores a token in redis" do - user_auth_keys = redis_rs.keys("rs:authorizations:#{user.address}:*") + user_auth_keys = redis_rs.keys("authorizations:#{user.cn}:*") expect(user_auth_keys.length).to eq(1) authorizations = redis_rs.smembers(user_auth_keys.first) @@ -44,7 +44,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do describe "#destroy" do after(:each) { clear_enqueued_jobs } - after(:all) { redis_rs_delete_keys("rs:authorizations:*") } + after(:all) { redis_rs_delete_keys("authorizations:*") } it "removes the token from redis" do auth = user.remote_storage_authorizations.create!( @@ -54,7 +54,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do ) auth.destroy! - expect(redis_rs.keys("rs:authorizations:#{user.address}:*")).to be_empty + expect(redis_rs.keys("authorizations:#{user.address}:*")).to be_empty end context "with expiry set" do -- 2.25.1 From 0c1b1b4afe2da5656abc1f1eb46724d268d462f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 1 Nov 2023 21:49:08 +0100 Subject: [PATCH 19/39] Adjust specs for web app metadata fetching --- spec/jobs/remote_storage_expire_authorization_job_spec.rb | 7 ++++++- spec/models/remote_storage_authorization_spec.rb | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spec/jobs/remote_storage_expire_authorization_job_spec.rb b/spec/jobs/remote_storage_expire_authorization_job_spec.rb index 8142983..8e6cbb7 100644 --- a/spec/jobs/remote_storage_expire_authorization_job_spec.rb +++ b/spec/jobs/remote_storage_expire_authorization_job_spec.rb @@ -2,8 +2,13 @@ require 'rails_helper' RSpec.describe RemoteStorageExpireAuthorizationJob, type: :job do before do + allow_any_instance_of(AppCatalog::WebApp).to( + receive(:update_metadata).and_return(true) + ) + @user = create :user, cn: "ronald", ou: "kosmos.org" - @rs_authorization = create :remote_storage_authorization, user: @user, expire_at: 1.day.ago + @rs_authorization = create :remote_storage_authorization, + user: @user, expire_at: 1.day.ago end after do diff --git a/spec/models/remote_storage_authorization_spec.rb b/spec/models/remote_storage_authorization_spec.rb index e98625b..f3514e3 100644 --- a/spec/models/remote_storage_authorization_spec.rb +++ b/spec/models/remote_storage_authorization_spec.rb @@ -5,6 +5,10 @@ RSpec.describe RemoteStorageAuthorization, type: :model do let(:user) { create :user } + before do + allow_any_instance_of(AppCatalog::WebApp).to receive(:update_metadata).and_return(true) + end + describe "#create" do after(:each) { clear_enqueued_jobs } after(:all) { redis_rs_delete_keys("authorizations:*") } -- 2.25.1 From 60c0a43f33ecb7ba2535f05c8b5a61fd4a48cd6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 1 Nov 2023 21:51:29 +0100 Subject: [PATCH 20/39] Add minio to Docker Compose setup, configure Liquor Cabinet --- docker-compose.yml | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1632c44..02895a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,12 +37,13 @@ services: environment: RAILS_ENV: development PRIMARY_DOMAIN: kosmos.org - REDIS_URL: redis://redis:6379/0 - RS_REDIS_URL: redis://redis:6379/1 LDAP_HOST: ldap LDAP_PORT: 3389 LDAP_ADMIN_PASSWORD: passthebutter LDAP_USE_TLS: "false" + REDIS_URL: redis://redis:6379/0 + RS_REDIS_URL: redis://redis:6379/1 + RS_STORAGE_URL: "http://localhost:4567" depends_on: - ldap - redis @@ -57,18 +58,31 @@ services: environment: RAILS_ENV: development PRIMARY_DOMAIN: kosmos.org - REDIS_URL: redis://redis:6379/0 - RS_REDIS_URL: redis://redis:6379/1 LDAP_HOST: ldap LDAP_PORT: 3389 LDAP_ADMIN_PASSWORD: passthebutter LDAP_USE_TLS: "false" LAUNCHY_DRY_RUN: true BROWSER: /dev/null + REDIS_URL: redis://redis:6379/0 + RS_REDIS_URL: redis://redis:6379/1 + RS_STORAGE_URL: "http://localhost:4567" depends_on: - ldap - redis + minio: + image: quay.io/minio/minio:latest + command: "server /data --console-address ':9001'" + networks: + - external_network + - internal_network + ports: + - "9000:9000" + - "9001:9001" + volumes: + - ./tmp/minio:/data + liquor-cabinet: image: gitea.kosmos.org/5apps/liquor-cabinet:latest networks: @@ -77,14 +91,14 @@ services: ports: - "4567:4567" environment: + RACK_ENV: staging REDIS_HOST: redis REDIS_PORT: 6379 - REDIS_DB: 2 - S3_ENDPOINT: https://garage - S3_REGION: garage - S3_ACCESS_KEY: - S3_SECRET_KEY: - S3_BUCKET: + REDIS_DB: 1 + S3_ENDPOINT: http://minio + S3_ACCESS_KEY: dev-key + S3_SECRET_KEY: 123456789 + S3_BUCKET: remotestorage # phpldapadmin: # image: osixia/phpldapadmin:0.9.0 -- 2.25.1 From 00049f3743cbdb93997a4447a0d20d2d38513fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 1 Nov 2023 22:01:14 +0100 Subject: [PATCH 21/39] Add info for running Minio/RS to README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 9bd72ea..564b833 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,20 @@ The setup task will first delete any existing entries in the directory tree Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over with a fresh installation, delete both that directory as well as the container. +#### Minio / RS + +If you want to run remoteStorage accounts locally, you will have to create the +respective bucket first: + +* `docker compose up web redis minio liquor-cabinet` +* Head to http://localhost:9001 and log in with user `minioadmin`, password + `minioadmin` +* Create a new bucket called `remotestorage` (or whatever you + change the `S3_BUCKET` config to) +* Create a new key with ID "dev-key" and secret "123456789" (or whatever you + change `S3_ACCESS_KEY` and `S3_SECRET_KEY` to). Leave the policy field empty, + as it will automatically allow access to the bucket you created. + ### Adding npm modules to use with Stimulus controllers The following command downloads the specified npm module to `vendor/javascript` -- 2.25.1 From 600cfe0f78d353b3bd4c69690270d4bd35ca495e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 16 Nov 2023 12:42:39 +0100 Subject: [PATCH 22/39] Update lockfile --- Gemfile.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile.lock b/Gemfile.lock index f9c03a3..3e551f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -461,6 +461,7 @@ GEM PLATFORMS arm64-darwin-22 + ruby x86_64-linux DEPENDENCIES -- 2.25.1 From 1995e6dda2e2055222ee17b26db7a942e46dc98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 16 Nov 2023 12:43:59 +0100 Subject: [PATCH 23/39] Fix RS OAuth URL in Webfinger record --- app/controllers/webfinger_controller.rb | 2 +- spec/requests/webfinger_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/webfinger_controller.rb b/app/controllers/webfinger_controller.rb index fbc1bcb..be50ebd 100644 --- a/app/controllers/webfinger_controller.rb +++ b/app/controllers/webfinger_controller.rb @@ -37,7 +37,7 @@ class WebfingerController < ApplicationController end def remotestorage_link - auth_url = new_rs_oauth_url("#{@username}@#{Setting.primary_domain}") + auth_url = new_rs_oauth_url(@username) storage_url = "#{Setting.rs_storage_url}/#{@username}" { diff --git a/spec/requests/webfinger_spec.rb b/spec/requests/webfinger_spec.rb index 9d0ae57..1dcdfa3 100644 --- a/spec/requests/webfinger_spec.rb +++ b/spec/requests/webfinger_spec.rb @@ -18,7 +18,7 @@ RSpec.describe "WebFinger", type: :request do expect(rs_link["href"]).to eql("https://storage.kosmos.org/tony") oauth_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-4.2"] - expect(oauth_url).to eql("http://www.example.com/rs/oauth/tony@kosmos.org") + expect(oauth_url).to eql("http://www.example.com/rs/oauth/tony") end end -- 2.25.1 From de67f59d5cf7f806bae42fe5b831d16bb27ec302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 16 Nov 2023 12:45:26 +0100 Subject: [PATCH 24/39] Fail gracefully and log error when token missing in Redis --- app/models/remote_storage_authorization.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb index 7a4078a..2d62e65 100644 --- a/app/models/remote_storage_authorization.rb +++ b/app/models/remote_storage_authorization.rb @@ -31,6 +31,9 @@ class RemoteStorageAuthorization < ApplicationRecord def delete_token_from_redis key = "authorizations:#{user.cn}:#{token}" redis.srem? key, redis.smembers(key) + rescue => e + Rails.logger.error e + Sentry.capture_exception(e) if Setting.sentry_enabled? end private -- 2.25.1 From 1d44181fb5d0ec94200f076432f87acfcb9aeabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 16 Nov 2023 12:46:05 +0100 Subject: [PATCH 25/39] Wording --- app/views/services/chat/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/services/chat/show.html.erb b/app/views/services/chat/show.html.erb index 5f65971..7e749b1 100644 --- a/app/views/services/chat/show.html.erb +++ b/app/views/services/chat/show.html.erb @@ -38,7 +38,7 @@

Chat Apps

Use your account with many different apps, and on any devices you wish! - When opening an app for the first time, just enter your user address and + When opening an app for the first time, just enter your address and password to log in.

-- 2.25.1 From 27bb7d1bfe11654bf9f657995b785e1dd0b0853c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 16 Nov 2023 12:46:19 +0100 Subject: [PATCH 26/39] Finish working liquor-cabinet setup for Docker Compose --- docker-compose.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 02895a8..5106dfd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,7 +84,7 @@ services: - ./tmp/minio:/data liquor-cabinet: - image: gitea.kosmos.org/5apps/liquor-cabinet:latest + image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-beta.2 networks: - external_network - internal_network @@ -95,10 +95,13 @@ services: REDIS_HOST: redis REDIS_PORT: 6379 REDIS_DB: 1 - S3_ENDPOINT: http://minio + S3_ENDPOINT: http://minio:9000 S3_ACCESS_KEY: dev-key S3_SECRET_KEY: 123456789 S3_BUCKET: remotestorage + depends_on: + - minio + - redis # phpldapadmin: # image: osixia/phpldapadmin:0.9.0 -- 2.25.1 From 721dccb49942ac390c1c7ce7b1db148b53662060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 18 Nov 2023 17:13:55 +0100 Subject: [PATCH 27/39] Add dropdown components, menus for RS auth items --- app/components/dropdown_component.html.erb | 26 +++++++++++++++++++ app/components/dropdown_component.rb | 5 ++++ .../dropdown_link_component.html.erb | 6 +++++ app/components/dropdown_link_component.rb | 18 +++++++++++++ app/components/rs_auth_component.html.erb | 19 ++++++++------ app/javascript/controllers/application.js | 3 ++- app/views/icons/_kebab-menu.html.erb | 10 +++++++ .../services/remotestorage/dashboard.html.erb | 4 +-- 8 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 app/components/dropdown_component.html.erb create mode 100644 app/components/dropdown_component.rb create mode 100644 app/components/dropdown_link_component.html.erb create mode 100644 app/components/dropdown_link_component.rb create mode 100644 app/views/icons/_kebab-menu.html.erb diff --git a/app/components/dropdown_component.html.erb b/app/components/dropdown_component.html.erb new file mode 100644 index 0000000..3ea3bce --- /dev/null +++ b/app/components/dropdown_component.html.erb @@ -0,0 +1,26 @@ +
+
+
+ + + <%= render partial: "icons/kebab-menu", locals: { + custom_class: "inline text-gray-500 h-6 w-6" + } %> + + +
+ +
+
diff --git a/app/components/dropdown_component.rb b/app/components/dropdown_component.rb new file mode 100644 index 0000000..2b76fff --- /dev/null +++ b/app/components/dropdown_component.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class DropdownComponent < ViewComponent::Base + +end diff --git a/app/components/dropdown_link_component.html.erb b/app/components/dropdown_link_component.html.erb new file mode 100644 index 0000000..eb15ffc --- /dev/null +++ b/app/components/dropdown_link_component.html.erb @@ -0,0 +1,6 @@ +<%= link_to @href, class: @class, data: { + 'dropdown-target': "menuItem", + 'action': "keydown.up->dropdown#previousItem:prevent keydown.down->dropdown#nextItem:prevent" + } do %> + <%= content %> +<% end %> diff --git a/app/components/dropdown_link_component.rb b/app/components/dropdown_link_component.rb new file mode 100644 index 0000000..9f9e618 --- /dev/null +++ b/app/components/dropdown_link_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class DropdownLinkComponent < ViewComponent::Base + def initialize(href:, separator: false, add_class: nil) + @href = href + @class = class_str(separator, add_class) + end + + private + + def class_str(separator, add_class) + str = "no-underline block px-4 py-2 text-sm text-gray-900 bg-white + hover:bg-gray-100 focus:bg-gray-100 whitespace-no-wrap" + str = "#{str} border-t" if separator + str = "#{str} #{add_class}" if add_class + str + end +end diff --git a/app/components/rs_auth_component.html.erb b/app/components/rs_auth_component.html.erb index 4295bc1..58ef7cd 100644 --- a/app/components/rs_auth_component.html.erb +++ b/app/components/rs_auth_component.html.erb @@ -1,4 +1,4 @@ -
+
<%= image_tag s3_image_url(@web_app.icon), class: "h-full w-full" %>
@@ -15,11 +15,14 @@ -
-

- <%= link_to "#", class: "btn-md btn-outline text-red-700 relative" do %> - Revoke access - <% end %> -

-
+ <%= render DropdownComponent.new do %> + <%= render DropdownLinkComponent.new(href: "#") do %> + Launch app + <% end %> + <%= render DropdownLinkComponent.new( + href: "#", separator: true, add_class: "text-red-700" + ) do %> + Revoke access + <% end %> + <% end %>
diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js index f93adf1..6958c14 100644 --- a/app/javascript/controllers/application.js +++ b/app/javascript/controllers/application.js @@ -1,8 +1,9 @@ import { Application } from "@hotwired/stimulus" -import { Modal, Tabs } from "tailwindcss-stimulus-components" +import { Dropdown, Modal, Tabs } from "tailwindcss-stimulus-components" const application = Application.start() +application.register('dropdown', Dropdown) application.register('modal', Modal) application.register('tabs', Tabs) diff --git a/app/views/icons/_kebab-menu.html.erb b/app/views/icons/_kebab-menu.html.erb new file mode 100644 index 0000000..ac1ee3a --- /dev/null +++ b/app/views/icons/_kebab-menu.html.erb @@ -0,0 +1,10 @@ + + + Menu + + + + + + + diff --git a/app/views/services/remotestorage/dashboard.html.erb b/app/views/services/remotestorage/dashboard.html.erb index 1a70995..5d90bfe 100644 --- a/app/views/services/remotestorage/dashboard.html.erb +++ b/app/views/services/remotestorage/dashboard.html.erb @@ -2,9 +2,9 @@ <%= render MainSimpleComponent.new do %>
-

Connected Apps

+

Connected Apps

<% if @rs_auths.any? %> -
+
<% @rs_auths.each do |auth| %> <%= render RsAuthComponent.new(auth: auth) %> <% end %> -- 2.25.1 From f451adcb536b8fc1db17c030a84d76cf2c335b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 18 Nov 2023 17:35:57 +0100 Subject: [PATCH 28/39] Try smaller icons if 256px not available --- app/services/app_catalog_manager/update_metadata.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/services/app_catalog_manager/update_metadata.rb b/app/services/app_catalog_manager/update_metadata.rb index e53e606..4530ae3 100644 --- a/app/services/app_catalog_manager/update_metadata.rb +++ b/app/services/app_catalog_manager/update_metadata.rb @@ -18,9 +18,12 @@ module AppCatalogManager @app.metadata[prop] = metadata.send(prop) if prop end - if icon = metadata.select_icon(sizes: "256x256") + if icon = metadata.select_icon(sizes: "256x256") || + icon = metadata.select_icon(sizes: "192x192") attach_remote_image(:icon, icon) + # TODO elsif get whatever is available end + if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon") attach_remote_image(:apple_touch_icon, apple_touch_icon) end -- 2.25.1 From 4fdf8accd64422ec7620d91fc8d7b94d935b7090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 18 Nov 2023 17:36:18 +0100 Subject: [PATCH 29/39] Add note --- app/controllers/services/remotestorage_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/services/remotestorage_controller.rb b/app/controllers/services/remotestorage_controller.rb index 5ed1a85..e6a556c 100644 --- a/app/controllers/services/remotestorage_controller.rb +++ b/app/controllers/services/remotestorage_controller.rb @@ -8,6 +8,7 @@ class Services::RemotestorageController < Services::BaseController # redirect_to service_remotestorage_info_path # end @rs_auths = current_user.remote_storage_authorizations + # TODO sort by app name end private -- 2.25.1 From 4ecf2c4246a6d735ef3e3fff9bc59c0e6334f207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 19 Nov 2023 18:48:44 +0100 Subject: [PATCH 30/39] Improve app list --- app/views/admin/app_catalog/web_apps/index.html.erb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/admin/app_catalog/web_apps/index.html.erb b/app/views/admin/app_catalog/web_apps/index.html.erb index aff8066..ebfa289 100644 --- a/app/views/admin/app_catalog/web_apps/index.html.erb +++ b/app/views/admin/app_catalog/web_apps/index.html.erb @@ -41,7 +41,11 @@ target: "_blank", rel: "nofollow noopener", class: "ks-text-link" %> <%= web_app.remote_storage_authorizations.count %> - <%= web_app.created_at %> + + + <%= time_ago_in_words web_app.created_at, include_seconds: false %> ago + + <% end %> -- 2.25.1 From 8ec2a6d7e45c7cc027805b8f6c46049585dd3407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 19 Nov 2023 18:49:06 +0100 Subject: [PATCH 31/39] Remove obsolete spec file --- spec/models/app_catalog/web_app_spec.rb | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 spec/models/app_catalog/web_app_spec.rb diff --git a/spec/models/app_catalog/web_app_spec.rb b/spec/models/app_catalog/web_app_spec.rb deleted file mode 100644 index 5d94508..0000000 --- a/spec/models/app_catalog/web_app_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe AppCatalog::WebApp, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end -- 2.25.1 From 713e91a72096e9107a6aada0cbbde09caa71ecbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 19 Nov 2023 18:49:17 +0100 Subject: [PATCH 32/39] Implement RS auth revocation --- app/components/dropdown_link_component.rb | 2 +- app/components/rs_auth_component.html.erb | 3 +- .../services/remotestorage_controller.rb | 3 +- .../services/rs_auths_controller.rb | 34 +++++++++++++++++++ .../{dashboard.html.erb => show.html.erb} | 2 +- config/routes.rb | 12 +++++-- 6 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 app/controllers/services/rs_auths_controller.rb rename app/views/services/remotestorage/{dashboard.html.erb => show.html.erb} (80%) diff --git a/app/components/dropdown_link_component.rb b/app/components/dropdown_link_component.rb index 9f9e618..4eabc8e 100644 --- a/app/components/dropdown_link_component.rb +++ b/app/components/dropdown_link_component.rb @@ -9,7 +9,7 @@ class DropdownLinkComponent < ViewComponent::Base private def class_str(separator, add_class) - str = "no-underline block px-4 py-2 text-sm text-gray-900 bg-white + str = "no-underline block px-5 py-3 text-sm text-gray-900 bg-white hover:bg-gray-100 focus:bg-gray-100 whitespace-no-wrap" str = "#{str} border-t" if separator str = "#{str} #{add_class}" if add_class diff --git a/app/components/rs_auth_component.html.erb b/app/components/rs_auth_component.html.erb index 58ef7cd..cf5e97d 100644 --- a/app/components/rs_auth_component.html.erb +++ b/app/components/rs_auth_component.html.erb @@ -20,7 +20,8 @@ Launch app <% end %> <%= render DropdownLinkComponent.new( - href: "#", separator: true, add_class: "text-red-700" + href: revoke_services_storage_rs_auth_url(@auth), + separator: true, add_class: "text-red-700" ) do %> Revoke access <% end %> diff --git a/app/controllers/services/remotestorage_controller.rb b/app/controllers/services/remotestorage_controller.rb index e6a556c..67c7e76 100644 --- a/app/controllers/services/remotestorage_controller.rb +++ b/app/controllers/services/remotestorage_controller.rb @@ -3,7 +3,8 @@ class Services::RemotestorageController < Services::BaseController before_action :require_feature_enabled before_action :require_service_available - def dashboard + # Dashboard + def show # unless current_user.services_enabled.include?(:remotestorage) # redirect_to service_remotestorage_info_path # end diff --git a/app/controllers/services/rs_auths_controller.rb b/app/controllers/services/rs_auths_controller.rb new file mode 100644 index 0000000..4d7d5d2 --- /dev/null +++ b/app/controllers/services/rs_auths_controller.rb @@ -0,0 +1,34 @@ +class Services::RsAuthsController < Services::BaseController + before_action :authenticate_user! + before_action :require_feature_enabled + before_action :require_service_available + # before_action :require_service_enabled + + def destroy + if @rs_auth = current_user.remote_storage_authorizations.find(params[:id]) + @rs_auth.destroy! + else + http_status :not_found + end + + respond_to do |format| + format.html do redirect_to services_storage_url, flash: { + success: 'App authorization revoked' + } + end + format.json { head :no_content } + end + end + + private + + def require_feature_enabled + unless Flipper.enabled?(:remotestorage, current_user) + http_status :forbidden + end + end + + def require_service_available + http_status :not_found unless Setting.remotestorage_enabled? + end +end diff --git a/app/views/services/remotestorage/dashboard.html.erb b/app/views/services/remotestorage/show.html.erb similarity index 80% rename from app/views/services/remotestorage/dashboard.html.erb rename to app/views/services/remotestorage/show.html.erb index 5d90bfe..58b7ed7 100644 --- a/app/views/services/remotestorage/dashboard.html.erb +++ b/app/views/services/remotestorage/show.html.erb @@ -4,7 +4,7 @@

Connected Apps

<% if @rs_auths.any? %> -
+
<% @rs_auths.each do |auth| %> <%= render RsAuthComponent.new(auth: auth) %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 6e8968c..ee7259d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,10 +19,10 @@ Rails.application.routes.draw do resources :invitations, only: ['index', 'show', 'create', 'destroy'] namespace :services do - get 'storage', to: 'remotestorage#dashboard' - resource :chat, only: [:show], controller: 'chat' + resource :mastodon, only: [:show], controller: 'mastodon' + resources :lightning, only: [:index] do collection do get 'transactions' @@ -30,7 +30,13 @@ Rails.application.routes.draw do end end - resource :mastodon, only: [:show], controller: 'mastodon' + resource :storage, controller: 'remotestorage', only: [:show] do + resources :rs_auths, only: [:destroy] do + member do + get 'revoke', to: 'rs_auths#destroy' + end + end + end end resources :settings, param: 'section', only: ['index', 'show', 'update'] do -- 2.25.1 From aa399b862acddff4975e76d564313b8052d093fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 19 Nov 2023 19:10:13 +0100 Subject: [PATCH 33/39] Allow to launch RS apps from dashboard --- app/components/rs_auth_component.html.erb | 9 ++--- app/controllers/rs/oauth_controller.rb | 13 ------- .../services/rs_auths_controller.rb | 11 +++++- app/models/remote_storage_authorization.rb | 2 +- config/routes.rb | 4 +- spec/controllers/rs/oauth_controller_spec.rb | 29 -------------- .../services/rs_auths_controller_spec.rb | 39 +++++++++++++++++++ 7 files changed, 54 insertions(+), 53 deletions(-) create mode 100644 spec/controllers/services/rs_auths_controller_spec.rb diff --git a/app/components/rs_auth_component.html.erb b/app/components/rs_auth_component.html.erb index cf5e97d..8a40970 100644 --- a/app/components/rs_auth_component.html.erb +++ b/app/components/rs_auth_component.html.erb @@ -10,13 +10,10 @@ <%= @auth.client_id %>

- - - - - <%= render DropdownComponent.new do %> - <%= render DropdownLinkComponent.new(href: "#") do %> + <%= render DropdownLinkComponent.new( + href: launch_app_services_storage_rs_auth_url(@auth) + ) do %> Launch app <% end %> <%= render DropdownLinkComponent.new( diff --git a/app/controllers/rs/oauth_controller.rb b/app/controllers/rs/oauth_controller.rb index 6d9d537..48a5739 100644 --- a/app/controllers/rs/oauth_controller.rb +++ b/app/controllers/rs/oauth_controller.rb @@ -95,13 +95,6 @@ class Rs::OauthController < ApplicationController allow_other_host: true end - # GET /rs/oauth/token/:id/launch_app - def launch_app - auth = current_user.remote_storage_authorizations.find(params[:id]) - - redirect_to app_auth_url(auth), allow_other_host: true - end - private def require_signed_in_with_username @@ -111,12 +104,6 @@ class Rs::OauthController < ApplicationController end end - def app_auth_url(auth) - url = "#{auth.url}#remotestorage=#{current_user.address}" - url += "&access_token=#{auth.token}" - url - end - def hostname_of(uri) uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0] end diff --git a/app/controllers/services/rs_auths_controller.rb b/app/controllers/services/rs_auths_controller.rb index 4d7d5d2..091ce7b 100644 --- a/app/controllers/services/rs_auths_controller.rb +++ b/app/controllers/services/rs_auths_controller.rb @@ -5,8 +5,8 @@ class Services::RsAuthsController < Services::BaseController # before_action :require_service_enabled def destroy - if @rs_auth = current_user.remote_storage_authorizations.find(params[:id]) - @rs_auth.destroy! + if auth = current_user.remote_storage_authorizations.find(params[:id]) + auth.destroy! else http_status :not_found end @@ -20,6 +20,13 @@ class Services::RsAuthsController < Services::BaseController end end + def launch_app + auth = current_user.remote_storage_authorizations.find(params[:id]) + launch_url = "#{auth.url}#remotestorage=#{current_user.address}&access_token=#{auth.token}" + + redirect_to launch_url, allow_other_host: true + end + private def require_feature_enabled diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb index 2d62e65..e5f2ec0 100644 --- a/app/models/remote_storage_authorization.rb +++ b/app/models/remote_storage_authorization.rb @@ -23,7 +23,7 @@ class RemoteStorageAuthorization < ApplicationRecord after_destroy :remove_token_expiry_job def url - # TODO use web app scope in addition to host + # TODO use web app scope in addition to host/client_id uri = URI.parse self.redirect_uri "#{uri.scheme}://#{client_id}" end diff --git a/config/routes.rb b/config/routes.rb index ee7259d..c9f4281 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -33,7 +33,8 @@ Rails.application.routes.draw do resource :storage, controller: 'remotestorage', only: [:show] do resources :rs_auths, only: [:destroy] do member do - get 'revoke', to: 'rs_auths#destroy' + get :revoke, to: 'rs_auths#destroy' + get :launch_app end end end @@ -86,7 +87,6 @@ Rails.application.routes.draw do resource :oauth, only: [:new, :create], path_names: { new: ':username', create: ':username' }, controller: 'oauth' - get 'oauth/token/:id/launch_app' => 'oauth#launch_app', as: :launch_app end get '.well-known/webfinger', to: 'webfinger#show' diff --git a/spec/controllers/rs/oauth_controller_spec.rb b/spec/controllers/rs/oauth_controller_spec.rb index c80e427..09b0750 100644 --- a/spec/controllers/rs/oauth_controller_spec.rb +++ b/spec/controllers/rs/oauth_controller_spec.rb @@ -437,33 +437,4 @@ RSpec.describe Rs::OauthController, type: :controller do end end end - - describe "GET /rs/oauth/token/:id/launch_app" do - context "when user is signed in" do - before do - sign_in user - end - - context "token exists" do - before do - @auth = user.remote_storage_authorizations.create!( - permissions: %w(documents), client_id: "app.example.com", - redirect_uri: "https://app.example.com", - expire_at: 2.days.from_now - ) - - get :launch_app, params: { id: @auth.id } - end - - after do - @auth.destroy - end - - it "redirects to the given URL with the correct RS URL fragment params" do - launch_url = "https://app.example.com#remotestorage=#{user.address}&access_token=#{@auth.token}" - expect(response).to redirect_to(launch_url) - end - end - end - end end diff --git a/spec/controllers/services/rs_auths_controller_spec.rb b/spec/controllers/services/rs_auths_controller_spec.rb new file mode 100644 index 0000000..44bcdc0 --- /dev/null +++ b/spec/controllers/services/rs_auths_controller_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +RSpec.describe Services::RsAuthsController, type: :controller do + let(:user) { create :user } + + before do + allow_any_instance_of(AppCatalog::WebApp).to receive(:update_metadata).and_return(true) + allow_any_instance_of(Flipper).to receive(:enabled?).and_return(true) + end + + describe "GET /services/storage/rs_auths/:id/launch_app" do + context "when user is signed in" do + before do + sign_in user + end + + context "token exists" do + before do + @auth = user.remote_storage_authorizations.create!( + permissions: %w(documents), client_id: "app.example.com", + redirect_uri: "https://app.example.com", + expire_at: 2.days.from_now + ) + + get :launch_app, params: { id: @auth.id } + end + + after do + @auth.destroy + end + + it "redirects to the given URL with the correct RS URL fragment params" do + launch_url = "https://app.example.com#remotestorage=#{user.address}&access_token=#{@auth.token}" + expect(response).to redirect_to(launch_url) + end + end + end + end +end -- 2.25.1 From bdf5a18ad439db673a1c5e2a345e7a9d9117e5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Nov 2023 12:21:57 +0100 Subject: [PATCH 34/39] Re-add more specs --- app/controllers/rs/oauth_controller.rb | 2 +- .../remote_storage_authorization_spec.rb | 193 +++++++++--------- 2 files changed, 98 insertions(+), 97 deletions(-) diff --git a/app/controllers/rs/oauth_controller.rb b/app/controllers/rs/oauth_controller.rb index 48a5739..2e2933f 100644 --- a/app/controllers/rs/oauth_controller.rb +++ b/app/controllers/rs/oauth_controller.rb @@ -87,7 +87,7 @@ class Rs::OauthController < ApplicationController permissions: permissions, client_id: client_id, redirect_uri: redirect_uri, - app_name: client_id, #TODO use user-defined name + app_name: client_id, expire_at: expire_at ) diff --git a/spec/models/remote_storage_authorization_spec.rb b/spec/models/remote_storage_authorization_spec.rb index f3514e3..c29308d 100644 --- a/spec/models/remote_storage_authorization_spec.rb +++ b/spec/models/remote_storage_authorization_spec.rb @@ -76,102 +76,103 @@ RSpec.describe RemoteStorageAuthorization, type: :model do end end - # describe "#find_or_create_web_app" do - # context "with origin that looks hosted" do - # before do - # auth = user.remote_storage_authorizations.create!( - # permissions: %w(documents photos contacts:rw videos:r tasks/work:r), - # client_id: "example.com", - # redirect_uri: "https://example.com", - # expire_at: 1.month.from_now - # ) - # end - # - # it "generates a web_app" do - # expect(auth.web_app).to be_a(AppCatalog::WebApp) - # end - # - # it "uses the Web App's name as app name" do - # expect(auth.app_name).to eq("Example Domain") - # end - # end - # - # context "when creating two authorizations for the same app" do - # before do - # user_2 = create :user - # ResqueSpec.reset! - # auth_1 = user.remote_storage_authorizations.create!( - # permissions: %w(documents photos contacts:rw videos:r tasks/work:r), - # client_id: "example.com", - # redirect_uri: "https://example.com", - # expire_at: 1.month.from_now - # ) - # auth_2 = user_2.remote_storage_authorizations.create!( - # permissions: %w(documents photos contacts:rw videos:r tasks/work:r), - # client_id: "example.com", - # redirect_uri: "https://example.com", - # expire_at: 1.month.from_now - # ) - # end - # - # after do - # auth_1.destroy - # auth_2.destroy - # user_2.destroy - # end - # - # it "uses the same web app instance for both authorizations" do - # expect(auth_1.web_app).to be_a(AppCatalog::WebApp) - # expect(auth_1.web_app).to eq(auth_2.web_app) - # end - # end - # - # describe "non-production app origins" do - # context "when host is not an FQDN" do - # before do - # auth = user.remote_storage_authorizations.create!( - # permissions: %w(recipes), - # client_id: "localhost:4200", - # redirect_uri: "http://localhost:4200" - # ) - # end - # - # it "does not create a web app" do - # expect(auth.web_app).to be_nil - # expect(auth.app_name).to eq("localhost:4200") - # end - # end - # - # context "when host is an IP address" do - # before do - # auth = user.remote_storage_authorizations.create!( - # permissions: %w(recipes), - # client_id: "192.168.0.23:3000", - # redirect_uri: "http://192.168.0.23:3000" - # ) - # end - # - # it "does not create a web app" do - # expect(auth.web_app).to be_nil - # expect(auth.app_name).to eq("192.168.0.23:3000") - # end - # end - # - # context "when host is an extension URL" do # before do - # auth = user.remote_storage_authorizations.create!( - # permissions: %w(bookmarks), - # client_id: "123.addons.allizom.org", - # redirect_uri: "123.addons.allizom.org/foo" - # ) - # end - # - # it "does not create a web app" do - # expect(auth.web_app).to be_nil - # expect(auth.app_name).to eq("123.addons.allizom.org") - # end - # end - # end - # end + describe "#find_or_create_web_app" do + context "with origin that looks hosted" do + after(:all) { redis_rs_delete_keys("authorizations:*") } + + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(documents:rw), + client_id: "example.com", + redirect_uri: "https://example.com", + expire_at: 1.month.from_now + ) + end + + it "generates a web_app" do + expect(auth.web_app).to be_a(AppCatalog::WebApp) + end + end + + context "when creating two authorizations for the same app" do + let(:user_2) { create :user, id: 23, cn: "michiel", email: "michiel@example.com" } + + let(:auth_1) do + user.remote_storage_authorizations.create!( + permissions: %w(documents photos contacts:rw videos:r tasks/work:r), + client_id: "example.com", + redirect_uri: "https://example.com", + expire_at: 1.month.from_now + ) + end + + let(:auth_2) do + user_2.remote_storage_authorizations.create!( + permissions: %w(documents photos contacts:rw videos:r tasks/work:r), + client_id: "example.com", + redirect_uri: "https://example.com", + expire_at: 1.month.from_now + ) + end + + after do + auth_1.destroy + auth_2.destroy + user_2.destroy + end + + it "uses the same web app for both authorizations" do + expect(auth_1.web_app).to eq(auth_2.web_app) + end + end + + describe "non-production app origins" do + context "when host is not an FQDN" do + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(recipes), + client_id: "localhost:4200", + redirect_uri: "http://localhost:4200" + ) + end + + it "does not create a web app" do + expect(auth.web_app).to be_nil + expect(auth.app_name).to eq("localhost:4200") + end + end + + context "when host is an IP address" do + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(recipes), + client_id: "192.168.0.23:3000", + redirect_uri: "http://192.168.0.23:3000" + ) + end + + it "does not create a web app" do + expect(auth.web_app).to be_nil + expect(auth.app_name).to eq("192.168.0.23:3000") + end + end + + context "when host is an extension URL" do + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(bookmarks), + client_id: "123.addons.allizom.org", + redirect_uri: "123.addons.allizom.org/foo" + ) + end + + it "does not create a web app" do + expect(auth.web_app).to be_nil + expect(auth.app_name).to eq("123.addons.allizom.org") + end + end + end + end # describe "auth notifications" do # context "with auth notifications enabled" do -- 2.25.1 From 9a9947f9ad5276fc329f1281a7f58c1eeb910035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Nov 2023 13:30:23 +0100 Subject: [PATCH 35/39] Respect "start_url" from manifest when launching web apps --- .../services/rs_auths_controller.rb | 2 +- app/models/remote_storage_authorization.rb | 14 +++- .../remote_storage_authorization_spec.rb | 83 +++++++++++++++++-- 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/app/controllers/services/rs_auths_controller.rb b/app/controllers/services/rs_auths_controller.rb index 091ce7b..c0750e6 100644 --- a/app/controllers/services/rs_auths_controller.rb +++ b/app/controllers/services/rs_auths_controller.rb @@ -22,7 +22,7 @@ class Services::RsAuthsController < Services::BaseController def launch_app auth = current_user.remote_storage_authorizations.find(params[:id]) - launch_url = "#{auth.url}#remotestorage=#{current_user.address}&access_token=#{auth.token}" + launch_url = "#{auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{auth.token}" redirect_to launch_url, allow_other_host: true end diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb index e5f2ec0..53ca1b1 100644 --- a/app/models/remote_storage_authorization.rb +++ b/app/models/remote_storage_authorization.rb @@ -23,11 +23,23 @@ class RemoteStorageAuthorization < ApplicationRecord after_destroy :remove_token_expiry_job def url - # TODO use web app scope in addition to host/client_id uri = URI.parse self.redirect_uri "#{uri.scheme}://#{client_id}" end + def launch_url + return url unless web_app && web_app.metadata[:start_url].present? + + start_url = web_app.metadata[:start_url] + + if start_url.match("^https?:\/\/") + return start_url.start_with?(url) ? start_url : url + else + path = start_url.gsub(/^\.\.\//, "").gsub(/^\.\//, "").gsub(/^\//, "") + "#{url}/#{path}" + end + end + def delete_token_from_redis key = "authorizations:#{user.cn}:#{token}" redis.srem? key, redis.smembers(key) diff --git a/spec/models/remote_storage_authorization_spec.rb b/spec/models/remote_storage_authorization_spec.rb index c29308d..8fadd34 100644 --- a/spec/models/remote_storage_authorization_spec.rb +++ b/spec/models/remote_storage_authorization_spec.rb @@ -84,8 +84,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do user.remote_storage_authorizations.create!( permissions: %w(documents:rw), client_id: "example.com", - redirect_uri: "https://example.com", - expire_at: 1.month.from_now + redirect_uri: "https://example.com" ) end @@ -101,8 +100,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do user.remote_storage_authorizations.create!( permissions: %w(documents photos contacts:rw videos:r tasks/work:r), client_id: "example.com", - redirect_uri: "https://example.com", - expire_at: 1.month.from_now + redirect_uri: "https://example.com" ) end @@ -110,8 +108,7 @@ RSpec.describe RemoteStorageAuthorization, type: :model do user_2.remote_storage_authorizations.create!( permissions: %w(documents photos contacts:rw videos:r tasks/work:r), client_id: "example.com", - redirect_uri: "https://example.com", - expire_at: 1.month.from_now + redirect_uri: "https://example.com" ) end @@ -174,6 +171,80 @@ RSpec.describe RemoteStorageAuthorization, type: :model do end end + describe "#launch_url" do + after(:all) { redis_rs_delete_keys("authorizations:*") } + + context "without start URL" do + before do + AppCatalog::WebApp.create!( + url: "https://webmarks.5apps.com", name: "Webmarks", + metadata: { name: "Webmarks", start_url: nil, scope: nil } + ) + end + + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(bookmarks:rw), client_id: "webmarks.5apps.com", + redirect_uri: "https://webmarks.5apps.com/connect" + ) + end + + it "uses the base URL (from client ID)" do + expect(auth.launch_url).to eq("https://webmarks.5apps.com") + end + end + + context "with start URL" do + before do + AppCatalog::WebApp.create!( + url: "https://hyperdraft.rosano.ca", name: "Hyperdraft", + metadata: { + name: "Hyperdraft", scope: nil, + start_url: "https://hyperdraft.rosano.ca/start" + } + ) + end + + let(:auth) do + user.remote_storage_authorizations.create!( + permissions: %w(notes:rw), client_id: "hyperdraft.rosano.ca", + redirect_uri: "https://hyperdraft.rosano.ca/write/foo" + ) + end + + describe "full URL" do + it "respects the start URL" do + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start") + end + + it "does not respect URLs outside of the client ID scope" do + auth.web_app.metadata[:start_url] = "https://uberdraft.rosano.ca/write" + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca") + end + end + + describe "relative paths" do + it "includes the path relative from the base URL" do + auth.web_app.metadata[:start_url] = "start.html" + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start.html") + + auth.web_app.metadata[:start_url] = "./start.html" + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start.html") + + auth.web_app.metadata[:start_url] = "../start.html" + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/start.html") + end + end + + describe "absolute path" do + it "includes the path relative from the base URL" do + auth.web_app.metadata[:start_url] = "/write" + expect(auth.launch_url).to eq("https://hyperdraft.rosano.ca/write") + end + end + end + end + # describe "auth notifications" do # context "with auth notifications enabled" do # before do -- 2.25.1 From 2a70bf2fb977bfd3f7eefb1ea6f322feec949717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Nov 2023 13:40:56 +0100 Subject: [PATCH 36/39] Small refactoring --- app/controllers/services/rs_auths_controller.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/controllers/services/rs_auths_controller.rb b/app/controllers/services/rs_auths_controller.rb index c0750e6..e31f046 100644 --- a/app/controllers/services/rs_auths_controller.rb +++ b/app/controllers/services/rs_auths_controller.rb @@ -3,13 +3,10 @@ class Services::RsAuthsController < Services::BaseController before_action :require_feature_enabled before_action :require_service_available # before_action :require_service_enabled + before_action :find_rs_auth def destroy - if auth = current_user.remote_storage_authorizations.find(params[:id]) - auth.destroy! - else - http_status :not_found - end + @auth.destroy! respond_to do |format| format.html do redirect_to services_storage_url, flash: { @@ -21,8 +18,7 @@ class Services::RsAuthsController < Services::BaseController end def launch_app - auth = current_user.remote_storage_authorizations.find(params[:id]) - launch_url = "#{auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{auth.token}" + launch_url = "#{@auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{@auth.token}" redirect_to launch_url, allow_other_host: true end @@ -38,4 +34,9 @@ class Services::RsAuthsController < Services::BaseController def require_service_available http_status :not_found unless Setting.remotestorage_enabled? end + + def find_rs_auth + @auth = current_user.remote_storage_authorizations.find(params[:id]) + http_status :not_found unless @auth.present? + end end -- 2.25.1 From c2dae105ff1e29139bde0ab0cbeba452f0776ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Nov 2023 18:22:06 +0100 Subject: [PATCH 37/39] Add settings page for Storage, add notification prefs --- app/controllers/settings_controller.rb | 5 ++++- app/views/icons/_remotestorage.html.erb | 6 +++++ app/views/settings/_remotestorage.html.erb | 25 +++++++++++++++++++++ app/views/shared/_sidenav_settings.html.erb | 6 +++++ config/default_preferences.yml | 1 + 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 app/views/icons/_remotestorage.html.erb create mode 100644 app/views/settings/_remotestorage.html.erb diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index d4efea5..8f30419 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -110,7 +110,9 @@ class SettingsController < ApplicationController def set_settings_section @settings_section = params[:section] - allowed_sections = [:profile, :account, :lightning, :xmpp, :experiments] + allowed_sections = [ + :profile, :account, :lightning, :remotestorage, :xmpp, :experiments + ] unless allowed_sections.include?(@settings_section.to_sym) redirect_to setting_path(:profile) @@ -124,6 +126,7 @@ class SettingsController < ApplicationController def user_params params.require(:user).permit(:display_name, :avatar, preferences: [ :lightning_notify_sats_received, + :remotestorage_notify_auth_created, :xmpp_exchange_contacts_with_invitees ]) end diff --git a/app/views/icons/_remotestorage.html.erb b/app/views/icons/_remotestorage.html.erb new file mode 100644 index 0000000..2daafc4 --- /dev/null +++ b/app/views/icons/_remotestorage.html.erb @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/views/settings/_remotestorage.html.erb b/app/views/settings/_remotestorage.html.erb new file mode 100644 index 0000000..dc93b8e --- /dev/null +++ b/app/views/settings/_remotestorage.html.erb @@ -0,0 +1,25 @@ +<%= form_for @user, url: setting_path(:remotestorage), html: { :method => :put } do |f| %> +
+

Notifications

+
    + <%= render FormElements::FieldsetComponent.new( + positioning: :horizontal, + title: "New connection authorized", + description: "Notify me when my storage is connected to a new app" + ) do %> + <% f.fields_for :preferences do |p| %> + <%= p.select :remotestorage_notify_auth_created, options_for_select([ + ["off", "disabled"], + ["Chat (Jabber)", "xmpp"], # TODO make DRY, check for XMPP enabled + ["E-Mail", "email"] + ], selected: @user.preferences[:remotestorage_notify_auth_created]) %> + <% end %> + <% end %> +
+
+
+

+ <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %> +

+
+<% end %> diff --git a/app/views/shared/_sidenav_settings.html.erb b/app/views/shared/_sidenav_settings.html.erb index aa30f60..359f3e1 100644 --- a/app/views/shared/_sidenav_settings.html.erb +++ b/app/views/shared/_sidenav_settings.html.erb @@ -18,6 +18,12 @@ active: @settings_section.to_s == "lightning" ) %> <% end %> +<% if Setting.remotestorage_enabled %> +<%= render SidenavLinkComponent.new( + name: "Storage", path: setting_path(:remotestorage), icon: "remotestorage", + active: @settings_section.to_s == "remotestorage" +) %> +<% end %> <% if Setting.nostr_enabled %> <%= render SidenavLinkComponent.new( name: "Experiments", path: setting_path(:experiments), icon: "science", diff --git a/config/default_preferences.yml b/config/default_preferences.yml index ff7f051..c65d658 100644 --- a/config/default_preferences.yml +++ b/config/default_preferences.yml @@ -1,2 +1,3 @@ lightning_notify_sats_received: disabled # or xmpp, email +remotestorage_notify_auth_created: email # or xmpp, email xmpp_exchange_contacts_with_invitees: true -- 2.25.1 From cfd0935bdcd39e4606d8ea2caf07776e785a912a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Nov 2023 18:22:28 +0100 Subject: [PATCH 38/39] Notify user about new RS authorizations --- app/mailers/notification_mailer.rb | 7 ++ app/models/remote_storage_authorization.rb | 20 +++- app/services/router.rb | 7 ++ .../remotestorage_auth_created.text.erb | 23 ++++ .../remote_storage_authorization_spec.rb | 105 +++++++++++------- 5 files changed, 121 insertions(+), 41 deletions(-) create mode 100644 app/services/router.rb create mode 100644 app/views/notification_mailer/remotestorage_auth_created.text.erb diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 84f7dd5..19fc8ec 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -5,4 +5,11 @@ class NotificationMailer < ApplicationMailer @subject = "Sats received" mail to: @user.email, subject: @subject end + + def remotestorage_auth_created + @user = params[:user] + @auth = params[:auth] + @subject = "New app connected to your storage" + mail to: @user.email, subject: @subject + end end diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb index 53ca1b1..2e574d5 100644 --- a/app/models/remote_storage_authorization.rb +++ b/app/models/remote_storage_authorization.rb @@ -18,7 +18,7 @@ class RemoteStorageAuthorization < ApplicationRecord before_create :store_token_in_redis before_create :find_or_create_web_app after_create :schedule_token_expiry - # after_create :notify_user + after_create :notify_user before_destroy :delete_token_from_redis after_destroy :remove_token_expiry_job @@ -93,4 +93,22 @@ class RemoteStorageAuthorization < ApplicationRecord rescue URI::InvalidURIError false end + + def notify_user + notify = user.preferences[:remotestorage_notify_auth_created] + + case notify + when "xmpp" + router = Router.new + payload = { + type: "normal", to: user.address, + from: Setting.xmpp_notifications_from_address, + body: "You have just granted '#{self.client_id}' access to your Kosmos Storage. Visit your Storage dashboard to check on your connected apps and revoke permissions anytime: #{router.services_storage_url}" + } + XmppSendMessageJob.perform_later(payload) + when "email" + NotificationMailer.with(user: user, auth: self) + .remotestorage_auth_created.deliver_later + end + end end diff --git a/app/services/router.rb b/app/services/router.rb new file mode 100644 index 0000000..a7536ff --- /dev/null +++ b/app/services/router.rb @@ -0,0 +1,7 @@ +class Router + include Rails.application.routes.url_helpers + + def self.default_url_options + ActionMailer::Base.default_url_options + end +end diff --git a/app/views/notification_mailer/remotestorage_auth_created.text.erb b/app/views/notification_mailer/remotestorage_auth_created.text.erb new file mode 100644 index 0000000..cc8cd31 --- /dev/null +++ b/app/views/notification_mailer/remotestorage_auth_created.text.erb @@ -0,0 +1,23 @@ +Hi <%= @user.display_name.presence || @user.cn %>, + +You have just granted '<%= @auth.client_id %>' access to your Kosmos Storage, with the following permissions: + +<% @permissions.each do |p| %> +* <%= p %> +<% end %> + +Visit your Storage dashboard to check on your connected apps and revoke permissions anytime: + +<%= services_storage_url %> + +Have fun! + +--- + +You can disable email notifications for new app authorizations in your account settings: +<%= setting_path(:remotestorage) %> + +<% if Setting.discourse_enabled %> +If you have any questions, please visit our community forums: +<%= Setting.discourse_public_url %> +<% end %> diff --git a/spec/models/remote_storage_authorization_spec.rb b/spec/models/remote_storage_authorization_spec.rb index 8fadd34..3673bde 100644 --- a/spec/models/remote_storage_authorization_spec.rb +++ b/spec/models/remote_storage_authorization_spec.rb @@ -245,44 +245,69 @@ RSpec.describe RemoteStorageAuthorization, type: :model do end end - # describe "auth notifications" do - # context "with auth notifications enabled" do - # before do - # ResqueSpec.reset! - # user.push(mailing_lists: "rs-auth-notifications-#{Rails.env}") - # auth = user.remote_storage_authorizations.create!( - # :permissions => %w(documents photos contacts:rw videos:r tasks/work:r), - # :client_id => "example.com", - # :redirect_uri => "https://example.com" - # ) - # end - # - # it "notifies the user via email" do - # expect(enqueued_jobs.size).to eq(1) - # job = enqueued_jobs.first - # expect(job).to eq( - # job: ActionMailer::DeliveryJob, - # args: ['StorageAuthorizationMailer', 'authorized_rs_app', 'deliver_now', - # auth.id.to_s], - # queue: 'mailers' - # ) - # end - # end - # - # context "with auth notifications disabled" do - # before do - # ResqueSpec.reset! - # user.pull(mailing_lists: "rs-auth-notifications-#{Rails.env}") - # auth = user.remote_storage_authorizations.create!( - # :permissions => %w(documents photos contacts:rw videos:r tasks/work:r), - # :client_id => "example.com", - # :redirect_uri => "https://example.com" - # ) - # end - # - # it "does not notify the user via email about new RS app" do - # expect(enqueued_jobs.size).to eq(0) - # end - # end - # end + describe "notifications" do + include ActiveJob::TestHelper + + after(:each) { clear_enqueued_jobs } + after(:all) { redis_rs_delete_keys("authorizations:*") } + + before { allow(user).to receive(:display_name).and_return("Jimmy") } + + context "with notifications disabled" do + before do + user.preferences.merge!({ remotestorage_notify_auth_created: "off" }) + user.save! + user.remote_storage_authorizations.create!( + :permissions => %w(photos), :client_id => "app.example.com", + :redirect_uri => "https://app.example.com" + ) + end + + it "does not notify the user via email about new RS app" do + expect(enqueued_jobs.size).to eq(0) + end + end + + context "with email notifications enabled" do + before do + user.preferences.merge!({ remotestorage_notify_auth_created: "email" }) + user.save! + user.remote_storage_authorizations.create!( + :permissions => %w(photos), :client_id => "app.example.com", + :redirect_uri => "https://app.example.com" + ) + end + + it "notifies the user via email" do + expect(enqueued_jobs.size).to eq(1) + job = enqueued_jobs.select{|j| j['job_class'] == "ActionMailer::MailDeliveryJob"}.first + expect(job['arguments'][0]).to eq('NotificationMailer') + expect(job['arguments'][1]).to eq('remotestorage_auth_created') + expect(job['arguments'][3]['params']['user']['_aj_globalid']).to eq('gid://akkounts/User/1') + expect(job['arguments'][3]['params']['auth']['_aj_globalid']).to eq('gid://akkounts/RemoteStorageAuthorization/1') + end + end + + context "with XMPP notifications enabled" do + before do + Setting.xmpp_notifications_from_address = "botka@kosmos.org" + user.preferences.merge!({ remotestorage_notify_auth_created: "xmpp" }) + user.save! + user.remote_storage_authorizations.create!( + :permissions => %w(photos), :client_id => "app.example.com", + :redirect_uri => "https://app.example.com" + ) + end + + it "sends an XMPP message to the account owner's JID" do + expect(enqueued_jobs.size).to eq(1) + expect(enqueued_jobs.first["job_class"]).to eq("XmppSendMessageJob") + msg = enqueued_jobs.first["arguments"].first + expect(msg["type"]).to eq("normal") + expect(msg["from"]).to eq("botka@kosmos.org") + expect(msg["to"]).to eq(user.address) + expect(msg["body"]).to match(/granted 'app\.example\.com' access to your Kosmos Storage/) + end + end + end end -- 2.25.1 From aab6793b868112f67db49f494e759a9e6be214da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Nov 2023 18:32:52 +0100 Subject: [PATCH 39/39] Improve permission list in RS emails --- app/mailers/notification_mailer.rb | 5 +++++ .../notification_mailer/remotestorage_auth_created.text.erb | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 19fc8ec..81132b9 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -9,6 +9,11 @@ class NotificationMailer < ApplicationMailer def remotestorage_auth_created @user = params[:user] @auth = params[:auth] + @permissions = @auth.permissions.map do |p| + access = p.split(":")[1] == 'r' ? 'read' : 'read/write' + directory = p.split(':')[0] == '' ? 'all folders and files' : p.split(':')[0] + "#{access} #{directory}" + end @subject = "New app connected to your storage" mail to: @user.email, subject: @subject end diff --git a/app/views/notification_mailer/remotestorage_auth_created.text.erb b/app/views/notification_mailer/remotestorage_auth_created.text.erb index cc8cd31..61338f3 100644 --- a/app/views/notification_mailer/remotestorage_auth_created.text.erb +++ b/app/views/notification_mailer/remotestorage_auth_created.text.erb @@ -15,9 +15,9 @@ Have fun! --- You can disable email notifications for new app authorizations in your account settings: -<%= setting_path(:remotestorage) %> - +<%= setting_url(:remotestorage) %> <% if Setting.discourse_enabled %> + If you have any questions, please visit our community forums: <%= Setting.discourse_public_url %> <% end %> -- 2.25.1