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/Gemfile b/Gemfile
index b169662..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'
@@ -59,6 +61,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..3e551f1 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,100 @@ 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)
+ 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)
- 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 +135,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 +144,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)
@@ -135,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)
@@ -149,9 +179,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,44 +190,47 @@ 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)
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
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 +243,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 +258,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 +269,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 +321,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 +335,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 +351,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 +362,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,55 +415,57 @@ 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
+ ruby
x86_64-linux
DEPENDENCIES
+ aws-sdk-s3
byebug (~> 11.1)
capybara
cssbundling-rails
@@ -435,6 +474,7 @@ DEPENDENCIES
devise_ldap_authenticatable
discourse_api
dotenv-rails
+ down
factory_bot_rails
faker
faraday
@@ -449,6 +489,7 @@ DEPENDENCIES
listen (~> 3.2)
lnurl
lockbox
+ manifique!
net-ldap
nostr!
pagy (~> 6.0, >= 6.0.2)
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`
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..4eabc8e
--- /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-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
+ str
+ end
+end
diff --git a/app/components/rs_auth_component.html.erb b/app/components/rs_auth_component.html.erb
new file mode 100644
index 0000000..8a40970
--- /dev/null
+++ b/app/components/rs_auth_component.html.erb
@@ -0,0 +1,26 @@
+
+
+ <%= image_tag s3_image_url(@web_app.icon), class: "h-full w-full" %>
+
+
+
+ <%= @web_app.name %>
+
+
+ <%= @auth.client_id %>
+
+
+ <%= render DropdownComponent.new do %>
+ <%= render DropdownLinkComponent.new(
+ href: launch_app_services_storage_rs_auth_url(@auth)
+ ) do %>
+ Launch app
+ <% end %>
+ <%= render DropdownLinkComponent.new(
+ href: revoke_services_storage_rs_auth_url(@auth),
+ separator: true, add_class: "text-red-700"
+ ) do %>
+ Revoke access
+ <% end %>
+ <% 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/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/controllers/rs/oauth_controller.rb b/app/controllers/rs/oauth_controller.rb
index 67a2beb..2e2933f 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],
@@ -88,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
)
@@ -96,29 +95,15 @@ 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
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
- 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/remotestorage_controller.rb b/app/controllers/services/remotestorage_controller.rb
index 5d455ba..67c7e76 100644
--- a/app/controllers/services/remotestorage_controller.rb
+++ b/app/controllers/services/remotestorage_controller.rb
@@ -3,10 +3,13 @@ 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
+ @rs_auths = current_user.remote_storage_authorizations
+ # TODO sort by app name
end
private
diff --git a/app/controllers/services/rs_auths_controller.rb b/app/controllers/services/rs_auths_controller.rb
new file mode 100644
index 0000000..e31f046
--- /dev/null
+++ b/app/controllers/services/rs_auths_controller.rb
@@ -0,0 +1,42 @@
+class Services::RsAuthsController < Services::BaseController
+ before_action :authenticate_user!
+ before_action :require_feature_enabled
+ before_action :require_service_available
+ # before_action :require_service_enabled
+ before_action :find_rs_auth
+
+ def destroy
+ @auth.destroy!
+
+ 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
+
+ def launch_app
+ launch_url = "#{@auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{@auth.token}"
+
+ redirect_to launch_url, allow_other_host: true
+ 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
+
+ def find_rs_auth
+ @auth = current_user.remote_storage_authorizations.find(params[:id])
+ http_status :not_found unless @auth.present?
+ end
+end
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/controllers/webfinger_controller.rb b/app/controllers/webfinger_controller.rb
index 5cf4012..be50ebd 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)
+ storage_url = "#{Setting.rs_storage_url}/#{@username}"
{
"rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage",
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/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index 84f7dd5..81132b9 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -5,4 +5,16 @@ class NotificationMailer < ApplicationMailer
@subject = "Sats received"
mail to: @user.email, subject: @subject
end
+
+ 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
end
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..b42bea7
--- /dev/null
+++ b/app/models/app_catalog/web_app.rb
@@ -0,0 +1,16 @@
+class AppCatalog::WebApp < ApplicationRecord
+ store :metadata, coder: JSON
+
+ has_many :remote_storage_authorizations
+
+ has_one_attached :icon
+ 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? }
+
+ def update_metadata
+ AppCatalogManager::UpdateMetadata.call(self)
+ end
+end
diff --git a/app/models/remote_storage_authorization.rb b/app/models/remote_storage_authorization.rb
index 82ac744..2e574d5 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,22 +16,36 @@ 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
def url
- if self.redirect_uri
- uri = URI.parse self.redirect_uri
- "#{uri.scheme}://#{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
- "http://#{client_id}"
+ path = start_url.gsub(/^\.\.\//, "").gsub(/^\.\//, "").gsub(/^\//, "")
+ "#{url}/#{path}"
end
end
def delete_token_from_redis
- key = "rs:authorizations:#{user.address}:#{token}"
+ 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
@@ -44,7 +59,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
@@ -60,4 +75,40 @@ 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)
+ web_app.update_metadata unless web_app.name.present?
+ 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
+
+ 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/app_catalog_manager/update_metadata.rb b/app/services/app_catalog_manager/update_metadata.rb
new file mode 100644
index 0000000..4530ae3
--- /dev/null
+++ b/app/services/app_catalog_manager/update_metadata.rb
@@ -0,0 +1,52 @@
+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") ||
+ 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
+
+ @app.save!
+ 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)
+ 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"
+
+ tempfile = Down.download(download_url)
+ @app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
+ 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/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/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..ebfa289
--- /dev/null
+++ b/app/views/admin/app_catalog/web_apps/index.html.erb
@@ -0,0 +1,56 @@
+<%= 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
+
+
+
+ Name
+ URL
+ RS Auths
+ Created at
+
+
+
+ <% @web_apps.each do |web_app| %>
+
+ <%= web_app.name %>
+ <%= link_to web_app.url, web_app.url,
+ target: "_blank", rel: "nofollow noopener",
+ class: "ks-text-link" %>
+ <%= web_app.remote_storage_authorizations.count %>
+
+
+ <%= time_ago_in_words web_app.created_at, include_seconds: false %> ago
+
+
+
+ <% end %>
+
+
+ <%== pagy_nav @pagy %>
+
+ <% end %>
+<% end %>
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 %>
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/_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/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/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/notification_mailer/remotestorage_auth_created.text.erb b/app/views/notification_mailer/remotestorage_auth_created.text.erb
new file mode 100644
index 0000000..61338f3
--- /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_url(:remotestorage) %>
+<% if Setting.discourse_enabled %>
+
+If you have any questions, please visit our community forums:
+<%= Setting.discourse_public_url %>
+<% end %>
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.
diff --git a/app/views/services/remotestorage/dashboard.html.erb b/app/views/services/remotestorage/dashboard.html.erb
deleted file mode 100644
index f6b0932..0000000
--- a/app/views/services/remotestorage/dashboard.html.erb
+++ /dev/null
@@ -1,7 +0,0 @@
-<%= render HeaderComponent.new(title: "Storage") %>
-
-<%= render MainSimpleComponent.new do %>
-
-<% end %>
diff --git a/app/views/services/remotestorage/show.html.erb b/app/views/services/remotestorage/show.html.erb
new file mode 100644
index 0000000..58b7ed7
--- /dev/null
+++ b/app/views/services/remotestorage/show.html.erb
@@ -0,0 +1,16 @@
+<%= render HeaderComponent.new(title: "Storage") %>
+
+<%= render MainSimpleComponent.new do %>
+
+ 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/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/_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/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
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/routes.rb b/config/routes.rb
index e7fef10..c9f4281 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,14 @@ 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'
+ get :launch_app
+ end
+ end
+ end
end
resources :settings, param: 'section', only: ['index', 'show', 'update'] do
@@ -60,11 +67,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']
@@ -73,9 +85,8 @@ Rails.application.routes.draw do
namespace :rs do
resource :oauth, only: [:new, :create], path_names: {
- new: ':useraddress', create: ':useraddress'
- }, controller: 'oauth', constraints: { useraddress: /[^\/]+/}
- get 'oauth/token/:id/launch_app' => 'oauth#launch_app', as: :launch_app
+ new: ':username', create: ':username'
+ }, controller: 'oauth'
end
get '.well-known/webfinger', to: 'webfinger#show'
@@ -95,4 +106,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
diff --git a/config/storage.yml b/config/storage.yml
index 1f93f73..3ddfbb8 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 %>
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/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 bff7e78..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_09_06_073324) 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
@@ -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"
@@ -88,8 +96,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_06_073324) 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|
@@ -124,5 +134,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_06_073324) 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/docker-compose.yml b/docker-compose.yml
index 3690616..5106dfd 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,51 @@ 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:2.0.0-beta.2
+ networks:
+ - external_network
+ - internal_network
+ ports:
+ - "4567:4567"
+ environment:
+ RACK_ENV: staging
+ REDIS_HOST: redis
+ REDIS_PORT: 6379
+ REDIS_DB: 1
+ 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
# ports:
diff --git a/spec/controllers/rs/oauth_controller_spec.rb b/spec/controllers/rs/oauth_controller_spec.rb
index 48ca14b..09b0750 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
@@ -433,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
diff --git a/spec/factories/app_catalog/web_apps.rb b/spec/factories/app_catalog/web_apps.rb
new file mode 100644
index 0000000..c07de2e
--- /dev/null
+++ b/spec/factories/app_catalog/web_apps.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :web_app, class: 'AppCatalog::WebApp' do
+ url { "https://myfavoritedrinks.remotestorage.io/" }
+ name { "My Favorite Drinks" }
+ end
+end
diff --git a/spec/factories/remote_storage_authorizations.rb b/spec/factories/remote_storage_authorizations.rb
index be7c810..0cb47b1 100644
--- a/spec/factories/remote_storage_authorizations.rb
+++ b/spec/factories/remote_storage_authorizations.rb
@@ -1,9 +1,10 @@
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 { nil }
+ expire_at { 1.month.from_now }
+ web_app
end
end
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/jobs/remote_storage_expire_authorization_job_spec.rb b/spec/jobs/remote_storage_expire_authorization_job_spec.rb
index e65662e..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
@@ -20,7 +25,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..3673bde 100644
--- a/spec/models/remote_storage_authorization_spec.rb
+++ b/spec/models/remote_storage_authorization_spec.rb
@@ -5,9 +5,13 @@ 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("rs:authorizations:*") }
+ after(:all) { redis_rs_delete_keys("authorizations:*") }
let(:auth) do
user.remote_storage_authorizations.create!(
@@ -22,7 +26,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 +48,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 +58,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
@@ -72,141 +76,238 @@ 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:*") }
- # 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
+ let(:auth) do
+ user.remote_storage_authorizations.create!(
+ permissions: %w(documents:rw),
+ client_id: "example.com",
+ redirect_uri: "https://example.com"
+ )
+ 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"
+ )
+ 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"
+ )
+ 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 "#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 "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
diff --git a/spec/requests/webfinger_spec.rb b/spec/requests/webfinger_spec.rb
index f944a7a..1dcdfa3 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")
end
end