1 Commits

Author SHA1 Message Date
0bd77bc37a WIP Add service accounts and ACIs
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-28 10:57:12 +04:00
212 changed files with 1433 additions and 5629 deletions

View File

@@ -29,7 +29,6 @@
# #
# Service Integrations # Service Integrations
# (sorted alphabetically by service name)
# #
# BTCPAY_PUBLIC_URL='https://btcpay.example.com' # BTCPAY_PUBLIC_URL='https://btcpay.example.com'
@@ -63,9 +62,5 @@
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org' # MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
# NOSTR_PRIVATE_KEY='123456abcdef...'
# NOSTR_PUBLIC_KEY='123456abcdef...'
# NOSTR_RELAY_URL='wss://nostr.kosmos.org'
# RS_STORAGE_URL='https://storage.kosmos.org' # RS_STORAGE_URL='https://storage.kosmos.org'
# RS_REDIS_URL='redis://localhost:6379/2' # RS_REDIS_URL='redis://localhost:6379/2'

View File

@@ -1,5 +1,4 @@
PRIMARY_DOMAIN=kosmos.org PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.kosmos.org
REDIS_URL='redis://localhost:6379/0' REDIS_URL='redis://localhost:6379/0'
@@ -12,15 +11,10 @@ DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
EJABBERD_API_URL='http://xmpp.example.com/api' EJABBERD_API_URL='http://xmpp.example.com/api'
MASTODON_PUBLIC_URL='http://example.social'
LNDHUB_API_URL='http://localhost:3026' LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946' LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
RS_STORAGE_URL='https://storage.kosmos.org' RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/1' RS_REDIS_URL='redis://localhost:6379/1'

4
.gitignore vendored
View File

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

View File

@@ -1,11 +1,18 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM ruby:3.3.4 FROM debian:bullseye-slim as base
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \ # TODO Remove when upstream Ruby works properly on Apple silicon
ldap-utils tini libvips RUN apt update && apt install -y build-essential wget autoconf libpq-dev pkg-config
RUN wget https://github.com/postmodern/ruby-install/releases/download/v0.9.3/ruby-install-0.9.3.tar.gz \
&& tar -xzvf ruby-install-0.9.3.tar.gz \
&& cd ruby-install-0.9.3/ \
&& make install
RUN ruby-install -p https://github.com/ruby/ruby/pull/9371.diff ruby 3.3.0
ENV PATH="/opt/rubies/ruby-3.3.0/bin:${PATH}"
RUN apt-get install -y --no-install-recommends curl ldap-utils tini libvips
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get install -y nodejs RUN apt-get update && apt-get install -y nodejs

23
Gemfile
View File

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

View File

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

View File

@@ -57,7 +57,7 @@ Running the test suite:
Running the test suite with Docker Compose requires overriding the Rails Running the test suite with Docker Compose requires overriding the Rails
environment: environment:
docker-compose exec -e "RAILS_ENV=test" web rspec docker-compose run -e "RAILS_ENV=test" web rspec
### Docker Compose ### Docker Compose

View File

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

View File

@@ -42,11 +42,6 @@
focus:ring-red-500 focus:ring-opacity-75; focus:ring-red-500 focus:ring-opacity-75;
} }
.btn-outline-purple {
@apply border-2 border-purple-500 hover:bg-purple-100
focus:ring-purple-400 focus:ring-opacity-75;
}
.btn:disabled { .btn:disabled {
@apply bg-gray-100 hover:bg-gray-200 text-gray-400 @apply bg-gray-100 hover:bg-gray-200 text-gray-400
focus:ring-gray-300 focus:ring-opacity-75; focus:ring-gray-300 focus:ring-opacity-75;

View File

@@ -1,5 +1,5 @@
@layer components { @layer components {
.services > div > a { .services > div > a {
background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 20%, rgba(255,255,255,0.88) 100%); background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 0, rgba(255,255,255,0.88) 100%);
} }
} }

View File

@@ -6,7 +6,6 @@
) do %> ) do %>
<%= method("#{@type}_field").call :setting, @key, <%= method("#{@type}_field").call :setting, @key,
value: Setting.public_send(@key), value: Setting.public_send(@key),
placeholder: @placeholder,
data: { data: {
:'default-value' => Setting.get_field(@key)[:default] :'default-value' => Setting.get_field(@key)[:default]
}, },

View File

@@ -2,7 +2,7 @@
module FormElements module FormElements
class FieldsetResettableSettingComponent < ViewComponent::Base class FieldsetResettableSettingComponent < ViewComponent::Base
def initialize(tag: "li", key:, type: :text, title:, description: nil, placeholder: nil) def initialize(tag: "li", key:, type: :text, title:, description: nil)
@tag = tag @tag = tag
@positioning = :vertical @positioning = :vertical
@title = title @title = title
@@ -10,7 +10,6 @@ module FormElements
@key = key.to_sym @key = key.to_sym
@type = type @type = type
@resettable = is_resettable?(@key) @resettable = is_resettable?(@key)
@placeholder = placeholder
end end
def is_resettable?(key) def is_resettable?(key)

View File

@@ -6,7 +6,7 @@
<div class="flex flex-col"> <div class="flex flex-col">
<label class="font-bold mb-1"><%= @title %></label> <label class="font-bold mb-1"><%= @title %></label>
<% if @description.present? %> <% if @description.present? %>
<p class="text-gray-500"><%= @description %></p> <p class="text-gray-500"><%= @descripton %></p>
<% end %> <% end %>
</div> </div>
<div class="relative ml-4 inline-flex flex-shrink-0"> <div class="relative ml-4 inline-flex flex-shrink-0">

View File

@@ -12,7 +12,7 @@ module FormElements
@enabled = enabled @enabled = enabled
@input_enabled = input_enabled @input_enabled = input_enabled
@title = title @title = title
@description = description @descripton = description
@button_text = @enabled ? "Switch off" : "Switch on" @button_text = @enabled ? "Switch off" : "Switch on"
end end
end end

View File

@@ -9,12 +9,4 @@ class Admin::Settings::RegistrationsController < Admin::SettingsController
success: "Settings saved" success: "Settings saved"
} }
end end
private
def setting_params
params.require(:setting).permit([
:reserved_usernames, default_services: []
])
end
end end

View File

@@ -9,12 +9,11 @@ class Admin::SettingsController < Admin::BaseController
changed_keys = [] changed_keys = []
setting_params.keys.each do |key| setting_params.keys.each do |key|
next if clean_param(key).nil? || next if setting_params[key].nil? ||
(Setting.send(key).to_s == clean_param(key)) (Setting.send(key).to_s == setting_params[key].strip)
changed_keys.push(key) changed_keys.push(key)
setting = Setting.new(var: key) setting = Setting.new(var: key)
setting.value = clean_param(key) setting.value = setting_params[key].strip
unless setting.valid? unless setting.valid?
@errors.merge!(setting.errors) @errors.merge!(setting.errors)
end end
@@ -25,7 +24,7 @@ class Admin::SettingsController < Admin::BaseController
end end
changed_keys.each do |key| changed_keys.each do |key|
Setting.send("#{key}=", clean_param(key)) Setting.send("#{key}=", setting_params[key].strip)
end end
end end
@@ -38,12 +37,4 @@ class Admin::SettingsController < Admin::BaseController
def setting_params def setting_params
params.require(:setting).permit(Setting.editable_keys.map(&:to_sym)) params.require(:setting).permit(Setting.editable_keys.map(&:to_sym))
end end
def clean_param(key)
if Setting.get_field(key)[:type] == :string
setting_params[key].strip
else
setting_params[key]
end
end
end end

View File

@@ -30,7 +30,7 @@ class Admin::UsersController < Admin::BaseController
amount = params[:amount].to_i amount = params[:amount].to_i
notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user]) 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: { redirect_to admin_user_path(@user.cn), flash: {
success: "Added #{amount} invitations to #{@user.cn}'s account" success: "Added #{amount} invitations to #{@user.cn}'s account"

View File

@@ -63,9 +63,4 @@ class ApplicationController < ActionController::Base
@fetch_balance_retried = true @fetch_balance_retried = true
lndhub_fetch_balance lndhub_fetch_balance
end end
def nostr_event_from_params
params.permit!
params[:signed_event].to_h.symbolize_keys
end
end end

View File

@@ -1,15 +1,13 @@
class LnurlpayController < ApplicationController class LnurlpayController < ApplicationController
before_action :check_service_available before_action :check_service_available
before_action :find_user before_action :find_user
before_action :set_cors_access_control_headers
MIN_SATS = 10 MIN_SATS = 10
MAX_SATS = 1_000_000 MAX_SATS = 1_000_000
MAX_COMMENT_CHARS = 100 MAX_COMMENT_CHARS = 100
# GET /.well-known/lnurlp/:username
def index def index
res = { render json: {
status: "OK", status: "OK",
callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice", callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice",
tag: "payRequest", tag: "payRequest",
@@ -18,16 +16,8 @@ class LnurlpayController < ApplicationController
metadata: metadata(@user.address), metadata: metadata(@user.address),
commentAllowed: MAX_COMMENT_CHARS commentAllowed: MAX_COMMENT_CHARS
} }
if Setting.nostr_enabled?
res[:allowsNostr] = true
res[:nostrPubkey] = Setting.nostr_public_key
end
render json: res
end end
# GET /.well-known/keysend/:username
def keysend def keysend
http_status :not_found and return unless Setting.lndhub_keysend_enabled? http_status :not_found and return unless Setting.lndhub_keysend_enabled?
@@ -42,9 +32,8 @@ class LnurlpayController < ApplicationController
} }
end end
# GET /lnurlpay/:username/invoice
def invoice def invoice
amount = params[:amount].to_i / 1000 # msats to sats amount = params[:amount].to_i / 1000 # msats
comment = params[:comment] || "" comment = params[:comment] || ""
address = @user.address address = @user.address
@@ -53,109 +42,53 @@ class LnurlpayController < ApplicationController
return return
end end
if params[:nostr].present? && Setting.nostr_enabled? if !valid_comment?(comment)
handle_zap_request amount, params[:nostr], params[:lnurl] render json: { status: "ERROR", reason: "Comment too long" }
else return
handle_pay_request address, amount, comment
end end
memo = "To #{address}"
memo = "#{memo}: \"#{comment}\"" if comment.present?
payment_request = @user.ln_create_invoice({
amount: amount, # we create invoices in sats
memo: memo,
description_hash: Digest::SHA2.hexdigest(metadata(address)),
})
render json: {
status: "OK",
successAction: {
tag: "message",
message: "Sats received. Thank you!"
},
routes: [],
pr: payment_request
}
end end
private private
def set_cors_access_control_headers def find_user
headers['Access-Control-Allow-Origin'] = "*" @user = User.where(cn: params[:username], ou: Setting.primary_domain).first
headers['Access-Control-Allow-Headers'] = "*" http_status :not_found if @user.nil?
headers['Access-Control-Allow-Methods'] = "GET" end
end
def check_service_available def metadata(address)
http_status :not_found unless Setting.lndhub_enabled? "[[\"text/identifier\", \"#{address}\"], [\"text/plain\", \"Send sats, receive thanks.\"]]"
end end
def find_user def valid_amount?(amount_in_sats)
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
http_status :not_found if @user.nil? end
end
def metadata(address) def valid_comment?(comment)
"[[\"text/identifier\",\"#{address}\"],[\"text/plain\",\"Sats for #{address}\"]]" comment.length <= MAX_COMMENT_CHARS
end end
def valid_amount?(amount_in_sats) private
amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
end
def valid_comment?(comment) def check_service_available
comment.length <= MAX_COMMENT_CHARS http_status :not_found unless Setting.lndhub_enabled?
end end
def handle_pay_request(address, amount, comment)
if !valid_comment?(comment)
render json: { status: "ERROR", reason: "Comment too long" }
return
end
desc = "To #{address}"
desc = "#{desc}: \"#{comment}\"" if comment.present?
invoice = LndhubManager::CreateUserInvoice.call(
user: @user, payload: {
amount: amount, # sats
description: desc,
description_hash: Digest::SHA256.hexdigest(metadata(address)),
}
)
render json: {
status: "OK",
successAction: {
tag: "message",
message: "Sats received. Thank you!"
},
routes: [],
pr: invoice["payment_request"]
}
end
def nostr_event_from_payload(nostr_param)
event_obj = JSON.parse(nostr_param).transform_keys(&:to_sym)
Nostr::Event.new(**event_obj)
rescue => e
return nil
end
def valid_zap_request?(amount, event, lnurl)
NostrManager::VerifyZapRequest.call(
amount: amount, event: event, lnurl: lnurl
)
end
def handle_zap_request(amount, nostr_param, lnurl_param)
event = nostr_event_from_payload(nostr_param)
unless event.present? && valid_zap_request?(amount*1000, event, lnurl_param)
render json: { status: "ERROR", reason: "Invalid zap request" }
return
end
# TODO might want to use the existing invoice and zap record if there are
# multiple calls with the same zap request
desc = "Zap for #{@user.address}"
desc = "#{desc}: \"#{event.content}\"" if event.content.present?
invoice = LndhubManager::CreateUserInvoice.call(
user: @user, payload: {
amount: amount, # sats
description: desc,
description_hash: Digest::SHA256.hexdigest(event.to_json),
}
)
@user.zaps.create! request: event,
payment_request: invoice["payment_request"],
amount: amount
render json: { status: "OK", pr: invoice["payment_request"] }
end
end end

View File

@@ -3,7 +3,7 @@ class Services::ChatController < Services::BaseController
before_action :require_service_available before_action :require_service_available
def show def show
@service_enabled = current_user.service_enabled?(:ejabberd) @service_enabled = current_user.services_enabled.include?(:xmpp)
end end
private private

View File

@@ -3,7 +3,7 @@ class Services::MastodonController < Services::BaseController
before_action :require_service_available before_action :require_service_available
def show def show
@service_enabled = current_user.service_enabled?(:mastodon) @service_enabled = current_user.services_enabled.include?(:mastodon)
end end
private private

View File

@@ -5,10 +5,11 @@ class Services::RemotestorageController < Services::BaseController
# Dashboard # Dashboard
def show def show
# unless current_user.service_enabled?(:remotestorage) # unless current_user.services_enabled.include?(:remotestorage)
# redirect_to service_remotestorage_info_path # redirect_to service_remotestorage_info_path
# end # end
# @rs_apps_connected = current_user.remote_storage_authorizations.any? @rs_auths = current_user.remote_storage_authorizations
# TODO sort by app name
end end
private private

View File

@@ -3,18 +3,13 @@ class Services::RsAuthsController < Services::BaseController
before_action :require_feature_enabled before_action :require_feature_enabled
before_action :require_service_available before_action :require_service_available
# before_action :require_service_enabled # before_action :require_service_enabled
before_action :find_rs_auth, only: [:destroy, :launch_app] before_action :find_rs_auth
def index
@rs_auths = current_user.remote_storage_authorizations
# TODO sort by app name?
end
def destroy def destroy
@auth.destroy! @auth.destroy!
respond_to do |format| respond_to do |format|
format.html do redirect_to apps_services_storage_url, flash: { format.html do redirect_to services_storage_url, flash: {
success: 'App authorization revoked' success: 'App authorization revoked'
} }
end end

View File

@@ -12,21 +12,15 @@ class SettingsController < ApplicationController
end end
def show def show
case @settings_section if @settings_section == "nostr"
when "lightning"
@notifications_enabled = @user.preferences[:lightning_notify_sats_received] != "disabled" ||
@user.preferences[:lightning_notify_zap_received] != "disabled"
when "nostr"
session[:shared_secret] ||= SecureRandom.base64(12) session[:shared_secret] ||= SecureRandom.base64(12)
end end
end end
# PUT /settings/:section
def update def update
@user.preferences.merge!(user_params[:preferences] || {}) @user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name] @user.display_name = user_params[:display_name]
@user.avatar_new = user_params[:avatar] @user.avatar_new = user_params[:avatar]
@user.pgp_pubkey = user_params[:pgp_pubkey]
if @user.save if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name]) if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
@@ -37,10 +31,6 @@ class SettingsController < ApplicationController
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new) LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
end end
if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key])
UserManager::UpdatePgpKey.call(user: @user)
end
redirect_to setting_path(@settings_section), flash: { redirect_to setting_path(@settings_section), flash: {
success: 'Settings saved.' success: 'Settings saved.'
} }
@@ -50,7 +40,6 @@ class SettingsController < ApplicationController
end end
end end
# POST /settings/update_email
def update_email def update_email
if @user.valid_ldap_authentication?(security_params[:current_password]) if @user.valid_ldap_authentication?(security_params[:current_password])
if @user.update email: email_params[:email] if @user.update email: email_params[:email]
@@ -68,7 +57,6 @@ class SettingsController < ApplicationController
end end
end end
# POST /settings/reset_email_password
def reset_email_password def reset_email_password
@user.current_password = security_params[:current_password] @user.current_password = security_params[:current_password]
@@ -91,7 +79,6 @@ class SettingsController < ApplicationController
end end
end end
# POST /settings/reset_password
def reset_password def reset_password
current_user.send_reset_password_instructions current_user.send_reset_password_instructions
sign_out current_user sign_out current_user
@@ -99,29 +86,26 @@ class SettingsController < ApplicationController
redirect_to check_your_email_path, notice: msg redirect_to check_your_email_path, notice: msg
end end
# POST /settings/set_nostr_pubkey
def set_nostr_pubkey def set_nostr_pubkey
signed_event = Nostr::Event.new(**nostr_event_from_params) signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
is_valid_sig = signed_event.verify_signature is_valid_id = NostrManager::ValidateId.call(event: signed_event)
is_valid_auth = NostrManager::VerifyAuth.call( is_valid_sig = NostrManager::VerifySignature.call(event: signed_event)
event: signed_event, is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
challenge: session[:shared_secret]
)
unless is_valid_sig && is_valid_auth unless is_valid_id && is_valid_sig && is_correct_content
flash[:alert] = "Public key could not be verified" flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return http_status :unprocessable_entity and return
end end
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey) user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event[:pubkey])
if user_with_pubkey.present? && (user_with_pubkey != current_user) if user_with_pubkey.present? && (user_with_pubkey != current_user)
flash[:alert] = "Public key already in use for a different account" flash[:alert] = "Public key already in use for a different account"
http_status :unprocessable_entity and return http_status :unprocessable_entity and return
end end
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event.pubkey) LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event[:pubkey])
session[:shared_secret] = nil session[:shared_secret] = nil
flash[:success] = "Public key verification successful" flash[:success] = "Public key verification successful"
@@ -161,10 +145,11 @@ class SettingsController < ApplicationController
end end
def user_params def user_params
params.require(:user).permit( params.require(:user).permit(:display_name, :avatar, preferences: [
:display_name, :avatar, :pgp_pubkey, :lightning_notify_sats_received,
preferences: UserPreferences.pref_keys :remotestorage_notify_auth_created,
) :xmpp_exchange_contacts_with_invitees
])
end end
def email_params def email_params
@@ -175,6 +160,12 @@ class SettingsController < ApplicationController
params.require(:user).permit(:current_password) params.require(:user).permit(:current_password)
end end
def nostr_event_params
params.permit(signed_event: [
:id, :pubkey, :created_at, :kind, :content, :sig, tags: []
])
end
def generate_email_password def generate_email_password
characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join

View File

@@ -96,7 +96,7 @@ class SignupController < ApplicationController
session[:new_user] = nil session[:new_user] = nil
session[:validation_error] = nil session[:validation_error] = nil
UserManager::CreateAccount.call(account: { CreateAccount.call(account: {
username: @user.cn, username: @user.cn,
domain: Setting.primary_domain, domain: Setting.primary_domain,
email: @user.email, email: @user.email,

View File

@@ -1,62 +0,0 @@
# frozen_string_literal: true
class Users::SessionsController < Devise::SessionsController
# before_action :configure_sign_in_params, only: [:create]
# GET /resource/sign_in
def new
session[:shared_secret] = SecureRandom.base64(12)
super
end
# POST /resource/sign_in
# def create
# super
# end
# DELETE /resource/sign_out
# def destroy
# super
# end
# POST /users/nostr_login
def nostr_login
signed_event = Nostr::Event.new(**nostr_event_from_params)
is_valid_sig = signed_event.verify_signature
is_valid_auth = NostrManager::VerifyAuth.call(
event: signed_event,
challenge: session[:shared_secret]
)
session[:shared_secret] = nil
unless is_valid_sig && is_valid_auth
flash[:alert] = "Login verification failed"
http_status :unauthorized and return
end
user = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
if user.present?
set_flash_message!(:notice, :signed_in)
sign_in("user", user)
render json: { redirect_url: after_sign_in_path_for(user) }, status: :ok
else
flash[:alert] = "Failed to find your account. Nostr login may be disabled."
http_status :unauthorized
end
end
protected
def set_flash_message(key, kind, options = {})
# Hide flash message after redirecting from a signin route while logged in
super unless key == :alert && kind == "already_authenticated"
end
# If you have extra params to permit, append them to the sanitizer.
# def configure_sign_in_params
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
# end
end

View File

@@ -1,35 +0,0 @@
class WebKeyDirectoryController < WellKnownController
before_action :allow_cross_origin_requests
# /.well-known/openpgpkey/hu/:hashed_username(.txt)
def show
@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

View File

@@ -1,19 +1,20 @@
class WebfingerController < WellKnownController class WebfingerController < ApplicationController
before_action :allow_cross_origin_requests, only: [:show] before_action :allow_cross_origin_requests, only: [:show]
layout false
def show def show
resource = params[:resource] resource = params[:resource]
if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1) if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1)
@username, @domain = @useraddress.split("@") @username, @org = @useraddress.split("@")
unless Rails.env.development? unless Rails.env.development?
# Allow different domains (e.g. localhost:3000) in development only # Allow different domains (e.g. localhost:3000) in development only
head 404 and return unless @domain == Setting.primary_domain head 404 and return unless @org == Setting.primary_domain
end end
unless @user = User.where(ou: Setting.primary_domain) unless User.where(cn: @username.downcase, ou: Setting.primary_domain).any?
.find_by(cn: @username.downcase)
head 404 and return head 404 and return
end end
@@ -27,60 +28,22 @@ class WebfingerController < WellKnownController
private private
def webfinger def webfinger
jrd = { links = [];
subject: "acct:#{@user.address}",
aliases: [],
links: []
}
if Setting.mastodon_enabled && @user.service_enabled?(:mastodon) # TODO check if storage service is enabled for user, not just globally
# https://docs.joinmastodon.org/spec/webfinger/ links << remotestorage_link if Setting.remotestorage_enabled
jrd[:aliases] += mastodon_aliases
jrd[:links] += mastodon_links
end
if Setting.remotestorage_enabled && @user.service_enabled?(:remotestorage) { "links" => links }
# https://datatracker.ietf.org/doc/draft-dejong-remotestorage/
jrd[:links] << remotestorage_link
end
jrd
end
def mastodon_aliases
[
"#{Setting.mastodon_public_url}/@#{@user.cn}",
"#{Setting.mastodon_public_url}/users/#{@user.cn}"
]
end
def mastodon_links
[
{
rel: "http://webfinger.net/rel/profile-page",
type: "text/html",
href: "#{Setting.mastodon_public_url}/@#{@user.cn}"
},
{
rel: "self",
type: "application/activity+json",
href: "#{Setting.mastodon_public_url}/users/#{@user.cn}"
},
{
rel: "http://ostatus.org/schema/1.0/subscribe",
template: "#{Setting.mastodon_public_url}/authorize_interaction?uri={uri}"
}
]
end end
def remotestorage_link def remotestorage_link
auth_url = new_rs_oauth_url(@username, host: Setting.rs_accounts_domain) auth_url = new_rs_oauth_url(@username)
storage_url = "#{Setting.rs_storage_url}/#{@username}" storage_url = "#{Setting.rs_storage_url}/#{@username}"
{ {
rel: "http://tools.ietf.org/id/draft-dejong-remotestorage", "rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage",
href: storage_url, "href" => storage_url,
properties: { "properties" => {
"http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13", "http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13",
"http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url, "http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url,
"http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter "http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter
@@ -89,4 +52,10 @@ class WebfingerController < WellKnownController
} }
} }
end end
def allow_cross_origin_requests
return unless Rails.env.development?
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end end

View File

@@ -2,76 +2,45 @@ class WebhooksController < ApplicationController
skip_forgery_protection skip_forgery_protection
before_action :authorize_request before_action :authorize_request
before_action :process_payload
def lndhub def lndhub
@user = User.find_by!(ln_account: @payload[:user_login]) begin
payload = JSON.parse(request.body.read, symbolize_names: true)
if @zap = @user.zaps.find_by(payment_request: @payload[:payment_request]) head :no_content and return unless payload[:type] == "incoming"
settled_at = Time.parse(@payload[:settled_at]) rescue
zap_receipt = NostrManager::CreateZapReceipt.call( head :unprocessable_entity and return
zap: @zap,
paid_at: settled_at.to_i,
preimage: @payload[:preimage]
)
@zap.update! settled_at: settled_at, receipt: zap_receipt.to_h
NostrManager::PublishZapReceipt.call(zap: @zap)
end end
send_notifications user = User.find_by!(ln_account: payload[:user_login])
notify = user.preferences[:lightning_notify_sats_received]
case notify
when "xmpp"
notify_xmpp(user.address, payload[:amount], payload[:memo])
when "email"
NotificationMailer.with(user: user, amount_sats: payload[:amount])
.lightning_sats_received.deliver_later
end
head :ok head :ok
end end
private private
# TODO refactor into mailer-like generic class/service
def notify_xmpp(address, amt_sats, memo)
payload = {
type: "normal",
from: Setting.xmpp_notifications_from_address,
to: address,
subject: "Sats received!",
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"
}
XmppSendMessageJob.perform_later(payload)
end
def authorize_request def authorize_request
if !ENV['WEBHOOKS_ALLOWED_IPS'].split(',').include?(request.remote_ip) if !ENV['WEBHOOKS_ALLOWED_IPS'].split(',').include?(request.remote_ip)
head :forbidden and return head :forbidden and return
end end
end end
def process_payload
@payload = JSON.parse(request.body.read, symbolize_names: true)
unless @payload[:type] == "incoming" &&
@payload[:state] == "settled"
head :no_content and return
end
rescue
head :unprocessable_entity and return
end
def send_notifications
return if @payload[:amount] < @user.preferences[:lightning_notify_min_sats]
if @user.preferences[:lightning_notify_only_with_message]
return if @payload[:memo].blank?
end
target = @zap.present? ? @user.preferences[:lightning_notify_zap_received] :
@user.preferences[:lightning_notify_sats_received]
case target
when "xmpp"
notify_xmpp
when "email"
notify_email
end
end
# TODO refactor into mailer-like generic class/service
def notify_xmpp
XmppSendMessageJob.perform_later({
type: "normal",
from: Setting.xmpp_notifications_from_address,
to: @user.address,
subject: "Sats received!",
body: "#{helpers.number_with_delimiter @payload[:amount]} sats received in your Lightning wallet:\n> #{@payload[:memo]}"
})
end
def notify_email
NotificationMailer.with(user: @user, amount_sats: @payload[:amount])
.lightning_sats_received.deliver_later
end
end end

View File

@@ -1,47 +1,16 @@
class WellKnownController < ApplicationController class WellKnownController < ApplicationController
before_action :require_nostr_enabled, only: [ :nostr ]
before_action :allow_cross_origin_requests, only: [ :nostr ]
layout false
def nostr def nostr
http_status :unprocessable_entity and return if params[:name].blank? http_status :unprocessable_entity and return if params[:name].blank?
domain = request.headers["X-Forwarded-Host"].presence || Setting.primary_domain domain = request.headers["X-Forwarded-Host"].presence || Setting.primary_domain
relay_url = Setting.nostr_relay_url.presence @user = User.where(cn: params[:name], ou: domain).first
http_status :not_found and return if @user.nil? || @user.nostr_pubkey.blank?
if params[:name] == "_"
if domain == Setting.primary_domain
# pubkey for the primary domain without a username (e.g. kosmos.org)
res = { names: { "_": Setting.nostr_public_key_primary_domain.presence || Setting.nostr_public_key } }
else
# pubkey for the akkounts domain without a username (e.g. accounts.kosmos.org)
res = { names: { "_": Setting.nostr_public_key } }
end
res[:relays] = { "_" => [ relay_url ] } if relay_url
else
@user = User.where(cn: params[:name], ou: domain).first
http_status :not_found and return if @user.nil? || @user.nostr_pubkey.blank?
res = { names: { @user.cn => @user.nostr_pubkey } }
res[:relays] = { @user.nostr_pubkey => [ relay_url ] } if relay_url
end
respond_to do |format| respond_to do |format|
format.json do format.json do
render json: res.to_json render json: {
names: { "#{@user.cn}": @user.nostr_pubkey }
}.to_json
end end
end end
end end
private
def require_nostr_enabled
http_status :not_found unless Setting.nostr_enabled?
end
def allow_cross_origin_requests
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end end

View File

@@ -0,0 +1,2 @@
module DashboardHelper
end

View File

@@ -0,0 +1,2 @@
module DonationsHelper
end

View File

@@ -0,0 +1,2 @@
module InvitationsHelper
end

View File

@@ -0,0 +1,2 @@
module LnurlpayHelper
end

View File

@@ -1,12 +0,0 @@
module ServicesHelper
def service_human_name(key, category = :external)
SERVICES[category][key][:name] || key.to_s
end
def service_display_name(key, category = :external)
SERVICES[category][key][:display_name] ||
service_human_name(key, category)
end
end

View File

@@ -0,0 +1,2 @@
module SettingsHelper
end

View File

@@ -0,0 +1,2 @@
module SignupHelper
end

View File

@@ -0,0 +1,2 @@
module UsersHelper
end

View File

@@ -0,0 +1,2 @@
module WalletHelper
end

View File

@@ -0,0 +1,2 @@
module WelcomeHelper
end

View File

@@ -1,53 +0,0 @@
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="nostr-login"
export default class extends Controller {
static targets = [ "loginForm", "loginButton" ]
static values = { site: String, sharedSecret: String }
connect() {
if (window.nostr) {
this.loginButtonTarget.disabled = false
this.loginFormTarget.classList.remove("hidden")
}
}
async login () {
this.loginButtonTarget.disabled = true
try {
// Auth based on NIP-42
const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000),
kind: 22242,
tags: [
["site", this.siteValue],
["challenge", this.sharedSecretValue]
],
content: ""
})
const res = await fetch("/users/nostr_login", {
method: "POST", credentials: "include", headers: {
"Accept": "application/json", 'Content-Type': 'application/json',
"X-CSRF-Token": this.csrfToken
}, body: JSON.stringify({ signed_event: signedEvent })
})
if (res.status === 200) {
res.json().then(r => { window.location.href = r.redirect_url })
} else {
window.location.reload()
}
} catch (error) {
console.warn('Unable to authenticate:', error.message)
} finally {
this.loginButtonTarget.disabled = false
}
}
get csrfToken () {
const element = document.head.querySelector('meta[name="csrf-token"]')
return element.getAttribute("content")
}
}

View File

@@ -3,12 +3,7 @@ import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="settings--nostr-pubkey" // Connects to data-controller="settings--nostr-pubkey"
export default class extends Controller { export default class extends Controller {
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ] static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
static values = { static values = { userAddress: String, pubkeyHex: String, sharedSecret: String }
userAddress: String,
pubkeyHex: String,
site: String,
sharedSecret: String
}
connect () { connect () {
if (window.nostr) { if (window.nostr) {
@@ -24,15 +19,11 @@ export default class extends Controller {
this.setPubkeyTarget.disabled = true this.setPubkeyTarget.disabled = true
try { try {
// Auth based on NIP-42
const signedEvent = await window.nostr.signEvent({ const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
kind: 22242, kind: 1,
tags: [ tags: [],
["site", this.siteValue], content: `Connect my public key to ${this.userAddressValue} (confirmation ${this.sharedSecretValue})`
["challenge", this.sharedSecretValue]
],
content: ""
}) })
const res = await fetch("/settings/set_nostr_pubkey", { const res = await fetch("/settings/set_nostr_pubkey", {

View File

@@ -1,7 +1,7 @@
class CreateLdapUserJob < ApplicationJob class CreateLdapUserJob < ApplicationJob
queue_as :default queue_as :default
def perform(username:, domain:, email:, hashed_pw:, confirmed: false) def perform(username, domain, email, hashed_pw)
dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org" dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org"
attr = { attr = {
objectclass: ["top", "account", "person", "extensibleObject"], objectclass: ["top", "account", "person", "extensibleObject"],
@@ -12,10 +12,6 @@ class CreateLdapUserJob < ApplicationJob
userPassword: hashed_pw userPassword: hashed_pw
} }
if confirmed
attr[:serviceEnabled] = Setting.default_services
end
ldap_client.add(dn: dn, attributes: attr) ldap_client.add(dn: dn, attributes: attr)
end end

View File

@@ -1,7 +0,0 @@
class NostrPublishEventJob < ApplicationJob
queue_as :nostr
def perform(event:, relay_url:)
NostrManager::PublishEvent.call(event: event, relay_url: relay_url)
end
end

View File

@@ -3,6 +3,8 @@ class RemoteStorageExpireAuthorizationJob < ApplicationJob
def perform(rs_auth_id) def perform(rs_auth_id)
rs_auth = RemoteStorageAuthorization.find 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! rs_auth.destroy!
end end
end end

View File

@@ -2,8 +2,8 @@ class XmppExchangeContactsJob < ApplicationJob
queue_as :default queue_as :default
def perform(inviter, invitee) def perform(inviter, invitee)
return unless inviter.service_enabled?(:ejabberd) && return unless inviter.services_enabled.include?("xmpp") &&
invitee.service_enabled?(:ejabberd) && invitee.services_enabled.include?("xmpp") &&
inviter.preferences[:xmpp_exchange_contacts_with_invitees] inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new ejabberd = EjabberdApiClient.new

View File

@@ -1,90 +1,3 @@
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default Rails.application.config.action_mailer.default_options
layout 'mailer' 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 end

View File

@@ -18,6 +18,6 @@ class CustomMailer < ApplicationMailer
@user = params[:user] @user = params[:user]
@subject = params[:subject] @subject = params[:subject]
@body = params[:body] @body = params[:body]
send_mail mail(to: @user.email, subject: @subject)
end end
end end

View File

@@ -3,7 +3,7 @@ class NotificationMailer < ApplicationMailer
@user = params[:user] @user = params[:user]
@amount_sats = params[:amount_sats] @amount_sats = params[:amount_sats]
@subject = "Sats received" @subject = "Sats received"
send_mail mail to: @user.email, subject: @subject
end end
def remotestorage_auth_created def remotestorage_auth_created
@@ -15,19 +15,19 @@ class NotificationMailer < ApplicationMailer
"#{access} #{directory}" "#{access} #{directory}"
end end
@subject = "New app connected to your storage" @subject = "New app connected to your storage"
send_mail mail to: @user.email, subject: @subject
end end
def new_invitations_available def new_invitations_available
@user = params[:user] @user = params[:user]
@subject = "New invitations added to your account" @subject = "New invitations added to your account"
send_mail mail to: @user.email, subject: @subject
end end
def bitcoin_donation_confirmed def bitcoin_donation_confirmed
@user = params[:user] @user = params[:user]
@donation = params[:donation] @donation = params[:donation]
@subject = "Donation confirmed" @subject = "Donation confirmed"
send_mail mail to: @user.email, subject: @subject
end end
end end

View File

@@ -1,24 +0,0 @@
module Settings
module BtcpaySettings
extend ActiveSupport::Concern
included do
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_public_url, type: :string,
default: ENV["BTCPAY_PUBLIC_URL"].presence
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
end
end
end

View File

@@ -1,16 +0,0 @@
module Settings
module DiscourseSettings
extend ActiveSupport::Concern
included do
field :discourse_public_url, type: :string,
default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean,
default: ENV["DISCOURSE_PUBLIC_URL"].present?
field :discourse_connect_secret, type: :string,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
end
end
end

View File

@@ -1,13 +0,0 @@
module Settings
module DroneCiSettings
extend ActiveSupport::Concern
included do
field :droneci_public_url, type: :string,
default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean,
default: ENV["DRONECI_PUBLIC_URL"].present?
end
end
end

View File

@@ -1,19 +0,0 @@
module Settings
module EjabberdSettings
extend ActiveSupport::Concern
included do
field :ejabberd_enabled, type: :boolean,
default: ENV["EJABBERD_API_URL"].present?
field :ejabberd_api_url, type: :string,
default: ENV["EJABBERD_API_URL"].presence
field :ejabberd_admin_url, type: :string,
default: ENV["EJABBERD_ADMIN_URL"].presence
field :ejabberd_buddy_roster, type: :string,
default: "Buddies"
end
end
end

View File

@@ -1,28 +0,0 @@
module Settings
module EmailSettings
extend ActiveSupport::Concern
included do
field :email_enabled, type: :boolean,
default: ENV["EMAIL_SMTP_HOST"].present?
# field :email_smtp_host, type: :string,
# default: ENV["EMAIL_SMTP_HOST"].presence
#
# field :email_smtp_port, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || 587
#
# field :email_smtp_enable_starttls, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || true
#
# field :email_auth_method, type: :string,
# default: ENV["EMAIL_AUTH_METHOD"].presence || "plain"
#
# field :email_imap_host, type: :string,
# default: ENV["EMAIL_IMAP_HOST"].presence
#
# field :email_imap_port, type: :string,
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
end
end
end

View File

@@ -1,34 +0,0 @@
module Settings
module GeneralSettings
extend ActiveSupport::Concern
included do
field :primary_domain, type: :string,
default: ENV["PRIMARY_DOMAIN"].presence
field :accounts_domain, type: :string,
default: ENV["AKKOUNTS_DOMAIN"].presence
#
# Internal services
#
field :redis_url, type: :string,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
field :s3_enabled, type: :boolean,
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
field :sentry_enabled, type: :boolean, readonly: true,
default: ENV["SENTRY_DSN"].present?
#
# Registrations
#
field :reserved_usernames, type: :array, default: %w[
account accounts donations mail webmaster support
]
end
end
end

View File

@@ -1,13 +0,0 @@
module Settings
module GiteaSettings
extend ActiveSupport::Concern
included do
field :gitea_public_url, type: :string,
default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean,
default: ENV["GITEA_PUBLIC_URL"].present?
end
end
end

View File

@@ -1,25 +0,0 @@
module Settings
module LightningNetworkSettings
extend ActiveSupport::Concern
included do
field :lndhub_api_url, type: :string,
default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean,
default: ENV["LNDHUB_API_URL"].present?
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean,
default: ENV["LNDHUB_ADMIN_UI"] || false
field :lndhub_public_key, type: :string,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present? }
end
end
end

View File

@@ -1,16 +0,0 @@
module Settings
module MastodonSettings
extend ActiveSupport::Concern
included do
field :mastodon_public_url, type: :string,
default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean,
default: ENV["MASTODON_PUBLIC_URL"].present?
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
end
end
end

View File

@@ -1,13 +0,0 @@
module Settings
module MediaWikiSettings
extend ActiveSupport::Concern
included do
field :mediawiki_public_url, type: :string,
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean,
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
end
end
end

View File

@@ -1,25 +0,0 @@
module Settings
module NostrSettings
extend ActiveSupport::Concern
included do
field :nostr_enabled, type: :boolean,
default: ENV["NOSTR_PRIVATE_KEY"].present?
field :nostr_private_key, type: :string,
default: ENV["NOSTR_PRIVATE_KEY"].presence
field :nostr_public_key, type: :string,
default: ENV["NOSTR_PUBLIC_KEY"].presence
field :nostr_public_key_primary_domain, type: :string,
default: ENV["NOSTR_PUBLIC_KEY_PRIMARY_DOMAIN"].presence
field :nostr_relay_url, type: :string,
default: ENV["NOSTR_RELAY_URL"].presence
field :nostr_zaps_relay_limit, type: :integer,
default: 12
end
end
end

View File

@@ -1,9 +0,0 @@
module Settings
module OpenCollectiveSettings
extend ActiveSupport::Concern
included do
field :opencollective_enabled, type: :boolean, default: true
end
end
end

View File

@@ -1,19 +0,0 @@
module Settings
module RemoteStorageSettings
extend ActiveSupport::Concern
included do
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
field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
end
end
end

View File

@@ -1,11 +0,0 @@
module Settings
module XmppSettings
extend ActiveSupport::Concern
included do
field :xmpp_default_rooms, type: :array, default: []
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
field :xmpp_notifications_from_address, type: :string, default: primary_domain
end
end
end

View File

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

View File

@@ -2,30 +2,208 @@
class Setting < RailsSettings::Base class Setting < RailsSettings::Base
cache_prefix { "v1" } cache_prefix { "v1" }
Dir[Rails.root.join('app', 'models', 'concerns', 'settings', '*.rb')].each do |file| field :primary_domain, type: :string,
require file default: ENV["PRIMARY_DOMAIN"].presence
end
include Settings::GeneralSettings field :accounts_domain, type: :string,
include Settings::BtcpaySettings default: ENV["AKKOUNTS_DOMAIN"].presence
include Settings::DiscourseSettings
include Settings::DroneCiSettings
include Settings::EjabberdSettings
include Settings::EmailSettings
include Settings::GiteaSettings
include Settings::LightningNetworkSettings
include Settings::MastodonSettings
include Settings::MediaWikiSettings
include Settings::NostrSettings
include Settings::OpenCollectiveSettings
include Settings::RemoteStorageSettings
include Settings::XmppSettings
def self.available_services #
known_services = SERVICES[:external].keys # Internal services
known_services.select {|s| Setting.send "#{s}_enabled?" } #
end
field :default_services, type: :array, field :redis_url, type: :string,
default: self.available_services default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
field :s3_enabled, type: :boolean,
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
#
# Registrations
#
field :reserved_usernames, type: :array, default: %w[
account accounts donations mail webmaster support
]
#
# XMPP
#
field :xmpp_default_rooms, type: :array, default: []
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
field :xmpp_notifications_from_address, type: :string, default: primary_domain
#
# Sentry
#
field :sentry_enabled, type: :boolean, readonly: true,
default: ENV["SENTRY_DSN"].present?
#
# BTCPay Server
#
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_public_url, type: :string,
default: ENV["BTCPAY_PUBLIC_URL"].presence
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
#
# Discourse
#
field :discourse_public_url, type: :string,
default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean,
default: ENV["DISCOURSE_PUBLIC_URL"].present?
field :discourse_connect_secret, type: :string,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
#
# Drone CI
#
field :droneci_public_url, type: :string,
default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean,
default: ENV["DRONECI_PUBLIC_URL"].present?
#
# ejabberd
#
field :ejabberd_enabled, type: :boolean,
default: ENV["EJABBERD_API_URL"].present?
field :ejabberd_api_url, type: :string,
default: ENV["EJABBERD_API_URL"].presence
field :ejabberd_admin_url, type: :string,
default: ENV["EJABBERD_ADMIN_URL"].presence
field :ejabberd_buddy_roster, type: :string,
default: "Buddies"
#
# Gitea
#
field :gitea_public_url, type: :string,
default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean,
default: ENV["GITEA_PUBLIC_URL"].present?
#
# Lightning Network
#
field :lndhub_api_url, type: :string,
default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean,
default: ENV["LNDHUB_API_URL"].present?
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean,
default: ENV["LNDHUB_ADMIN_UI"] || false
field :lndhub_public_key, type: :string,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present? }
#
# Mastodon
#
field :mastodon_public_url, type: :string,
default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean,
default: ENV["MASTODON_PUBLIC_URL"].present?
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
#
# MediaWiki
#
field :mediawiki_public_url, type: :string,
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean,
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
#
# Nostr
#
field :nostr_enabled, type: :boolean, default: false
#
# OpenCollective
#
field :opencollective_enabled, type: :boolean, default: true
#
# RemoteStorage
#
field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present?
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence
field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
#
# E-Mail Service
#
field :email_enabled, type: :boolean,
default: ENV["EMAIL_SMTP_HOST"].present?
# field :email_smtp_host, type: :string,
# default: ENV["EMAIL_SMTP_HOST"].presence
#
# field :email_smtp_port, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || 587
#
# field :email_smtp_enable_starttls, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || true
#
# field :email_auth_method, type: :string,
# default: ENV["EMAIL_AUTH_METHOD"].presence || "plain"
#
# field :email_imap_host, type: :string,
# default: ENV["EMAIL_IMAP_HOST"].presence
#
# field :email_imap_port, type: :string,
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
end end

View File

@@ -3,10 +3,9 @@ require 'nostr'
class User < ApplicationRecord class User < ApplicationRecord
include EmailValidatable include EmailValidatable
attr_accessor :current_password
attr_accessor :avatar_new
attr_accessor :display_name attr_accessor :display_name
attr_accessor :pgp_pubkey attr_accessor :avatar_new
attr_accessor :current_password
serialize :preferences, coder: UserPreferences serialize :preferences, coder: UserPreferences
@@ -18,15 +17,16 @@ class User < ApplicationRecord
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id' has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user has_one :inviter, through: :invitation, source: :user
has_many :invitees, through: :invitations has_many :invitees, through: :invitations
has_many :donations, dependent: :nullify has_many :donations, dependent: :nullify
has_many :remote_storage_authorizations
has_many :zaps
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user", has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
primary_key: "ln_account", foreign_key: "login" primary_key: "ln_account", foreign_key: "login"
has_many :accounts, through: :lndhub_user has_many :accounts, through: :lndhub_user
has_many :remote_storage_authorizations
# #
# Validations # Validations
# #
@@ -52,8 +52,6 @@ class User < ApplicationRecord
validate :acceptable_avatar validate :acceptable_avatar
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
# #
# Scopes # Scopes
# #
@@ -95,7 +93,9 @@ class User < ApplicationRecord
LdapManager::UpdateEmail.call(dn: self.dn, address: self.email) LdapManager::UpdateEmail.call(dn: self.dn, address: self.email)
else else
# E-Mail from signup confirmed (i.e. account activation) # E-Mail from signup confirmed (i.e. account activation)
enable_default_services
# TODO Make configurable, only activate globally enabled services
enable_service %w[ discourse gitea mediawiki xmpp ]
# TODO enable in development when we have easy setup of ejabberd etc. # TODO enable in development when we have easy setup of ejabberd etc.
return if Rails.env.development? || !Setting.ejabberd_enabled? return if Rails.env.development? || !Setting.ejabberd_enabled?
@@ -133,7 +133,7 @@ class User < ApplicationRecord
def mastodon_address def mastodon_address
return nil unless Setting.mastodon_enabled? return nil unless Setting.mastodon_enabled?
"#{self.cn.gsub("-", "_")}@#{Setting.mastodon_address_domain}" "#{self.cn}@#{Setting.mastodon_address_domain}"
end end
def valid_attribute?(attribute_name) def valid_attribute?(attribute_name)
@@ -141,8 +141,10 @@ class User < ApplicationRecord
self.errors[attribute_name].blank? self.errors[attribute_name].blank?
end end
def enable_default_services def ln_create_invoice(payload)
enable_service Setting.default_services lndhub = Lndhub.new
lndhub.authenticate self
lndhub.addinvoice payload
end end
def dn def dn
@@ -168,24 +170,6 @@ class User < ApplicationRecord
Nostr::PublicKey.new(nostr_pubkey).to_bech32 Nostr::PublicKey.new(nostr_pubkey).to_bech32
end 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))
end
def avatar def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn) @avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end end
@@ -194,21 +178,17 @@ class User < ApplicationRecord
ldap_entry[:services_enabled] || [] ldap_entry[:services_enabled] || []
end end
def service_enabled?(name)
services_enabled.map(&:to_sym).include?(name.to_sym)
end
def enable_service(service) def enable_service(service)
current_services = services_enabled current_services = services_enabled
new_services = Array(service).map(&:to_s) new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq.sort services = (current_services + new_services).uniq
ldap.replace_attribute(dn, :serviceEnabled, services) ldap.replace_attribute(dn, :serviceEnabled, services)
end end
def disable_service(service) def disable_service(service)
current_services = services_enabled current_services = services_enabled
disabled_services = Array(service).map(&:to_s) disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq.sort services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :serviceEnabled, services) ldap.replace_attribute(dn, :serviceEnabled, services)
end end
@@ -235,10 +215,4 @@ class User < ApplicationRecord
errors.add(:avatar, "must be a JPEG or PNG file") errors.add(:avatar, "must be a JPEG or PNG file")
end end
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 end

View File

@@ -26,8 +26,4 @@ class UserPreferences
end end
hash.stringify_keys!.to_h hash.stringify_keys!.to_h
end end
def self.pref_keys
DEFAULT_PREFS.keys.map(&:to_sym)
end
end end

View File

@@ -1,20 +0,0 @@
class Zap < ApplicationRecord
belongs_to :user
scope :settled, -> { where.not(settled_at: nil) }
scope :unpaid, -> { where(settled_at: nil) }
def request_event
nostr_event_from_hash(request)
end
def receipt_event
nostr_event_from_hash(receipt)
end
private
def nostr_event_from_hash(hash)
Nostr::Event.new(**hash.symbolize_keys)
end
end

View File

@@ -0,0 +1,50 @@
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
# TODO move to confirmation
# (and/or add email_confirmed to entry and use in login filter)
def add_ldap_document
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw)
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

View 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

View File

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

View File

@@ -58,7 +58,7 @@ class LdapService < ApplicationService
attributes = %w[ attributes = %w[
dn cn uid mail displayName admin serviceEnabled dn cn uid mail displayName admin serviceEnabled
mailRoutingAddress mailpassword nostrKey pgpKey mailRoutingAddress mailpassword nostrKey
] ]
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
@@ -73,8 +73,7 @@ class LdapService < ApplicationService
services_enabled: e.try(:serviceEnabled), services_enabled: e.try(:serviceEnabled),
email_maildrop: e.try(:mailRoutingAddress), email_maildrop: e.try(:mailRoutingAddress),
email_password: e.try(:mailpassword), email_password: e.try(:mailpassword),
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil, nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil
pgp_key: e.try(:pgpKey) ? e.pgpKey.first : nil
} }
end end
end end
@@ -102,7 +101,7 @@ class LdapService < ApplicationService
dn = "ou=#{ou},cn=users,#{ldap_suffix}" dn = "ou=#{ou},cn=users,#{ldap_suffix}"
aci = <<-EOS 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 || mail || userPassword || 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 EOS
attrs = { attrs = {

View File

@@ -1,13 +0,0 @@
module LndhubManager
class CreateUserInvoice < LndhubV2
def initialize(user:, payload:)
@user = user
@payload = payload
end
def call
authenticate @user
create_invoice @payload
end
end
end

View File

@@ -1,25 +0,0 @@
module NostrManager
class CreateZapReceipt < NostrManagerService
def initialize(zap:, paid_at:, preimage:)
@zap, @paid_at, @preimage = zap, paid_at, preimage
end
def call
request_tags = parse_tags(@zap.request_event.tags)
site_user.create_event(
kind: 9735,
created_at: @paid_at,
content: "",
tags: [
["p", request_tags[:p].first],
["e", request_tags[:e]&.first],
["a", request_tags[:a]&.first],
["bolt11", @zap.payment_request],
["preimage", @preimage],
["description", @zap.request_event.to_json]
].reject { |t| t[1].nil? }
)
end
end
end

View File

@@ -1,50 +0,0 @@
module NostrManager
class PublishEvent < NostrManagerService
def initialize(event:, relay_url:)
relay_name = URI.parse(relay_url).host
@relay = Nostr::Relay.new(url: relay_url, name: relay_name)
if event.is_a?(Nostr::Event)
@event = event
else
@event = Nostr::Event.new(**event.symbolize_keys)
end
@client = Nostr::Client.new
end
def call
client, relay, event = @client, @relay, @event
log_prefix = "[nostr][#{relay.name}]"
thread = Thread.new do
client.on :connect do
puts "#{log_prefix} Publishing #{event.id}..."
client.publish event
end
client.on :error do |e|
puts "#{log_prefix} Error: #{e}"
puts "#{log_prefix} Closing thread..."
thread.exit
end
client.on :message do |m|
puts "#{log_prefix} Message: #{m}"
msg = JSON.parse(m) rescue []
if msg[0] == "OK" && msg[1] == event.id && msg[2]
puts "#{log_prefix} Event published. Closing thread..."
else
puts "#{log_prefix} Unexpected message from relay. Closing thread..."
end
thread.exit
end
puts "#{log_prefix} Connecting to #{relay.url}..."
client.connect relay
end
thread.join
end
end
end

View File

@@ -1,24 +0,0 @@
module NostrManager
class PublishZapReceipt < NostrManagerService
def initialize(zap:, delayed: true)
@zap, @delayed = zap, delayed
end
def call
tags = parse_tags(@zap.request_event.tags)
relays = tags[:relays].take(Setting.nostr_zaps_relay_limit)
if Setting.nostr_relay_url.present?
relays << Setting.nostr_relay_url
end
relays.uniq.each do |relay_url|
if @delayed
NostrPublishEventJob.perform_later(event: @zap.receipt, relay_url: relay_url)
else
NostrManager::PublishEvent.call(event: @zap.receipt_event, relay_url: relay_url)
end
end
end
end
end

View File

@@ -0,0 +1,11 @@
module NostrManager
class ValidateId < NostrManagerService
def initialize(event:)
@event = Nostr::Event.new(**event)
end
def call
@event.id == Digest::SHA256.hexdigest(JSON.generate(@event.serialize))
end
end
end

View File

@@ -1,18 +0,0 @@
module NostrManager
class VerifyAuth < NostrManagerService
def initialize(event:, challenge:)
@event = event
@challenge_expected = challenge
@site_expected = Setting.accounts_domain
end
def call
tags = parse_tags(@event.tags)
site_given = tags[:site].first
challenge_given = tags[:challenge].first
site_given == @site_expected &&
challenge_given == @challenge_expected
end
end
end

View File

@@ -0,0 +1,17 @@
module NostrManager
class VerifySignature < NostrManagerService
def initialize(event:)
@event = Nostr::Event.new(**event)
end
def call
Schnorr.check_sig!(
[@event.id].pack('H*'),
[@event.pubkey].pack('H*'),
[@event.sig].pack('H*')
)
rescue Schnorr::InvalidSignatureError
false
end
end
end

View File

@@ -1,51 +0,0 @@
module NostrManager
class VerifyZapRequest < NostrManagerService
def initialize(amount:, event:, lnurl: nil)
@amount, @event, @lnurl = amount, event, lnurl
end
# https://github.com/nostr-protocol/nips/blob/27fef638e2460139cc9078427a0aec0ce4470517/57.md#appendix-d-lnurl-server-zap-request-validation
def call
tags = parse_tags(@event.tags)
@event.verify_signature &&
@event.kind == 9734 &&
tags.present? &&
valid_p_tag?(tags[:p]) &&
valid_e_tag?(tags[:e]) &&
valid_a_tag?(tags[:a]) &&
valid_amount_tag?(tags[:amount]) &&
valid_lnurl_tag?(tags[:lnurl])
end
def valid_p_tag?(tag)
return false unless tag.present? && tag.length == 1
key = Nostr::PublicKey.new(tag.first) rescue nil
key.present?
end
def valid_e_tag?(tag)
return true unless tag.present?
# TODO validate format of event ID properly
tag.length == 1 && tag.first.is_a?(String)
end
def valid_a_tag?(tag)
return true unless tag.present?
# TODO validate format of event coordinate properly
tag.length == 1 && tag.first.is_a?(String)
end
def valid_amount_tag?(tag)
return true unless tag.present?
amount = tag.first
amount.is_a?(String) && amount.to_i == @amount
end
def valid_lnurl_tag?(tag)
return true unless tag.present?
# TODO validate lnurl matching recipient's lnurlp
tag.first.is_a?(String)
end
end
end

View File

@@ -1,22 +1,4 @@
require "nostr" require "nostr"
class NostrManagerService < ApplicationService class NostrManagerService < ApplicationService
def parse_tags(tags)
out = {}
tags.each do |tag|
out[tag[0].to_sym] = tag[1, tag.length]
end
out
end
def site_keypair
Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.new(Setting.nostr_private_key),
public_key: Nostr::PublicKey.new(Setting.nostr_public_key)
)
end
def site_user
Nostr::User.new(keypair: site_keypair)
end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,8 +38,8 @@
<tr> <tr>
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td> <td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td> <td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
<td><%= link_to invitation.user.cn, admin_user_path(invitation.user.cn), class: "ks-text-link" %></td> <td><%= link_to invitation.user.address, admin_user_path(invitation.user.address), class: "ks-text-link" %></td>
<td><%= link_to invitation.invitee.cn, admin_user_path(invitation.invitee.cn), class: "ks-text-link" %></td> <td><%= link_to invitation.invitee.address, admin_user_path(invitation.invitee.address), class: "ks-text-link" %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>

View File

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

View File

@@ -9,36 +9,18 @@
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %> <%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
<% end %> <% end %>
<ul role="list"> <label class="block">
<%= render FormElements::FieldsetComponent.new( <p class="font-bold mb-1">Reserved usernames</p>
title: "Reserved usernames", <p class="text-gray-500">
description: "These usernames cannot be registered as accounts." These usernames cannot be registered as accounts:
) do %> </p>
<%= f.text_area :reserved_usernames, <%= f.text_area :reserved_usernames,
value: Setting.reserved_usernames.join("\n"), value: Setting.reserved_usernames.join("\n"),
class: "h-44 w-60" %> class: "h-44 mb-2" %>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
One username per line One username per line
</p> </p>
<% end %> </label>
<li>
<p class="font-bold mb-1">Default services</p>
<p class="text-gray-500">
These services are enabled for new users by default after signup.
</p>
<div class="flex flex-wrap gap-x-6 gap-y-2">
<% Setting.available_services.each do |option| %>
<div class="md:inline-block">
<%= f.check_box :default_services,
{ multiple: true, checked: Setting.default_services.include?(option),
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600 mr-0.5" },
option, nil %>
<%= f.label "default_services_#{option.parameterize}", service_human_name(option) %>
</div>
<% end %>
</div>
</li>
</ul>
</section> </section>
<section> <section>

View File

@@ -7,37 +7,4 @@
title: "Enable Nostr integration (experimental)", title: "Enable Nostr integration (experimental)",
description: "Allow adding nostr pubkeys and resolve user addresses via NIP-05" description: "Allow adding nostr pubkeys and resolve user addresses via NIP-05"
) %> ) %>
<% if Setting.nostr_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_private_key,
type: :password,
title: "Private key",
description: "The private key of the accounts service, used when publishing events (e.g. zap receipts)"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_public_key,
title: "Public key",
description: "The corresponding public key of the accounts service"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_public_key_primary_domain,
title: "Public key for primary domain (NIP-05)",
description: "(optional) A different pubkey to announce for the _@#{Setting.primary_domain} Nostr address"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_relay_url,
title: "Relay URL",
description: "Websockets URL of a relay associated with #{Setting.primary_domain}"
) %>
</ul> </ul>
</section>
<section>
<h3>Zaps</h3>
<ul role="list">
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_zaps_relay_limit,
title: "Relay limit",
description: "The maximum number of relays to publish zap receipts to"
) %>
</ul>
<% end %>

View File

@@ -1,4 +1,5 @@
<h3>RemoteStorage</h3> <h3>RemoteStorage</h3>
<p class="text-red-600 mb-8">Feature currently in development.</p>
<ul role="list"> <ul role="list">
<%= render FormElements::FieldsetToggleComponent.new( <%= render FormElements::FieldsetToggleComponent.new(
form: f, form: f,

View File

@@ -36,7 +36,7 @@
<th>Invited by</th> <th>Invited by</th>
<td> <td>
<% if @user.inviter %> <% if @user.inviter %>
<%= link_to @user.inviter.cn, admin_user_path(@user.inviter.cn), class: 'ks-text-link' %> <%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %>
<% else %>&mdash;<% end %> <% else %>&mdash;<% end %>
</td> </td>
</tr> </tr>
@@ -78,7 +78,7 @@
<% if @user.invitees.length > 0 %> <% if @user.invitees.length > 0 %>
<ul class="mb-0"> <ul class="mb-0">
<% @user.invitees.order(cn: :asc).each do |invitee| %> <% @user.invitees.order(cn: :asc).each do |invitee| %>
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.cn, admin_user_path(invitee.cn), class: 'ks-text-link' %></li> <li class="leading-none mb-2 last:mb-0"><%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %></li>
<% end %> <% end %>
</ul> </ul>
<% else %>&mdash;<% end %> <% else %>&mdash;<% end %>
@@ -89,47 +89,13 @@
</section> </section>
<section class="sm:flex-1 sm:pt-0"> <section class="sm:flex-1 sm:pt-0">
<h3>LDAP</h3> <% if @avatar.present? %>
<table class="divided"> <h3>LDAP<h3>
<tbody> <p>
<tr> <img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
<th>Avatar</th> </p>
<td> <% end %>
<% if @avatar.present? %> <!-- <h3>Actions</h3> -->
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
<% else %>
&mdash;
<% end %>
</td>
</tr>
<tr>
<th>Display name</th>
<td><%= @user.display_name || "—" %></td>
</tr>
<tr>
<th class="align-top">PGP key</th>
<td class="align-top leading-5">
<% if @user.pgp_pubkey.present? %>
<span class="font-mono" title="<%= @user.pgp_fpr %>">
<% if @user.pgp_pubkey_contains_user_address? %>
<%= link_to wkd_key_url(hashed_username: @user.wkd_hash, l: @user.cn, format: :txt),
class: "ks-text-link", target: "_blank" do %>
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
<% end %>
<% else %>
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
<% end %>
</span><br />
<% @user.gnupg_key.uids.each do |uid| %>
<%= uid.uid %><br />
<% end %>
<% else %>
&mdash;
<% end %>
</td>
</tr>
</tbody>
</table>
</section> </section>
</div> </div>
@@ -158,19 +124,6 @@
</td> </td>
</tr> </tr>
<% end %> <% end %>
<% if Setting.email_enabled %>
<tr>
<td>E-Mail</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: Flipper.enabled?(:email, current_user),
input_enabled: false
) %>
</td>
<td class="text-right">
</td>
</tr>
<% end %>
<% if Setting.gitea_enabled %> <% if Setting.gitea_enabled %>
<tr> <tr>
<td>Gitea</td> <td>Gitea</td>
@@ -218,7 +171,7 @@
<td>XMPP (ejabberd)</td> <td>XMPP (ejabberd)</td>
<td> <td>
<%= render FormElements::ToggleComponent.new( <%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("ejabberd"), enabled: @services_enabled.include?("xmpp"),
input_enabled: false input_enabled: false
) %> ) %>
</td> </td>
@@ -229,33 +182,6 @@
</td> </td>
</tr> </tr>
<% end %> <% end %>
<% if Setting.nostr_enabled %>
<tr>
<td>Nostr</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @user.nostr_pubkey.present?,
input_enabled: false
) %>
</td>
<td class="text-right">
<%= link_to "Open profile", "https://njump.me/#{@user.nostr_pubkey_bech32}", class: "btn-sm btn-gray" %>
</td>
</tr>
<% end %>
<% if Setting.remotestorage_enabled %>
<tr>
<td>remoteStorage</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: Flipper.enabled?(:remotestorage, current_user) && @services_enabled.include?("remotestorage"),
input_enabled: false
) %>
</td>
<td class="text-right">
</td>
</tr>
<% end %>
</tbody> </tbody>
</table> </table>
</section> </section>

View File

@@ -16,8 +16,8 @@
<p> <p>
There's something to do for everyone, especially non-programmers! For There's something to do for everyone, especially non-programmers! For
example, we need more help with graphics, UI/UX design, and example, we need more help with graphics, UI/UX design, and
content/copywriting. Also, testing any of our software and reporting content/copywriting. We also need moderators for social media. And beta
issues you encounter along the way is very valuable. testers for our software. The list doesn't end there.
</p> </p>
<p> <p>
A good way to get started is to join one of our A good way to get started is to join one of our
@@ -43,7 +43,7 @@
</p> </p>
<p> <p>
We have run two 6-month trials so far, with the next trial period We have run two 6-month trials so far, with the next trial period
starting sometime soon. Watch your email for notifications about it! starting sometime in Q1 2024. Watch your email for notifications about it!
</p> </p>
</section> </section>
<% end %> <% end %>

View File

@@ -5,7 +5,7 @@
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6"> <div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<% if Setting.ejabberd_enabled? %> <% if Setting.ejabberd_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:86%] bg-[center_top_-40px] bg-no-repeat bg-cover bg-[center_top_-50px] bg-no-repeat
bg-[url(/img/logos/icon_xmpp.svg)]"> bg-[url(/img/logos/icon_xmpp.svg)]">
<%= link_to services_chat_path, <%= link_to services_chat_path,
class: "block h-full px-6 py-6 rounded-md" do %> class: "block h-full px-6 py-6 rounded-md" do %>
@@ -18,7 +18,7 @@
<% end %> <% end %>
<% if Setting.mastodon_enabled? %> <% if Setting.mastodon_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:88%] bg-[center_top_-40px] bg-no-repeat bg-[length:80%] bg-[right_top_-30px] bg-no-repeat
bg-[url(/img/logos/icon_mastodon.svg)]"> bg-[url(/img/logos/icon_mastodon.svg)]">
<%= link_to services_mastodon_path, class: "block h-full px-6 py-6 rounded-md" do %> <%= link_to services_mastodon_path, class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Mastodon</h3> <h3 class="mb-3.5">Mastodon</h3>
@@ -30,9 +30,7 @@
<% end %> <% end %>
<% if Setting.email_enabled? && <% if Setting.email_enabled? &&
Flipper.enabled?(:email, current_user) %> Flipper.enabled?(:email, current_user) %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <div class="border border-gray-300 rounded-md hover:border-gray-400">
bg-[length:90%] bg-[center_top_-160px] bg-no-repeat
bg-[url(/img/logos/icon_mail.svg)]">
<%= link_to services_email_path, class: "block h-full px-6 py-6 rounded-md" do %> <%= link_to services_email_path, class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">E-Mail</h3> <h3 class="mb-3.5">E-Mail</h3>
<p class="text-gray-600"> <p class="text-gray-600">
@@ -41,16 +39,15 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if Setting.remotestorage_enabled? && <% if Setting.discourse_enabled? %>
Flipper.enabled?(:remotestorage, current_user) %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:80%] bg-[center_top_-156px] bg-no-repeat bg-[length:95%] bg-center bg-no-repeat
bg-[url(/img/logos/icon_remotestorage.svg)]"> bg-[url(/img/logos/icon_discourse.svg)]">
<%= link_to services_storage_path, <%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
class: "block h-full px-6 py-6 rounded-md" do %> class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Storage</h3> <h3 class="mb-3.5">Discourse</h3>
<p class="text-gray-600"> <p class="text-gray-600">
Sync your data between apps and devices Kosmos community forums and user support/help site
</p> </p>
<% end %> <% end %>
</div> </div>
@@ -68,22 +65,21 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if Setting.discourse_enabled? %> <% if Setting.remotestorage_enabled? &&
<div class="border border-gray-300 rounded-md hover:border-gray-400 Flipper.enabled?(:remotestorage, current_user) %>
bg-[length:80%] bg-center bg-no-repeat <div class="border border-gray-300 rounded-md hover:border-gray-400">
bg-[url(/img/logos/icon_discourse.svg)]"> <%= link_to services_storage_path,
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
class: "block h-full px-6 py-6 rounded-md" do %> class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Discourse</h3> <h3 class="mb-3.5">Storage</h3>
<p class="text-gray-600"> <p class="text-gray-600">
Community forums and support/help site Sync your data between apps and devices
</p> </p>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if Setting.gitea_enabled? %> <% if Setting.gitea_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:92%] bg-center bg-no-repeat bg-cover bg-center bg-no-repeat
bg-[url(/img/logos/icon_gitea.png)]"> bg-[url(/img/logos/icon_gitea.png)]">
<%= link_to Setting.gitea_public_url, <%= link_to Setting.gitea_public_url,
class: "block h-full px-6 py-6 rounded-md" do %> class: "block h-full px-6 py-6 rounded-md" do %>
@@ -96,7 +92,7 @@
<% end %> <% end %>
<% if Setting.droneci_enabled? %> <% if Setting.droneci_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:86%] bg-[center_top_-60px] bg-no-repeat bg-cover bg-[center_top_-70px] bg-no-repeat
bg-[url(/img/logos/icon_droneci.svg)]"> bg-[url(/img/logos/icon_droneci.svg)]">
<%= link_to Setting.droneci_public_url, <%= link_to Setting.droneci_public_url,
class: "block h-full px-6 py-6 rounded-md" do %> class: "block h-full px-6 py-6 rounded-md" do %>

View File

@@ -55,27 +55,4 @@
<%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %> <%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %>
</p> </p>
<% end %> <% end %>
<div data-controller="nostr-login"
data-nostr-login-target="loginForm"
data-nostr-login-site-value="<%= Setting.accounts_domain %>"
data-nostr-login-shared-secret-value="<%= session[:shared_secret] %>"
class="hidden">
<div class="relative my-6">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
<div class="w-full border-t border-gray-200"></div>
</div>
<div class="relative flex justify-center">
<span class="bg-white px-2 text-sm text-gray-500 italic">or</span>
</div>
</div>
<p>
<button disabled tabindex="5"
class="w-full btn-md btn-gray text-purple-600"
data-nostr-login-target="loginButton"
data-action="nostr-login#login">
Log in with Nostr
</button>
</p>
</div>
<% end %> <% end %>

View File

@@ -100,14 +100,6 @@
["Website", "https://www.thunderbird.net"] ["Website", "https://www.thunderbird.net"]
] ]
) %> ) %>
<%= render AppInfoComponent.new(
name: "Geary",
description: "Built around conversations, for the GNOME desktop",
icon_path: "/img/logos/icon_geary.png",
links: [
["Website", "https://wiki.gnome.org/Apps/Geary"]
]
) %>
</div> </div>
<div id="apps-windows" class="hidden grid grid-cols-1 gap-6" <div id="apps-windows" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel"> data-tabs-target="panel">

View File

@@ -98,17 +98,7 @@
description: "The official Web app", description: "The official Web app",
icon_path: "/img/logos/icon_mastodon-2.svg", icon_path: "/img/logos/icon_mastodon-2.svg",
links: [ links: [
["Launch", "https://kosmos.social"], ["Launch", "https://kosmos.social"]
["GitHub", "https://github.com/mastodon/mastodon"]
]
) %>
<%= render AppInfoComponent.new(
name: "Phanpy",
description: " A slick, feature-rich Web app for mobile and desktop",
icon_path: "/img/logos/icon_phanpy.svg",
links: [
["Launch", "https://phanpy.social"],
["GitHub", "https://github.com/cheeaun/phanpy"]
] ]
) %> ) %>
<%= render AppInfoComponent.new( <%= render AppInfoComponent.new(
@@ -160,15 +150,6 @@
["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"] ["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"]
] ]
) %> ) %>
<%= render AppInfoComponent.new(
name: "Phanpy",
description: " A slick, feature-rich Web app for mobile and desktop",
icon_path: "/img/logos/icon_phanpy.svg",
links: [
["Launch", "https://phanpy.social"],
["GitHub", "https://github.com/cheeaun/phanpy"]
]
) %>
</div> </div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel"> <div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new( <%= render AppInfoComponent.new(
@@ -199,15 +180,6 @@
["App Store", "https://apps.apple.com/app/mammoth-for-mastodon/id1667573899"] ["App Store", "https://apps.apple.com/app/mammoth-for-mastodon/id1667573899"]
] ]
) %> ) %>
<%= render AppInfoComponent.new(
name: "Phanpy",
description: " A slick, feature-rich Web app for mobile and desktop",
icon_path: "/img/logos/icon_phanpy.svg",
links: [
["Launch", "https://phanpy.social"],
["GitHub", "https://github.com/cheeaun/phanpy"]
]
) %>
</div> </div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel"> <div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new( <%= render AppInfoComponent.new(

View File

@@ -2,163 +2,15 @@
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<section> <section>
<p class="mb-6"> <h3 class="mb-10">Connected Apps</h3>
Store and synchronize your app data across different devices. <% if @rs_auths.any? %>
</p> <div class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-10 gap-x-12">
</section> <% @rs_auths.each do |auth| %>
<%= render RsAuthComponent.new(auth: auth) %>
<%= render partial: "shared/tabnav_remotestorage" %> <% end %>
<section>
<h3>Your Storage Address</h3>
<p class="mb-6">
In order to connect an app to your storage account, give it your address:
</p>
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
<img src="/img/logos/icon_remotestorage.svg"
class="inline-block h-6 w-6 mr-1 self-center">
<input type="text" id="user_address" class="grow"
value=<%= current_user.address %> disabled="disabled"
data-clipboard-target="source" />
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard">
<span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
<span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
</button>
</p>
</section>
<section>
<h3>Compatible Apps</h3>
<p>
Your Storage account is based on a new open standard called
<a href="https://remotestorage.io" target="_blank">
<img src="/img/logos/icon_remotestorage.svg" class="h-4 w-4 inline">
<strong>remoteStorage</strong>
</a>, which is not yet widely supported. Look
for the remoteStorage icon, or check the Sync settings in apps.
</p>
<p>
If you want your favorite apps to support syncing data with your own
Storage account, let the developers know! All relevant information is
available on the <a href="https://remotestorage.io"
target="_blank" class="ks-text-link">remoteStorage website</a>.
</p>
</section>
<section>
<h3>Recommended Apps</h3>
<div data-controller="tabs"
data-tabs-active-tab-class="-mb-px border-gray-200 border-l border-t border-r rounded-t text-indigo-600 hover:text-indigo-600"
data-tabs-inactive-tab-class="text-gray-500 hover:text-gray-700"
class="mb-12">
<select data-action="tabs#change" data-tabs-target="select"
class="block w-full mb-8 sm:hidden">
<option>Productivity</option>
<option>Bookmarks</option>
<option>Reading</option>
<option>File sharing</option>
<option>Learning</option>
</select>
<ul class="hidden sm:flex list-reset mb-8 border-gray-200 border-b">
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Productivity
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Bookmarks
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Reading
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
File sharing
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Learning
</a>
</li>
</ul>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Hyperdraft",
description: "Create text notes and (optionally) turn them into a website",
icon_path: "/img/app_icons/hyperdraft.png",
links: [
["Website", "https://hyperdraft.rosano.ca"],
]
) %>
<%= render AppInfoComponent.new(
name: "Notes Together",
description: "A powerful note-taking app, with support for attaching images and other files",
icon_path: "/img/app_icons/notes-together.png",
links: [
["Web App", "https://notestogether.hominidsoftware.com"],
]
) %>
<%= render AppInfoComponent.new(
name: "Papiers",
description: "A simple note-taking app",
icon_path: "/img/app_icons/papiers.png",
links: [
["Web App", "https://papiers.gitlab.io"],
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Webmarks",
description: "Archive your bookmarks in your remote storage",
icon_path: "/img/app_icons/webmarks.png",
links: [
["Web App", "https://webmarks.5apps.com"],
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Pétrolette",
description: "A news aggregator that syncs with your remote storage",
icon_path: "/img/app_icons/petrolette.png",
links: [
["Web App", "https://petrolette.space"],
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Sharesome",
description: "Quickly and easily share files from your remote storage",
icon_path: "/img/app_icons/sharesome.png",
links: [
["Web App", "https://sharesome.5apps.com"],
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Kommit",
description: "Create flashcards and learn them with spaced-repetition",
icon_path: "/img/app_icons/kommit.png",
links: [
["Website", "https://kommit.rosano.ca"],
]
) %>
</div>
</div> </div>
<% else %>
<p>No apps connected yet.</p>
<% end %>
</section> </section>
<% end %> <% end %>

View File

@@ -1,33 +0,0 @@
<%= render HeaderComponent.new(title: "Storage") %>
<%= render MainSimpleComponent.new do %>
<section>
<p class="mb-6">
Store and synchronize your app data across different devices.
</p>
</section>
<%= render partial: "shared/tabnav_remotestorage" %>
<section>
<% if @rs_auths.any? %>
<div class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-10 gap-x-12 mt-4">
<% @rs_auths.each do |auth| %>
<%= render RsAuthComponent.new(auth: auth) %>
<% end %>
</div>
<% else %>
<div class="text-center">
<p class="mt-4 mb-12 inline-flex align-center items-center">
<%= image_tag("/img/illustrations/undraw_friends_r511.svg", class: 'h-48') %>
</p>
<h3>
No apps connected
</h3>
<p class="text-gray-500">
When connected, your apps will show up here.
</p>
</div>
<% end %>
</section>
<% end %>

View File

@@ -1,6 +1,6 @@
<%= tag.section data: { <%= tag.section data: {
controller: "settings--account--email", controller: "settings--account--email",
"settings--account--email-validation-failed-value": @validation_errors&.[](:email)&.present? "settings--account--email-validation-failed-value": @validation_errors.present?
} do %> } do %>
<h3>E-Mail</h3> <h3>E-Mail</h3>
<%= form_for(@user, url: update_email_settings_path, method: "post") do |f| %> <%= form_for(@user, url: update_email_settings_path, method: "post") do |f| %>
@@ -23,7 +23,7 @@
</span> </span>
</button> </button>
</p> </p>
<% if @validation_errors&.[](:email)&.present? %> <% if @validation_errors.present? && @validation_errors[:email].present? %>
<p class="error-msg"><%= @validation_errors[:email].first %></p> <p class="error-msg"><%= @validation_errors[:email].first %></p>
<% end %> <% end %>
<div class="initial-hidden"> <div class="initial-hidden">
@@ -41,33 +41,10 @@
<% end %> <% end %>
<section> <section>
<h3>Password</h3> <h3>Password</h3>
<p class="mb-6">Use the following button to request an email with a password reset link:</p> <p class="mb-8">Use the following button to request an email with a password reset link:</p>
<%= form_with(url: reset_password_settings_path, method: :post) do %> <%= form_with(url: reset_password_settings_path, method: :post) do %>
<p> <p>
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %> <%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
</p> </p>
<% end %> <% end %>
</section> </section>
<%= form_for(@user, url: setting_path(:account), html: { :method => :put }) do |f| %>
<section class="!pt-8 sm:!pt-12">
<h3>OpenPGP</h3>
<ul role="list">
<%= render FormElements::FieldsetComponent.new(
title: "Public key",
description: "Your OpenPGP public key in ASCII Armor format"
) do %>
<%= f.text_area :pgp_pubkey,
value: @user.pgp_pubkey,
class: "h-24 w-full" %>
<% if @validation_errors&.[](:pgp_pubkey)&.present? %>
<p class="error-msg">This <%= @validation_errors[:pgp_pubkey].first %></p>
<% end %>
<% end %>
</ul>
</section>
<section>
<p class="pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p>
</section>
<% end %>

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