Compare commits
	
		
			2 Commits
		
	
	
		
			master
			...
			feature/no
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 011386fb8d | |||
| 4d77f5d38c | 
							
								
								
									
										23
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								.env.example
									
									
									
									
									
								
							| @ -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' | ||||
| 
 | ||||
|  | ||||
| @ -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
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -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 | ||||
|  | ||||
							
								
								
									
										24
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								Gemfile
									
									
									
									
									
								
							| @ -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' | ||||
| @ -40,9 +44,6 @@ gem 'pagy', '~> 6.0', '>= 6.0.2' | ||||
| gem 'flipper' | ||||
| 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 +51,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 +63,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 | ||||
|  | ||||
							
								
								
									
										571
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										571
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @ -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,105 @@ 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 +249,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 +268,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 +285,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 +332,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 +472,18 @@ 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) | ||||
|     zbase32 (0.1.1) | ||||
|     zeitwerk (2.7.2) | ||||
|     yard (0.9.34) | ||||
|     zeitwerk (2.6.12) | ||||
| 
 | ||||
| PLATFORMS | ||||
|   arm64-darwin-22 | ||||
| @ -528,7 +491,6 @@ PLATFORMS | ||||
|   x86_64-linux | ||||
| 
 | ||||
| DEPENDENCIES | ||||
|   aasm | ||||
|   aws-sdk-s3 | ||||
|   bcrypt (~> 3.1) | ||||
|   capybara | ||||
| @ -545,34 +507,32 @@ DEPENDENCIES | ||||
|   flipper | ||||
|   flipper-active_record | ||||
|   flipper-ui | ||||
|   gpgme (~> 2.0.24) | ||||
|   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 | ||||
| @ -580,7 +540,6 @@ DEPENDENCIES | ||||
|   warden | ||||
|   web-console (~> 4.2) | ||||
|   webmock | ||||
|   zbase32 (~> 0.1.1) | ||||
| 
 | ||||
| BUNDLED WITH | ||||
|    2.5.5 | ||||
|  | ||||
| @ -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/) | ||||
|  | ||||
							
								
								
									
										4
									
								
								app/assets/config/manifest.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/assets/config/manifest.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| //= link_tree ../images
 | ||||
| //= link_tree ../../javascript .js
 | ||||
| //= link_tree ../builds
 | ||||
| //= link_tree ../../../vendor/javascript .js
 | ||||
| @ -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"; | ||||
|  | ||||
| @ -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; } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										8
									
								
								app/assets/stylesheets/components/links.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/assets/stylesheets/components/links.css
									
									
									
									
									
										Normal 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; } | ||||
|   } | ||||
| } | ||||
| @ -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 { | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 %> | ||||
|  | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -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> | ||||
| @ -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 | ||||
| @ -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 %> | ||||
| @ -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 | ||||
| @ -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> | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
| @ -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"> | ||||
|  | ||||
| @ -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 %> | ||||
|  | ||||
| @ -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 %> | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
| @ -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 | ||||
|  | ||||
| @ -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 = {} | ||||
|  | ||||
| @ -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 | ||||
| @ -4,37 +4,25 @@ 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)) | ||||
|     ldap   = LdapService.new | ||||
|     @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 | ||||
| @ -42,7 +30,7 @@ class Admin::UsersController < Admin::BaseController | ||||
|     amount = params[:amount].to_i | ||||
|     notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user]) | ||||
| 
 | ||||
|     UserManager::CreateInvitations.call(user: @user, amount: amount, notify: notify_user) | ||||
|     CreateInvitations.call(user: @user, amount: amount, notify: notify_user) | ||||
| 
 | ||||
|     redirect_to admin_user_path(@user.cn), flash: { | ||||
|       success: "Added #{amount} invitations to #{@user.cn}'s account" | ||||
|  | ||||
| @ -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 | ||||
| @ -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,11 +81,14 @@ class Contributions::DonationsController < ApplicationController | ||||
| 
 | ||||
|     case invoice["status"] | ||||
|     when "Settled" | ||||
|       @donation.complete! | ||||
|       @donation.paid_at = DateTime.now | ||||
|       @donation.payment_status = "settled" | ||||
|       @donation.save! | ||||
|       flash_message = { success: "Thank you!" } | ||||
|     when "Processing" | ||||
|       unless @donation.processing? | ||||
|         @donation.start_processing! | ||||
|         @donation.payment_status = "processing" | ||||
|         @donation.save! | ||||
|         flash_message = { success: "Thank you! We will send you an email when the payment is confirmed." } | ||||
|         BtcpayCheckDonationJob.set(wait: 20.seconds).perform_later(@donation) | ||||
|       end | ||||
|  | ||||
| @ -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 | ||||
							
								
								
									
										8
									
								
								app/controllers/contributions/projects_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/controllers/contributions/projects_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| class Contributions::ProjectsController < ApplicationController | ||||
|   before_action :authenticate_user! | ||||
| 
 | ||||
|   # GET /contributions | ||||
|   def index | ||||
|     @current_section = :contributions | ||||
|   end | ||||
| end | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -37,7 +37,7 @@ class LnurlpayController < ApplicationController | ||||
|       pubkey: Setting.lndhub_public_key, | ||||
|       customData: [{ | ||||
|         customKey: "696969", | ||||
|         customValue: @user.lndhub_username | ||||
|         customValue: @user.ln_account | ||||
|       }] | ||||
|     } | ||||
|   end | ||||
|  | ||||
| @ -1,9 +0,0 @@ | ||||
| class PagesController < ApplicationController | ||||
|   def privacy | ||||
|     @current_section = :privacy | ||||
|   end | ||||
| 
 | ||||
|   def tos | ||||
|     @current_section = :tos | ||||
|   end | ||||
| end | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -21,12 +21,10 @@ class SettingsController < ApplicationController | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   # PUT /settings/:section | ||||
|   def update | ||||
|     @user.preferences.merge!(user_params[:preferences] || {}) | ||||
|     @user.display_name = user_params[:display_name] | ||||
|     @user.avatar_new   = user_params[:avatar_new] | ||||
|     @user.pgp_pubkey   = user_params[:pgp_pubkey] | ||||
|     @user.avatar_new = user_params[:avatar] | ||||
| 
 | ||||
|     if @user.save | ||||
|       if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name]) | ||||
| @ -34,16 +32,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 | ||||
|       end | ||||
| 
 | ||||
|       if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key]) | ||||
|         UserManager::UpdatePgpKey.call(user: @user) | ||||
|         LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new) | ||||
|       end | ||||
| 
 | ||||
|       redirect_to setting_path(@settings_section), flash: { | ||||
| @ -55,7 +44,6 @@ class SettingsController < ApplicationController | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   # POST /settings/update_email | ||||
|   def update_email | ||||
|     if @user.valid_ldap_authentication?(security_params[:current_password]) | ||||
|       if @user.update email: email_params[:email] | ||||
| @ -73,7 +61,6 @@ class SettingsController < ApplicationController | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   # POST /settings/reset_email_password | ||||
|   def reset_email_password | ||||
|     @user.current_password = security_params[:current_password] | ||||
| 
 | ||||
| @ -96,7 +83,6 @@ class SettingsController < ApplicationController | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   # POST /settings/reset_password | ||||
|   def reset_password | ||||
|     current_user.send_reset_password_instructions | ||||
|     sign_out current_user | ||||
| @ -104,7 +90,6 @@ class SettingsController < ApplicationController | ||||
|     redirect_to check_your_email_path, notice: msg | ||||
|   end | ||||
| 
 | ||||
|   # POST /settings/set_nostr_pubkey | ||||
|   def set_nostr_pubkey | ||||
|     signed_event = Nostr::Event.new(**nostr_event_from_params) | ||||
| 
 | ||||
| @ -167,8 +152,7 @@ class SettingsController < ApplicationController | ||||
| 
 | ||||
|     def user_params | ||||
|       params.require(:user).permit( | ||||
|         :display_name, :avatar_new, :pgp_pubkey, | ||||
|         preferences: UserPreferences.pref_keys | ||||
|         :display_name, :avatar, preferences: UserPreferences.pref_keys | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
| @ -189,30 +173,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 | ||||
|  | ||||
| @ -96,7 +96,7 @@ class SignupController < ApplicationController | ||||
|     session[:new_user] = nil | ||||
|     session[:validation_error] = nil | ||||
| 
 | ||||
|     UserManager::CreateAccount.call(account: { | ||||
|     CreateAccount.call(account: { | ||||
|       username: @user.cn, | ||||
|       domain: Setting.primary_domain, | ||||
|       email: @user.email, | ||||
|  | ||||
| @ -1,43 +0,0 @@ | ||||
| class WebKeyDirectoryController < WellKnownController | ||||
|   before_action :allow_cross_origin_requests | ||||
| 
 | ||||
|   # /.well-known/openpgpkey/hu/:hashed_username(.txt)?l=username | ||||
|   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? || | ||||
|        @user.pgp_pubkey.blank? || | ||||
|        !@user.pgp_pubkey_contains_user_address? | ||||
|       http_status :not_found and return | ||||
|     end | ||||
| 
 | ||||
|     if params[:hashed_username] != @user.wkd_hash | ||||
|       http_status :unprocessable_entity and return | ||||
|     end | ||||
| 
 | ||||
|     respond_to do |format| | ||||
|       format.text do | ||||
|         response.headers['Content-Type'] = 'text/plain' | ||||
|         render plain: @user.pgp_pubkey | ||||
|       end | ||||
| 
 | ||||
|       format.any do | ||||
|         key = @user.gnupg_key.export | ||||
|         send_data key, filename: "#{@user.wkd_hash}.pem", | ||||
|                        type: "application/octet-stream" | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def policy | ||||
|     head :ok | ||||
|   end | ||||
| end | ||||
| @ -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}" | ||||
| 
 | ||||
|     { | ||||
|  | ||||
| @ -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]) | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -1,8 +1,14 @@ | ||||
| import { Controller } from "@hotwired/stimulus" | ||||
| import { Nostrify } from "nostrify" | ||||
| 
 | ||||
| // Connects to data-controller="settings--nostr-pubkey"
 | ||||
| export default class extends Controller { | ||||
|   static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ] | ||||
|   static targets = [ | ||||
|     "noExtension", | ||||
|     "setPubkey", "pubkeyBech32Input", | ||||
|     "relayList", "relayListStatus", | ||||
|     "profileStatusNip05", "profileStatusLud16" | ||||
|   ] | ||||
|   static values  = { | ||||
|     userAddress: String, | ||||
|     pubkeyHex: String, | ||||
| @ -15,6 +21,14 @@ export default class extends Controller { | ||||
|       if (this.hasSetPubkeyTarget) { | ||||
|         this.setPubkeyTarget.disabled = false | ||||
|       } | ||||
| 
 | ||||
|       if (this.pubkeyHexValue) { | ||||
|         this.discoverUserOnNostr().then(() => { | ||||
|           this.renderRelayStatus() | ||||
|           this.renderProfileNip05Status() | ||||
|           this.renderProfileLud16Status() | ||||
|         }) | ||||
|       } | ||||
|     } else { | ||||
|       this.noExtensionTarget.classList.remove("hidden") | ||||
|     } | ||||
| @ -49,8 +63,172 @@ export default class extends Controller { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async discoverUserOnNostr () { | ||||
|     this.nip65Relays = await this.findUserRelays() | ||||
|     this.profile = await this.findUserProfile() | ||||
|   } | ||||
| 
 | ||||
|   async findUserRelays () { | ||||
|     const controller = new AbortController(); | ||||
|     const signal = controller.signal; | ||||
|     const filters = [{ kinds: [10002], authors: [this.pubkeyHexValue], limit: 1 }] | ||||
|     const messages = [] | ||||
| 
 | ||||
|     for await (const msg of this.discoveryPool.req(filters, { signal })) { | ||||
|       if (msg[0] === 'EVENT') { | ||||
|         if (!messages.find(m => m.id === msg[2].id)) { | ||||
|           messages.push(msg[2]) | ||||
|         } | ||||
|       } | ||||
|       if (msg[0] === 'EOSE') { break } | ||||
|     } | ||||
| 
 | ||||
|     // Close the relay subscription
 | ||||
|     controller.abort() | ||||
|     if (messages.length === 0) { return messages } | ||||
| 
 | ||||
|     const sortedMessages = messages.sort((a, b) => a.createdAt - b.createdAt) | ||||
|     const newestMessage = messages[messages.length - 1] | ||||
| 
 | ||||
|     return newestMessage.tags.filter(t => t[0] === 'r') | ||||
|                              .map(t => { return { url: t[1], marker: t[2] } }) | ||||
|   } | ||||
| 
 | ||||
|   async findUserProfile () { | ||||
|     const controller = new AbortController(); | ||||
|     const signal = controller.signal; | ||||
|     const filters = [{ kinds: [0], authors: [this.pubkeyHexValue], limit: 1 }] | ||||
|     const messages = [] | ||||
| 
 | ||||
|     for await (const msg of this.discoveryPool.req(filters, { signal })) { | ||||
|       if (msg[0] === 'EVENT') { | ||||
|         if (!messages.find(m => m.id === msg[2].id)) { | ||||
|           messages.push(msg[2]) | ||||
|         } | ||||
|       } | ||||
|       if (msg[0] === 'EOSE') { break } | ||||
|     } | ||||
| 
 | ||||
|     // Close the relay subscription
 | ||||
|     controller.abort() | ||||
|     if (messages.length === 0) { return null } | ||||
| 
 | ||||
|     const sortedMessages = messages.sort((a, b) => a.createdAt - b.createdAt) | ||||
|     const newestMessage = messages[messages.length - 1] | ||||
| 
 | ||||
|     return JSON.parse(newestMessage.content) | ||||
|   } | ||||
| 
 | ||||
|   renderRelayStatus () { | ||||
|     let showStatus | ||||
| 
 | ||||
|     if (this.nip65Relays.length > 0) { | ||||
|       if (this.relaysContainAccountsRelay) { | ||||
|         showStatus = 'green' | ||||
|       } else { | ||||
|         showStatus = 'orange' | ||||
|       } | ||||
|     } else { | ||||
|       showStatus = 'red' | ||||
|     } | ||||
|     // showStatus = 'red'
 | ||||
| 
 | ||||
|     this.relayListStatusTarget | ||||
|       .querySelector(`.status-${showStatus}`) | ||||
|       .classList.remove("hidden") | ||||
|   } | ||||
| 
 | ||||
|   renderProfileNip05Status () { | ||||
|     let showStatus | ||||
| 
 | ||||
|     if (this.profile?.nip05) { | ||||
|       if (this.profile.nip05 === this.userAddressValue) { | ||||
|         showStatus = 'green' | ||||
|       } else { | ||||
|         showStatus = 'red' | ||||
|       } | ||||
|     } else { | ||||
|       showStatus = 'orange' | ||||
|     } | ||||
| 
 | ||||
|     this.profileStatusNip05Target | ||||
|       .querySelector(`.status-${showStatus}`) | ||||
|       .classList.remove("hidden") | ||||
|   } | ||||
| 
 | ||||
|   renderProfileLud16Status () { | ||||
|     let showStatus | ||||
| 
 | ||||
|     if (this.profile?.lud16) { | ||||
|       if (this.profile.lud16 === this.userAddressValue) { | ||||
|         showStatus = 'green' | ||||
|       } else { | ||||
|         showStatus = 'red' | ||||
|       } | ||||
|     } else { | ||||
|       showStatus = 'orange' | ||||
|     } | ||||
| 
 | ||||
|     this.profileStatusLud16Target | ||||
|       .querySelector(`.status-${showStatus}`) | ||||
|       .classList.remove("hidden") | ||||
|   } | ||||
| 
 | ||||
|   // renderRelayList (relays) {
 | ||||
|   //   const html = relays.map(relay => `
 | ||||
|   //     <li class="flex items-center justify-between p-2 border-b">
 | ||||
|   //       <span>${relay.url}</span>
 | ||||
|   //       <button 
 | ||||
|   //         data-action="click->list#handleItemClick" 
 | ||||
|   //         data-item="${relay.url}"
 | ||||
|   //         class="bg-blue-500 text-white px-3 py-1 rounded">
 | ||||
|   //         Action
 | ||||
|   //       </button>
 | ||||
|   //     </li>
 | ||||
|   //   `).join("")
 | ||||
|   //
 | ||||
|   //   this.relayListTarget.innerHTML = html
 | ||||
|   // }
 | ||||
| 
 | ||||
|   get csrfToken () { | ||||
|     const element = document.head.querySelector('meta[name="csrf-token"]') | ||||
|     return element.getAttribute("content") | ||||
|   } | ||||
| 
 | ||||
|   // Used to find a user's profile and relays
 | ||||
|   get discoveryRelays () { | ||||
|     return [ | ||||
|       'ws://localhost:4777', | ||||
|       'wss://nostr.kosmos.org', | ||||
|       'wss://purplepag.es', | ||||
|       // 'wss://relay.nostr.band',
 | ||||
|       // 'wss://njump.me',
 | ||||
|       // 'wss://relay.damus.io',
 | ||||
|       // 'wss://nos.lol',
 | ||||
|       // 'wss://eden.nostr.land',
 | ||||
|       // 'wss://relay.snort.social',
 | ||||
|       // 'wss://nostr.wine',
 | ||||
|       // 'wss://relay.primal.net',
 | ||||
|       // 'wss://nostr.bitcoiner.social',
 | ||||
|     ] | ||||
|   } | ||||
| 
 | ||||
|   get discoveryPool () { | ||||
|     if (!this._discoveryPool) { | ||||
|       this._discoveryPool = new Nostrify.NPool({ | ||||
|         open: (url) => new Nostrify.NRelay1(url), | ||||
|         reqRouter: async (filters) => new Map( | ||||
|           this.discoveryRelays.map(relayUrl => [ relayUrl, filters ]) | ||||
|         ), | ||||
|         eventRouter: async (event) => [], | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     return this._discoveryPool | ||||
|   } | ||||
| 
 | ||||
|   get relaysContainAccountsRelay () { | ||||
|     // TODO use URL from view/settings
 | ||||
|     return !!this.nip65Relays.find(r => r.url.match('wss://nostr.kosmos.org')) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -10,7 +10,9 @@ class BtcpayCheckDonationJob < ApplicationJob | ||||
| 
 | ||||
|     case invoice["status"] | ||||
|     when "Settled" | ||||
|       donation.complete! | ||||
|       donation.paid_at = DateTime.now | ||||
|       donation.payment_status = "settled" | ||||
|       donation.save! | ||||
| 
 | ||||
|       NotificationMailer.with(user: donation.user) | ||||
|                         .bitcoin_donation_confirmed | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -1,7 +0,0 @@ | ||||
| class XmppSetAvatarJob < ApplicationJob | ||||
|   queue_as :default | ||||
| 
 | ||||
|   def perform(user:, overwrite: false) | ||||
|     EjabberdManager::SetAvatar.call(user:, overwrite:) | ||||
|   end | ||||
| end | ||||
| @ -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 | ||||
|  | ||||
| @ -1,90 +1,3 @@ | ||||
| class ApplicationMailer < ActionMailer::Base | ||||
|   default Rails.application.config.action_mailer.default_options | ||||
|   layout 'mailer' | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|     def send_mail | ||||
|       @template ||= "#{self.class.name.underscore}/#{caller[0][/`([^']*)'/, 1]}" | ||||
|       headers['Message-ID'] = message_id | ||||
| 
 | ||||
|       if @user.pgp_pubkey.present? | ||||
|         mail(to: @user.email, subject: "...", content_type: pgp_content_type) do |format| | ||||
|           format.text { render plain: pgp_content } | ||||
|         end | ||||
|       else | ||||
|         mail(to: @user.email, subject: @subject) do |format| | ||||
|           format.text { render @template } | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def from_address | ||||
|       self.class.default[:from] | ||||
|     end | ||||
| 
 | ||||
|     def from_domain | ||||
|       Mail::Address.new(from_address).domain | ||||
|     end | ||||
| 
 | ||||
|     def message_id | ||||
|       @message_id ||= "#{SecureRandom.uuid}@#{from_domain}" | ||||
|     end | ||||
| 
 | ||||
|     def boundary | ||||
|       @boundary ||= SecureRandom.hex(8) | ||||
|     end | ||||
| 
 | ||||
|     def pgp_content_type | ||||
|       "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"------------#{boundary}\"" | ||||
|     end | ||||
| 
 | ||||
|     def pgp_nested_content | ||||
|       message_content = render_to_string(template: @template) | ||||
|       message_content_base64 = Base64.encode64(message_content) | ||||
|       nested_boundary = SecureRandom.hex(8) | ||||
| 
 | ||||
|       <<~NESTED_CONTENT | ||||
|         Content-Type: multipart/mixed; boundary="------------#{nested_boundary}"; protected-headers="v1" | ||||
|         Subject: #{@subject} | ||||
|         From: <#{from_address}> | ||||
|         To: #{@user.display_name || @user.cn} <#{@user.email}> | ||||
|         Message-ID: <#{message_id}> | ||||
| 
 | ||||
|         --------------#{nested_boundary} | ||||
|         Content-Type: text/plain; charset=UTF-8; format=flowed | ||||
|         Content-Transfer-Encoding: base64 | ||||
| 
 | ||||
|         #{message_content_base64} | ||||
| 
 | ||||
|         --------------#{nested_boundary}-- | ||||
|       NESTED_CONTENT | ||||
|     end | ||||
| 
 | ||||
|     def pgp_content | ||||
|       encrypted_content = UserManager::PgpEncrypt.call(user: @user, text: pgp_nested_content) | ||||
|       encrypted_base64 = Base64.encode64(encrypted_content.to_s) | ||||
| 
 | ||||
|       <<~EMAIL_CONTENT | ||||
|         This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156) | ||||
|         --------------#{boundary} | ||||
|         Content-Type: application/pgp-encrypted | ||||
|         Content-Description: PGP/MIME version identification | ||||
| 
 | ||||
|         Version: 1 | ||||
| 
 | ||||
|         --------------#{boundary} | ||||
|         Content-Type: application/octet-stream; name="encrypted.asc" | ||||
|         Content-Description: OpenPGP encrypted message | ||||
|         Content-Disposition: inline; filename="encrypted.asc" | ||||
| 
 | ||||
|         -----BEGIN PGP MESSAGE----- | ||||
| 
 | ||||
|         #{encrypted_base64} | ||||
| 
 | ||||
|         -----END PGP MESSAGE----- | ||||
| 
 | ||||
|         --------------#{boundary}-- | ||||
|       EMAIL_CONTENT | ||||
|     end | ||||
| end | ||||
|  | ||||
| @ -18,6 +18,6 @@ class CustomMailer < ApplicationMailer | ||||
|     @user = params[:user] | ||||
|     @subject = params[:subject] | ||||
|     @body = params[:body] | ||||
|     send_mail | ||||
|     mail(to: @user.email, subject: @subject) | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -3,7 +3,7 @@ class NotificationMailer < ApplicationMailer | ||||
|     @user = params[:user] | ||||
|     @amount_sats = params[:amount_sats] | ||||
|     @subject = "Sats received" | ||||
|     send_mail | ||||
|     mail to: @user.email, subject: @subject | ||||
|   end | ||||
| 
 | ||||
|   def remotestorage_auth_created | ||||
| @ -15,19 +15,19 @@ class NotificationMailer < ApplicationMailer | ||||
|       "#{access} #{directory}" | ||||
|     end | ||||
|     @subject = "New app connected to your storage" | ||||
|     send_mail | ||||
|     mail to: @user.email, subject: @subject | ||||
|   end | ||||
| 
 | ||||
|   def new_invitations_available | ||||
|     @user = params[:user] | ||||
|     @subject = "New invitations added to your account" | ||||
|     send_mail | ||||
|     mail to: @user.email, subject: @subject | ||||
|   end | ||||
| 
 | ||||
|   def bitcoin_donation_confirmed | ||||
|     @user = params[:user] | ||||
|     @donation = params[:donation] | ||||
|     @subject = "Donation confirmed" | ||||
|     send_mail | ||||
|     mail to: @user.email, subject: @subject | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|   scope :completed, -> { where(payment_status: "settled") } | ||||
| 
 | ||||
|   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 | ||||
|  | ||||
| @ -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 | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -3,10 +3,9 @@ require 'nostr' | ||||
| class User < ApplicationRecord | ||||
|   include EmailValidatable | ||||
| 
 | ||||
|   attr_accessor :current_password | ||||
|   attr_accessor :display_name | ||||
|   attr_accessor :avatar_new | ||||
|   attr_accessor :pgp_pubkey | ||||
|   attr_accessor :current_password | ||||
| 
 | ||||
|   serialize :preferences, coder: UserPreferences | ||||
| 
 | ||||
| @ -23,16 +22,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,11 +49,8 @@ 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? } | ||||
| 
 | ||||
|   # | ||||
|   # Scopes | ||||
|   # | ||||
| @ -73,7 +63,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 +74,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,11 +117,11 @@ class User < ApplicationRecord | ||||
|   end | ||||
| 
 | ||||
|   def is_admin? | ||||
|     @admin ||= if admin = Devise::LDAP::Adapter.get_ldap_param(self.cn, :admin) | ||||
|                  !!admin.first | ||||
|                else | ||||
|                  false | ||||
|                end | ||||
|     admin ||= if admin = Devise::LDAP::Adapter.get_ldap_param(self.cn, :admin) | ||||
|                 !!admin.first | ||||
|               else | ||||
|                 false | ||||
|               end | ||||
|   end | ||||
| 
 | ||||
|   def address | ||||
| @ -163,41 +149,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 | ||||
| @ -207,22 +165,8 @@ class User < ApplicationRecord | ||||
|     Nostr::PublicKey.new(nostr_pubkey).to_bech32 | ||||
|   end | ||||
| 
 | ||||
|   def pgp_pubkey | ||||
|     @pgp_pubkey ||= ldap_entry[:pgp_key] | ||||
|   end | ||||
| 
 | ||||
|   def gnupg_key | ||||
|     return nil unless pgp_pubkey.present? | ||||
|     GPGME::Key.import(pgp_pubkey) | ||||
|     GPGME::Key.get(pgp_fpr) | ||||
|   end | ||||
| 
 | ||||
|   def pgp_pubkey_contains_user_address? | ||||
|     gnupg_key.uids.map(&:email).include?(address) | ||||
|   end | ||||
| 
 | ||||
|   def wkd_hash | ||||
|     ZBase32.encode(Digest::SHA1.digest(cn)) | ||||
|   def avatar | ||||
|     @avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn) | ||||
|   end | ||||
| 
 | ||||
|   def services_enabled | ||||
| @ -234,39 +178,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 +206,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"] | ||||
| @ -288,10 +214,4 @@ class User < ApplicationRecord | ||||
|         errors.add(:avatar, "must be a JPEG or PNG file") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def acceptable_pgp_key_format | ||||
|       unless GPGME::Key.valid?(pgp_pubkey) | ||||
|         errors.add(:pgp_pubkey, 'is not a valid armored PGP public key block') | ||||
|       end | ||||
|     end | ||||
| end | ||||
|  | ||||
| @ -1,22 +1,36 @@ | ||||
| # | ||||
| # API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/ | ||||
| # | ||||
| class BtcpayManagerService < RestApiService | ||||
| class BtcpayManagerService < ApplicationService | ||||
|   private | ||||
| 
 | ||||
|   def base_url | ||||
|     @base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}" | ||||
|   end | ||||
|     def base_url | ||||
|       @base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}" | ||||
|     end | ||||
| 
 | ||||
|   def auth_token | ||||
|     @auth_token ||= Setting.btcpay_auth_token | ||||
|   end | ||||
|     def auth_token | ||||
|       @auth_token ||= Setting.btcpay_auth_token | ||||
|     end | ||||
| 
 | ||||
|   def headers | ||||
|     { | ||||
|       "Content-Type" => "application/json", | ||||
|       "Accept" => "application/json", | ||||
|       "Authorization" => "token #{auth_token}" | ||||
|     } | ||||
|   end | ||||
|     def headers | ||||
|       { | ||||
|         "Content-Type" => "application/json", | ||||
|         "Accept" => "application/json", | ||||
|         "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 | ||||
|  | ||||
							
								
								
									
										54
									
								
								app/services/create_account.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/services/create_account.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| class CreateAccount < ApplicationService | ||||
|   def initialize(account:) | ||||
|     @username   = account[:username] | ||||
|     @domain     = account[:ou] || Setting.primary_domain | ||||
|     @email      = account[:email] | ||||
|     @password   = account[:password] | ||||
|     @invitation = account[:invitation] | ||||
|     @confirmed  = account[:confirmed] | ||||
|   end | ||||
| 
 | ||||
|   def call | ||||
|     user = create_user_in_database | ||||
|     add_ldap_document | ||||
|     create_lndhub_account(user) if Setting.lndhub_enabled | ||||
| 
 | ||||
|     if @invitation.present? | ||||
|       update_invitation(user.id) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def create_user_in_database | ||||
|     User.create!( | ||||
|       cn: @username, | ||||
|       ou: @domain, | ||||
|       email: @email, | ||||
|       password: @password, | ||||
|       password_confirmation: @password, | ||||
|       confirmed_at: @confirmed ? DateTime.now : nil | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   def update_invitation(user_id) | ||||
|     @invitation.update! invited_user_id: user_id, used_at: DateTime.now | ||||
|   end | ||||
| 
 | ||||
|   def add_ldap_document | ||||
|     hashed_pw = Devise.ldap_auth_password_builder.call(@password) | ||||
|     CreateLdapUserJob.perform_later( | ||||
|       username: @username, | ||||
|       domain: @domain, | ||||
|       email: @email, | ||||
|       hashed_pw: hashed_pw, | ||||
|       confirmed: @confirmed | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   def create_lndhub_account(user) | ||||
|     #TODO enable in development when we have a local lndhub (mock?) API | ||||
|     return if Rails.env.development? | ||||
|     CreateLndhubAccountJob.perform_later(user) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										17
									
								
								app/services/create_invitations.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/services/create_invitations.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| class CreateInvitations < ApplicationService | ||||
|   def initialize(user:, amount:, notify: true) | ||||
|     @user = user | ||||
|     @amount = amount | ||||
|     @notify = notify | ||||
|   end | ||||
| 
 | ||||
|   def call | ||||
|     @amount.times do | ||||
|       Invitation.create(user: @user) | ||||
|     end | ||||
| 
 | ||||
|     if @notify | ||||
|       NotificationMailer.with(user: @user).new_invitations_available.deliver_later | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										29
									
								
								app/services/ejabberd_api_client.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								app/services/ejabberd_api_client.rb
									
									
									
									
									
										Normal 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 | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -1,11 +0,0 @@ | ||||
| module EjabberdManager | ||||
|   class SendMessage < EjabberdManagerService | ||||
|     def initialize(payload:) | ||||
|       @payload = payload | ||||
|     end | ||||
| 
 | ||||
|     def call | ||||
|       send_message @payload | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -5,12 +5,12 @@ module LdapManager | ||||
|     end | ||||
| 
 | ||||
|     def call | ||||
|       treebase = ldap_config["base"] | ||||
|       treebase   = ldap_config["base"] | ||||
|       attributes = %w{ jpegPhoto } | ||||
|       filter = Net::LDAP::Filter.eq("cn", @cn) | ||||
|       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 | ||||
|  | ||||
| @ -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| | ||||
|         processed = ImageProcessing::Vips | ||||
|           .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 | ||||
|     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 | ||||
|  | ||||
| @ -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 | ||||
|       replace_attribute @dn, :displayName, @display_name | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -1,16 +0,0 @@ | ||||
| module LdapManager | ||||
|   class UpdatePgpKey < LdapManagerService | ||||
|     def initialize(dn:, pubkey:) | ||||
|       @dn = dn | ||||
|       @pubkey = pubkey | ||||
|     end | ||||
| 
 | ||||
|     def call | ||||
|       if @pubkey.present? | ||||
|         replace_attribute @dn, :pgpKey, @pubkey | ||||
|       else | ||||
|         delete_attribute @dn, :pgpKey | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -50,17 +50,19 @@ class LdapService < ApplicationService | ||||
|   end | ||||
| 
 | ||||
|   def fetch_users(args={}) | ||||
|     attributes = %w[ | ||||
|       dn cn uid mail displayName admin serviceEnabled memberStatus | ||||
|       mailRoutingAddress mailpassword nostrKey pgpKey | ||||
|     ] | ||||
|     filter = Net::LDAP::Filter.eq('objectClass', 'person') & | ||||
|              Net::LDAP::Filter.eq("cn", args[:cn] || "*") | ||||
|     if args[:ou] | ||||
|       treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}" | ||||
|     else | ||||
|       treebase = ldap_config["base"] | ||||
|     end | ||||
| 
 | ||||
|     entries = client.search( | ||||
|       base: ldap_config["base"], filter: filter, | ||||
|       attributes: attributes | ||||
|     ) | ||||
|     attributes = %w[ | ||||
|       dn cn uid mail displayName admin serviceEnabled | ||||
|       mailRoutingAddress mailpassword nostrKey | ||||
|     ] | ||||
|     filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") | ||||
| 
 | ||||
|     entries = client.search(base: treebase, filter: filter, attributes: attributes) | ||||
|     entries.sort_by! { |e| e.cn[0] } | ||||
|     entries = entries.collect do |e| | ||||
|       { | ||||
| @ -69,29 +71,17 @@ 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, | ||||
|         pgp_key: e.try(:pgpKey) ? e.pgpKey.first : nil | ||||
|         nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil | ||||
|       } | ||||
|     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) | ||||
| @ -111,7 +101,7 @@ class LdapService < ApplicationService | ||||
|     dn = "ou=#{ou},cn=users,#{ldap_suffix}" | ||||
| 
 | ||||
|     aci = <<-EOS | ||||
| (target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || pgpKey || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";) | ||||
| (target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";) | ||||
|     EOS | ||||
| 
 | ||||
|     attrs = { | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -1,56 +0,0 @@ | ||||
| module UserManager | ||||
|   class CreateAccount < UserManagerService | ||||
|     def initialize(account:) | ||||
|       @username   = account[:username] | ||||
|       @domain     = account[:ou] || Setting.primary_domain | ||||
|       @email      = account[:email] | ||||
|       @password   = account[:password] | ||||
|       @invitation = account[:invitation] | ||||
|       @confirmed  = account[:confirmed] | ||||
|     end | ||||
| 
 | ||||
|     def call | ||||
|       user = create_user_in_database | ||||
|       add_ldap_document | ||||
|       create_lndhub_account(user) if Setting.lndhub_enabled | ||||
| 
 | ||||
|       if @invitation.present? | ||||
|         update_invitation(user.id) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def create_user_in_database | ||||
|       User.create!( | ||||
|         cn: @username, | ||||
|         ou: @domain, | ||||
|         email: @email, | ||||
|         password: @password, | ||||
|         password_confirmation: @password, | ||||
|         confirmed_at: @confirmed ? DateTime.now : nil | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     def update_invitation(user_id) | ||||
|       @invitation.update! invited_user_id: user_id, used_at: DateTime.now | ||||
|     end | ||||
| 
 | ||||
|     def add_ldap_document | ||||
|       hashed_pw = Devise.ldap_auth_password_builder.call(@password) | ||||
|       CreateLdapUserJob.perform_later( | ||||
|         username: @username, | ||||
|         domain: @domain, | ||||
|         email: @email, | ||||
|         hashed_pw: hashed_pw, | ||||
|         confirmed: @confirmed | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     def create_lndhub_account(user) | ||||
|       #TODO enable in development when we have a local lndhub (mock?) API | ||||
|       return if Rails.env.development? | ||||
|       CreateLndhubAccountJob.perform_later(user) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1,19 +0,0 @@ | ||||
| module UserManager | ||||
|   class CreateInvitations < UserManagerService | ||||
|     def initialize(user:, amount:, notify: true) | ||||
|       @user = user | ||||
|       @amount = amount | ||||
|       @notify = notify | ||||
|     end | ||||
| 
 | ||||
|     def call | ||||
|       @amount.times do | ||||
|         Invitation.create(user: @user) | ||||
|       end | ||||
| 
 | ||||
|       if @notify | ||||
|         NotificationMailer.with(user: @user).new_invitations_available.deliver_later | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -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 | ||||
| @ -1,19 +0,0 @@ | ||||
| require 'gpgme' | ||||
| 
 | ||||
| module UserManager | ||||
|   class PgpEncrypt < UserManagerService | ||||
|     def initialize(user:, text:) | ||||
|       @user = user | ||||
|       @text = text | ||||
|     end | ||||
| 
 | ||||
|     def call | ||||
|       crypto = GPGME::Crypto.new | ||||
|       crypto.encrypt( | ||||
|         @text, | ||||
|         recipients: @user.gnupg_key, | ||||
|         always_trust: true | ||||
|       ) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -1,24 +0,0 @@ | ||||
| module UserManager | ||||
|   class UpdatePgpKey < UserManagerService | ||||
|     def initialize(user:) | ||||
|       @user = user | ||||
|     end | ||||
| 
 | ||||
|     def call | ||||
|       if @user.pgp_pubkey.blank? | ||||
|         @user.update! pgp_fpr: nil | ||||
|       else | ||||
|         result = GPGME::Key.import(@user.pgp_pubkey) | ||||
| 
 | ||||
|         if result.imports.present? | ||||
|           @user.update! pgp_fpr: result.imports.first.fpr | ||||
|         else | ||||
|           # TODO notify Sentry, user | ||||
|           raise "Failed to import OpenPGP pubkey" | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       LdapManager::UpdatePgpKey.call(dn: @user.dn, pubkey: @user.pgp_pubkey) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -1,2 +0,0 @@ | ||||
| class UserManagerService < ApplicationService | ||||
| end | ||||
| @ -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 %> | ||||
| @ -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"> | ||||
|  | ||||
| @ -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 %> | ||||
| @ -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> | ||||
|   <% end %> | ||||
| 
 | ||||
|   <section> | ||||
|   <% if @donations.present? %> | ||||
|     <h3>Received</h3> | ||||
|     <%= render partial: "admin/donations/list", locals: { | ||||
|           donations: @donations, pagy: @pagy | ||||
|         } %> | ||||
|   <% 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 %> | ||||
|       </tbody> | ||||
|     </table> | ||||
|     <%== pagy_nav @pagy %> | ||||
|   <% else %> | ||||
|     <p> | ||||
|       No donations received yet. | ||||
|       No donations yet. | ||||
|     </p> | ||||
|   <% end %> | ||||
|   </section> | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user