69 Commits

Author SHA1 Message Date
Râu Cao
aab6793b86 Improve permission list in RS emails
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-11-20 18:32:52 +01:00
Râu Cao
cfd0935bdc Notify user about new RS authorizations 2023-11-20 18:24:34 +01:00
Râu Cao
c2dae105ff Add settings page for Storage, add notification prefs 2023-11-20 18:22:06 +01:00
Râu Cao
2a70bf2fb9 Small refactoring
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-20 13:40:56 +01:00
Râu Cao
9a9947f9ad Respect "start_url" from manifest when launching web apps
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-20 13:32:40 +01:00
Râu Cao
bdf5a18ad4 Re-add more specs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-20 12:21:57 +01:00
Râu Cao
aa399b862a Allow to launch RS apps from dashboard
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-19 19:10:13 +01:00
Râu Cao
713e91a720 Implement RS auth revocation
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-19 18:49:17 +01:00
Râu Cao
8ec2a6d7e4 Remove obsolete spec file 2023-11-19 18:49:06 +01:00
Râu Cao
4ecf2c4246 Improve app list 2023-11-19 18:48:44 +01:00
Râu Cao
4fdf8accd6 Add note
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-18 17:36:18 +01:00
Râu Cao
f451adcb53 Try smaller icons if 256px not available 2023-11-18 17:35:57 +01:00
Râu Cao
721dccb499 Add dropdown components, menus for RS auth items 2023-11-18 17:13:55 +01:00
Râu Cao
27bb7d1bfe Finish working liquor-cabinet setup for Docker Compose
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-16 12:46:19 +01:00
Râu Cao
1d44181fb5 Wording 2023-11-16 12:46:05 +01:00
Râu Cao
de67f59d5c Fail gracefully and log error when token missing in Redis 2023-11-16 12:45:26 +01:00
Râu Cao
1995e6dda2 Fix RS OAuth URL in Webfinger record
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-16 12:44:59 +01:00
Râu Cao
600cfe0f78 Update lockfile 2023-11-16 12:42:39 +01:00
Râu Cao
00049f3743 Add info for running Minio/RS to README
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-01 22:01:14 +01:00
Râu Cao
60c0a43f33 Add minio to Docker Compose setup, configure Liquor Cabinet 2023-11-01 21:51:29 +01:00
Râu Cao
0c1b1b4afe Adjust specs for web app metadata fetching 2023-11-01 21:49:08 +01:00
Râu Cao
92310d434a Remove rs namespace from Redis keys
Superfluous, since the whole db should be RS only
2023-11-01 21:48:16 +01:00
Râu Cao
56c127ca0c Only allow primary domain for RS
Replace user addresses with usernames in the respective URLs
2023-11-01 21:46:38 +01:00
Râu Cao
5075fef616 Only show avatar when available on admin user page
Some checks failed
continuous-integration/drone/push Build is failing
2023-10-25 22:16:16 +02:00
Râu Cao
8e090daa9c Fetch web app metadata when creating RS auth 2023-10-25 22:16:16 +02:00
Râu Cao
def87a1621 Remove variants from attachment 2023-10-25 22:16:16 +02:00
Râu Cao
00ec7fa21c WIP Add RS auths/apps to Storage dashboard 2023-10-25 22:16:13 +02:00
Râu Cao
2b8bfaaca8 Add admin page for web apps
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-24 22:42:16 +02:00
Râu Cao
3e9a08a266 Remove (long) obsolete edge case 2023-10-24 17:29:24 +02:00
Râu Cao
fcea11f0e5 Associate RS authorizations with web apps 2023-10-24 17:29:24 +02:00
Râu Cao
261a782963 Only complete icon URLs when given relative or absolute paths 2023-10-24 17:29:24 +02:00
Râu Cao
e964e7e52c Save web app metadata explicitly 2023-10-24 17:29:24 +02:00
Râu Cao
e508407df4 Remove debug statement 2023-10-24 17:29:23 +02:00
Râu Cao
bec827acb1 Store web app icons with proper folder paths 2023-10-24 17:29:23 +02:00
Râu Cao
0a69603643 Update web app metadata when first creating a record 2023-10-24 17:29:23 +02:00
Râu Cao
d4f71e98ed Download and attach icons for web apps 2023-10-24 17:29:23 +02:00
Râu Cao
e56c9bd0d5 Add web app model, service to fetch metadata 2023-10-24 17:29:23 +02:00
Râu Cao
e1b7e1b2ef Update dependencies, add manifique 2023-10-24 17:29:23 +02:00
Râu Cao
1056ffd08e Add optional S3 config/backend for ActiveStorage 2023-10-24 17:29:23 +02:00
be5fe00f20 Merge pull request 'Fix XMPP from-address config not being used' (#150) from bugfix/xmpp_from_address into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #150
2023-10-19 10:47:45 +00:00
Râu Cao
e9c4929726 Fix XMPP from-address config not being used
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-10-17 15:21:57 +02:00
14ff0c0e16 Merge pull request 'BTCPay settings, admin page, and new Lightning balance API' (#147) from feature/btcpay_configs into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #147
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-09-26 10:13:09 +00:00
Râu Cao
d939f5d649 Merge branch 'master' into feature/btcpay_configs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2023-09-20 19:12:24 +02:00
Râu Cao
69fffb29d8 Make publishing of BTCPay wallet balances optional
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2023-09-20 18:36:53 +02:00
Râu Cao
91d3b977e9 Fix spec 2023-09-20 18:26:50 +02:00
7a5fd46835 Merge pull request 'Add user avatars to LDAP, upload on profile settings page' (#148) from feature/123-user_avatars into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #148
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-09-13 13:01:25 +00:00
Râu Cao
9c4c5c2553 Use correct content type for image
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-09-13 14:49:16 +02:00
Râu Cao
8f819d12c0 Remove debug output 2023-09-13 14:48:51 +02:00
Râu Cao
b810e27480 Use custom docker image with libvips installed in CI
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-07 19:40:43 +02:00
Râu Cao
1949f1876f Use attr_reader instead of shared instance variables
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-07 19:22:15 +02:00
Râu Cao
2ba0116ca6 Fix wrong inheritance
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-07 19:17:46 +02:00
Râu Cao
2c2ddabdff Fix code being silly
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-07 19:15:14 +02:00
Râu Cao
dfcdbec0dd Add specs for avatar upload
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-09-07 11:42:42 +02:00
Râu Cao
3b67a8791c Add libvips package to Docker container 2023-09-07 11:42:24 +02:00
Râu Cao
d5ab532947 Store and retrieve avatars in/from LDAP exclusively
Some checks failed
continuous-integration/drone/push Build is failing
No need to keep them in two places at the same time. We can fetch them
from LDAP whenever we want to do something with them.
2023-09-06 20:42:26 +02:00
Râu Cao
50c63d5c38 Update user avatar in LDAP 2023-09-06 19:02:07 +02:00
Râu Cao
64d09cfb7f Use variant declarations instead of custom methods 2023-09-06 12:38:47 +02:00
Râu Cao
def44618ef Comments
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-06 12:16:00 +02:00
Râu Cao
9e5aeaf572 Add user avatars 2023-09-06 12:15:53 +02:00
Râu Cao
86f85a90f4 Add/configure ActiveStorage 2023-09-06 12:14:28 +02:00
d8a35ac3fd Merge pull request 'Fix wrong redirect after sign-in for RS OAuth' (#146) from bugfix/rs_oauth_login into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #146
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-09-05 11:03:02 +00:00
Râu Cao
5a5f62e98a Refactor BTCPay service and API, add lightning balance
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-04 16:02:54 +02:00
Râu Cao
074f9afcbb Fix descriptions not being shown for resettable form fields 2023-09-04 15:37:02 +02:00
Râu Cao
725fd2e5ea Move lndhub admin token to env var/setting 2023-09-04 15:36:22 +02:00
Râu Cao
8349ca5e12 Add admin settings page for BTCPay 2023-09-04 15:25:20 +02:00
Râu Cao
46d59e3371 Improve icons in admin service settings sidenav 2023-09-04 15:24:35 +02:00
Râu Cao
e8e6ee0bc4 Add configurable settings for BTCPay 2023-09-04 15:23:27 +02:00
Râu Cao
a91ee2bd0a Fix generated usernames in seeds potentially being too short
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-04 11:35:51 +02:00
Râu Cao
fcb6923c92 Fix wrong redirect after sign-in for RS OAuth
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
We use a custom auth method to pre-fill the username when reaching the
RS OAuth while signed out. However, it needs to redirect back to the RS
OAuth page after sign-in, and not to the root path.
2023-09-04 11:33:16 +02:00
99 changed files with 1771 additions and 554 deletions

View File

@@ -17,7 +17,7 @@ steps:
branch:
- master
- name: rspec
image: guildeducation/rails:2.7.2-14.20.0
image: gitea.kosmos.org/kosmos/akkounts-ci:0.1.0
environment:
RAILS_ENV: test
REDIS_URL: redis://redis:6379/0

View File

@@ -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
@@ -34,6 +42,8 @@ EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
BTCPAY_API_URL='http://localhost:23001/api/v1'
BTCPAY_STORE_ID=''
BTCPAY_AUTH_TOKEN=''
LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'

View File

@@ -2,13 +2,14 @@ PRIMARY_DOMAIN=kosmos.org
REDIS_URL='redis://localhost:6379/0'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
BTCPAY_STORE_ID='123456'
DISCOURSE_PUBLIC_URL='http://discourse.example.com'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
EJABBERD_API_URL='http://xmpp.example.com/api'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@
!/tmp/pids/
!/tmp/pids/.keep
/storage
/public/assets
.byebug_history

View File

@@ -4,7 +4,7 @@ FROM ruby:2.7.6
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
ldap-utils tini
ldap-utils tini libvips
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get install -y nodejs

View File

@@ -37,6 +37,7 @@ gem 'devise_ldap_authenticatable'
gem 'net-ldap'
# Utilities
gem "image_processing", "~> 1.12.2"
gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3'
gem 'pagy', '~> 6.0', '>= 6.0.2'
@@ -46,6 +47,8 @@ gem 'flipper-ui'
# HTTP requests
gem 'faraday'
gem 'down'
gem 'aws-sdk-s3', require: false
# Background/scheduled jobs
gem 'sidekiq', '< 7'
@@ -58,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

View File

@@ -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,41 +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)
importmap-rails (1.1.6)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
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)
@@ -206,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)
@@ -220,10 +257,11 @@ GEM
marcel (1.0.2)
matrix (0.4.2)
method_source (1.0.0)
mini_mime (1.1.2)
minitest (5.18.0)
mini_magick (4.12.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)
@@ -231,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)
@@ -282,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)
@@ -296,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)
@@ -312,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)
@@ -323,32 +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.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)
@@ -372,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
@@ -429,12 +474,14 @@ DEPENDENCIES
devise_ldap_authenticatable
discourse_api
dotenv-rails
down
factory_bot_rails
faker
faraday
flipper
flipper-active_record
flipper-ui
image_processing (~> 1.12.2)
importmap-rails
jbuilder (~> 2.7)
letter_opener
@@ -442,6 +489,7 @@ DEPENDENCIES
listen (~> 3.2)
lnurl
lockbox
manifique!
net-ldap
nostr!
pagy (~> 6.0, >= 6.0.2)

View File

@@ -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`

View File

@@ -0,0 +1,26 @@
<div data-controller="dropdown" data-action="click->dropdown#toggle click@window->dropdown#hide">
<div class="relative inline-block">
<div role="button" tabindex="0" data-dropdown-target="button"
class="inline-block select-none">
<span class="appearance-none flex items-center inline-block">
<span class="p-2 bg-gray-50 hover:bg-gray-100 rounded-full">
<%= render partial: "icons/kebab-menu", locals: {
custom_class: "inline text-gray-500 h-6 w-6"
} %>
</span>
</span>
</div>
<div data-dropdown-target="menu"
data-transition-enter="transition ease-out duration-200"
data-transition-enter-from="opacity-0 translate-y-1"
data-transition-enter-to="opacity-100 translate-y-0"
data-transition-leave="transition ease-in duration-150"
data-transition-leave-from="opacity-100 translate-y-0"
data-transition-leave-to="opacity-0 translate-y-1"
class="hidden absolute top-4 right-0 z-10 mt-5 flex w-screen max-w-max">
<div class="bg-white shadow-lg rounded border overflow-hidden w-auto">
<%= content %>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
class DropdownComponent < ViewComponent::Base
end

View File

@@ -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 %>

View File

@@ -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

View File

@@ -6,7 +6,7 @@ module FormElements
@tag = tag
@positioning = :vertical
@title = title
@descripton = description
@description = description
@key = key.to_sym
@type = type
@resettable = is_resettable?(@key)

View File

@@ -0,0 +1,26 @@
<div class="flex items-center gap-4">
<div class="h-16 w-16 flex-none">
<%= image_tag s3_image_url(@web_app.icon), class: "h-full w-full" %>
</div>
<div class="flex-grow">
<h4 class="mb-1 text-lg font-bold">
<%= @web_app.name %>
</h4>
<p class="text-sm text-gray-500">
<%= @auth.client_id %>
</p>
</div>
<%= 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 %>
</div>

View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
class RsAuthComponent < ViewComponent::Base
def initialize(auth:)
@auth = auth
@web_app = auth.web_app
end
end

View File

@@ -1,4 +1,8 @@
<%= link_to @path, class: @link_class, title: (@disabled ? "Coming soon" : nil) do %>
<% if @icon.present? %>
<%= render partial: "icons/#{@icon}", locals: { custom_class: @icon_class } %>
<% elsif @text_icon.present? %>
<span class="mr-3"><%= @text_icon %></span>
<% end %>
<span class="truncate"><%= @name %></span>
<% end %>

View File

@@ -1,11 +1,13 @@
# frozen_string_literal: true
class SidenavLinkComponent < ViewComponent::Base
def initialize(name:, level: 1, path:, icon:, active: false, disabled: false)
def initialize(name:, level: 1, path:, icon: nil, text_icon: nil,
active: false, disabled: false)
@name = name
@level = level
@path = path
@icon = icon
@text_icon = text_icon
@active = active
@disabled = disabled
@link_class = class_names_link(path)

View File

@@ -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

View File

@@ -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

View File

@@ -3,7 +3,7 @@ class Admin::Settings::ServicesController < Admin::SettingsController
@service = params[:s]
if @service.blank?
redirect_to admin_settings_services_path(params: { s: "discourse" })
redirect_to admin_settings_services_path(params: { s: "btcpay" })
end
end

View File

@@ -20,6 +20,8 @@ class Admin::UsersController < Admin::BaseController
end
@services_enabled = @user.services_enabled
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn, ou: @user.ou)
end
private

View File

@@ -0,0 +1,29 @@
class Api::BtcpayController < Api::BaseController
before_action :require_feature_enabled
def onchain_btc_balance
balance = BtcpayManager::FetchOnchainWalletBalance.call
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch BTC wallet balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
def lightning_btc_balance
balance = BtcpayManager::FetchLightningWalletBalance.call
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch BTC lightning balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
private
def require_feature_enabled
unless Setting.btcpay_publish_wallet_balances
http_status :not_found and return
end
end
end

View File

@@ -1,13 +0,0 @@
class Api::KreditsController < Api::BaseController
def onchain_btc_balance
btcpay = BtcPay.new
balance = btcpay.onchain_wallet_balance
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch kredits BTC wallet balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
end

View File

@@ -37,4 +37,8 @@ class ApplicationController < ActionController::Base
format.any { head status }
end
end
def after_sign_in_path_for(user)
session[:user_return_to] || root_path
end
end

View File

@@ -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,28 +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("@")
redirect_to new_user_session_path(cn: username, ou: org)
session[:user_return_to] = request.url
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

View File

@@ -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

View File

@@ -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

View File

@@ -19,10 +19,15 @@ class SettingsController < ApplicationController
def update
@user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name]
@user.avatar_new = user_params[:avatar]
if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
LdapManager::UpdateDisplayName.call(@user.dn, user_params[:display_name])
LdapManager::UpdateDisplayName.call(@user.dn, @user.display_name)
end
if @user.avatar_new.present?
LdapManager::UpdateAvatar.call(@user.dn, @user.avatar_new)
end
redirect_to setting_path(@settings_section), flash: {
@@ -105,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)
@@ -117,8 +124,9 @@ class SettingsController < ApplicationController
end
def user_params
params.require(:user).permit(:display_name, preferences: [
params.require(:user).permit(:display_name, :avatar, preferences: [
:lightning_notify_sats_received,
:remotestorage_notify_auth_created,
:xmpp_exchange_contacts_with_invitees
])
end

View File

@@ -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",

View File

@@ -30,7 +30,7 @@ class WebhooksController < ApplicationController
def notify_xmpp(address, amt_sats, memo)
payload = {
type: "normal",
from: Setting.primary_domain,
from: Setting.xmpp_notifications_from_address,
to: address,
subject: "Sats received!",
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"

View File

@@ -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)

View File

@@ -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

View File

@@ -0,0 +1,5 @@
module AppCatalog
def self.table_name_prefix
"app_catalog_"
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -36,7 +36,25 @@ class Setting < RailsSettings::Base
#
field :sentry_enabled, type: :boolean, readonly: true,
default: (ENV["SENTRY_DSN"].present?.to_s || false)
default: ENV["SENTRY_DSN"].present?
#
# BTCPay Server
#
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
#
# Discourse
@@ -46,7 +64,7 @@ class Setting < RailsSettings::Base
default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean,
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
default: ENV["DISCOURSE_PUBLIC_URL"].present?
field :discourse_connect_secret, type: :string,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
@@ -59,14 +77,14 @@ class Setting < RailsSettings::Base
default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean,
default: (ENV["DRONECI_PUBLIC_URL"].present?.to_s || false)
default: ENV["DRONECI_PUBLIC_URL"].present?
#
# ejabberd
#
field :ejabberd_enabled, type: :boolean,
default: (ENV["EJABBERD_API_URL"].present?.to_s || false)
default: ENV["EJABBERD_API_URL"].present?
field :ejabberd_api_url, type: :string,
default: ENV["EJABBERD_API_URL"].presence
@@ -85,7 +103,7 @@ class Setting < RailsSettings::Base
default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean,
default: (ENV["GITEA_PUBLIC_URL"].present?.to_s || false)
default: ENV["GITEA_PUBLIC_URL"].present?
#
# Lightning Network
@@ -95,16 +113,19 @@ class Setting < RailsSettings::Base
default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean,
default: (ENV["LNDHUB_API_URL"].present?.to_s || false)
default: ENV["LNDHUB_API_URL"].present?
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean,
default: (ENV["LNDHUB_ADMIN_UI"] || false)
default: ENV["LNDHUB_ADMIN_UI"] || false
field :lndhub_public_key, type: :string,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present?.to_s || false }
default: -> { self.lndhub_public_key.present? }
#
# Mastodon
@@ -114,7 +135,7 @@ class Setting < RailsSettings::Base
default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean,
default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false)
default: ENV["MASTODON_PUBLIC_URL"].present?
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
@@ -127,7 +148,7 @@ class Setting < RailsSettings::Base
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean,
default: (ENV["MEDIAWIKI_PUBLIC_URL"].present?.to_s || false)
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
#
# Nostr
@@ -140,7 +161,7 @@ class Setting < RailsSettings::Base
#
field :remotestorage_enabled, type: :boolean,
default: (ENV["RS_STORAGE_URL"].present?.to_s || false)
default: ENV["RS_STORAGE_URL"].present?
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence

View File

@@ -2,10 +2,14 @@ class User < ApplicationRecord
include EmailValidatable
attr_accessor :display_name
attr_accessor :avatar_new
serialize :preferences, UserPreferences
#
# Relations
#
has_many :invitations, dependent: :destroy
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user
@@ -20,6 +24,10 @@ class User < ApplicationRecord
has_many :remote_storage_authorizations
#
# Validations
#
validates_uniqueness_of :cn, scope: :ou
validates_length_of :cn, minimum: 3
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
@@ -40,10 +48,20 @@ class User < ApplicationRecord
validates_uniqueness_of :nostr_pubkey, allow_blank: true
validate :acceptable_avatar
#
# Scopes
#
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :pending, -> { where(confirmed_at: nil) }
scope :all_except, -> (user) { where.not(id: user) }
#
# Encrypted database columns
#
has_encrypted :ln_login, :ln_password
# Include default devise modules. Others available are:
@@ -140,6 +158,10 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name]
end
def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn, ou: ou)
end
def services_enabled
ldap_entry[:service] || []
end
@@ -168,4 +190,17 @@ class User < ApplicationRecord
return @ldap_service if defined?(@ldap_service)
@ldap_service = LdapService.new
end
def acceptable_avatar
return unless avatar_new.present?
if avatar_new.size > 1.megabyte
errors.add(:avatar, "file size is too large")
end
acceptable_types = ["image/jpeg", "image/png"]
unless acceptable_types.include?(avatar_new.content_type)
errors.add(:avatar, "must be a JPEG or PNG file")
end
end
end

View File

@@ -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

View File

@@ -0,0 +1,2 @@
class AppCatalogManagerService < ApplicationService
end

View File

@@ -1,32 +0,0 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcPay
def initialize
@base_url = ENV["BTCPAY_API_URL"]
@store_id = Rails.application.credentials.btcpay[:store_id]
@auth_token = Rails.application.credentials.btcpay[:auth_token]
end
def onchain_wallet_balance
res = get "stores/#{@store_id}/payment-methods/onchain/BTC/wallet"
{
balance: res["balance"].to_f,
unconfirmed_balance: res["unconfirmedBalance"].to_f,
confirmed_balance: res["confirmedBalance"].to_f
}
end
private
def get(endpoint)
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{@auth_token}"
})
JSON.parse(res.body)
end
end

View File

@@ -0,0 +1,11 @@
module BtcpayManager
class FetchLightningWalletBalance < BtcpayManagerService
def call
res = get "stores/#{store_id}/lightning/BTC/balance"
{
balance: res["offchain"]["local"].to_i / 1000 # msats to sats
}
end
end
end

View File

@@ -0,0 +1,13 @@
module BtcpayManager
class FetchOnchainWalletBalance < BtcpayManagerService
def call
res = get "stores/#{store_id}/payment-methods/onchain/BTC/wallet"
{
balance: (res["balance"].to_f * 100000000).to_i, # BTC to sats
unconfirmed_balance: (res["unconfirmedBalance"].to_f * 100000000).to_i,
confirmed_balance: (res["confirmedBalance"].to_f * 100000000).to_i
}
end
end
end

View File

@@ -0,0 +1,24 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcpayManagerService < ApplicationService
attr_reader :base_url, :store_id, :auth_token
def initialize
@base_url = Setting.btcpay_api_url
@store_id = Setting.btcpay_store_id
@auth_token = Setting.btcpay_auth_token
end
private
def get(endpoint)
res = Faraday.get("#{base_url}/#{endpoint}", {}, {
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{auth_token}"
})
JSON.parse(res.body)
end
end

View File

@@ -0,0 +1,17 @@
module LdapManager
class FetchAvatar < LdapManagerService
def initialize(cn:, ou: nil)
@cn = cn
@ou = ou
end
def call
treebase = @ou ? "ou=#{@ou},cn=users,#{suffix}" : ldap_config["base"]
attributes = %w{ jpegPhoto }
filter = Net::LDAP::Filter.eq("cn", @cn)
entry = ldap_client.search(base: treebase, filter: filter, attributes: attributes).first
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
end
end
end

View File

@@ -0,0 +1,27 @@
require "image_processing/vips"
module LdapManager
class UpdateAvatar < LdapManagerService
def initialize(dn, file)
@dn = dn
@img_data = process(file)
end
def call
replace_attribute @dn, :jpegPhoto, @img_data
end
private
def process(file)
processed = ImageProcessing::Vips
.resize_to_fill(512, 512)
.source(file)
.convert("jpeg")
.saver(strip: true)
.call
Base64.strict_encode64 processed.read
end
end
end

View File

@@ -1,2 +1,5 @@
class LdapManagerService < LdapService
def suffix
@suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end
end

View File

@@ -14,7 +14,7 @@ class LndhubV2 < Lndhub
end
def create_account(payload={})
post "v2/users", payload, admin_token: Rails.application.credentials.lndhub[:admin_token]
post "v2/users", payload, admin_token: Setting.lndhub_admin_token
end
def create_invoice(payload)

7
app/services/router.rb Normal file
View File

@@ -0,0 +1,7 @@
class Router
include Rails.application.routes.url_helpers
def self.default_url_options
ActionMailer::Base.default_url_options
end
end

View File

@@ -0,0 +1,56 @@
<%= render HeaderComponent.new(title: "App Catalog") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_app_catalog') do %>
<section>
<%= 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 %>
</section>
<% if @web_apps.any? %>
<section>
<h3>Web Apps</h3>
<table class="divided mb-8">
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th class="hidden md:table-cell">RS Auths</th>
<th class="hidden md:table-cell">Created at</th>
</tr>
</thead>
<tbody>
<% @web_apps.each do |web_app| %>
<tr>
<td><%= web_app.name %></td>
<td><%= link_to web_app.url, web_app.url,
target: "_blank", rel: "nofollow noopener",
class: "ks-text-link" %></td>
<td class="hidden md:table-cell"><%= web_app.remote_storage_authorizations.count %></td>
<td class="hidden md:table-cell">
<span title="<%= web_app.created_at %>" class="cursor-help">
<%= time_ago_in_words web_app.created_at, include_seconds: false %> ago
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
<%== pagy_nav @pagy %>
</section>
<% end %>
<% end %>

View File

@@ -0,0 +1,37 @@
<h3>BTCPay Server</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :btcpay_enabled,
enabled: Setting.btcpay_enabled?,
title: "Enable BTCPay integration",
description: "BTCPay configuration present and features enabled"
) %>
<% if Setting.btcpay_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :btcpay_api_url,
title: "API URL"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :btcpay_store_id,
title: "Store ID"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :btcpay_auth_token,
type: :password,
title: "Auth Token"
) %>
</ul>
</section>
<section>
<h3>REST API</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :btcpay_publish_wallet_balances,
enabled: Setting.btcpay_publish_wallet_balances?,
title: "Publish wallet balances",
description: "Publish the store's on-chain and Lightning wallet balances"
) %>
<% end %>
</ul>

View File

@@ -9,29 +9,35 @@
) %>
<% if Setting.lndhub_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :lndhub_api_url,
title: "API URL"
) %>
<% end %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_admin_enabled,
enabled: Setting.lndhub_admin_enabled?,
title: "Enable LNDHub admin panel",
description: "LNDHub database configuration present and admin panel enabled"
) %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_keysend_enabled,
enabled: Setting.lndhub_keysend_enabled?,
title: "Enable keysend payments",
description: "Allow users to receive invoice-less payments to their Lightning Address"
) %>
<% if Setting.lndhub_keysend_enabled? %>
key: :lndhub_api_url,
title: "API URL"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :lndhub_public_key,
title: "Public key",
description: "The public key of the Lightning node used by LNDHub"
) %>
key: :lndhub_admin_token,
type: :password,
title: "Admin token",
description: "Auth token for creating new lndhub accounts"
) %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_admin_enabled,
enabled: Setting.lndhub_admin_enabled?,
title: "Enable LNDHub admin panel",
description: "LNDHub database configuration present and admin panel enabled"
) %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_keysend_enabled,
enabled: Setting.lndhub_keysend_enabled?,
title: "Enable keysend payments",
description: "Allow users to receive invoice-less payments to their Lightning Address"
) %>
<% if Setting.lndhub_keysend_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :lndhub_public_key,
title: "Public key",
description: "The public key of the Lightning node used by LNDHub"
) %>
<% end %>
<% end %>
</ul>

View File

@@ -63,6 +63,12 @@
</section>
<section class="sm:flex-1 sm:pt-0">
<% if @avatar.present? %>
<h3>LDAP<h3>
<p>
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
</p>
<% end %>
<!-- <h3>Actions</h3> -->
</section>
</div>

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe <%= custom_class %>"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>

Before

Width:  |  Height:  |  Size: 409 B

After

Width:  |  Height:  |  Size: 430 B

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" class="<%= custom_class %>" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Menu</title>
<g id="kebap-menu" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Container" x="0" y="0" width="24" height="24"></rect>
<path d="M12,6 C12.5522847,6 13,5.55228475 13,5 C13,4.44771525 12.5522847,4 12,4 C11.4477153,4 11,4.44771525 11,5 C11,5.55228475 11.4477153,6 12,6 Z" stroke="#030819" stroke-width="2" stroke-linecap="round" stroke-dasharray="0,0"></path>
<path d="M12,13 C12.5522847,13 13,12.5522847 13,12 C13,11.4477153 12.5522847,11 12,11 C11.4477153,11 11,11.4477153 11,12 C11,12.5522847 11.4477153,13 12,13 Z" stroke="#030819" stroke-width="2" stroke-linecap="round" stroke-dasharray="0,0"></path>
<path d="M12,20 C12.5522847,20 13,19.5522847 13,19 C13,18.4477153 12.5522847,18 12,18 C11.4477153,18 11,18.4477153 11,19 C11,19.5522847 11.4477153,20 12,20 Z" stroke="#030819" stroke-width="2" stroke-linecap="round" stroke-dasharray="0,0"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" class="<%= custom_class %>" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" version="1.1" viewBox="0 0 250 249.9" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-66.822 -.16484)">
<polygon id="polygon1" fill="currentColor" transform="matrix(.29308 0 0 .29308 83.528 -.028385)" points="228 181 370 100 511 181 652 263 370 425 87 263 87 263 0 213 0 213 0 311 0 378 0 427 0 476 86 525 185 582 370 689 554 582 653 525 653 590 653 592 370 754 0 542 0 640 185 747 370 853 554 747 739 640 739 525 739 476 739 427 739 378 653 427 370 589 86 427 86 361 185 418 370 524 554 418 653 361 739 311 739 213 554 107 370 0 185 107 58 180 144 230"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 848 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star <%= custom_class %>"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 360 B

View File

@@ -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 %>

View File

@@ -38,7 +38,7 @@
<h3>Chat Apps</h3>
<p>
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.
</p>
</section>

View File

@@ -1,7 +0,0 @@
<%= render HeaderComponent.new(title: "Storage") %>
<%= render MainSimpleComponent.new do %>
<section>
<h3>Feature enabled</h3>
</section>
<% end %>

View File

@@ -0,0 +1,16 @@
<%= render HeaderComponent.new(title: "Storage") %>
<%= render MainSimpleComponent.new do %>
<section>
<h3 class="mb-10">Connected Apps</h3>
<% if @rs_auths.any? %>
<div class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-10 gap-x-12">
<% @rs_auths.each do |auth| %>
<%= render RsAuthComponent.new(auth: auth) %>
<% end %>
</div>
<% else %>
<p>No apps connected yet.</p>
<% end %>
</section>
<% end %>

View File

@@ -1,33 +1,62 @@
<section>
<h3>Profile</h3>
<p class="mb-2">
<%= label :user_address, 'User address', class: 'font-bold' %>
</p>
<p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
<input type="text" id="user_address" class="grow"
value=<%= @user.address %> disabled="disabled"
data-clipboard-target="source" />
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard">
<span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
<span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
</button>
</p>
<p class="text-sm text-gray-500">
Your user address for Chat and Lightning Network.
</p>
<div class="mb-6">
<p class="mb-2">
<%= label :user_address, 'User address', class: 'font-bold' %>
</p>
<p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
<input type="text" id="user_address" class="grow"
value=<%= @user.address %> disabled="disabled"
data-clipboard-target="source" />
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard">
<span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
<span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
</button>
</p>
<p class="text-sm text-gray-500">
Your user address for Chat and Lightning Network.
</p>
</div>
<%= form_for(@user, url: setting_path(:profile), html: { :method => :put }) do |f| %>
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Display name") do %>
<%= f.text_field :display_name, class: "w-full sm:w-3/5 mb-2" %>
<%= f.text_field :display_name, class: "w-full sm:w-3/5" %>
<% if @validation_errors.present? && @validation_errors[:display_name].present? %>
<p class="error-msg"><%= @validation_errors[:display_name].first %></p>
<p class="error-msg mt-2"><%= @validation_errors[:display_name].first %></p>
<% end %>
<% end %>
<label class="block">
<p class="font-bold mb-1">
Avatar
</p>
<p class="text-gray-500">
Default profile picture
</p>
<div class="flex items-center gap-6">
<% if current_user.avatar.present? %>
<p class="flex-none">
<%= image_tag "data:image/jpeg;base64,#{current_user.avatar}", class: "h-24 w-24 rounded-lg" %>
</p>
<% end %>
<div class="grow">
<p class="mb-2">
<%= f.file_field :avatar, class: "" %>
<p class="text-sm text-gray-500">
JPEG or PNG image, not larger than 1 megabyte
</p>
<% if @validation_errors.present? && @validation_errors[:avatar].present? %>
<p class="error-msg mb-2"><%= @validation_errors[:avatar].first %></p>
<% end %>
</div>
</div>
</label>
<p class="mt-8 pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p>

View File

@@ -0,0 +1,25 @@
<%= form_for @user, url: setting_path(:remotestorage), html: { :method => :put } do |f| %>
<section>
<h3>Notifications</h3>
<ul role="list">
<%= 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 %>
</ul>
</section>
<section>
<p class="pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p>
</section>
<% end %>

View File

@@ -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) %>

View File

@@ -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
) %>

View File

@@ -1,63 +1,70 @@
<%= render SidenavLinkComponent.new(
level: 2,
name: "BTCPay",
path: admin_settings_services_path(params: { s: "btcpay" }),
text_icon: Setting.btcpay_enabled? ? "◉" : "○",
active: current_page?(admin_settings_services_path(params: { s: "btcpay" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "Discourse",
path: admin_settings_services_path(params: { s: "discourse" }),
icon: Setting.discourse_enabled? ? "check" : "x",
text_icon: Setting.discourse_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "discourse" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "Drone CI",
path: admin_settings_services_path(params: { s: "droneci" }),
icon: Setting.droneci_enabled? ? "check" : "x",
text_icon: Setting.droneci_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "droneci" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "ejabberd",
path: admin_settings_services_path(params: { s: "ejabberd" }),
icon: Setting.ejabberd_enabled? ? "check" : "x",
text_icon: Setting.ejabberd_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "ejabberd" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "Gitea",
path: admin_settings_services_path(params: { s: "gitea" }),
icon: Setting.gitea_enabled? ? "check" : "x",
text_icon: Setting.gitea_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "gitea" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "LNDHub",
path: admin_settings_services_path(params: { s: "lndhub" }),
icon: Setting.lndhub_enabled? ? "check" : "x",
text_icon: Setting.lndhub_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "lndhub" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "Mastodon",
path: admin_settings_services_path(params: { s: "mastodon" }),
icon: Setting.mastodon_enabled? ? "check" : "x",
text_icon: Setting.mastodon_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "mastodon" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "MediaWiki",
path: admin_settings_services_path(params: { s: "mediawiki" }),
icon: Setting.mediawiki_enabled? ? "check" : "x",
text_icon: Setting.mediawiki_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "mediawiki" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "Nostr",
path: admin_settings_services_path(params: { s: "nostr" }),
icon: Setting.nostr_enabled? ? "check" : "x",
text_icon: Setting.nostr_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "nostr" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "RemoteStorage",
path: admin_settings_services_path(params: { s: "remotestorage" }),
icon: Setting.remotestorage_enabled? ? "check" : "x",
text_icon: Setting.remotestorage_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "remotestorage" })),
) %>

View File

@@ -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",

4
ci/Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
# syntax=docker/dockerfile:1
FROM guildeducation/rails:2.7.2-14.20.0
RUN apt-get update -qq && apt-get install -y --no-install-recommends ldap-utils libvips

View File

@@ -5,7 +5,7 @@ require "rails"
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
# require "active_storage/engine"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"

View File

@@ -1 +1 @@
yEs5CyuAbqphlDWgtw/YQvkPn+EN4ecen2dAjs7zvYErkRRWp99FinGlQIMe6NRkMLLLSIj2BwR/wlscn1kLpIfwGpxfSZ89srK3do6Mb5QogpxdUsnQB8qv5PTGRQFBcjM47s1Q5m0t+OKxGvOnLyKnQp+cVS2KFJMbSzQarW8wIZSz2gKArn9Ttk0kqUHMlJWNY7Yh6xIrrxlEalaTOVzPdtnF7u8Tobminu15eeWHMormMRz4dYSaDc6hUtfpdy1NzOHaeXIU9A9RY/iytxuIQNgcMAlcWbPe//rVk/unH2F8xqSOfed4h/nC08F/qq4z8va3kEXBSdW/G91aIDMu1mo0kX3YNibq8s25C/CfGpzw39ozJ9erTBH7hy6nfmxU6qZuWcTGDj3NOfKe/XIfDcpOjsqkT2IOFARrYodb67q23IuOufraK1/FD4LXu8l0S8/Oi0cqMjtPPs7tS0M1C3DrbmlEzGKETrHpmoKHqjA0rgOmK4ZZM9LeI+l8Z+fDpYcCak9fLGGxnjf+nKiYMSUtm9+1dwycG2lpBV6fbmIKHJWngO2jVGcycODkc525oUaAO4hdPMqrz1AdU3AzYmLJTxW3aZ4uL5NyEJ7TbUBC0HT7h2gEi/tUry4cfD2EsM9bCrCUNuMBrnPqd4r8AvORoqqYIw1IEsP0RgWa2+hfeG1QCjBRPFHQOcqo+W25CelivMe79qI08w0iC8S4hfOQO4QrmMgtd1BhcR+wVpVE3X9EJZi3Hl7z14hXcSic+gkswJMtVZcnJL4rmZ0iEW1mpqUuegsX5vB/4qPxiQyeB80pg8Q33shvUbixzSBkl6znmLSiIffsiDsGOsnuzfl/MUT+JBs3UswNt4tSp7nEwhUjKFHrZHrAJiGCdtIS6yDPGe3HfQv1JkQ+9A8zv88hRmzeIx2JyT/shtIqGo+4ZTJd5cma--Lij/n0+cpstyZD28--FOUhwW3y+0jdaYkKvG2xrg==
tmI5vm7qZhaigr52jEBVWkRdj+EE+9OmPh3vWXC7kA/OHuuucpr7SodychuMkQDPLM0BLk88LFsqvRIR+mqnLWpRC+P9aeUFE6ohxSWzcAd7Y4sgxUD8zpCRPndrwTw0hxXXj1WZSYeWn4BoAB34aV+gYen2MajZF3a95hJGtS5yjgWxvLVkQQKqRDfykkfX6fCS0BPo5X7sT7m4xwCATD/D4219wajm5W3TIdkriHtwt28ZLspaRWA5e0UkzKf8+/Gaj2CrW7UWcvew8R93zQ5RA2/Sp3sDTVN+kLz9I9Q095lQC0ywCAEFYHeKmc2tjrzqRaAAWu06xmWLqGIg21G+A/UU9lUJOkIpxQACWoOfS2IoXR1nXhgXMopkz3aCBXDxKw554v4H2QyOceOsuRf2C685ibMqzQkKMmJ4tcbiOJL77DUc08JTjB8Dq4Ohr8sMzXbV/hATevjYoRP0XarLekqhLv90ZLuIVY16DwB0CzACeNBKeKbeLqJF51upRRWgi+gTbYpV04yUwnXdyssF8mydWocgihrTryBi8F6PsuhBGcaYdP+0yibnGxDCC4x2rupbBfMj2OIX7pYzgtIHB3Eo954Y+bCoggqbE/Qrb9VVXNMgtKgLt8EGWU2tg6wl9QicitIq87uLDAade93zTn6rmcKPywjMDo6jbVIs653ZdUhiKdHGdpnJccbgQ/iLSPB1umNnCeaEX5jM+K9zBvl7ZMCdSk1YIQ==--ekKumqLiSlVJNwMe--K/ecXmmMT1x+WnIXMbHBDw==

View File

@@ -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

View File

@@ -70,4 +70,10 @@ Rails.application.configure do
# Allow requests from any IP
config.web_console.whiny_requests = false
if ENV["S3_ENABLED"]
config.active_storage.service = :s3
else
config.active_storage.service = :local
end
end

View File

@@ -110,6 +110,12 @@ 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
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).
config.i18n.fallbacks = true

View File

@@ -51,4 +51,11 @@ Rails.application.configure do
}
config.active_job.queue_adapter = :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

View File

@@ -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
@@ -54,16 +61,22 @@ Rails.application.routes.draw do
post 'webhooks/lndhub', to: 'webhooks#lndhub'
namespace :api do
get 'kredits/onchain_btc_balance', to: 'kredits#onchain_btc_balance'
get 'btcpay/onchain_btc_balance', to: 'btcpay#onchain_btc_balance'
get 'btcpay/lightning_btc_balance', to: 'btcpay#lightning_btc_balance'
end
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']
@@ -72,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'
@@ -94,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

View File

@@ -1,7 +1,17 @@
local:
service: Disk
root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("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 %>

View File

@@ -0,0 +1,57 @@
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :key ], unique: true
end
create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[primary_key_type, foreign_key_type]
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -10,7 +10,43 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_05_23_120753) 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
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
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"
@@ -60,8 +96,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_23_120753) 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|
@@ -94,5 +132,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_23_120753) do
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
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

View File

@@ -13,6 +13,7 @@ Sidekiq::Testing.inline! do
35.times do |n|
username = Faker::Name.unique.first_name.downcase
email = Faker::Internet.unique.email
next if username.length < 3
CreateAccount.call(
username: username, domain: "kosmos.org", email: email,

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -23,7 +23,7 @@ RSpec.describe 'Admin/global settings', type: :feature do
scenario "Opening service settings shows page for first service" do
visit admin_settings_services_path
expect(current_url).to eq(admin_settings_services_url(params: { s: "discourse" }))
expect(current_url).to eq(admin_settings_services_url(params: { s: "btcpay" }))
end
scenario "View service settings" do

View File

@@ -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")
@@ -54,13 +54,32 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do
context "when signed out" do
let(:user) { create :user }
before do
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
.with(user.password).and_return(true)
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")
expect(find("#user_cn").value).to eq(user.cn)
end
it "redirects to the OAuth dialog after sign-in" do
auth_url = new_rs_oauth_url(username: user.cn,
redirect_uri: "http://example.com",
client_id: "http://example.com",
scope: "documents,[photos], contacts:r")
visit auth_url
fill_in "User", with: user.cn
fill_in "Password", with: user.password
click_button "Log in"
expect(current_url).to eq(auth_url)
end
end
end

View File

@@ -2,20 +2,19 @@ require 'rails_helper'
RSpec.describe 'Profile settings', type: :feature do
let(:user) { create :user, cn: "mwahlberg" }
let(:avatar_base64) { File.read("#{Rails.root}/spec/fixtures/files/avatar-base64.txt") }
before do
login_as user, :scope => :user
allow(user).to receive(:display_name).and_return("Mark")
allow_any_instance_of(User).to receive(:dn).and_return("cn=mwahlberg,ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, display_name: "Mark"
})
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
end
feature "Update display name" do
before do
allow(user).to receive(:display_name).and_return("Mark")
allow_any_instance_of(User).to receive(:dn).and_return("cn=mwahlberg,ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, display_name: "Mark"
})
end
scenario 'fails with validation error' do
visit setting_path(:profile)
fill_in 'Display name', with: "M"
@@ -42,4 +41,59 @@ RSpec.describe 'Profile settings', type: :feature do
end
end
end
feature "Update avatar" do
scenario "fails with validation error for wrong content type" do
visit setting_path(:profile)
attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/bitcoin.pdf"
click_button "Save"
expect(current_url).to eq(setting_url(:profile))
within ".error-msg" do
expect(page).to have_content("must be a JPEG or PNG file")
end
end
scenario "fails with validation error for file size too large" do
visit setting_path(:profile)
attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/fsociety-irc.png"
click_button "Save"
expect(current_url).to eq(setting_url(:profile))
within ".error-msg" do
expect(page).to have_content("file size is too large")
end
end
scenario 'works with valid JPG file' do
file_path = "#{Rails.root}/spec/fixtures/files/taipei.jpg"
expect_any_instance_of(LdapManager::UpdateAvatar).to receive(:replace_attribute)
.with(user.dn, :jpegPhoto, avatar_base64).and_return(true)
visit setting_path(:profile)
attach_file "Avatar", file_path
click_button "Save"
expect(current_url).to eq(setting_url(:profile))
within ".flash-msg" do
expect(page).to have_content("Settings saved")
end
end
scenario 'works with valid PNG file' do
file_path = "#{Rails.root}/spec/fixtures/files/bender.png"
expect(LdapManager::UpdateAvatar).to receive(:call).and_return(true)
visit setting_path(:profile)
attach_file "Avatar", file_path
click_button "Save"
expect(current_url).to eq(setting_url(:profile))
within ".flash-msg" do
expect(page).to have_content("Settings saved")
end
end
end
end

1
spec/fixtures/files/avatar-base64.txt vendored Normal file

File diff suppressed because one or more lines are too long

BIN
spec/fixtures/files/bender.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
spec/fixtures/files/bitcoin.pdf vendored Normal file

Binary file not shown.

BIN
spec/fixtures/files/fsociety-irc.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
spec/fixtures/files/taipei.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View File

@@ -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 }

View File

@@ -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

View File

@@ -0,0 +1,103 @@
require 'rails_helper'
require 'webmock/rspec'
RSpec.describe "/api/btcpay", type: :request do
describe "GET /onchain_btc_balance" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/payment-methods/onchain/BTC/wallet")
.to_return(status: 200, headers: {}, body: {
balance: 0.91108606,
unconfirmedBalance: 0,
confirmedBalance: 0.91108606
}.to_json)
end
it "returns a formatted result for the onchain wallet balance" do
get api_btcpay_onchain_btc_balance_path
expect(response).to have_http_status(:ok)
res = JSON.parse(response.body)
expect(res["balance"]).to eq(91108606)
expect(res["unconfirmed_balance"]).to eq(0)
expect(res["confirmed_balance"]).to eq(91108606)
end
context "upstream request error" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/payment-methods/onchain/BTC/wallet")
.to_return(status: 500, headers: {}, body: "")
end
it "returns a formatted error" do
get api_btcpay_onchain_btc_balance_path
expect(response).to have_http_status(:server_error)
res = JSON.parse(response.body)
expect(res["error"]).not_to be_nil
end
end
context "feature disabled" do
before do
Setting.btcpay_publish_wallet_balances = false
end
it "returns a 404 status" do
get api_btcpay_onchain_btc_balance_path
expect(response).to have_http_status(:not_found)
end
end
end
describe "GET /lightning_btc_balance" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/lightning/BTC/balance")
.to_return(status: 200, headers: {}, body: {
offchain: {
local: 4200000000
},
}.to_json)
end
it "returns a formatted result for the onchain wallet balance" do
get api_btcpay_lightning_btc_balance_path
expect(response).to have_http_status(:ok)
res = JSON.parse(response.body)
expect(res["balance"]).to eq(4200000)
end
context "upstream request error" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/lightning/BTC/balance")
.to_return(status: 500, headers: {}, body: "")
end
it "returns a formatted error" do
get api_btcpay_lightning_btc_balance_path
expect(response).to have_http_status(:server_error)
res = JSON.parse(response.body)
expect(res["error"]).not_to be_nil
end
end
context "feature disabled" do
before do
Setting.btcpay_publish_wallet_balances = false
end
it "returns a 404 status" do
get api_btcpay_lightning_btc_balance_path
expect(response).to have_http_status(:not_found)
end
end
end
end

View File

@@ -1,43 +0,0 @@
require 'rails_helper'
require 'webmock/rspec'
RSpec.describe "/api/kredits", type: :request do
describe "GET /onchain_btc_balance" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/payment-methods/onchain/BTC/wallet")
.to_return(status: 200, headers: {}, body: {
balance: 0.91108606,
unconfirmedBalance: 0,
confirmedBalance: 0.91108606
}.to_json)
end
it "returns a formatted result for the onchain wallet balance" do
get api_kredits_onchain_btc_balance_path
expect(response).to have_http_status(:ok)
res = JSON.parse(response.body)
expect(res["balance"]).to eq(0.91108606)
expect(res["unconfirmed_balance"]).to eq(0)
expect(res["confirmed_balance"]).to eq(0.91108606)
end
context "upstream request error" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/payment-methods/onchain/BTC/wallet")
.to_return(status: 500, headers: {}, body: "")
end
it "returns a formatted error" do
get api_kredits_onchain_btc_balance_path
expect(response).to have_http_status(:server_error)
res = JSON.parse(response.body)
expect(res["error"]).not_to be_nil
end
end
end
end

View File

@@ -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

View File

@@ -68,6 +68,7 @@ RSpec.describe "Webhooks", type: :request do
context "notification preference set to 'xmpp'" do
before do
Setting.xmpp_notifications_from_address = "botka@kosmos.org"
user.update! preferences: { lightning_notify_sats_received: "xmpp" }
post "/webhooks/lndhub", params: payload.to_json
end
@@ -78,7 +79,7 @@ RSpec.describe "Webhooks", type: :request do
msg = enqueued_jobs.first["arguments"].first
expect(msg["type"]).to eq("normal")
expect(msg["from"]).to eq("kosmos.org")
expect(msg["from"]).to eq("botka@kosmos.org")
expect(msg["to"]).to eq(user.address)
expect(msg["subject"]).to eq("Sats received!")
expect(msg["body"]).to match(/^12,300 sats received/)