Compare commits

..

1 Commits

Author SHA1 Message Date
aca13a25c3
WIP Process payments for expired invoices 2025-01-02 08:42:33 -05:00
493 changed files with 1761 additions and 3805 deletions

View File

@ -1,23 +1,6 @@
# PRIMARY_DOMAIN=kosmos.org
# AKKOUNTS_DOMAIN=accounts.example.com
# Generate this using `rails secret`
# SECRET_KEY_BASE=
# Generate these using `rails db:encryption:init`
# (Optional, needed for LndHub integration)
# ENCRYPTION_PRIMARY_KEY=
# ENCRYPTION_KEY_DERIVATION_SALT=
# The default backend is SQLite
# DB_ADAPTER=postgresql
# PG_HOST=localhost
# PG_PORT=5432
# PG_DATABASE=akkounts
# PG_DATABASE_QUEUE=akkounts_queue
# PG_USERNAME=akkounts
# PG_PASSWORD=
# SMTP_SERVER=smtp.example.com
# SMTP_PORT=587
# SMTP_LOGIN=accounts
@ -37,12 +20,8 @@
# LDAP_HOST=localhost
# LDAP_PORT=389
# LDAP_USE_TLS=false
# LDAP_UID_ATTR=cn
# LDAP_BASE="ou=kosmos.org,cn=users,dc=kosmos,dc=org"
# LDAP_ADMIN_USER="cn=Directory Manager"
# LDAP_ADMIN_PASSWORD=passthebutter
# LDAP_SUFFIX="dc=kosmos,dc=org"
# LDAP_SUFFIX='dc=kosmos,dc=org'
# REDIS_URL='redis://localhost:6379/1'

View File

@ -1,9 +1,6 @@
PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.kosmos.org
ENCRYPTION_PRIMARY_KEY=YhNLBgCFMAzw5dV3gISxnGrhNDMQwRdn
ENCRYPTION_KEY_DERIVATION_SALT=h28g16MRZ1sghF2jTCos1DiLZXUswinR
REDIS_URL='redis://localhost:6379/0'
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
@ -24,8 +21,7 @@ LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de55648
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
RS_REDIS_URL='redis://localhost:6379/1'
RS_STORAGE_URL='https://storage.kosmos.org'
RS_AKKOUNTS_DOMAIN=localhost
RS_REDIS_URL='redis://localhost:6379/1'
WEBHOOKS_ALLOWED_IPS='10.1.1.23'

4
.gitignore vendored
View File

@ -37,7 +37,6 @@
/yarn-error.log
yarn-debug.log*
.yarn-integrity
bun.lock
# Ignore local dotenv config file
.env
@ -48,6 +47,3 @@ dump.rdb
/app/assets/builds/*
!/app/assets/builds/.keep
# Ignore generated ctags
*.tags

22
Gemfile
View File

@ -2,13 +2,13 @@ source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 8.0'
gem 'rails', '~> 7.1'
# Use Puma as the app server
gem 'puma', '~> 6.6'
gem 'puma', '~> 4.1'
# View components
gem "view_component"
# Asset bundler
gem 'propshaft'
# Separate dependency since Rails 7.0
gem 'sprockets-rails'
# Allows custom JS build tasks to integrate with the asset pipeline
gem 'cssbundling-rails'
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
@ -19,12 +19,17 @@ gem "turbo-rails"
gem "stimulus-rails"
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1'
# Configuration
gem 'dotenv-rails'
# Security
gem 'lockbox'
# Authentication
gem 'warden'
gem 'devise', '~> 4.9.0'
@ -32,7 +37,6 @@ gem 'devise_ldap_authenticatable'
gem 'net-ldap'
# Utilities
gem 'aasm'
gem "image_processing", "~> 1.12.2"
gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3'
@ -42,7 +46,6 @@ gem 'flipper-active_record'
gem 'flipper-ui'
gem 'gpgme', '~> 2.0.24'
gem 'zbase32', '~> 0.1.1'
gem 'kramdown'
# HTTP requests
gem 'faraday'
@ -50,8 +53,8 @@ gem 'down'
gem 'aws-sdk-s3', require: false
# Background/scheduled jobs
gem 'solid_queue'
gem "mission_control-jobs"
gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler'
# Monitoring
gem "sentry-ruby"
@ -62,11 +65,10 @@ gem 'discourse_api'
gem "lnurl"
gem 'manifique', '~> 1.1.0'
gem 'nostr', '~> 0.6.0'
gem "redis", "~> 5.4"
group :development, :test do
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '>= 2.1'
gem 'sqlite3', '~> 1.7.2'
gem 'rspec-rails'
gem 'rails-controller-testing'
end

View File

@ -1,111 +1,110 @@
GEM
remote: https://rubygems.org/
specs:
aasm (5.5.0)
concurrent-ruby (~> 1.0)
actioncable (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
actioncable (7.1.3)
actionpack (= 7.1.3)
activesupport (= 7.1.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
actionmailer (8.0.2)
actionpack (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
actionmailbox (7.1.3)
actionpack (= 7.1.3)
activejob (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.3)
actionpack (= 7.1.3)
actionview (= 7.1.3)
activejob (= 7.1.3)
activesupport (= 7.1.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2)
actionpack (8.0.2)
actionview (= 8.0.2)
activesupport (= 8.0.2)
actionpack (7.1.3)
actionview (= 7.1.3)
activesupport (= 7.1.3)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.2)
actionpack (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
actiontext (7.1.3)
actionpack (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.2)
activesupport (= 8.0.2)
actionview (7.1.3)
activesupport (= 7.1.3)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.2)
activesupport (= 8.0.2)
activejob (7.1.3)
activesupport (= 7.1.3)
globalid (>= 0.3.6)
activemodel (8.0.2)
activesupport (= 8.0.2)
activerecord (8.0.2)
activemodel (= 8.0.2)
activesupport (= 8.0.2)
activemodel (7.1.3)
activesupport (= 7.1.3)
activerecord (7.1.3)
activemodel (= 7.1.3)
activesupport (= 7.1.3)
timeout (>= 0.4.0)
activestorage (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activesupport (= 8.0.2)
activestorage (7.1.3)
actionpack (= 7.1.3)
activejob (= 7.1.3)
activerecord (= 7.1.3)
activesupport (= 7.1.3)
marcel (~> 1.0)
activesupport (8.0.2)
activesupport (7.1.3)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.3)
aws-eventstream (1.3.2)
aws-partitions (1.1092.0)
aws-sdk-core (3.222.2)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.886.0)
aws-sdk-core (3.191.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.183.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (1.77.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.143.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
backport (1.2.0)
base64 (0.2.0)
bcrypt (3.1.20)
bech32 (1.5.0)
bech32 (1.4.2)
thor (>= 1.1.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
benchmark (0.3.0)
bigdecimal (3.1.6)
bindex (0.8.1)
bip-schnorr (0.7.0)
ecdsa_ext (~> 0.5.0)
builder (3.3.0)
builder (3.2.4)
capybara (3.40.0)
addressable
matrix
@ -115,25 +114,23 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0)
concurrent-ruby (1.3.4)
connection_pool (2.5.2)
crack (1.0.0)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
crack (0.4.6)
bigdecimal
rexml
crass (1.0.6)
cssbundling-rails (1.4.3)
cssbundling-rails (1.4.0)
railties (>= 6.0.0)
database_cleaner (2.1.0)
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.2.0)
database_cleaner-active_record (2.1.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.4.1)
devise (4.9.4)
date (3.3.4)
devise (4.9.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@ -142,112 +139,107 @@ GEM
devise_ldap_authenticatable (0.8.7)
devise (>= 3.4.1)
net-ldap (>= 0.16.0)
diff-lcs (1.6.1)
diff-lcs (1.5.1)
discourse_api (2.0.1)
faraday (~> 2.7)
faraday-follow_redirects
faraday-multipart
rack (>= 1.6)
dotenv (3.1.8)
dotenv-rails (3.1.8)
dotenv (= 3.1.8)
railties (>= 6.1)
down (5.4.2)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
down (5.4.1)
addressable (~> 2.8)
drb (2.2.1)
drb (2.2.0)
ruby2_keywords
e2mmap (0.1.0)
ecdsa (1.2.0)
ecdsa_ext (0.5.1)
ecdsa (~> 1.2.0)
erubi (1.13.1)
et-orbi (1.2.11)
erubi (1.12.0)
et-orbi (1.2.7)
tzinfo
event_emitter (0.2.6)
eventmachine (1.2.7)
factory_bot (6.5.1)
activesupport (>= 6.1.0)
factory_bot_rails (6.4.4)
factory_bot (~> 6.5)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
faker (3.5.1)
faker (3.2.3)
i18n (>= 1.8.11, < 2)
faraday (2.9.2)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (3.1.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.1.0)
net-http
faye-websocket (0.11.3)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.17.2)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
flipper (1.3.4)
ffi (1.16.3)
flipper (1.2.2)
concurrent-ruby (< 2)
flipper-active_record (1.3.4)
activerecord (>= 4.2, < 9)
flipper (~> 1.3.4)
flipper-ui (1.3.4)
flipper-active_record (1.2.2)
activerecord (>= 4.2, < 8)
flipper (~> 1.2.2)
flipper-ui (1.2.2)
erubi (>= 1.0.0, < 2.0.0)
flipper (~> 1.3.4)
flipper (~> 1.2.2)
rack (>= 1.4, < 4)
rack-protection (>= 1.5.3, < 5.0.0)
rack-session (>= 1.0.2, < 3.0.0)
sanitize (< 8)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
rack-protection (>= 1.5.3, <= 4.0.0)
sanitize (< 7)
fugit (1.9.0)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
gpgme (2.0.24)
mini_portile2 (~> 2.7)
hashdiff (1.1.2)
i18n (1.14.7)
hashdiff (1.1.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (2.1.0)
importmap-rails (2.0.1)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.8.0)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
io-console (0.7.2)
irb (1.11.1)
rdoc
reline (>= 0.4.2)
jaro_winkler (1.6.0)
jbuilder (2.13.0)
jaro_winkler (1.5.6)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jmespath (1.6.2)
json (2.11.3)
kramdown (2.5.1)
rexml (>= 3.3.9)
json (2.7.1)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.4)
launchy (3.1.1)
language_server-protocol (3.17.0.3)
launchy (2.5.2)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (3.0.0)
actionmailer (>= 6.1)
letter_opener (~> 1.9)
railties (>= 6.1)
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
letter_opener_web (2.0.0)
actionmailer (>= 5.2)
letter_opener (~> 1.7)
railties (>= 5.2)
rexml
lint_roller (1.1.0)
listen (3.9.0)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
lnurl (1.1.1)
lnurl (1.1.0)
bech32 (~> 1.1)
logger (1.7.0)
loofah (2.24.0)
lockbox (1.3.2)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@ -259,27 +251,18 @@ GEM
faraday (~> 2.9.0)
faraday-follow_redirects (= 0.3.0)
nokogiri (~> 1.16.0)
marcel (1.0.4)
marcel (1.0.2)
matrix (0.4.2)
method_source (1.1.0)
mini_magick (4.13.2)
method_source (1.0.0)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.8)
minitest (5.25.5)
mission_control-jobs (1.0.2)
actioncable (>= 7.1)
actionpack (>= 7.1)
activejob (>= 7.1)
activerecord (>= 7.1)
importmap-rails (>= 1.2.1)
irb (~> 1.13)
railties (>= 7.1)
stimulus-rails
turbo-rails
multipart-post (2.4.1)
net-http (0.6.0)
mini_portile2 (2.8.5)
minitest (5.21.2)
multipart-post (2.3.0)
mutex_m (0.2.0)
net-http (0.4.1)
uri
net-imap (0.5.7)
net-imap (0.4.9.1)
date
net-protocol
net-ldap (0.19.0)
@ -287,15 +270,15 @@ GEM
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.1)
net-smtp (0.4.0.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.16.8)
nio4r (2.7.0)
nokogiri (1.16.0)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.8-arm64-darwin)
nokogiri (1.16.0-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.8-x86_64-linux)
nokogiri (1.16.0-x86_64-linux)
racc (~> 1.4)
nostr (0.6.0)
bech32 (~> 1.4)
@ -304,57 +287,45 @@ GEM
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
json (~> 2.6)
observer (0.1.2)
orm_adapter (0.5.0)
ostruct (0.6.1)
pagy (6.5.0)
parallel (1.27.0)
parser (3.3.8.0)
pagy (6.4.3)
parallel (1.24.0)
parser (3.3.0.5)
ast (~> 2.4.1)
racc
pg (1.5.9)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
prism (1.4.0)
propshaft (1.1.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
railties (>= 7.0.0)
psych (5.2.3)
date
pg (1.5.4)
psych (5.1.2)
stringio
public_suffix (6.0.1)
puma (6.6.0)
public_suffix (5.0.4)
puma (4.3.12)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.13)
racc (1.7.3)
rack (2.2.8)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-session (1.0.2)
rack (< 3)
rack-test (2.2.0)
rack-test (2.1.0)
rack (>= 1.3)
rackup (1.0.1)
rackup (1.0.0)
rack (< 3)
webrick
rails (8.0.2)
actioncable (= 8.0.2)
actionmailbox (= 8.0.2)
actionmailer (= 8.0.2)
actionpack (= 8.0.2)
actiontext (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activemodel (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
rails (7.1.3)
actioncable (= 7.1.3)
actionmailbox (= 7.1.3)
actionmailer (= 7.1.3)
actionpack (= 7.1.3)
actiontext (= 7.1.3)
actionview (= 7.1.3)
activejob (= 7.1.3)
activemodel (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
bundler (>= 1.15.0)
railties (= 8.0.2)
railties (= 7.1.3)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@ -363,140 +334,138 @@ GEM
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.2)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
nokogiri (~> 1.14)
rails-settings-cached (2.8.3)
activerecord (>= 5.0.0)
railties (>= 5.0.0)
railties (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
irb (~> 1.13)
railties (7.1.3)
actionpack (= 7.1.3)
activesupport (= 7.1.3)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rake (13.1.0)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbs (3.9.2)
logger
rdoc (6.13.1)
rbs (2.8.4)
rdoc (6.6.2)
psych (>= 4.0.0)
redis (5.4.0)
redis-client (>= 0.22.0)
redis-client (0.24.0)
connection_pool
regexp_parser (2.10.0)
reline (0.6.1)
redis (4.8.1)
regexp_parser (2.9.0)
reline (0.4.2)
io-console (~> 0.5)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
reverse_markdown (3.0.0)
reverse_markdown (2.1.1)
nokogiri
rexml (3.4.1)
rexml (3.2.6)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec-core (3.13.3)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.2)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.1.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.2)
rubocop (1.75.3)
rspec-support (~> 3.12.0)
rspec-rails (6.1.1)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.1)
rubocop (1.60.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.30.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.44.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
ruby-progressbar (1.13.0)
ruby-vips (2.2.3)
ruby-vips (2.2.0)
ffi (~> 1.12)
logger
sanitize (7.0.0)
ruby2_keywords (0.0.5)
rufus-scheduler (3.9.1)
fugit (~> 1.1, >= 1.1.6)
sanitize (6.1.0)
crass (~> 1.0.2)
nokogiri (>= 1.16.8)
securerandom (0.4.1)
sentry-rails (5.23.0)
nokogiri (>= 1.12.0)
sentry-rails (5.16.1)
railties (>= 5.0)
sentry-ruby (~> 5.23.0)
sentry-ruby (5.23.0)
bigdecimal
sentry-ruby (~> 5.16.1)
sentry-ruby (5.16.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
solargraph (0.54.2)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
sidekiq-scheduler (5.0.3)
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0)
solargraph (0.50.0)
backport (~> 1.2)
benchmark (~> 0.4)
benchmark
bundler (~> 2.0)
diff-lcs (~> 1.4)
jaro_winkler (~> 1.6)
e2mmap
jaro_winkler (~> 1.5)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1)
logger (~> 1.6)
observer (~> 0.1)
ostruct (~> 0.6)
parser (~> 3.0)
rbs (~> 3.3)
reverse_markdown (~> 3.0)
rbs (~> 2.0)
reverse_markdown (~> 2.0)
rubocop (~> 1.38)
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
yard-solargraph (~> 0.1)
solid_queue (1.1.5)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11.0)
railties (>= 7.1)
thor (~> 1.3.1)
sqlite3 (2.6.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.7.2)
mini_portile2 (~> 2.8.0)
sqlite3 (2.6.0-arm64-darwin)
sqlite3 (2.6.0-x86_64-linux-gnu)
stimulus-rails (1.3.4)
sqlite3 (1.7.2-arm64-darwin)
sqlite3 (1.7.2-x86_64-linux)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
stringio (3.1.0)
thor (1.3.0)
tilt (2.3.0)
timeout (0.4.1)
turbo-rails (1.5.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
stringio (3.1.7)
thor (1.3.2)
tilt (2.6.0)
timeout (0.4.3)
turbo-rails (2.0.13)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
useragent (0.16.11)
view_component (3.22.0)
activesupport (>= 5.2.0, < 8.1)
concurrent-ruby (= 1.3.4)
unicode-display_width (2.5.0)
uri (0.13.0)
view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
warden (1.2.9)
rack (>= 2.0.9)
@ -505,22 +474,19 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.25.1)
webmock (3.19.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket-driver (0.7.7)
base64
webrick (1.8.1)
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.37)
yard-solargraph (0.1.0)
yard (~> 0.9)
yard (0.9.34)
zbase32 (0.1.1)
zeitwerk (2.7.2)
zeitwerk (2.6.12)
PLATFORMS
arm64-darwin-22
@ -528,7 +494,6 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
aasm
aws-sdk-s3
bcrypt (~> 3.1)
capybara
@ -549,30 +514,29 @@ DEPENDENCIES
image_processing (~> 1.12.2)
importmap-rails
jbuilder (~> 2.7)
kramdown
letter_opener
letter_opener_web
listen (~> 3.2)
lnurl
lockbox
manifique (~> 1.1.0)
mission_control-jobs
net-ldap
nostr (~> 0.6.0)
pagy (~> 6.0, >= 6.0.2)
pg (~> 1.5)
propshaft
puma (~> 6.6)
rails (~> 8.0)
puma (~> 4.1)
rails (~> 7.1)
rails-controller-testing
rails-settings-cached (~> 2.8.3)
redis (~> 5.4)
rqrcode (~> 2.0)
rspec-rails
sentry-rails
sentry-ruby
sidekiq (< 7)
sidekiq-scheduler
solargraph
solid_queue
sqlite3 (>= 2.1)
sprockets-rails
sqlite3 (~> 1.7.2)
stimulus-rails
turbo-rails
tzinfo-data

View File

@ -57,7 +57,7 @@ Running the test suite:
Running the test suite with Docker Compose requires overriding the Rails
environment:
docker-compose exec -e "RAILS_ENV=test" web rspec
docker-compose run -e "RAILS_ENV=test" web rspec
### Docker Compose
@ -128,7 +128,6 @@ command:
### Front-end
* [Icons](https://feathericons.com)
* [Tailwind CSS](https://tailwindcss.com/)
* [Sass](https://sass-lang.com/documentation)
* [Stimulus](https://stimulus.hotwired.dev/handbook/)

View File

@ -0,0 +1,4 @@
//= link_tree ../images
//= link_tree ../../javascript .js
//= link_tree ../builds
//= link_tree ../../../vendor/javascript .js

View File

@ -7,6 +7,7 @@
@import "components/buttons";
@import "components/dashboard_services";
@import "components/forms";
@import "components/links";
@import "components/notifications";
@import "components/pagination";
@import "components/tables";

View File

@ -6,7 +6,6 @@
body {
@apply leading-none bg-cover bg-fixed;
background-image: linear-gradient(35deg, rgba(255,0,255,0.2) 0, rgba(13,79,153,0.8) 100%), url('/img/bg-1.jpg');
color: black;
}
body#admin {
@ -33,10 +32,6 @@
@apply pt-8 sm:pt-12;
}
main section h3:not(:first-child) {
@apply mt-8;
}
main section:first-of-type {
@apply pt-0;
}
@ -60,11 +55,4 @@
main ul li {
@apply leading-6;
}
main a:not(nav > *) {
@apply text-blue-600;
&:hover { @apply underline; }
&:visited { @apply text-indigo-600; }
&:active { @apply text-red-600; }
}
}

View File

@ -1,15 +1,5 @@
@layer components {
.btn-text-dark { @apply text-black; }
.btn-text-dark:hover { @apply text-black no-underline; }
.btn-text-dark:visited { @apply text-black; }
.btn-text-dark:active { @apply text-black; }
.btn-text-light { @apply text-white; }
.btn-text-light:hover { @apply text-white no-underline; }
.btn-text-light:visited { @apply text-white; }
.btn-text-light:active { @apply text-white; }
.btn {
@apply btn-text-dark;
@apply inline-block font-semibold rounded-md leading-none cursor-pointer text-center
transition-colors duration-75 focus:outline-none focus:ring-4;
}
@ -38,20 +28,17 @@
}
.btn-blue {
@apply btn-text-light;
@apply bg-blue-500 hover:bg-blue-600
@apply bg-blue-500 hover:bg-blue-600 text-white
focus:ring-blue-400 focus:ring-opacity-75;
}
.btn-emerald {
@apply btn-text-light;
@apply bg-emerald-500 hover:bg-emerald-600
@apply bg-emerald-500 hover:bg-emerald-600 text-white
focus:ring-emerald-400 focus:ring-opacity-75;
}
.btn-red {
@apply btn-text-light;
@apply bg-red-600 hover:bg-red-700
@apply bg-red-600 hover:bg-red-700 text-white
focus:ring-red-500 focus:ring-opacity-75;
}

View File

@ -0,0 +1,8 @@
@layer components {
.ks-text-link {
@apply text-blue-600;
&:hover { @apply underline; }
&:visited { @apply text-indigo-600; }
&:active { @apply text-red-600; }
}
}

View File

@ -34,7 +34,7 @@
.pagy-nav .page a, .page.gap {
@apply bg-white border-gray-300 text-gray-500 hover:bg-gray-100 relative
inline-flex items-center border px-4 py-2 text-sm font-medium
no-underline focus:z-20;
focus:z-20;
}
.pagy-nav .page.active {

View File

@ -2,8 +2,6 @@
module AppCatalog
class WebAppIconComponent < ViewComponent::Base
include ApplicationHelper
def initialize(web_app:)
if web_app&.icon&.attached?
@image_url = image_url_for(web_app.icon)
@ -11,5 +9,13 @@ module AppCatalog
@image_url = image_url_for(web_app.apple_touch_icon)
end
end
def image_url_for(attachment)
if Setting.s3_enabled?
s3_image_url(attachment)
else
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
end
end
end
end

View File

@ -1,4 +1,4 @@
<%= link_to @href, class: @class, target: @target, data: {
<%= link_to @href, class: @class, data: {
'dropdown-target': "menuItem",
'action': "keydown.up->dropdown#previousItem:prevent keydown.down->dropdown#nextItem:prevent"
} do %>

View File

@ -1,9 +1,8 @@
# frozen_string_literal: true
class DropdownLinkComponent < ViewComponent::Base
def initialize(href:, open_in_new_tab: false, separator: false, add_class: nil)
def initialize(href:, separator: false, add_class: nil)
@href = href
@target = open_in_new_tab ? "_blank" : nil
@class = class_str(separator, add_class)
end

View File

@ -1,30 +0,0 @@
<div class="inline-block text-left" data-controller="modal" data-action="keydown.esc->modal#close">
<button class="btn-md btn-outline text-red-600" data-action="click->modal#open" title="Edit">
<%= content || "Edit" %>
</button>
<%= render ModalComponent.new(show_close_button: false) do %>
<%= form_with model: [:admin, @editable_content],
html: { autocomplete: "off" } do |form| %>
<%= form.hidden_field :redirect_to, value: @redirect_to %>
<p class="mb-2">
<%= form.label :content, @editable_content.key.capitalize, class: 'font-bold' %>
</p>
<% if @editable_content.rich_text %>
<p>
<%= form.textarea :content, class: "md:w-[56rem] md:h-[28rem]" %>
</p>
<p class="text-right">
<%= form.submit "Save", class: "ml-2 btn-md btn-blue" %>
</p>
<% else %>
<p class="">
<%= form.text_field :content, class: "w-80" %>
</p>
<p>
<%= form.submit "Save", class: "btn-md btn-blue w-full" %>
</p>
<% end %>
<% end %>
<% end %>
</div>

View File

@ -1,6 +0,0 @@
class EditContentButtonComponent < ViewComponent::Base
def initialize(context:, key:, rich_text: false, redirect_to: nil)
@editable_content = EditableContent.find_or_create_by(context:, key:, rich_text:)
@redirect_to = redirect_to
end
end

View File

@ -1,9 +0,0 @@
<% if @editable_content.has_content? %>
<% if @editable_content.rich_text %>
<%= helpers.markdown_to_html @editable_content.content %>
<% else %>
<%= @editable_content.content %>
<% end %>
<% else %>
<%= @default %>
<% end %>

View File

@ -1,6 +0,0 @@
class EditableContentComponent < ViewComponent::Base
def initialize(context:, key:, rich_text: false, default: nil)
@editable_content = EditableContent.find_or_create_by(context:, key:, rich_text:)
@default = default
end
end

View File

@ -1,4 +1,4 @@
<main class="w-full max-w-xl mx-auto px-4 sm:px-6 lg:px-8">
<main class="w-full max-w-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
<div class="bg-white rounded-lg shadow px-6 sm:px-12 py-8 sm:py-12">
<%= content %>
</div>

View File

@ -1,4 +1,4 @@
<main class="w-full max-w-6xl mx-auto px-4 md:px-6 lg:px-8">
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
<div class="md:min-h-[50vh] bg-white rounded-lg shadow px-6 sm:px-12 py-8 sm:py-12">
<%= content %>
</div>

View File

@ -1,4 +1,4 @@
<main class="w-full max-w-6xl mx-auto px-4 md:px-6 lg:px-8">
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
<div class="bg-white rounded-lg shadow">
<div class="md:min-h-[50vh] divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<aside class="py-6 sm:py-8 lg:col-span-3">

View File

@ -1,4 +1,4 @@
<main class="w-full max-w-6xl mx-auto px-4 md:px-6 lg:px-8">
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
<div class="md:min-h-[50vh] bg-white rounded-lg shadow">
<div class="px-6 sm:px-12 pt-2 sm:pt-4">
<%= render partial: @tabnav_partial %>

View File

@ -12,8 +12,7 @@
</div>
<%= render DropdownComponent.new do %>
<%= render DropdownLinkComponent.new(
href: launch_app_services_storage_rs_auth_url(@auth),
open_in_new_tab: true
href: launch_app_services_storage_rs_auth_url(@auth)
) do %>
Launch app
<% end %>

View File

@ -29,7 +29,7 @@ class SidenavLinkComponent < ViewComponent::Base
def class_names_icon(path)
if @active
"text-teal-600 group-hover:text-teal-600 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
"text-teal-500 group-hover:text-teal-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
elsif @disabled
"text-gray-300 group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
else

View File

@ -4,22 +4,11 @@ class Admin::DonationsController < Admin::BaseController
# GET /donations
def index
@username = params[:username].presence
pending_scope = Donation.incomplete.joins(:user).order('paid_at desc')
completed_scope = Donation.completed.joins(:user).order('paid_at desc')
if @username
pending_scope = pending_scope.where(users: { cn: @username })
completed_scope = completed_scope.where(users: { cn: @username })
end
@pending_donations = pending_scope
@pagy, @donations = pagy(completed_scope)
@pagy, @donations = pagy(Donation.completed.order('paid_at desc'))
@stats = {
overall_sats: completed_scope.sum("amount_sats"),
donor_count: completed_scope.distinct.count(:user_id)
overall_sats: @donations.sum("amount_sats"),
donor_count: Donation.completed.count(:user_id)
}
end

View File

@ -1,45 +0,0 @@
class Admin::EditableContentsController < Admin::BaseController
before_action :set_content, only: [:show, :edit, :update]
before_action :set_current_section, only: [:index, :show, :edit]
def index
@path = params[:path].presence
scope = EditableContent.order(path: :asc)
scope = scope.where(path: @path) if @path
@pagy, @contents = pagy(scope)
end
def show
end
def edit
end
def update
return_to = params[:editable_content][:redirect_to].presence
if @editable_content.update(content_params)
if return_to
redirect_to return_to
else
render status: :ok
end
else
render :edit, status: :unprocessable_entity
end
end
private
def set_content
@editable_content = EditableContent.find(params[:id])
end
def content_params
params.require(:editable_content).permit(:path, :key, :lang, :content, :rich_text)
end
def set_current_section
@current_section = :content
end
end

View File

@ -1,28 +1,12 @@
class Admin::InvitationsController < Admin::BaseController
before_action :set_current_section
def index
@username = params[:username].presence
accepted_scope = Invitation.used.order('used_at desc')
unused_scope = Invitation.unused
if @username
accepted_scope = accepted_scope.joins(:user).where(users: { cn: @username })
unused_scope = unused_scope.joins(:user).where(users: { cn: @username })
end
@pagy, @invitations_used = pagy(accepted_scope)
@current_section = :invitations
@pagy, @invitations_used = pagy(Invitation.used.order('used_at desc'))
@stats = {
available: unused_scope.count,
accepted: accepted_scope.count,
users_with_referrals: accepted_scope.distinct.count(:user_id)
available: Invitation.unused.count,
accepted: @invitations_used.length,
users_with_referrals: Invitation.used.distinct.count(:user_id)
}
end
private
def set_current_section
@current_section = :invitations
end
end

View File

@ -4,7 +4,7 @@ class Admin::LightningController < Admin::BaseController
def index
@current_section = :lightning
@users = User.pluck(:cn, :ou, :lndhub_username)
@users = User.pluck(:cn, :ou, :ln_account)
@accounts = LndhubAccount.with_balances.order(balance: :desc).to_a
@ln = {}

View File

@ -1,23 +0,0 @@
class Admin::Settings::MembershipController < Admin::SettingsController
def show
end
def update
update_settings
redirect_to admin_settings_membership_path, flash: {
success: "Settings saved"
}
end
private
def setting_params
params.require(:setting).permit([
:member_status_contributor,
:member_status_sustainer,
:user_index_show_contributors,
:user_index_show_sustainers
])
end
end

View File

@ -5,36 +5,24 @@ class Admin::UsersController < Admin::BaseController
# GET /admin/users
def index
ldap = LdapService.new
ou = Setting.primary_domain
@show_contributors = Setting.user_index_show_contributors
@show_sustainers = Setting.user_index_show_sustainers
@contributors = ldap.search_users(:memberStatus, :contributor, :cn) if @show_contributors
@sustainers = ldap.search_users(:memberStatus, :sustainer, :cn) if @show_sustainers
@admins = ldap.search_users(:admin, true, :cn)
@pagy, @users = pagy(User.where(ou: ou).order(cn: :asc))
@ou = Setting.primary_domain
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
@stats = {
users_confirmed: User.where(ou: ou).confirmed.count,
users_pending: User.where(ou: ou).pending.count
users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: @ou).pending.count
}
@stats[:users_contributing] = @contributors.size if @show_contributors
@stats[:users_paying] = @sustainers.size if @show_sustainers
end
# GET /admin/users/:username
def show
@invitees = @user.invitees
@recent_invitees = @user.invitees.order(created_at: :desc).limit(5)
@more_invitees = (@invitees - @recent_invitees).count
if Setting.lndhub_admin_enabled?
@lndhub_user = @user.lndhub_user
end
@services_enabled = @user.services_enabled
@ldap_avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
end
# POST /admin/users/:username/invitations

View File

@ -1,27 +0,0 @@
class AvatarsController < ApplicationController
def show
if user = User.find_by(cn: params[:username])
http_status :not_found and return unless user.avatar.attached?
sha256_hash = params[:hash]
format = params[:format]&.to_sym || :png
# size = params[:size]&.to_sym || :original
unless user.avatar.filename.to_s == "#{sha256_hash}.#{format}"
http_status :not_found and return
end
# TODO See note for avatar_variant in user model
# blob = if size == :original
# user.avatar.blob
# else
# user.avatar_variant(size: size)&.blob
# end
data = user.avatar.blob.download
send_data data, type: "image/#{format}", disposition: "inline"
else
http_status :not_found
end
end
end

View File

@ -11,7 +11,7 @@ class Contributions::DonationsController < ApplicationController
def index
@current_section = :contributions
@donations_completed = current_user.donations.completed.order('paid_at desc')
@donations_processing = current_user.donations.processing.order('created_at desc')
@donations_pending = current_user.donations.processing.order('created_at desc')
if Setting.lndhub_enabled?
begin
@ -81,21 +81,29 @@ class Contributions::DonationsController < ApplicationController
case invoice["status"]
when "Settled"
@donation.complete!
flash_message = { success: "Thank you!" }
@donation.paid_at = DateTime.now
@donation.payment_status = "settled"
@donation.save!
msg = { success: "Thank you!" }
when "Processing"
unless @donation.processing?
@donation.start_processing!
flash_message = { success: "Thank you! We will send you an email when the payment is confirmed." }
@donation.payment_status = "processing"
@donation.save!
msg = { success: "Thank you! We will send you an email when the payment is confirmed." }
BtcpayCheckDonationJob.set(wait: 20.seconds).perform_later(@donation)
end
when "Expired"
flash_message = { warning: "The payment request for this donation has expired" }
if invoice["additionalStatus"] &&
invoice["additionalStatus"] == "PaidLate"
# TODO introduce state machine
mark_as_paid(donation)
end
msg = { warning: "The payment request for this donation has expired" }
else
flash_message = { warning: "Could not determine status of payment" }
msg = { warning: "Could not determine status of payment" }
end
redirect_to contributions_donations_url, flash: flash_message
redirect_to contributions_donations_url, flash: msg
end
private

View File

@ -1,16 +0,0 @@
class Contributions::OtherController < ApplicationController
before_action :authenticate_user!
before_action :set_content_editing
# GET /contributions/other
def index
@current_section = :contributions
end
private
def set_content_editing
return unless params[:edit] && current_user.is_admin?
@edit_content = true
end
end

View File

@ -0,0 +1,8 @@
class Contributions::ProjectsController < ApplicationController
before_action :authenticate_user!
# GET /contributions
def index
@current_section = :contributions
end
end

View File

@ -8,9 +8,6 @@ class Discourse::SsoController < ApplicationController
sso.email = current_user.email
sso.username = current_user.cn
sso.name = current_user.display_name
if current_user.avatar.attached?
sso.avatar_url = helpers.image_url_for(current_user.avatar)
end
sso.admin = current_user.is_admin?
sso.sso_secret = secret

View File

@ -37,7 +37,7 @@ class LnurlpayController < ApplicationController
pubkey: Setting.lndhub_public_key,
customData: [{
customKey: "696969",
customValue: @user.lndhub_username
customValue: @user.ln_account
}]
}
end

View File

@ -1,9 +0,0 @@
class PagesController < ApplicationController
def privacy
@current_section = :privacy
end
def tos
@current_section = :tos
end
end

View File

@ -9,7 +9,7 @@ class Services::LightningController < ApplicationController
before_action :lndhub_fetch_balance
def index
@wallet_setup_url = "lndhub://#{current_user.lndhub_username}:#{current_user.lndhub_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
end
def transactions

View File

@ -23,11 +23,7 @@ class Services::RsAuthsController < Services::BaseController
end
def launch_app
user_address = Rails.env.development? ?
"#{current_user.cn}@localhost:3000" :
current_user.address
launch_url = "#{@auth.launch_url}#remotestorage=#{user_address}"
launch_url = "#{@auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{@auth.token}"
redirect_to launch_url, allow_other_host: true
end

View File

@ -25,7 +25,7 @@ class SettingsController < ApplicationController
def update
@user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name]
@user.avatar_new = user_params[:avatar_new]
@user.avatar_new = user_params[:avatar]
@user.pgp_pubkey = user_params[:pgp_pubkey]
if @user.save
@ -34,12 +34,7 @@ class SettingsController < ApplicationController
end
if @user.avatar_new.present?
if store_user_avatar
UserManager::UpdateAvatar.call(user: @user)
else
@validation_errors = @user.errors
render :show, status: :unprocessable_entity and return
end
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
end
if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key])
@ -167,7 +162,7 @@ class SettingsController < ApplicationController
def user_params
params.require(:user).permit(
:display_name, :avatar_new, :pgp_pubkey,
:display_name, :avatar, :pgp_pubkey,
preferences: UserPreferences.pref_keys
)
end
@ -189,30 +184,4 @@ class SettingsController < ApplicationController
salt = BCrypt::Engine.generate_salt
BCrypt::Engine.hash_secret(password, salt)
end
def store_user_avatar
io = @user.avatar_new.tempfile
img_data = UserManager::ProcessAvatar.call(io: io)
if img_data.blank?
@user.errors.add(:avatar, "failed to process file")
false
end
tempfile = Tempfile.create
tempfile.binmode
tempfile.write(img_data)
tempfile.rewind
hash = Digest::SHA256.hexdigest(img_data)
ext = @user.avatar_new.content_type == "image/png" ? "png" : "jpg"
filename = "#{hash}.#{ext}"
if filename == @user.avatar.filename.to_s
@user.errors.add(:avatar, "must be a new file/picture")
false
else
key = "users/#{@user.cn}/avatars/#{filename}"
@user.avatar.attach io: tempfile, key: key, filename: filename
@user.save
end
end
end

View File

@ -1,16 +1,8 @@
class WebKeyDirectoryController < WellKnownController
before_action :allow_cross_origin_requests
# /.well-known/openpgpkey/hu/:hashed_username(.txt)?l=username
# /.well-known/openpgpkey/hu/:hashed_username(.txt)
def show
if params[:l].blank?
# TODO store hashed username in db if existing implementations trigger
# this a lot
msg = "WKD request with \"l\" param omitted for hu: #{params[:hashed_username]}"
Sentry.capture_message(msg) if Setting.sentry_enabled?
http_status :bad_request and return
end
@user = User.find_by(cn: params[:l].downcase)
if @user.nil? ||

View File

@ -33,10 +33,6 @@ class WebfingerController < WellKnownController
links: []
}
if @user.avatar.attached?
jrd[:links] += avatar_link
end
if Setting.mastodon_enabled && @user.service_enabled?(:mastodon)
# https://docs.joinmastodon.org/spec/webfinger/
jrd[:aliases] += mastodon_aliases
@ -51,16 +47,6 @@ class WebfingerController < WellKnownController
jrd
end
def avatar_link
[
{
rel: "http://webfinger.net/rel/avatar",
type: @user.avatar.content_type,
href: helpers.image_url_for(@user.avatar)
}
]
end
def mastodon_aliases
[
"#{Setting.mastodon_public_url}/@#{@user.cn}",
@ -88,7 +74,7 @@ class WebfingerController < WellKnownController
end
def remotestorage_link
auth_url = new_rs_oauth_url(@username, host: Setting.rs_accounts_domain)
auth_url = new_rs_oauth_url(@username, host: Setting.accounts_domain)
storage_url = "#{Setting.rs_storage_url}/#{@username}"
{

View File

@ -5,7 +5,7 @@ class WebhooksController < ApplicationController
before_action :process_payload
def lndhub
@user = User.find_by!(lndhub_username: @payload[:user_login])
@user = User.find_by!(ln_account: @payload[:user_login])
if @zap = @user.zaps.find_by(payment_request: @payload[:payment_request])
settled_at = Time.parse(@payload[:settled_at])

View File

@ -14,23 +14,4 @@ module ApplicationHelper
def badge(text, color)
tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800"
end
def markdown_to_html(string)
raw Kramdown::Document.new(string, { input: "GFM" }).to_html
end
def image_url_for(attachment)
return s3_image_url(attachment) if Setting.s3_enabled?
if attachment.record.is_a?(User) && attachment.name == "avatar"
hash, format = attachment.blob.filename.to_s.split(".", 2)
user_avatar_url(
username: attachment.record.cn,
hash: hash,
format: format
)
else
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
end
end
end

View File

@ -9,18 +9,29 @@ class BtcpayCheckDonationJob < ApplicationJob
)
case invoice["status"]
when "Settled"
donation.complete!
NotificationMailer.with(user: donation.user)
.bitcoin_donation_confirmed
.deliver_later
when "Processing"
re_enqueue_job(donation)
when "Settled"
mark_as_paid(donation)
when "Expired"
if invoice["additionalStatus"] &&
invoice["additionalStatus"] == "PaidLate"
mark_as_paid(donation)
end
end
end
def re_enqueue_job(donation)
self.class.set(wait: 20.seconds).perform_later(donation)
end
def mark_as_paid(donation)
donation.paid_at = DateTime.now
donation.payment_status = "settled"
donation.save!
NotificationMailer.with(user: donation.user)
.bitcoin_donation_confirmed
.deliver_later
end
end

View File

@ -4,7 +4,7 @@ class CreateLdapUserJob < ApplicationJob
def perform(username:, domain:, email:, hashed_pw:, confirmed: false)
dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org"
attr = {
objectclass: ["top", "account", "person", "inetOrgPerson", "extensibleObject"],
objectclass: ["top", "account", "person", "extensibleObject"],
cn: username,
sn: username,
uid: username,

View File

@ -2,12 +2,12 @@ class CreateLndhubAccountJob < ApplicationJob
queue_as :default
def perform(user)
return if user.lndhub_username.present? && user.lndhub_password.present?
return if user.ln_account.present? && user.ln_password.present?
lndhub = LndhubV2.new
credentials = lndhub.create_account
user.update! lndhub_username: credentials["login"],
lndhub_password: credentials["password"]
user.update! ln_account: credentials["login"],
ln_password: credentials["password"]
end
end

View File

@ -3,6 +3,8 @@ class RemoteStorageExpireAuthorizationJob < ApplicationJob
def perform(rs_auth_id)
rs_auth = RemoteStorageAuthorization.find rs_auth_id
return unless rs_auth.expire_at.nil? || rs_auth.expire_at <= DateTime.now
rs_auth.destroy!
end
end

View File

@ -2,6 +2,21 @@ class XmppExchangeContactsJob < ApplicationJob
queue_as :default
def perform(inviter, invitee)
EjabberdManager::ExchangeContacts.call(inviter:, invitee:)
return unless inviter.service_enabled?(:ejabberd) &&
invitee.service_enabled?(:ejabberd) &&
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new
ejabberd.add_rosteritem({
"localuser": invitee.cn, "localhost": invitee.ou,
"user": inviter.cn, "host": inviter.ou,
"nick": inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
ejabberd.add_rosteritem({
"localuser": inviter.cn, "localhost": inviter.ou,
"user": invitee.cn, "host": invitee.ou,
"nick": invitee.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
end
end

View File

@ -2,6 +2,7 @@ class XmppSendMessageJob < ApplicationJob
queue_as :default
def perform(payload)
EjabberdManager::SendMessage.call(payload:)
ejabberd = EjabberdApiClient.new
ejabberd.send_message payload
end
end

View File

@ -1,7 +0,0 @@
class XmppSetAvatarJob < ApplicationJob
queue_as :default
def perform(user:, overwrite: false)
EjabberdManager::SetAvatar.call(user:, overwrite:)
end
end

View File

@ -2,6 +2,25 @@ class XmppSetDefaultBookmarksJob < ApplicationJob
queue_as :default
def perform(user)
EjabberdManager::SetDefaultBookmarks.call(user:)
return unless Setting.xmpp_default_rooms.any?
@user = user
ejabberd = EjabberdApiClient.new
ejabberd.private_set user, storage_content
end
def storage_content
bookmarks = ""
Setting.xmpp_default_rooms.each do |r|
bookmarks << conference_element(
jid: r[/<(.+)>/, 1], name: r[/^(.+)\s/, 1], nick: @user.cn,
autojoin: Setting.xmpp_autojoin_default_rooms
)
end
"<storage xmlns='storage:bookmarks'>#{bookmarks}</storage>"
end
def conference_element(jid:, name:, autojoin: false, nick:)
"<conference jid='#{jid}' name='#{name}' autojoin='#{autojoin.to_s}'><nick>#{nick}</nick></conference>"
end
end

View File

@ -11,9 +11,6 @@ module Settings
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
field :mastodon_auth_token, type: :string,
default: ENV["MASTODON_AUTH_TOKEN"].presence
end
end
end

View File

@ -1,18 +0,0 @@
module Settings
module MembershipSettings
extend ActiveSupport::Concern
included do
field :member_status_contributor, type: :string,
default: "Contributor"
field :member_status_sustainer, type: :string,
default: "Sustainer"
# Admin panel
field :user_index_show_contributors, type: :boolean,
default: false
field :user_index_show_sustainers, type: :boolean,
default: false
end
end
end

View File

@ -6,9 +6,6 @@ module Settings
field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present?
field :rs_accounts_domain, type: :string,
default: ENV["RS_AKKOUNTS_DOMAIN"] || ENV["AKKOUNTS_DOMAIN"]
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence

View File

@ -1,42 +1,22 @@
class Donation < ApplicationRecord
include AASM
# Relations
belongs_to :user
# Validations
validates_presence_of :user
validates_presence_of :donation_method,
inclusion: { in: %w[ custom btcpay lndhub ] }
validates_presence_of :payment_status, allow_nil: true,
inclusion: { in: %w[ pending processing settled ] }
inclusion: { in: %w[ processing settled ] }
validates_presence_of :paid_at, allow_nil: true
validates_presence_of :amount_sats, allow_nil: true
validates_presence_of :fiat_amount, allow_nil: true
validates_presence_of :fiat_currency, allow_nil: true,
inclusion: { in: %w[ EUR USD ] }
scope :pending, -> { where(payment_status: "pending") }
#Scopes
scope :processing, -> { where(payment_status: "processing") }
scope :completed, -> { where(payment_status: "settled") }
scope :incomplete, -> { where.not(payment_status: "settled") }
aasm column: :payment_status do
state :pending, initial: true
state :processing
state :settled
event :start_processing do
transitions from: :pending, to: :processing
end
event :complete do
transitions from: :processing, to: :settled, after: [:set_paid_at, :set_sustainer_status]
transitions from: :pending, to: :settled, after: [:set_paid_at, :set_sustainer_status]
end
end
def pending?
payment_status == "pending"
end
def processing?
payment_status == "processing"
@ -45,17 +25,4 @@ class Donation < ApplicationRecord
def completed?
payment_status == "settled"
end
private
def set_paid_at
update paid_at: DateTime.now if paid_at.nil?
end
def set_sustainer_status
user.add_member_status :sustainer
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error("Failed to set memberStatus: #{e.message}")
end
end

View File

@ -1,12 +0,0 @@
class EditableContent < ApplicationRecord
validates :key, presence: true,
uniqueness: { scope: :context }
def has_content?
content.present?
end
def is_empty?
content.blank?
end
end

View File

@ -6,7 +6,7 @@ class LndhubUser < LndhubBase
foreign_key: "user_id"
belongs_to :user, class_name: "User",
primary_key: "lndhub_username",
primary_key: "ln_account",
foreign_key: "login"
def balance

View File

@ -2,7 +2,7 @@ class RemoteStorageAuthorization < ApplicationRecord
belongs_to :user
belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true
serialize :permissions, coder: YAML unless Rails.env.production?
serialize :permissions unless Rails.env.production?
validates_presence_of :permissions
validates_presence_of :client_id
@ -69,19 +69,11 @@ class RemoteStorageAuthorization < ApplicationRecord
end
def remove_token_expiry_job
job_class = RemoteStorageExpireAuthorizationJob
job_args = [id]
query = SolidQueue::Job.where(class_name: job_class.to_s)
case ActiveRecord::Base.connection.adapter_name.downcase
when /sqlite/
query.where("json_extract(arguments, '$.arguments') = ?", job_args.to_json)
when /postgres/
query.where("CAST(arguments AS jsonb)->>'arguments' = ?", job_args.to_json)
else
raise "Unsupported database adapter"
end.destroy_all
queue = Sidekiq::Queue.new(RemoteStorageExpireAuthorizationJob.queue_name)
queue.each do |job|
next unless job.display_class == "RemoteStorageExpireAuthorizationJob"
job.delete if job.display_args == [id]
end
end
def find_or_create_web_app

View File

@ -16,7 +16,6 @@ class Setting < RailsSettings::Base
include Settings::LightningNetworkSettings
include Settings::MastodonSettings
include Settings::MediaWikiSettings
include Settings::MembershipSettings
include Settings::NostrSettings
include Settings::OpenCollectiveSettings
include Settings::RemoteStorageSettings

View File

@ -4,8 +4,8 @@ class User < ApplicationRecord
include EmailValidatable
attr_accessor :current_password
attr_accessor :display_name
attr_accessor :avatar_new
attr_accessor :display_name
attr_accessor :pgp_pubkey
serialize :preferences, coder: UserPreferences
@ -23,16 +23,10 @@ class User < ApplicationRecord
has_many :zaps
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
primary_key: "lndhub_username", foreign_key: "login"
primary_key: "ln_account", foreign_key: "login"
has_many :accounts, through: :lndhub_user
#
# Attachments
#
has_one_attached :avatar
#
# Validations
#
@ -56,7 +50,6 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) }
validate :acceptable_avatar
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
@ -73,7 +66,7 @@ class User < ApplicationRecord
# Encrypted database columns
#
encrypts :lndhub_password
has_encrypted :ln_login, :ln_password
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
@ -84,10 +77,6 @@ class User < ApplicationRecord
:timeoutable,
:rememberable
#
# Methods
#
def ldap_before_save
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
self.ou = dn.split(',')
@ -131,7 +120,7 @@ class User < ApplicationRecord
end
def is_admin?
@admin ||= if admin = Devise::LDAP::Adapter.get_ldap_param(self.cn, :admin)
admin ||= if admin = Devise::LDAP::Adapter.get_ldap_param(self.cn, :admin)
!!admin.first
else
false
@ -163,41 +152,13 @@ class User < ApplicationRecord
def ldap_entry(reload: false)
return @ldap_entry if defined?(@ldap_entry) && !reload
@ldap_entry = ldap.fetch_users(cn: self.cn).first
end
def add_to_ldap_array(attr_key, ldap_attr, value)
current_entries = ldap_entry[attr_key.to_sym] || []
new_entries = Array(value).map(&:to_s)
entries = (current_entries + new_entries).uniq.sort
ldap.replace_attribute(dn, ldap_attr.to_sym, entries)
end
def remove_from_ldap_array(attr_key, ldap_attr, value)
current_entries = ldap_entry[attr_key.to_sym] || []
entries_to_remove = Array(value).map(&:to_s)
entries = (current_entries - entries_to_remove).uniq.sort
ldap.replace_attribute(dn, ldap_attr.to_sym, entries)
@ldap_entry = ldap.fetch_users(uid: self.cn, ou: self.ou).first
end
def display_name
@display_name ||= ldap_entry[:display_name]
end
# TODO Variant keys are currently broken for some reason
# (They use the same key as the main blob, when it should be
# "/variants/#{key)"
# def avatar_variant(size: :medium)
# dimensions = case size
# when :large then [400, 400]
# when :medium then [256, 256]
# when :small then [64, 64]
# else [256, 256]
# end
# format = avatar.content_type == "image/png" ? :png : :jpeg
# avatar.variant(resize_to_fill: dimensions, format: format)
# end
def nostr_pubkey
@nostr_pubkey ||= ldap_entry[:nostr_key]
end
@ -225,6 +186,10 @@ class User < ApplicationRecord
ZBase32.encode(Digest::SHA1.digest(cn))
end
def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end
def services_enabled
ldap_entry[:services_enabled] || []
end
@ -234,39 +199,21 @@ class User < ApplicationRecord
end
def enable_service(service)
add_to_ldap_array :services_enabled, :serviceEnabled, service
ldap_entry(reload: true)[:services_enabled]
current_services = services_enabled
new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq.sort
ldap.replace_attribute(dn, :serviceEnabled, services)
end
def disable_service(service)
remove_from_ldap_array :services_enabled, :serviceEnabled, service
ldap_entry(reload: true)[:services_enabled]
current_services = services_enabled
disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq.sort
ldap.replace_attribute(dn, :serviceEnabled, services)
end
def disable_all_services
ldap.delete_attribute(dn, :serviceEnabled)
end
def member_status
ldap_entry[:member_status] || []
end
def add_member_status(status)
add_to_ldap_array :member_status, :memberStatus, status
ldap_entry(reload: true)[:member_status]
end
def remove_member_status(status)
remove_from_ldap_array :member_status, :memberStatus, status
ldap_entry(reload: true)[:member_status]
end
def is_contributing_member?
member_status.map(&:to_sym).include?(:contributor)
end
def is_paying_member?
member_status.map(&:to_sym).include?(:sustainer)
ldap.delete_attribute(dn,:service)
end
private
@ -280,7 +227,7 @@ class User < ApplicationRecord
return unless avatar_new.present?
if avatar_new.size > 1.megabyte
errors.add(:avatar, "must be less than 1MB file size")
errors.add(:avatar, "file size is too large")
end
acceptable_types = ["image/jpeg", "image/png"]

View File

@ -1,7 +1,7 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcpayManagerService < RestApiService
class BtcpayManagerService < ApplicationService
private
def base_url
@ -19,4 +19,18 @@ class BtcpayManagerService < RestApiService
"Authorization" => "token #{auth_token}"
}
end
def endpoint_url(path)
"#{base_url}/#{path.gsub(/^\//, '')}"
end
def get(path, params = {})
res = Faraday.get endpoint_url(path), params, headers
JSON.parse(res.body)
end
def post(path, payload)
res = Faraday.post endpoint_url(path), payload.to_json, headers
JSON.parse(res.body)
end
end

View File

@ -0,0 +1,29 @@
class EjabberdApiClient
def initialize
@base_url = Setting.ejabberd_api_url
end
def post(endpoint, payload)
res = Faraday.post("#{@base_url}/#{endpoint}", payload.to_json,
"Content-Type" => "application/json")
if res.status != 200
Rails.logger.error "[ejabberd] API request failed:"
Rails.logger.error res.body
#TODO Send custom event to Sentry
end
end
def add_rosteritem(payload)
post "add_rosteritem", payload
end
def send_message(payload)
post "send_message", payload
end
def private_set(user, content)
payload = { user: user.cn, host: user.ou, element: content }
post "private_set", payload
end
end

View File

@ -1,25 +0,0 @@
module EjabberdManager
class ExchangeContacts < EjabberdManagerService
def initialize(inviter:, invitee:)
@inviter = inviter
@invitee = invitee
end
def call
return unless @inviter.service_enabled?(:ejabberd) &&
@invitee.service_enabled?(:ejabberd) &&
@inviter.preferences[:xmpp_exchange_contacts_with_invitees]
add_rosteritem({
"localuser": @invitee.cn, "localhost": @invitee.ou,
"user": @inviter.cn, "host": @inviter.ou,
"nick": @inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
add_rosteritem({
"localuser": @inviter.cn, "localhost": @inviter.ou,
"user": @invitee.cn, "host": @invitee.ou,
"nick": @invitee.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
end
end
end

View File

@ -1,25 +0,0 @@
module EjabberdManager
class GetAvatar < EjabberdManagerService
def initialize(user:)
@user = user
end
def call
res = get_vcard2 @user, "PHOTO", "BINVAL"
if res.status == 200
# VCARD PHOTO/BINVAL prop exists
img_base64 = JSON.parse(res.body)["content"]
ct_res = get_vcard2 @user, "PHOTO", "TYPE"
content_type = JSON.parse(ct_res.body)["content"]
{ content_type:, img_base64: }
elsif res.status == 400
# VCARD or PHOTO/BINVAL prop does not exist
nil
else
# Unexpected error, let job fail
raise res.inspect
end
end
end
end

View File

@ -1,11 +0,0 @@
module EjabberdManager
class SendMessage < EjabberdManagerService
def initialize(payload:)
@payload = payload
end
def call
send_message @payload
end
end
end

View File

@ -1,80 +0,0 @@
require 'digest'
require "image_processing/vips"
module EjabberdManager
class SetAvatar < EjabberdManagerService
def initialize(user:, overwrite: false)
@user = user
@overwrite = overwrite
end
def call
unless @overwrite
current_avatar = EjabberdManager::GetAvatar.call(user: @user)
Rails.logger.info { "User #{@user.cn} already has an avatar set" }
return if current_avatar.present?
end
Rails.logger.debug { "Setting XMPP avatar for user #{@user.cn}" }
stanzas = build_xep0084_stanzas
stanzas.each do |stanza|
payload = { from: @user.address, to: @user.address, stanza: stanza }
res = send_stanza payload
raise res.inspect if res.status != 200
end
end
end
private
def process_avatar
@user.avatar.blob.open do |file|
processed = ImageProcessing::Vips
.source(file)
.resize_to_fill(256, 256)
.convert("png")
.call
processed.read
end
end
# See https://xmpp.org/extensions/xep-0084.html
def build_xep0084_stanzas
img_data = process_avatar
sha1_hash = Digest::SHA1.hexdigest(img_data)
base64_data = Base64.strict_encode64(img_data)
[
"""
<iq type='set' from='#{@user.address}' id='avatar-data-#{rand(101)}'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:avatar:data'>
<item id='#{sha1_hash}'>
<data xmlns='urn:xmpp:avatar:data'>#{base64_data}</data>
</item>
</publish>
</pubsub>
</iq>
""".strip,
"""
<iq type='set' from='#{@user.address}' id='avatar-metadata-#{rand(101)}'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:avatar:metadata'>
<item id='#{sha1_hash}'>
<metadata xmlns='urn:xmpp:avatar:metadata'>
<info bytes='#{img_data.size}'
id='#{sha1_hash}'
height='256'
type='image/png'
width='256'/>
</metadata>
</item>
</publish>
</pubsub>
</iq>
""".strip,
]
end
end

View File

@ -1,31 +0,0 @@
module EjabberdManager
class SetDefaultBookmarks < EjabberdManagerService
def initialize(user:)
@user = user
end
def call
return unless Setting.xmpp_default_rooms.any?
private_set @user, storage_content
end
private
def storage_content
bookmarks = ""
Setting.xmpp_default_rooms.each do |r|
bookmarks << conference_element(
jid: r[/<(.+)>/, 1], name: r[/^(.+)\s/, 1], nick: @user.cn,
autojoin: Setting.xmpp_autojoin_default_rooms
)
end
"<storage xmlns='storage:bookmarks'>#{bookmarks}</storage>"
end
def conference_element(jid:, name:, autojoin: false, nick:)
"<conference jid='#{jid}' name='#{name}' autojoin='#{autojoin.to_s}'><nick>#{nick}</nick></conference>"
end
end
end

View File

@ -1,55 +0,0 @@
class EjabberdManagerService < RestApiService
private
def base_url
@base_url ||= Setting.ejabberd_api_url
end
def headers
{ "Content-Type" => "application/json" }
end
def parse_responses?
false
end
#
# API endpoints
#
def add_rosteritem(payload)
post "add_rosteritem", payload
end
def send_message(payload)
post "send_message", payload
end
def send_stanza(payload)
post "send_stanza", payload
end
def get_vcard2(user, name, subname)
payload = {
user: user.cn, host: user.ou,
name: name, subname: subname
}
post "get_vcard2", payload
end
def private_get(user, element_name, namespace)
payload = {
user: user.cn, host: user.ou,
element: element_name, ns: namespace
}
post "private_get", payload
end
def private_set(user, content)
payload = {
user: user.cn, host: user.ou,
element: content
}
post "private_set", payload
end
end

View File

@ -10,7 +10,7 @@ module LdapManager
filter = Net::LDAP::Filter.eq("cn", @cn)
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
entry[:jpegPhoto].present? ? entry.jpegPhoto.first : nil
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
end
end
end

View File

@ -2,41 +2,26 @@ require "image_processing/vips"
module LdapManager
class UpdateAvatar < LdapManagerService
def initialize(user:)
@user = user
@dn = user.dn
def initialize(dn:, file:)
@dn = dn
@img_data = process(file)
end
def call
unless @user.avatar.attached?
Rails.logger.error { "Cannot store empty jpegPhoto for user #{@user.cn}" }
return false
end
img_data = @user.avatar.blob.download
jpg_data = process_avatar
Rails.logger.debug { "Storing new jpegPhoto for user #{@user.cn} in LDAP" }
result = replace_attribute(@dn, :jpegPhoto, jpg_data)
result == 0
replace_attribute @dn, :jpegPhoto, @img_data
end
private
def process_avatar
@user.avatar.blob.open do |file|
def process(file)
processed = ImageProcessing::Vips
.resize_to_fill(512, 512)
.source(file)
.resize_to_fill(256, 256)
.convert("jpeg")
.saver(strip: true)
.call
processed.read
end
rescue Vips::Error => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error { "Image processing failed for LDAP avatar: #{e.message}" }
nil
Base64.strict_encode64 processed.read
end
end
end

View File

@ -6,11 +6,7 @@ module LdapManager
end
def call
if @display_name.present?
replace_attribute @dn, :displayName, @display_name
else
delete_attribute @dn, :displayName
end
end
end
end

View File

@ -50,17 +50,19 @@ class LdapService < ApplicationService
end
def fetch_users(args={})
if args[:ou]
treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}"
else
treebase = ldap_config["base"]
end
attributes = %w[
dn cn uid mail displayName admin serviceEnabled memberStatus
dn cn uid mail displayName admin serviceEnabled
mailRoutingAddress mailpassword nostrKey pgpKey
]
filter = Net::LDAP::Filter.eq('objectClass', 'person') &
Net::LDAP::Filter.eq("cn", args[:cn] || "*")
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
entries = client.search(
base: ldap_config["base"], filter: filter,
attributes: attributes
)
entries = client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] }
entries = entries.collect do |e|
{
@ -69,7 +71,6 @@ class LdapService < ApplicationService
display_name: e.try(:displayName) ? e.displayName.first : nil,
admin: e.try(:admin) ? 'admin' : nil,
services_enabled: e.try(:serviceEnabled),
member_status: e.try(:memberStatus),
email_maildrop: e.try(:mailRoutingAddress),
email_password: e.try(:mailpassword),
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil,
@ -78,20 +79,10 @@ class LdapService < ApplicationService
end
end
def search_users(search_attr, value, return_attr)
filter = Net::LDAP::Filter.eq('objectClass', 'person') &
Net::LDAP::Filter.eq(search_attr.to_s, value.to_s) &
Net::LDAP::Filter.present('cn')
entries = client.search(
base: ldap_config["base"], filter: filter,
attributes: [return_attr]
)
entries.map { |entry| entry[return_attr].first }.compact
end
def fetch_organizations
attributes = %w{dn ou description}
filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit")
# filter = Net::LDAP::Filter.eq("objectClass", "*")
treebase = "cn=users,#{ldap_suffix}"
entries = client.search(base: treebase, filter: filter, attributes: attributes)

View File

@ -33,10 +33,7 @@ class Lndhub < ApplicationService
end
def authenticate(user)
credentials = post "auth?type=auth", {
login: user.lndhub_username,
password: user.lndhub_password
}
credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password }
self.auth_token = credentials["access_token"]
self.auth_token
end

View File

@ -1,12 +0,0 @@
module MastodonManager
class FetchUser < MastodonManagerService
def initialize(mastodon_id:)
@mastodon_id = mastodon_id
end
def call
user = get "v1/admin/accounts/#{@mastodon_id}"
user.with_indifferent_access
end
end
end

View File

@ -1,14 +0,0 @@
module MastodonManager
class FindUser < MastodonManagerService
def initialize(username:)
@username = username
end
def call
users = get "v2/admin/accounts?username=#{@username}&origin=local"
users = users.map { |u| u.with_indifferent_access }
# Results may contain partial matches
users.find { |u| u.dig(:username).downcase == @username.downcase }
end
end
end

View File

@ -1,64 +0,0 @@
module MastodonManager
class SyncAccountProfiles < MastodonManagerService
def initialize(direction: "down", overwrite: false, user: nil)
@direction = direction
@overwrite = overwrite
@user = user
if @direction != "down"
raise NotImplementedError
end
end
def call
if @user
Rails.logger.debug { "Syncing account profile for user #{@user.cn} (direction: #{@direction}, overwrite: #{@overwrite})"}
users = User.where(cn: @user.cn)
else
Rails.logger.debug { "Syncing account profiles (direction: #{@direction}, overwrite: #{@overwrite})"}
users = User
end
users.find_each do |user|
if user.mastodon_id.blank?
mastodon_user = MastodonManager::FindUser.call username: user.cn
if mastodon_user
Rails.logger.debug { "Setting mastodon_id for user #{user.cn}" }
user.update! mastodon_id: mastodon_user.dig(:account, :id).to_i
else
Rails.logger.debug { "No Mastodon user found for username #{user.cn}" }
next
end
end
next if user.avatar.attached? && user.display_name.present?
unless mastodon_user
Rails.logger.debug { "Fetching Mastodon account with ID #{user.mastodon_id} for #{user.cn}" }
mastodon_user = MastodonManager::FetchUser.call mastodon_id: user.mastodon_id
end
if user.display_name.blank?
if mastodon_display_name = mastodon_user.dig(:account, :display_name)
Rails.logger.debug { "Setting display name for user #{user.cn} from Mastodon" }
LdapManager::UpdateDisplayName.call(
dn: user.dn, display_name: mastodon_display_name
)
end
end
if !user.avatar.attached?
if avatar_url = mastodon_user.dig(:account, :avatar_static)
Rails.logger.debug { "Importing Mastodon avatar for user #{user.cn}" }
UserManager::ImportRemoteAvatar.call(
user: user, avatar_url: avatar_url
)
end
end
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error e
end
end
end
end

View File

@ -1,22 +0,0 @@
#
# API Docs: https://docs.joinmastodon.org/methods/
#
class MastodonManagerService < RestApiService
private
def base_url
@base_url ||= "#{Setting.mastodon_public_url}/api"
end
def auth_token
@auth_token ||= Setting.mastodon_auth_token
end
def headers
{
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "Bearer #{auth_token}"
}
end
end

View File

@ -1,29 +0,0 @@
class RestApiService < ApplicationService
private
def base_url
raise NotImplementedError
end
def headers
raise NotImplementedError
end
def endpoint_url(path)
"#{base_url}/#{path.gsub(/^\//, '')}"
end
def parse_responses?
true
end
def get(path, params = {})
res = Faraday.get endpoint_url(path), params, headers
parse_responses? ? JSON.parse(res.body) : res
end
def post(path, payload)
res = Faraday.post endpoint_url(path), payload.to_json, headers
parse_responses? ? JSON.parse(res.body) : res
end
end

View File

@ -1,42 +0,0 @@
module UserManager
class ImportRemoteAvatar < UserManagerService
def initialize(user:, avatar_url:)
@user = user
@avatar_url = avatar_url
end
def call
if import_remote_avatar
UserManager::UpdateAvatar.call(user: @user)
end
end
private
def import_remote_avatar
tempfile = Down.download(@avatar_url)
content_type = tempfile.content_type
unless %w[image/jpeg image/png].include?(content_type)
Rails.logger.warn { "Wrong content type of remote avatar for user #{user.cn}: '#{content_type}'" }
return false
end
img_data = UserManager::ProcessAvatar.call(io: tempfile)
tempfile = Tempfile.create
tempfile.binmode
tempfile.write(img_data)
tempfile.rewind
hash = Digest::SHA256.hexdigest(img_data)
ext = content_type == "image/png" ? "png" : "jpg"
filename = "#{hash}.#{ext}"
key = "users/#{@user.cn}/avatars/#{filename}"
@user.avatar.attach io: tempfile, key: key, filename: filename
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.warn "Importing remote avatar failed: \"#{e.message}\""
false
end
end
end

View File

@ -1,21 +0,0 @@
module UserManager
class ProcessAvatar < UserManagerService
def initialize(io:)
@io = io
end
def call
processed = ImageProcessing::Vips
.source(@io)
.resize_to_fill(400, 400)
.saver(strip: true)
.call
@io.rewind
processed.read
rescue Vips::Error => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.warn { "Image processing failed for avatar: #{e.message}" }
nil
end
end
end

View File

@ -1,16 +0,0 @@
module UserManager
class UpdateAvatar < UserManagerService
def initialize(user:)
@user = user
end
def call
LdapManager::UpdateAvatar.call(user: @user)
if Setting.ejabberd_enabled?
return if Rails.env.development?
XmppSetAvatarJob.perform_later(user: @user)
end
end
end
end

View File

@ -1,11 +0,0 @@
<%= form_with url: path, method: :get, local: true, class: "flex gap-1" do %>
<%= text_field_tag :username, @username, placeholder: 'Filter by username' %>
<%= button_tag type: 'submit', name: nil, title: "Filter", class: 'btn-md btn-icon btn-outline' do %>
<%= render partial: "icons/filter", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
<% end %>
<% if @username %>
<%= link_to path, title: "Remove filter", class: 'btn-md btn-icon btn-outline' do %>
<%= render partial: "icons/x", locals: { custom_class: "text-red-600 h-4 w-4 inline" } %>
<% end %>
<% end %>
<% end %>

View File

@ -38,7 +38,8 @@
<tr>
<td><%= web_app.name %></td>
<td><%= link_to web_app.url, web_app.url,
target: "_blank", rel: "nofollow noopener" %></td>
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">

View File

@ -1,34 +0,0 @@
<table class="divided">
<thead>
<tr>
<th>User</th>
<th class="text-right">Sats</th>
<th class="text-right">Fiat Amount</th>
<th class="pl-2">Public name</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
<% donations.each do |donation| %>
<tr>
<td><%= link_to donation.user.cn, admin_user_path(donation.user.cn) %></td>
<td class="text-right"><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></td>
<td class="text-right"><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %></td>
<td class="pl-2"><%= donation.public_name %></td>
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : donation.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
<td class="text-right">
<%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% if defined?(pagy) %>
<div class="mt-8">
<%== pagy_nav pagy %>
</div>
<% end %>

View File

@ -5,7 +5,7 @@
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Received',
title: 'Overall',
value: @stats[:overall_sats],
unit: 'sats'
) %>
@ -19,28 +19,41 @@
</section>
<section>
<%= render partial: "admin/username_search_form",
locals: { path: admin_donations_path } %>
</section>
<% if @pending_donations.present? %>
<section>
<h3>Pending</h3>
<%= render partial: "admin/donations/list", locals: {
donations: @pending_donations
} %>
</section>
<% if @donations.any? %>
<h3>Recent Donations</h3>
<table class="divided mb-8">
<thead>
<tr>
<th>User</th>
<th class="text-right">Sats</th>
<th class="text-right">Fiat Amount</th>
<th class="pl-2">Public name</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
<% @donations.each do |donation| %>
<tr>
<td><%= link_to donation.user.cn, admin_user_path(donation.user.cn), class: 'ks-text-link' %></td>
<td class="text-right"><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></td>
<td class="text-right"><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %></td>
<td class="pl-2"><%= donation.public_name %></td>
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td>
<td class="text-right">
<%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
<section>
<% if @donations.present? %>
<h3>Received</h3>
<%= render partial: "admin/donations/list", locals: {
donations: @donations, pagy: @pagy
} %>
</tbody>
</table>
<%== pagy_nav @pagy %>
<% else %>
<p>
No donations received yet.
No donations yet.
</p>
<% end %>
</section>

View File

@ -6,7 +6,7 @@
<tbody>
<tr>
<th>User</th>
<td><%= link_to @donation.user.cn, admin_user_path(@donation.user.cn) %></td>
<td><%= link_to @donation.user.cn, admin_user_path(@donation.user.cn), class: 'ks-text-link' %></td>
</tr>
<tr>
<th>Donation Method</th>
@ -25,15 +25,7 @@
<td><%= @donation.public_name %></td>
</tr>
<tr>
<th>Payment status</th>
<td><%= @donation.payment_status %></td>
</tr>
<tr>
<th>Created at</th>
<td><%= @donation.created_at&.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
<tr>
<th>Paid at</th>
<th>Date</th>
<td><%= @donation.paid_at&.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
</tbody>

View File

@ -21,15 +21,9 @@
) %>
<% end %>
</section>
<section>
<%= render partial: "admin/username_search_form",
locals: { path: admin_invitations_path } %>
</section>
<% if @invitations_used.any? %>
<section>
<h3>Accepted</h3>
<h3>Recently Accepted</h3>
<table class="divided mb-8">
<thead>
<tr>
@ -44,8 +38,8 @@
<tr>
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
<td><%= link_to invitation.user.cn, admin_user_path(invitation.user.cn) %></td>
<td><%= link_to invitation.invitee.cn, admin_user_path(invitation.invitee.cn) %></td>
<td><%= link_to invitation.user.cn, admin_user_path(invitation.user.cn), class: "ks-text-link" %></td>
<td><%= link_to invitation.invitee.cn, admin_user_path(invitation.invitee.cn), class: "ks-text-link" %></td>
</tr>
<% end %>
</tbody>

View File

@ -36,7 +36,7 @@
</td>
<td>
<% if user = @users.find{ |u| u[2] == account.login } %>
<%= link_to user[0], admin_user_path(user[0]) %>
<%= link_to user[0], admin_user_path(user[0]), class: "ks-text-link" %>
<% end %>
</td>
<td><%= number_with_delimiter account.balance.to_i.to_s %></td>

View File

@ -1,53 +0,0 @@
<%= render HeaderComponent.new(title: "Settings") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
<%= form_for(Setting.new, url: admin_settings_membership_path, method: :put) do |f| %>
<section>
<h3>Membership</h3>
<% if @errors && @errors.any? %>
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
<% end %>
<ul role="list">
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :member_status_contributor,
title: "Status name for contributing users",
description: "A contributing member of your organization/group"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :member_status_sustainer,
title: "Status name for paying users",
description: "A paying/donating member or customer"
) %>
</ul>
</section>
<section>
<h3>Admin panel</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :user_index_show_contributors,
enabled: Setting.user_index_show_contributors?,
title: "Show #{Setting.member_status_contributor.downcase} status in user list",
description: "Can slow down page rendering with large user base"
) %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :user_index_show_sustainers,
enabled: Setting.user_index_show_sustainers?,
title: "Show #{Setting.member_status_sustainer.downcase} status in user list",
description: "Can slow down page rendering with large user base"
) %>
</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 %>
<% end %>

View File

@ -16,10 +16,5 @@
key: :mastodon_address_domain,
title: "User address domain"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :mastodon_auth_token,
type: :password,
title: "API auth token"
) %>
<% end %>
</ul>

View File

@ -13,20 +13,6 @@
title: 'Pending',
value: @stats[:users_pending],
) %>
<% if @show_contributors %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: Setting.member_status_contributor.pluralize,
value: @stats[:users_contributing],
) %>
<% end %>
<% if @show_sustainers %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: Setting.member_status_sustainer.pluralize,
value: @stats[:users_paying],
) %>
<% end %>
<% end %>
</section>
@ -42,13 +28,9 @@
<tbody>
<% @users.each do |user| %>
<tr>
<td><%= link_to(user.cn, admin_user_path(user.cn)) %></td>
<td>
<%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %>
<% if @show_contributors %><%= @contributors.include?(user.cn) ? badge("contributor", :green) : "" %><% end %>
<% if @show_sustainers %><%= @sustainers.include?(user.cn) ? badge("sustainer", :green) : "" %><% end %>
</td>
<td><%= @admins.include?(user.cn) ? badge("admin", :red) : "" %></td>
<td><%= link_to(user.cn, admin_user_path(user.cn), class: 'ks-text-link') %></td>
<td><%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %></td>
<td><%= user.is_admin? ? badge("admin", :red) : "" %></td>
</tr>
<% end %>
</tbody>

View File

@ -32,35 +32,11 @@
<th>Roles</th>
<td><%= @user.is_admin? ? badge("admin", :red) : "—" %></td>
</tr>
<tr>
<th>Status</th>
<td>
<% if @user.is_contributing_member? || @user.is_paying_member? %>
<%= @user.is_contributing_member? ? badge("contributor", :green) : "" %>
<%= @user.is_paying_member? ? badge("sustainer", :green) : "" %>
<% else %>
<% end %>
</td>
</tr>
<tr>
<th>Donations</th>
<td>
<% if @user.donations.any? %>
<%= link_to admin_donations_path(username: @user.cn) do %>
<%= @user.donations.completed.count %> for
<%= number_with_delimiter @user.donations.completed.sum("amount_sats") %> sats
<% end %>
<% else %>
<% end %>
</td>
</tr>
<tr>
<th>Invited by</th>
<td>
<% if @user.inviter %>
<%= link_to @user.inviter.cn, admin_user_path(@user.inviter.cn) %>
<%= link_to @user.inviter.cn, admin_user_path(@user.inviter.cn), class: 'ks-text-link' %>
<% else %>&mdash;<% end %>
</td>
</tr>
@ -69,7 +45,7 @@
<td data-controller="modal" data-action="keydown.esc->modal#close">
<div class="flex justify-between">
<span>
<%= @user.invitations.unused.count %>
<%= @user.invitations.count %>
</span>
<span>
<button id="add-invitations" data-action="click->modal#open">
@ -99,13 +75,10 @@
<tr>
<th class="align-top">Invited users</th>
<td class="align-top">
<% if @invitees.any? %>
<% if @user.invitees.length > 0 %>
<ul class="mb-0">
<% @recent_invitees.each do |invitee| %>
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.cn, admin_user_path(invitee.cn) %></li>
<% end %>
<% if @more_invitees > 0 %>
<li>and <%= link_to "#{@more_invitees} more", admin_invitations_path(username: @user.cn) %></li>
<% @user.invitees.order(cn: :asc).each do |invitee| %>
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.cn, admin_user_path(invitee.cn), class: 'ks-text-link' %></li>
<% end %>
</ul>
<% else %>&mdash;<% end %>
@ -116,42 +89,14 @@
</section>
<section class="sm:flex-1 sm:pt-0">
<h3>Avatar</h3>
<% if @user.avatar.attached? %>
<table class="divided">
<tbody>
<tr>
<th class="align-top">Image</th>
<td class="align-top">
<%= image_tag image_url_for(@user.avatar), class: "h-20 w-20 rounded-lg" %>
</td>
</tr>
<tr>
<th>Content type</th>
<td>
<%= @user.avatar.content_type %>
</td>
</tr>
<tr>
<th>Size</th>
<td>
<%= number_to_human_size(@user.avatar.blob.byte_size) %>
</td>
</tr>
</tbody>
</table>
<% else %>
<p class="text-gray-500">No avatar uploaded</p>
<% end %>
<h3 class="mt-12">LDAP</h3>
<h3>LDAP</h3>
<table class="divided">
<tbody>
<tr>
<th>Avatar</th>
<td>
<% if @ldap_avatar.present? %>
JPEG size: <%= number_to_human_size(@ldap_avatar.size) %>
<% if @avatar.present? %>
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
<% else %>
&mdash;
<% end %>
@ -168,7 +113,7 @@
<span class="font-mono" title="<%= @user.pgp_fpr %>">
<% if @user.pgp_pubkey_contains_user_address? %>
<%= link_to wkd_key_url(hashed_username: @user.wkd_hash, l: @user.cn, format: :txt),
target: "_blank" do %>
class: "ks-text-link", target: "_blank" do %>
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
<% end %>
<% else %>
@ -294,9 +239,7 @@
) %>
</td>
<td class="text-right">
<% if @user.nostr_pubkey.present? %>
<%= link_to "Open profile", "https://njump.me/#{@user.nostr_pubkey_bech32}", class: "btn-sm btn-gray" %>
<% end %>
</td>
</tr>
<% end %>
@ -333,7 +276,7 @@
</thead>
<tbody>
<tr>
<td><%= @user.lndhub_username %></td>
<td><%= @user.ln_account %></td>
<td><%= number_with_delimiter @lndhub_user.balance %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_incoming %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_outgoing %> sats</td>
@ -342,7 +285,7 @@
</tbody>
</table>
<% else %>
<p>No LndHub user found for account <strong class="font-mono"><%= @user.lndhub_username %></strong>.
<p>No LndHub user found for account <strong class="font-mono"><%= @user.ln_account %></strong>.
<% end %>
</section>
<% end %>

View File

@ -3,7 +3,7 @@
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
<section>
<p class="mb-12">
Your financial contributions to the development and upkeep of our
Your financial contributions to the development and upkeep of Kosmos
software and services.
</p>
</section>
@ -22,17 +22,17 @@
</div>
</section>
<% if @donations_processing.any? %>
<% if @donations_pending.any? %>
<section class="donation-list">
<h2>Pending</h2>
<%= render partial: "contributions/donations/list",
locals: { donations: @donations_processing } %>
locals: { donations: @donations_pending } %>
</section>
<% end %>
<% if @donations_completed.any? %>
<section class="donation-list">
<h2>Contributions</h2>
<h2>Past contributions</h2>
<%= render partial: "contributions/donations/list",
locals: { donations: @donations_completed } %>
</section>

View File

@ -1,20 +0,0 @@
<%= render HeaderComponent.new(title: "Contributions") %>
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
<section>
<%= render EditableContentComponent.new(
context: "contributions/other", key: "body", rich_text: true,
default: "No content yet") %>
<% if current_user.is_admin? %>
<div class="mt-8 pt-6 border-t border-gray-200 text-right">
<%= render EditContentButtonComponent.new(
context: "contributions/other", key: "title",
redirect_to: request.path) do %>Edit title<% end %>
<%= render EditContentButtonComponent.new(
context: "contributions/other", key: "body", rich_text: true,
redirect_to: request.path) do %>Edit content<% end %>
</div>
<% end %>
</section>
<% end %>

View File

@ -0,0 +1,49 @@
<%= render HeaderComponent.new(title: "Contributions") %>
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
<section>
<p class="mb-8">
Project contributions are how we develop and run all Kosmos software and
services. Everything we create and provide is free and open-source
software, even the page you're looking at right now!
</p>
<h3>Start contributing</h3>
<p>
Check out our
<a href="https://kosmos.org/projects/" target="_blank" class="ks-text-link">projects page</a>
for some (but not all) potential places that can use your help.
</p>
<p>
There's something to do for everyone, especially non-programmers! For
example, we need more help with graphics, UI/UX design, and
content/copywriting. Also, testing any of our software and reporting
issues you encounter along the way is very valuable.
</p>
<p>
A good way to get started is to join one of our
<a href="https://wiki.kosmos.org/Main_Page#Chat" target="_blank" class="ks-text-link">chat rooms</a>
and introduce yourself. Alternatively, you can also ping us on any other
medium, or even just grab an open issue on our
<a href="https://gitea.kosmos.org/kosmos/" target="_blank" class="ks-text-link">Gitea</a>
or on
<a href="https://github.com/67P/" target="_blank" class="ks-text-link">GitHub</a>
and dive right in.
</p>
<p class="mb-8">
Last but not least, if you want to help by proposing new features or
services, or by giving feedback on existing ones, head over to the
<a href="https://community.kosmos.org/" target="_blank" class="ks-text-link">community forums</a>,
where you can do just that.
</p>
<h3>Open Source Grants</h3>
<p>
Money coming in from financial contributions is first used to pay for our
bills. Additional funds are being paid out directly to our contributors,
including you, according to their rough share of contributions.
</p>
<p>
We have run two 6-month trials so far, with the next trial period
starting sometime soon. Watch your email for notifications about it!
</p>
</section>
<% end %>

View File

@ -8,7 +8,7 @@
bg-[length:86%] bg-[center_top_-40px] bg-no-repeat
bg-[url(/img/logos/icon_xmpp.svg)]">
<%= link_to services_chat_path,
class: "block h-full px-6 py-6 rounded-md btn-text-dark" do %>
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Chat</h3>
<p class="text-gray-600">
Federated chat rooms and instant messaging
@ -20,8 +20,7 @@
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:88%] bg-[center_top_-40px] bg-no-repeat
bg-[url(/img/logos/icon_mastodon.svg)]">
<%= link_to services_mastodon_path,
class: "block h-full px-6 py-6 rounded-md btn-text-dark" do %>
<%= link_to services_mastodon_path, class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Mastodon</h3>
<p class="text-gray-600">
Your account on the Open Social Web
@ -34,8 +33,7 @@
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:90%] bg-[center_top_-160px] bg-no-repeat
bg-[url(/img/logos/icon_mail.svg)]">
<%= link_to services_email_path,
class: "block h-full px-6 py-6 rounded-md btn-text-dark" do %>
<%= link_to services_email_path, class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">E-Mail</h3>
<p class="text-gray-600">
A no-bullshit email account
@ -49,7 +47,7 @@
bg-[length:80%] bg-[center_top_-156px] bg-no-repeat
bg-[url(/img/logos/icon_remotestorage.svg)]">
<%= link_to services_storage_path,
class: "block h-full px-6 py-6 rounded-md btn-text-dark" do %>
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Storage</h3>
<p class="text-gray-600">
Sync your data between apps and devices
@ -62,7 +60,7 @@
bg-cover bg-center sm:bg-[center_top_-140px] bg-no-repeat
bg-[url(/img/logos/icon_lightning.svg)]">
<%= link_to services_lightning_index_path,
class: "block h-full px-6 py-6 rounded-md btn-text-dark" do %>
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Lightning Network</h3>
<p class="text-gray-600">
Send and receive sats over the Bitcoin Lightning Network
@ -75,7 +73,7 @@
bg-[length:80%] bg-center bg-no-repeat
bg-[url(/img/logos/icon_discourse.svg)]">
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
class: "block h-full px-6 py-6 rounded-md btn-text-dark" do %>
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Discourse</h3>
<p class="text-gray-600">
Community forums and support/help site
@ -88,7 +86,7 @@
bg-[length:92%] bg-center bg-no-repeat
bg-[url(/img/logos/icon_gitea.png)]">
<%= link_to Setting.gitea_public_url,
class: "block h-full px-6 py-6 rounded-md btn-text-dark" do %>
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Gitea</h3>
<p class="text-gray-600">
Code hosting and collaboration for software projects
@ -101,7 +99,7 @@
bg-[length:86%] bg-[center_top_-60px] bg-no-repeat
bg-[url(/img/logos/icon_droneci.svg)]">
<%= link_to Setting.droneci_public_url,
class: "block h-full px-6 py-6 rounded-md btn-text-dark" do %>
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Drone CI</h3>
<p class="text-gray-600">
Continuous integration for software projects on Gitea
@ -114,10 +112,10 @@
bg-cover bg-[center_top_-20px] bg-no-repeat
bg-[url(/img/logos/icon_mediawiki.svg)]">
<%= link_to Setting.mediawiki_public_url,
class: "block h-full px-6 py-6 rounded-md btn-text-dark" do %>
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Wiki</h3>
<p class="text-gray-600">
Documentation and knowledge base
Kosmos documentation and knowledge base
</p>
<% end %>
</div>

View File

@ -47,7 +47,7 @@
data: { action: "click->settings--toggle#toggleSwitch" } %>
<p class="grow text-sm text-right">
<%= link_to "Forgot your password?", new_password_path(resource_name),
class: "text-gray-500 visited:text-gray-500 underline" %><br />
class: "text-gray-500 underline" %><br />
</p>
<% end %>

View File

@ -2,28 +2,28 @@
<%- if controller_name != 'sessions' %>
<p class="mb-2">
<%= link_to "Log in", new_session_path(resource_name),
class: "text-gray-500 visited:text-gray-500 underline" %>
class: "text-gray-500 underline" %>
</p>
<% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<p class="mb-2">
<%= link_to "Forgot your password?", new_password_path(resource_name),
class: "text-gray-500 visited:text-gray-500 underline" %>
class: "text-gray-500 underline" %>
</p>
<% end %>
<%- if devise_mapping.confirmable? && !controller_name.match(/^(confirmations|sessions)$/) %>
<p class="mb-2">
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name),
class: "text-gray-500 visited:text-gray-500 underline" %>
class: "text-gray-500 underline" %>
</p>
<% end %>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<p class="mb-2">
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name),
class: "text-gray-500 visited:text-gray-500 underline" %>
class: "text-gray-500 underline" %>
</p>
<% end %>
</div>

Some files were not shown because too many files have changed in this diff Show More