Compare commits

..

No commits in common. "master" and "v0.8.0" have entirely different histories.

715 changed files with 2790 additions and 11706 deletions

View File

@ -17,7 +17,7 @@ steps:
branch: branch:
- master - master
- name: rspec - name: rspec
image: gitea.kosmos.org/kosmos/akkounts-ci:0.9.1 image: guildeducation/rails:2.7.2-14.20.0
environment: environment:
RAILS_ENV: test RAILS_ENV: test
REDIS_URL: redis://redis:6379/0 REDIS_URL: redis://redis:6379/0
@ -28,8 +28,6 @@ steps:
- bundle config set cache_path 'vendor/cache' - bundle config set cache_path 'vendor/cache'
- bundle config set with 'development test' - bundle config set with 'development test'
- bundle install --jobs=3 --retry=3 - bundle install --jobs=3 --retry=3
- bundle exec rails db:create
- bundle exec rails db:migrate
- yarn install - yarn install
- rake css:build - rake css:build
- bundle exec rspec - bundle exec rspec

View File

@ -1,92 +1,46 @@
# PRIMARY_DOMAIN=kosmos.org PRIMARY_DOMAIN=kosmos.org
# AKKOUNTS_DOMAIN=accounts.example.com AKKOUNTS_DOMAIN=accounts.example.com
# Generate this using `rails secret` SMTP_SERVER=smtp.example.com
# SECRET_KEY_BASE= SMTP_PORT=587
SMTP_LOGIN=accounts
SMTP_PASSWORD=123abc
SMTP_FROM_ADDRESS=accounts@example.com
SMTP_DOMAIN=example.com
SMTP_AUTH_METHOD=plain
SMTP_ENABLE_STARTTLS=auto
# Generate these using `rails db:encryption:init` LDAP_HOST=localhost
# (Optional, needed for LndHub integration) LDAP_PORT=389
# ENCRYPTION_PRIMARY_KEY= LDAP_ADMIN_PASSWORD=passthebutter
# ENCRYPTION_KEY_DERIVATION_SALT= LDAP_SUFFIX='dc=kosmos,dc=org'
# The default backend is SQLite REDIS_URL='redis://localhost:6379/1'
# DB_ADAPTER=postgresql
# PG_HOST=localhost
# PG_PORT=5432
# PG_DATABASE=akkounts
# PG_DATABASE_QUEUE=akkounts_queue
# PG_USERNAME=akkounts
# PG_PASSWORD=
# SMTP_SERVER=smtp.example.com WEBHOOKS_ALLOWED_IPS='10.1.1.163'
# SMTP_PORT=587
# SMTP_LOGIN=accounts
# SMTP_PASSWORD=123abc
# SMTP_FROM_ADDRESS=accounts@example.com
# SMTP_DOMAIN=example.com
# SMTP_AUTH_METHOD=plain
# SMTP_ENABLE_STARTTLS=auto
# S3_ENABLED=true DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
# S3_ENDPOINT=https://s3.kosmos.org DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
# S3_REGION=garage
# S3_BUCKET=akkounts-production
# S3_ALIAS_HOST=https://accounts.web.s3.kosmos.org
# S3_ACCESS_KEY=123456abcdefg
# S3_SECRET_KEY=123456789123456789123456789
# LDAP_HOST=localhost DRONECI_PUBLIC_URL='https://drone.kosmos.org'
# LDAP_PORT=389
# LDAP_USE_TLS=false
# LDAP_UID_ATTR=cn
# LDAP_BASE="ou=kosmos.org,cn=users,dc=kosmos,dc=org"
# LDAP_ADMIN_USER="cn=Directory Manager"
# LDAP_ADMIN_PASSWORD=passthebutter
# LDAP_SUFFIX="dc=kosmos,dc=org"
# REDIS_URL='redis://localhost:6379/1' GITEA_PUBLIC_URL='https://gitea.kosmos.org'
MASTODON_PUBLIC_URL='https://kosmos.social'
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/2'
# WEBHOOKS_ALLOWED_IPS='10.1.1.163' EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
# BTCPAY_API_URL='http://localhost:23001/api/v1'
# Service Integrations
# (sorted alphabetically by service name)
#
# BTCPAY_PUBLIC_URL='https://btcpay.example.com' LNDHUB_API_URL='http://localhost:3023'
# BTCPAY_API_URL='http://localhost:23001/api/v1' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
# BTCPAY_STORE_ID='' LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
# BTCPAY_AUTH_TOKEN='' LNDHUB_ADMIN_UI=true
LNDHUB_PG_HOST=localhost
# DISCOURSE_PUBLIC_URL='https://community.kosmos.org' LNDHUB_PG_PORT=5432
# DISCOURSE_CONNECT_SECRET='discourse_connect_ftw' LNDHUB_PG_DATABASE=lndhub
LNDHUB_PG_USERNAME=lndhub
# DRONECI_PUBLIC_URL='https://drone.kosmos.org' LNDHUB_PG_PASSWORD=''
# EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
# EJABBERD_API_URL='https://xmpp.kosmos.org/api'
# GITEA_PUBLIC_URL='https://gitea.kosmos.org'
# LNDHUB_API_URL='http://localhost:3023'
# LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
# LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
# LNDHUB_ADMIN_UI=true
# LNDHUB_ADMIN_TOKEN=123456789
# LNDHUB_PG_HOST=localhost
# LNDHUB_PG_PORT=5432
# LNDHUB_PG_DATABASE=lndhub
# LNDHUB_PG_USERNAME=lndhub
# LNDHUB_PG_PASSWORD=''
# MASTODON_PUBLIC_URL='https://kosmos.social'
# MASTODON_ADDRESS_DOMAIN='https://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_REDIS_URL='redis://localhost:6379/2'

View File

@ -1,31 +1,19 @@
PRIMARY_DOMAIN=kosmos.org PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.kosmos.org
ENCRYPTION_PRIMARY_KEY=YhNLBgCFMAzw5dV3gISxnGrhNDMQwRdn
ENCRYPTION_KEY_DERIVATION_SALT=h28g16MRZ1sghF2jTCos1DiLZXUswinR
REDIS_URL='redis://localhost:6379/0' REDIS_URL='redis://localhost:6379/0'
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
BTCPAY_STORE_ID='123456'
DISCOURSE_PUBLIC_URL='http://discourse.example.com' DISCOURSE_PUBLIC_URL='http://discourse.example.com'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw' 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' BTCPAY_API_URL='http://btcpay.example.com/api/v1'
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_REDIS_URL='redis://localhost:6379/1'
RS_STORAGE_URL='https://storage.kosmos.org' RS_STORAGE_URL='https://storage.kosmos.org'
RS_AKKOUNTS_DOMAIN=localhost RS_REDIS_URL='redis://localhost:6379/1'
WEBHOOKS_ALLOWED_IPS='10.1.1.23' WEBHOOKS_ALLOWED_IPS='10.1.1.23'

5
.gitignore vendored
View File

@ -23,7 +23,6 @@
!/tmp/pids/ !/tmp/pids/
!/tmp/pids/.keep !/tmp/pids/.keep
/storage
/public/assets /public/assets
.byebug_history .byebug_history
@ -37,7 +36,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 +46,3 @@ dump.rdb
/app/assets/builds/* /app/assets/builds/*
!/app/assets/builds/.keep !/app/assets/builds/.keep
# Ignore generated ctags
*.tags

View File

@ -1 +1 @@
3.3.0 2.7.2

View File

@ -1,11 +1,10 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM ruby:3.3.4 FROM ruby:2.7.6
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 \ RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
ldap-utils tini libvips ldap-utils tini
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

39
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.0.2'
# 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,12 +19,17 @@ 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.7'
# Configuration # Configuration
gem 'dotenv-rails' gem 'dotenv-rails'
# Security
gem 'lockbox'
# Authentication # Authentication
gem 'warden' gem 'warden'
gem 'devise', '~> 4.9.0' gem 'devise', '~> 4.9.0'
@ -32,26 +37,19 @@ gem 'devise_ldap_authenticatable'
gem 'net-ldap' gem 'net-ldap'
# Utilities # Utilities
gem 'aasm'
gem "image_processing", "~> 1.12.2"
gem "rqrcode", "~> 2.0" gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3' gem 'rails-settings-cached', '~> 2.8.3'
gem 'pagy', '~> 6.0', '>= 6.0.2' 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'
gem 'kramdown'
# HTTP requests # HTTP requests
gem 'faraday' gem 'faraday'
gem 'down'
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"
@ -60,20 +58,19 @@ gem "sentry-rails"
# Services # Services
gem 'discourse_api' gem 'discourse_api'
gem "lnurl" gem "lnurl"
gem 'manifique', '~> 1.1.0' gem 'nostr', git: 'https://gitea.kosmos.org/kosmos/nostr-gem.git', branch: 'feature/ruby_2.7_compat'
gem 'nostr', '~> 0.6.0'
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.4'
gem 'rspec-rails' gem 'rspec-rails'
gem 'rails-controller-testing' gem 'rails-controller-testing'
gem "byebug", "~> 11.1"
end end
group :development do group :development do
# Access an interactive console on exception pages or by calling 'console' anywhere in the code. # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
gem 'web-console', '~> 4.2' gem 'web-console', '>= 3.3.0'
gem 'listen', '~> 3.2' gem 'listen', '~> 3.2'
gem 'letter_opener' gem 'letter_opener'
gem 'letter_opener_web' gem 'letter_opener_web'
@ -89,8 +86,8 @@ group :test do
end end
group :production do group :production do
gem 'pg', '~> 1.5' # Use postgresql as the database for Active Record
gem 'pg', '~> 1.2.3'
end end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

View File

@ -1,139 +1,122 @@
GIT
remote: https://gitea.kosmos.org/kosmos/nostr-gem.git
revision: 596529d9eb50d13b3f385245636698fccf37b442
branch: feature/ruby_2.7_compat
specs:
nostr (0.4.0)
bech32 (~> 1.3)
bip-schnorr (~> 0.4)
ecdsa (~> 1.2)
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
json (~> 2.6)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
aasm (5.5.0) actioncable (7.0.5)
concurrent-ruby (~> 1.0) actionpack (= 7.0.5)
actioncable (8.0.2) activesupport (= 7.0.5)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) actionmailbox (7.0.5)
actionmailbox (8.0.2) actionpack (= 7.0.5)
actionpack (= 8.0.2) activejob (= 7.0.5)
activejob (= 8.0.2) activerecord (= 7.0.5)
activerecord (= 8.0.2) activestorage (= 7.0.5)
activestorage (= 8.0.2) activesupport (= 7.0.5)
activesupport (= 8.0.2) mail (>= 2.7.1)
mail (>= 2.8.0) net-imap
actionmailer (8.0.2) net-pop
actionpack (= 8.0.2) net-smtp
actionview (= 8.0.2) actionmailer (7.0.5)
activejob (= 8.0.2) actionpack (= 7.0.5)
activesupport (= 8.0.2) actionview (= 7.0.5)
mail (>= 2.8.0) activejob (= 7.0.5)
rails-dom-testing (~> 2.2) activesupport (= 7.0.5)
actionpack (8.0.2) mail (~> 2.5, >= 2.5.4)
actionview (= 8.0.2) net-imap
activesupport (= 8.0.2) net-pop
nokogiri (>= 1.8.5) net-smtp
rack (>= 2.2.4) rails-dom-testing (~> 2.0)
rack-session (>= 1.0.1) actionpack (7.0.5)
actionview (= 7.0.5)
activesupport (= 7.0.5)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.0, >= 1.2.0)
useragent (~> 0.16) actiontext (7.0.5)
actiontext (8.0.2) actionpack (= 7.0.5)
actionpack (= 8.0.2) activerecord (= 7.0.5)
activerecord (= 8.0.2) activestorage (= 7.0.5)
activestorage (= 8.0.2) activesupport (= 7.0.5)
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.0.5)
activesupport (= 8.0.2) activesupport (= 7.0.5)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.4)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (8.0.2) activejob (7.0.5)
activesupport (= 8.0.2) activesupport (= 7.0.5)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.0.2) activemodel (7.0.5)
activesupport (= 8.0.2) activesupport (= 7.0.5)
activerecord (8.0.2) activerecord (7.0.5)
activemodel (= 8.0.2) activemodel (= 7.0.5)
activesupport (= 8.0.2) activesupport (= 7.0.5)
timeout (>= 0.4.0) activestorage (7.0.5)
activestorage (8.0.2) actionpack (= 7.0.5)
actionpack (= 8.0.2) activejob (= 7.0.5)
activejob (= 8.0.2) activerecord (= 7.0.5)
activerecord (= 8.0.2) activesupport (= 7.0.5)
activesupport (= 8.0.2)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.0.2) mini_mime (>= 1.1.0)
base64 activesupport (7.0.5)
benchmark (>= 0.3) concurrent-ruby (~> 1.0, >= 1.0.2)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1) minitest (>= 5.1)
securerandom (>= 0.3) tzinfo (~> 2.0)
tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.4)
uri (>= 0.13.1) public_suffix (>= 2.0.2, < 6.0)
addressable (2.8.7) ast (2.4.2)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.3)
aws-eventstream (1.3.2)
aws-partitions (1.1092.0)
aws-sdk-core (3.222.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.183.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
backport (1.2.0) backport (1.2.0)
base64 (0.2.0) bcrypt (3.1.18)
bcrypt (3.1.20) bech32 (1.3.0)
bech32 (1.5.0)
thor (>= 1.1.0) thor (>= 1.1.0)
benchmark (0.4.0) benchmark (0.2.1)
bigdecimal (3.1.9)
bindex (0.8.1) bindex (0.8.1)
bip-schnorr (0.7.0) bip-schnorr (0.6.0)
ecdsa_ext (~> 0.5.0) ecdsa_ext (~> 0.5.0)
builder (3.3.0) builder (3.2.4)
capybara (3.40.0) byebug (11.1.3)
capybara (3.39.2)
addressable addressable
matrix matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.11) nokogiri (~> 1.8)
rack (>= 1.6.0) rack (>= 1.6.0)
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.2)
connection_pool (2.5.2) connection_pool (2.4.1)
crack (1.0.0) crack (0.4.5)
bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
cssbundling-rails (1.4.3) cssbundling-rails (1.1.2)
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.3)
devise (4.9.4) devise (4.9.2)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@ -142,112 +125,91 @@ 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.0)
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) e2mmap (0.1.0)
addressable (~> 2.8)
drb (2.2.1)
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.2.1)
activesupport (>= 6.1.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.4) factory_bot_rails (6.2.0)
factory_bot (~> 6.5) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.5.1) faker (3.2.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.9.2) faraday (2.7.6)
faraday-net_http (>= 2.0, < 3.2) faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
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.0.2)
net-http faye-websocket (0.11.2)
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.15.5)
ffi (1.17.2-arm64-darwin) flipper (0.28.0)
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 (0.28.0)
activerecord (>= 4.2, < 9) activerecord (>= 4.2, < 8)
flipper (~> 1.3.4) flipper (~> 0.28.0)
flipper-ui (1.3.4) flipper-ui (0.28.0)
erubi (>= 1.0.0, < 2.0.0) erubi (>= 1.0.0, < 2.0.0)
flipper (~> 1.3.4) flipper (~> 0.28.0)
rack (>= 1.4, < 4) rack (>= 1.4, < 3)
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.8.1)
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.1.0)
activesupport (>= 6.1) activesupport (>= 5.0)
gpgme (2.0.24) hashdiff (1.0.1)
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) importmap-rails (1.1.6)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (2.1.0)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
io-console (0.8.0) jaro_winkler (1.5.6)
irb (1.15.2) jbuilder (2.11.5)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jaro_winkler (1.6.0)
jbuilder (2.13.0)
actionview (>= 5.0.0) actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
jmespath (1.6.2) json (2.6.3)
json (2.11.3) kramdown (2.4.0)
kramdown (2.5.1) rexml
rexml (>= 3.3.9)
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) launchy (2.5.2)
launchy (3.1.1)
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.0.1)
bech32 (~> 1.1) bech32 (~> 1.1)
logger (1.7.0) lockbox (1.2.0)
loofah (2.24.0) loofah (2.21.3)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@ -255,282 +217,211 @@ GEM
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
manifique (1.1.0) marcel (1.0.2)
faraday (~> 2.9.0)
faraday-follow_redirects (= 0.3.0)
nokogiri (~> 1.16.0)
marcel (1.0.4)
matrix (0.4.2) matrix (0.4.2)
method_source (1.1.0) method_source (1.0.0)
mini_magick (4.13.2) mini_mime (1.1.2)
mini_mime (1.1.5) minitest (5.18.0)
mini_portile2 (2.8.8) multipart-post (2.3.0)
minitest (5.25.5) net-imap (0.3.6)
mission_control-jobs (1.0.2)
actioncable (>= 7.1)
actionpack (>= 7.1)
activejob (>= 7.1)
activerecord (>= 7.1)
importmap-rails (>= 1.2.1)
irb (~> 1.13)
railties (>= 7.1)
stimulus-rails
turbo-rails
multipart-post (2.4.1)
net-http (0.6.0)
uri
net-imap (0.5.7)
date date
net-protocol net-protocol
net-ldap (0.19.0) net-ldap (0.18.0)
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.1)
timeout timeout
net-smtp (0.5.1) net-smtp (0.3.3)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.5.9)
nokogiri (1.16.8) nokogiri (1.15.2-arm64-darwin)
mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.8-arm64-darwin) nokogiri (1.15.2-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.8-x86_64-linux)
racc (~> 1.4)
nostr (0.6.0)
bech32 (~> 1.4)
bip-schnorr (~> 0.7)
ecdsa (~> 1.2)
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
json (~> 2.6)
observer (0.1.2)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostruct (0.6.1) pagy (6.0.4)
pagy (6.5.0) parallel (1.23.0)
parallel (1.27.0) parser (3.2.2.3)
parser (3.3.8.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.9) pg (1.2.3)
pp (0.6.2) public_suffix (5.0.1)
prettyprint puma (4.3.12)
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
public_suffix (6.0.1)
puma (6.6.0)
nio4r (~> 2.0) nio4r (~> 2.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.7.1)
rack (2.2.13) rack (2.2.7)
rack-protection (3.2.0) rack-protection (3.0.6)
base64 (>= 0.1.0) rack
rack (~> 2.2, >= 2.2.4) rack-test (2.1.0)
rack-session (1.0.2)
rack (< 3)
rack-test (2.2.0)
rack (>= 1.3) rack (>= 1.3)
rackup (1.0.1) rails (7.0.5)
rack (< 3) actioncable (= 7.0.5)
webrick actionmailbox (= 7.0.5)
rails (8.0.2) actionmailer (= 7.0.5)
actioncable (= 8.0.2) actionpack (= 7.0.5)
actionmailbox (= 8.0.2) actiontext (= 7.0.5)
actionmailer (= 8.0.2) actionview (= 7.0.5)
actionpack (= 8.0.2) activejob (= 7.0.5)
actiontext (= 8.0.2) activemodel (= 7.0.5)
actionview (= 8.0.2) activerecord (= 7.0.5)
activejob (= 8.0.2) activestorage (= 7.0.5)
activemodel (= 8.0.2) activesupport (= 7.0.5)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.2) railties (= 7.0.5)
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)
activesupport (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0) rails-dom-testing (2.0.3)
activesupport (>= 5.0.0) activesupport (>= 4.2.0)
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.0.5)
actionpack (= 8.0.2) actionpack (= 7.0.5)
activesupport (= 8.0.2) activesupport (= 7.0.5)
irb (~> 1.13) method_source
rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0)
zeitwerk (~> 2.6) zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.0.6)
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 redis (4.8.1)
rdoc (6.13.1) regexp_parser (2.8.1)
psych (>= 4.0.0) responders (3.1.0)
redis (5.4.0)
redis-client (>= 0.22.0)
redis-client (0.24.0)
connection_pool
regexp_parser (2.10.0)
reline (0.6.1)
io-console (~> 0.5)
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.5)
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.5)
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.0.3)
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.0)
rubocop (1.75.3) rubocop (1.52.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.2.2.3)
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.28.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.29.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) ruby2_keywords (0.0.5)
ffi (~> 1.12) rufus-scheduler (3.9.1)
logger fugit (~> 1.1, >= 1.1.6)
sanitize (7.0.0) sanitize (6.0.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.16.8) nokogiri (>= 1.12.0)
securerandom (0.4.1) sentry-rails (5.9.0)
sentry-rails (5.23.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.23.0) sentry-ruby (~> 5.9.0)
sentry-ruby (5.23.0) sentry-ruby (5.9.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
solargraph (0.54.2) sidekiq (6.5.9)
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.49.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.0)
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.6.3-arm64-darwin)
sqlite3 (2.6.0) sqlite3 (1.6.3-x86_64-linux)
mini_portile2 (~> 2.8.0) stimulus-rails (1.2.1)
sqlite3 (2.6.0-arm64-darwin) railties (>= 6.0.0)
sqlite3 (2.6.0-x86_64-linux-gnu) thor (1.2.2)
stimulus-rails (1.3.4) tilt (2.2.0)
timeout (0.3.2)
turbo-rails (1.4.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.4.2)
unicode-emoji (~> 4.0, >= 4.0.4) view_component (3.2.0)
unicode-emoji (4.0.4) activesupport (>= 5.2.0, < 8.0)
uri (1.0.3) concurrent-ruby (~> 1.0)
useragent (0.16.11)
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)
web-console (4.2.1) web-console (4.2.0)
actionview (>= 6.0.0) actionview (>= 6.0.0)
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.18.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) websocket-driver (0.7.5)
websocket-driver (0.7.7)
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.8)
yard (~> 0.9)
zbase32 (0.1.1)
zeitwerk (2.7.2)
PLATFORMS PLATFORMS
arm64-darwin-22 arm64-darwin-22
ruby
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
aasm byebug (~> 11.1)
aws-sdk-s3
bcrypt (~> 3.1)
capybara capybara
cssbundling-rails cssbundling-rails
database_cleaner database_cleaner
@ -538,49 +429,43 @@ DEPENDENCIES
devise_ldap_authenticatable devise_ldap_authenticatable
discourse_api discourse_api
dotenv-rails dotenv-rails
down
factory_bot_rails factory_bot_rails
faker faker
faraday faraday
flipper flipper
flipper-active_record flipper-active_record
flipper-ui flipper-ui
gpgme (~> 2.0.24)
image_processing (~> 1.12.2)
importmap-rails importmap-rails
jbuilder (~> 2.7) jbuilder (~> 2.7)
kramdown
letter_opener letter_opener
letter_opener_web letter_opener_web
listen (~> 3.2) listen (~> 3.2)
lnurl lnurl
manifique (~> 1.1.0) lockbox
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.2.3)
propshaft puma (~> 4.1)
puma (~> 6.6) rails (~> 7.0.2)
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.4)
stimulus-rails stimulus-rails
turbo-rails turbo-rails
tzinfo-data tzinfo-data
view_component view_component
warden warden
web-console (~> 4.2) web-console (>= 3.3.0)
webmock webmock
zbase32 (~> 0.1.1)
BUNDLED WITH BUNDLED WITH
2.5.5 2.3.7

View File

@ -14,10 +14,8 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in 1. Make sure [Docker Compose is installed][1] and Docker is running (included in
Docker Desktop) Docker Desktop)
3. Run `docker compose up --build` and wait until all services have started 3. Run `docker compose up` and wait until 389ds announces its successful start
(389ds might take an extra minute to be ready). This will take a while when in the log output
running for the first time, so you might want to do something else in the
meantime.
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"` 4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
5. `docker compose run web rails ldap:setup` 5. `docker compose run web rails ldap:setup`
6. `docker compose run web rails db:setup` 6. `docker compose run web rails db:setup`
@ -30,44 +28,38 @@ have the password "user is user".
### Rails app ### Rails app
_Note: when using Docker Compose, prefix the following commands with `docker-compose
run web`._
Installing dependencies: Installing dependencies:
bundle install bundle install
yarn install yarn install
Migrating the local database (after schema changes): Setting up local database (SQLite):
bundle exec rails db:create
bundle exec rails db:migrate bundle exec rails db:migrate
Running the dev server, and auto-building CSS files on change _(automatic with Docker Compose)_: Running the dev server and auto-building CSS files on change:
bin/dev bin/dev
Running the background workers (requires Redis) _(automatic with Docker Compose)_: Running the background workers (requires Redis):
bundle exec sidekiq -C config/sidekiq.yml bundle exec sidekiq -C config/sidekiq.yml
Running the test suite: Running all specs:
bundle exec rspec bundle exec rspec
Running the test suite with Docker Compose requires overriding the Rails ### Docker (Compose)
environment:
docker-compose exec -e "RAILS_ENV=test" web rspec There is a working Docker Compose config file, which define a number of services including
an app server for Rails as well as a local 389ds (LDAP) server.
### Docker Compose For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`,
listening on port 389 on your machine.
Services/containers are configured in `docker-compose.yml`. You can pick and choose your services adding them by name (listed in `docker-compose.yml`) at
the end of the docker compose command. eg. `docker compose up ldap redis`
You can run services selectively, for example if you want to run the Rails app
and test suite on the host machine. Just add the service names of the
containers you want to run to the `up` command, like so:
docker-compose up ldap redis
#### LDAP server #### LDAP server
@ -84,24 +76,8 @@ Now you can seed the back-end with data using this Rails task:
The setup task will first delete any existing entries in the directory tree The setup task will first delete any existing entries in the directory tree
("dc=kosmos,dc=org"), and then create our development entries. ("dc=kosmos,dc=org"), and then create our development entries.
Note that all 389ds data is stored in the `389ds-data` volume. So if you want Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
to start over with a fresh installation, delete both that volume as well as the with a fresh installation, delete both that directory as well as the container.
container.
#### Minio / remoteStorage
If you want to run remoteStorage accounts locally, you will have to create the
respective bucket first. With the `minio` container running (run by default
when using Docker Compose), follow these steps:
* `docker compose up web redis minio liquor-cabinet`
* Head to http://localhost:9001 and log in with user `minioadmin`, password
`minioadmin`
* Create a new bucket called `remotestorage` (or whatever you
change the `S3_BUCKET` config to)
* Create a new key with ID "dev-key" and secret "123456789" (or whatever you
change `S3_ACCESS_KEY` and `S3_SECRET_KEY` to). Leave the policy field empty,
as it will automatically allow access to the bucket you created.
### Adding npm modules to use with Stimulus controllers ### Adding npm modules to use with Stimulus controllers
@ -128,7 +104,6 @@ command:
### Front-end ### Front-end
* [Icons](https://feathericons.com)
* [Tailwind CSS](https://tailwindcss.com/) * [Tailwind CSS](https://tailwindcss.com/)
* [Sass](https://sass-lang.com/documentation) * [Sass](https://sass-lang.com/documentation)
* [Stimulus](https://stimulus.hotwired.dev/handbook/) * [Stimulus](https://stimulus.hotwired.dev/handbook/)

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -1,5 +0,0 @@
<% if @image_url %>
<%= image_tag @image_url, class: "h-full w-full" %>
<% else %>
<%= render partial: "icons/remotestorage", locals: { custom_class: "h-full w-full p-0.5 text-gray-200" } %>
<% end %>

View File

@ -1,15 +0,0 @@
# frozen_string_literal: true
module AppCatalog
class WebAppIconComponent < ViewComponent::Base
include ApplicationHelper
def initialize(web_app:)
if web_app&.icon&.attached?
@image_url = image_url_for(web_app.icon)
elsif web_app&.apple_touch_icon&.attached?
@image_url = image_url_for(web_app.apple_touch_icon)
end
end
end
end

View File

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

View File

@ -1,8 +0,0 @@
# frozen_string_literal: true
class DropdownComponent < ViewComponent::Base
def initialize(size: :large, icon_name: "kebap-menu")
@size = size.to_sym
@icon_name = icon_name
end
end

View File

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

View File

@ -1,19 +0,0 @@
# frozen_string_literal: true
class DropdownLinkComponent < ViewComponent::Base
def initialize(href:, open_in_new_tab: false, separator: false, add_class: nil)
@href = href
@target = open_in_new_tab ? "_blank" : nil
@class = class_str(separator, add_class)
end
private
def class_str(separator, add_class)
str = "no-underline block px-5 py-3 text-sm text-gray-900 bg-white
hover:bg-gray-100 focus:bg-gray-100 whitespace-no-wrap"
str = "#{str} border-t" if separator
str = "#{str} #{add_class}" if add_class
str
end
end

View File

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

View File

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

View File

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

View File

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

View File

@ -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,15 +2,14 @@
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
@description = description @descripton = description
@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

@ -5,9 +5,7 @@
} : nil do %> } : nil do %>
<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? %> <p class="text-gray-500"><%= @descripton %></p>
<p class="text-gray-500"><%= @description %></p>
<% end %>
</div> </div>
<div class="relative ml-4 inline-flex flex-shrink-0"> <div class="relative ml-4 inline-flex flex-shrink-0">
<%= render FormElements::ToggleComponent.new( <%= render FormElements::ToggleComponent.new(

View File

@ -3,7 +3,7 @@
module FormElements module FormElements
class FieldsetToggleComponent < ViewComponent::Base class FieldsetToggleComponent < ViewComponent::Base
def initialize(tag: "li", form: nil, attribute: nil, field_name: nil, def initialize(tag: "li", form: nil, attribute: nil, field_name: nil,
enabled: false, input_enabled: true, title:, description: nil) enabled: false, input_enabled: true, title:, description:)
@tag = tag @tag = tag
@form = form @form = form
@attribute = attribute @attribute = attribute
@ -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

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

View File

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

View File

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

View File

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

View File

@ -12,17 +12,15 @@
<!-- Modal Container --> <!-- Modal Container -->
<div data-modal-target="container" <div data-modal-target="container"
class="relative m-4 max-h-screen w-auto max-w-full class="max-h-screen w-auto max-w-lg relative
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center"> hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
<!-- Modal Card --> <!-- Modal Card -->
<div class="m-1 bg-white rounded shadow"> <div class="m-1 bg-white rounded shadow">
<div class="p-8"> <div class="p-8">
<%= content %> <%= content %>
<% if @show_close_button %>
<div class="flex justify-end items-center flex-wrap mt-6"> <div class="flex justify-end items-center flex-wrap mt-6">
<button class="btn-md btn-blue" data-action="click->modal#close:prevent">Close</button> <button class="btn-md btn-blue" data-action="click->modal#close:prevent">Close</button>
</div> </div>
<% end %>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,2 @@
class ModalComponent < ViewComponent::Base class ModalComponent < ViewComponent::Base
def initialize(show_close_button: true)
@show_close_button = show_close_button
end
end end

View File

@ -34,8 +34,6 @@ class NotificationComponent < ViewComponent::Base
'alert-octagon' 'alert-octagon'
when 'alert' when 'alert'
'alert-octagon' 'alert-octagon'
when 'warning'
'alert-octagon'
else else
'info' 'info'
end end

View File

@ -1,27 +0,0 @@
<div class="flex items-center gap-4">
<div class="h-16 w-16 flex-none">
<%= render AppCatalog::WebAppIconComponent.new(web_app: @web_app) %>
</div>
<div class="flex-grow">
<h4 class="mb-1 text-lg font-bold">
<%= @web_app&.name || @auth.app_name %>
</h4>
<p class="text-sm text-gray-500">
<%= @auth.client_id %>
</p>
</div>
<%= render DropdownComponent.new do %>
<%= render DropdownLinkComponent.new(
href: launch_app_services_storage_rs_auth_url(@auth),
open_in_new_tab: true
) do %>
Launch app
<% end %>
<%= render DropdownLinkComponent.new(
href: revoke_services_storage_rs_auth_url(@auth),
separator: true, add_class: "text-red-700"
) do %>
Revoke access
<% end %>
<% end %>
</div>

View File

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

View File

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

View File

@ -1,13 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class SidenavLinkComponent < ViewComponent::Base class SidenavLinkComponent < ViewComponent::Base
def initialize(name:, level: 1, path:, icon: nil, text_icon: nil, def initialize(name:, level: 1, path:, icon:, active: false, disabled: false)
active: false, disabled: false)
@name = name @name = name
@level = level @level = level
@path = path @path = path
@icon = icon @icon = icon
@text_icon = text_icon
@active = active @active = active
@disabled = disabled @disabled = disabled
@link_class = class_names_link(path) @link_class = class_names_link(path)
@ -29,7 +27,7 @@ class SidenavLinkComponent < ViewComponent::Base
def class_names_icon(path) def class_names_icon(path)
if @active if @active
"text-teal-600 group-hover:text-teal-600 flex-shrink-0 -ml-1 mr-3 h-6 w-6" "text-teal-500 group-hover:text-teal-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
elsif @disabled elsif @disabled
"text-gray-300 group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6" "text-gray-300 group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
else else

View File

@ -1,9 +0,0 @@
class Admin::AppCatalog::WebAppsController < Admin::AppCatalogController
def index
@pagy, @web_apps = pagy(AppCatalog::WebApp.order('created_at desc'))
@stats = {
known_apps: AppCatalog::WebApp.count
}
end
end

View File

@ -1,9 +0,0 @@
class Admin::AppCatalogController < Admin::BaseController
before_action :set_current_section
private
def set_current_section
@current_section = :app_catalog
end
end

View File

@ -3,27 +3,18 @@ class Admin::DonationsController < Admin::BaseController
before_action :set_current_section, only: [:index, :show, :new, :edit] before_action :set_current_section, only: [:index, :show, :new, :edit]
# GET /donations # GET /donations
# GET /donations.json
def index def index
@username = params[:username].presence @pagy, @donations = pagy(Donation.all.order('created_at desc'))
pending_scope = Donation.incomplete.joins(:user).order('paid_at desc')
completed_scope = Donation.completed.joins(:user).order('paid_at desc')
if @username
pending_scope = pending_scope.where(users: { cn: @username })
completed_scope = completed_scope.where(users: { cn: @username })
end
@pending_donations = pending_scope
@pagy, @donations = pagy(completed_scope)
@stats = { @stats = {
overall_sats: completed_scope.sum("amount_sats"), overall_sats: @donations.all.sum("amount_sats"),
donor_count: completed_scope.distinct.count(:user_id) donor_count: Donation.distinct.count(:user_id)
} }
end end
# GET /donations/1 # GET /donations/1
# GET /donations/1.json
def show def show
end end
@ -37,41 +28,54 @@ class Admin::DonationsController < Admin::BaseController
end end
# POST /donations # POST /donations
# POST /donations.json
def create def create
@donation = Donation.new(donation_params) @donation = Donation.new(donation_params)
if @donation.paid_at == nil respond_to do |format|
@donation.errors.add(:paid_at, message: "is required") if @donation.save
render :new, status: :unprocessable_entity and return format.html do
end redirect_to admin_donation_url(@donation), flash: {
success: 'Donation was successfully created.'
if @donation.save }
redirect_to admin_donation_url(@donation), flash: { end
success: 'Donation was successfully created.' format.json { render :show, status: :created, location: @donation }
} else
else format.html { render :new, status: :unprocessable_entity }
render :new, status: :unprocessable_entity format.json { render json: @donation.errors, status: :unprocessable_entity }
end
end end
end end
# PUT /donations/1 # PATCH/PUT /donations/1
# PATCH/PUT /donations/1.json
def update def update
if @donation.update(donation_params) respond_to do |format|
redirect_to admin_donation_url(@donation), flash: { if @donation.update(donation_params)
success: 'Donation was successfully updated.' format.html do
} redirect_to admin_donation_url(@donation), flash: {
else success: 'Donation was successfully updated.'
render :edit, status: :unprocessable_entity }
end
format.json { render :show, status: :ok, location: @donation }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @donation.errors, status: :unprocessable_entity }
end
end end
end end
# DELETE /donations/1 # DELETE /donations/1
# DELETE /donations/1.json
def destroy def destroy
@donation.destroy @donation.destroy
respond_to do |format|
redirect_to admin_donations_url, flash: { format.html do redirect_to admin_donations_url, flash: {
success: 'Donation was successfully destroyed.' success: 'Donation was successfully destroyed.'
} }
end
format.json { head :no_content }
end
end end
private private
@ -82,10 +86,7 @@ class Admin::DonationsController < Admin::BaseController
# Only allow a list of trusted parameters through. # Only allow a list of trusted parameters through.
def donation_params def donation_params
params.require(:donation).permit( params.require(:donation).permit(:user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :paid_at)
:user_id, :donation_method,
:amount_sats, :fiat_amount, :fiat_currency,
:public_name, :paid_at)
end end
def set_current_section def set_current_section

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,12 @@
class Admin::Settings::RegistrationsController < Admin::SettingsController class Admin::Settings::RegistrationsController < Admin::SettingsController
def show def index
end end
def update def create
update_settings update_settings
redirect_to admin_settings_registrations_path, flash: { redirect_to admin_settings_registrations_path, flash: {
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

@ -1,32 +1,19 @@
class Admin::Settings::ServicesController < Admin::SettingsController class Admin::Settings::ServicesController < Admin::SettingsController
before_action :set_service, only: [:show, :update]
def index def index
redirect_to admin_settings_service_path("btcpay") @service = params[:s]
if @service.blank?
redirect_to admin_settings_services_path(params: { s: "discourse" })
end
end end
def show def create
end service = params.require(:service)
def update
update_settings update_settings
redirect_to admin_settings_service_path(@service), flash: { redirect_to admin_settings_services_path(params: { s: service }), flash: {
success: "Settings saved" success: "Settings saved"
} }
end end
private
def set_subsection
@subsection = "services"
end
def set_service
@service = params[:service]
if @service.blank?
redirect_to admin_settings_services_path and return
end
end
end end

View File

@ -9,23 +9,22 @@ 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
end end
if @errors.any? if @errors.any?
render :show and return render :index and return
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

@ -1,71 +1,32 @@
class Admin::UsersController < Admin::BaseController class Admin::UsersController < Admin::BaseController
before_action :set_user, except: [:index] before_action :set_user, only: [:show]
before_action :set_current_section before_action :set_current_section
# GET /admin/users
def index def index
ldap = LdapService.new ldap = LdapService.new
ou = Setting.primary_domain @ou = params[:ou] || Setting.primary_domain
@show_contributors = Setting.user_index_show_contributors @orgs = ldap.fetch_organizations
@show_sustainers = Setting.user_index_show_sustainers @pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
@contributors = ldap.search_users(:memberStatus, :contributor, :cn) if @show_contributors
@sustainers = ldap.search_users(:memberStatus, :sustainer, :cn) if @show_sustainers
@admins = ldap.search_users(:admin, true, :cn)
@pagy, @users = pagy(User.where(ou: ou).order(cn: :asc))
@stats = { @stats = {
users_confirmed: User.where(ou: ou).confirmed.count, users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: ou).pending.count users_pending: User.where(ou: @ou).pending.count
} }
@stats[:users_contributing] = @contributors.size if @show_contributors
@stats[:users_paying] = @sustainers.size if @show_sustainers
end end
# GET /admin/users/:username
def show def show
@invitees = @user.invitees
@recent_invitees = @user.invitees.order(created_at: :desc).limit(5)
@more_invitees = (@invitees - @recent_invitees).count
if Setting.lndhub_admin_enabled? if Setting.lndhub_admin_enabled?
@lndhub_user = @user.lndhub_user @lndhub_user = @user.lndhub_user
end end
@services_enabled = @user.services_enabled @services_enabled = @user.services_enabled
@ldap_avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
end
# POST /admin/users/:username/invitations
def create_invitations
amount = params[:amount].to_i
notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user])
UserManager::CreateInvitations.call(user: @user, amount: amount, notify: notify_user)
redirect_to admin_user_path(@user.cn), flash: {
success: "Added #{amount} invitations to #{@user.cn}'s account"
}
end
# DELETE /admin/users/:username/invitations
def delete_invitations
invitations = @user.invitations.unused
amount = invitations.count
invitations.destroy_all
redirect_to admin_user_path(@user.cn), flash: {
success: "Removed #{amount} invitations from #{@user.cn}'s account"
}
end end
private private
def set_user def set_user
@user = User.find_by(cn: params[:username], ou: Setting.primary_domain) address = params[:address].split("@")
http_status :not_found unless @user @user = User.where(cn: address.first, ou: address.last).first
end end
def set_current_section def set_current_section

View File

@ -1,37 +0,0 @@
class Api::BtcpayController < Api::BaseController
before_action :require_feature_enabled
before_action :set_cors_access_control_headers
def onchain_btc_balance
balance = BtcpayManager::FetchOnchainWalletBalance.call
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch BTC wallet balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
def lightning_btc_balance
balance = BtcpayManager::FetchLightningWalletBalance.call
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch BTC lightning balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
private
def require_feature_enabled
unless Setting.btcpay_publish_wallet_balances
http_status :not_found and return
end
end
def set_cors_access_control_headers
return unless Rails.env.development?
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Headers'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end

View File

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

View File

@ -37,35 +37,4 @@ class ApplicationController < ActionController::Base
format.any { head status } format.any { head status }
end end
end end
def after_sign_in_path_for(user)
session[:user_return_to] || root_path
end
def lndhub_authenticate(options={})
if session[:ln_auth_token].present? && !options[:force_reauth]
@ln_auth_token = session[:ln_auth_token]
else
lndhub = Lndhub.new
auth_token = lndhub.authenticate(current_user)
session[:ln_auth_token] = auth_token
@ln_auth_token = auth_token
end
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
def lndhub_fetch_balance
@balance = LndhubManager::FetchUserBalance.call(auth_token: @ln_auth_token)
rescue AuthError
lndhub_authenticate(force_reauth: true)
raise if @fetch_balance_retried
@fetch_balance_retried = true
lndhub_fetch_balance
end
def nostr_event_from_params
params.permit!
params[:signed_event].to_h.symbolize_keys
end
end end

View File

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

View File

@ -1,126 +1,10 @@
class Contributions::DonationsController < ApplicationController class Contributions::DonationsController < ApplicationController
include BtcpayHelper
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_donation_methods, only: [:index, :create]
before_action :require_donation_method_enabled, only: [:create]
before_action :validate_donation_params, only: [:create]
before_action :set_donation, only: [:confirm_btcpay]
# GET /contributions/donations # GET /donations
# GET /donations.json
def index def index
@donations = current_user.donations.completed
@current_section = :contributions @current_section = :contributions
@donations_completed = current_user.donations.completed.order('paid_at desc')
@donations_processing = current_user.donations.processing.order('created_at desc')
if Setting.lndhub_enabled?
begin
lndhub_authenticate
lndhub_fetch_balance
rescue
@balance = 0
end
end
end end
# POST /contributions/donations
def create
if params[:currency] == "sats"
fiat_amount = nil
fiat_currency = nil
amount_sats = params[:amount]
else
fiat_amount = params[:amount].to_i
fiat_currency = params[:currency]
amount_sats = nil
end
@donation = current_user.donations.create!(
donation_method: params[:donation_method],
payment_method: nil,
paid_at: nil,
amount_sats: amount_sats,
fiat_amount: (fiat_amount.nil? ? nil : fiat_amount * 100), # store in cents
fiat_currency: fiat_currency,
public_name: params[:public_name]
)
case params[:donation_method]
when "btcpay"
res = BtcpayManager::CreateInvoice.call(
amount: fiat_amount || (amount_sats.to_f / 100000000),
currency: fiat_currency || "BTC",
redirect_url: confirm_btcpay_contributions_donation_url(@donation)
)
@donation.update! btcpay_invoice_id: res["id"]
redirect_to btcpay_checkout_url(res["id"]), allow_other_host: true
else
redirect_to contributions_donations_url, flash: {
error: "Donation method currently not available"
}
end
end
def confirm_btcpay
redirect_to contributions_donations_url and return if @donation.completed?
invoice = BtcpayManager::FetchInvoice.call(invoice_id: @donation.btcpay_invoice_id)
if @donation.amount_sats.present?
# TODO make default fiat currency configurable and/or determine from user's
# i18n browser settings
@donation.fiat_currency = "EUR"
exchange_rate = BtcpayManager::FetchExchangeRate.call(fiat_currency: @donation.fiat_currency)
@donation.fiat_amount = (((@donation.amount_sats.to_f / 100000000) * exchange_rate) * 100).to_i
else
amt_str = invoice["paymentMethods"].first["amount"]
@donation.amount_sats = amt_str.tr(".","").sub(/0*$/, "").to_i
end
case invoice["status"]
when "Settled"
@donation.complete!
flash_message = { success: "Thank you!" }
when "Processing"
unless @donation.processing?
@donation.start_processing!
flash_message = { success: "Thank you! We will send you an email when the payment is confirmed." }
BtcpayCheckDonationJob.set(wait: 20.seconds).perform_later(@donation)
end
when "Expired"
flash_message = { warning: "The payment request for this donation has expired" }
else
flash_message = { warning: "Could not determine status of payment" }
end
redirect_to contributions_donations_url, flash: flash_message
end
private
def set_donation
@donation = current_user.donations.find_by(id: params[:id])
http_status :not_found unless @donation.present?
end
def set_donation_methods
@donation_methods = []
@donation_methods.push :btcpay if Setting.btcpay_enabled?
@donation_methods.push :lndhub if Setting.lndhub_enabled?
@donation_methods.push :opencollective if Setting.opencollective_enabled?
end
def require_donation_method_enabled
http_status :forbidden unless @donation_methods.include?(
params[:donation_method].to_sym
)
end
def validate_donation_params
if !%w[EUR USD sats].include?(params[:currency]) || (params[:amount].to_i <= 0)
http_status :unprocessable_entity
end
end
end end

View File

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

View File

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

View File

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

View File

@ -1,33 +1,23 @@
class LnurlpayController < ApplicationController class LnurlpayController < ApplicationController
before_action :check_service_available before_action :check_feature_enabled
before_action :find_user before_action :find_user_by_address
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://accounts.kosmos.org/lnurlpay/#{@user.address}/invoice",
tag: "payRequest", tag: "payRequest",
maxSendable: MAX_SATS * 1000, # msat maxSendable: MAX_SATS * 1000, # msat
minSendable: MIN_SATS * 1000, # msat minSendable: MIN_SATS * 1000, # msat
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?
@ -37,125 +27,69 @@ class LnurlpayController < ApplicationController
pubkey: Setting.lndhub_public_key, pubkey: Setting.lndhub_public_key,
customData: [{ customData: [{
customKey: "696969", customKey: "696969",
customValue: @user.lndhub_username customValue: @user.ln_account
}] }]
} }
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
address = params[:address]
comment = params[:comment] || "" comment = params[:comment] || ""
address = @user.address
if !valid_amount?(amount) if !valid_amount?(amount)
render json: { status: "ERROR", reason: "Invalid amount" } render json: { status: "ERROR", reason: "Invalid amount" }
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_by_address
headers['Access-Control-Allow-Origin'] = "*" address = params[:address].split("@")
headers['Access-Control-Allow-Headers'] = "*" @user = User.where(cn: address.first, ou: address.last).first
headers['Access-Control-Allow-Methods'] = "GET" http_status :not_found if @user.nil?
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_feature_enabled
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

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

View File

@ -3,7 +3,8 @@ class Rs::OauthController < ApplicationController
before_action :authenticate_user!, only: :create before_action :authenticate_user!, only: :create
def new def new
@user = User.where(cn: params[:username].downcase, ou: Setting.primary_domain).first username, org = params[:useraddress].split("@")
@user = User.where(cn: username.downcase, ou: org).first
@scopes = parse_scopes params[:scope] @scopes = parse_scopes params[:scope]
@redirect_uri = params[:redirect_uri] @redirect_uri = params[:redirect_uri]
@client_id = params[:client_id] @client_id = params[:client_id]
@ -21,7 +22,7 @@ class Rs::OauthController < ApplicationController
unless current_user == @user unless current_user == @user
sign_out :user sign_out :user
redirect_to new_rs_oauth_url(@user.cn, redirect_to new_rs_oauth_url(@user.address,
scope: params[:scope], scope: params[:scope],
redirect_uri: params[:redirect_uri], redirect_uri: params[:redirect_uri],
client_id: params[:client_id], client_id: params[:client_id],
@ -87,7 +88,7 @@ class Rs::OauthController < ApplicationController
permissions: permissions, permissions: permissions,
client_id: client_id, client_id: client_id,
redirect_uri: redirect_uri, redirect_uri: redirect_uri,
app_name: client_id, app_name: client_id, #TODO use user-defined name
expire_at: expire_at expire_at: expire_at
) )
@ -95,15 +96,28 @@ class Rs::OauthController < ApplicationController
allow_other_host: true allow_other_host: true
end end
# GET /rs/oauth/token/:id/launch_app
def launch_app
auth = current_user.remote_storage_authorizations.find(params[:id])
redirect_to app_auth_url(auth), allow_other_host: true
end
private private
def require_signed_in_with_username def require_signed_in_with_username
unless user_signed_in? unless user_signed_in?
session[:user_return_to] = request.url username, org = params[:useraddress].split("@")
redirect_to new_user_session_path(cn: params[:username], ou: Setting.primary_domain) redirect_to new_user_session_path(cn: username, ou: org)
end end
end end
def app_auth_url(auth)
url = "#{auth.url}#remotestorage=#{current_user.address}"
url += "&access_token=#{auth.token}"
url
end
def hostname_of(uri) def hostname_of(uri)
uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0] uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0]
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

@ -1,34 +0,0 @@
class Services::EmailController < Services::BaseController
before_action :authenticate_user!
before_action :require_service_available
before_action :require_feature_enabled
def show
ldap_entry = current_user.ldap_entry
@service_enabled = ldap_entry[:email_password].present?
@maildrop = ldap_entry[:email_maildrop]
@email_forwarding_active = @maildrop.present? &&
@maildrop.split("@").first != current_user.cn
end
def new_password
if session[:new_email_password].present?
@new_password = session.delete(:new_email_password)
else
redirect_to setting_path(:email)
end
end
private
def require_service_available
http_status :not_found unless Setting.email_enabled?
end
def require_feature_enabled
unless Flipper.enabled?(:email, current_user)
http_status :forbidden
end
end
end

View File

@ -2,14 +2,13 @@ require "rqrcode"
require "lnurl" require "lnurl"
class Services::LightningController < ApplicationController class Services::LightningController < ApplicationController
before_action :set_current_section
before_action :require_service_available
before_action :authenticate_user! before_action :authenticate_user!
before_action :lndhub_authenticate before_action :authenticate_with_lndhub
before_action :lndhub_fetch_balance before_action :set_current_section
before_action :fetch_balance
def index def index
@wallet_setup_url = "lndhub://#{current_user.lndhub_username}:#{current_user.lndhub_password}@#{ENV['LNDHUB_PUBLIC_URL']}" @wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
end end
def transactions def transactions
@ -56,12 +55,32 @@ class Services::LightningController < ApplicationController
private private
def authenticate_with_lndhub(options={})
if session[:ln_auth_token].present? && !options[:force_reauth]
@ln_auth_token = session[:ln_auth_token]
else
lndhub = Lndhub.new
auth_token = lndhub.authenticate(current_user)
session[:ln_auth_token] = auth_token
@ln_auth_token = auth_token
end
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
def set_current_section def set_current_section
@current_section = :services @current_section = :services
end end
def require_service_available def fetch_balance
http_status :not_found unless Setting.lndhub_enabled? lndhub = Lndhub.new
data = lndhub.balance @ln_auth_token
@balance = data["BTC"]["AvailableBalance"] rescue nil
rescue AuthError
authenticate_with_lndhub(force_reauth: true)
raise if @fetch_balance_retried
@fetch_balance_retried = true
fetch_balance
end end
def fetch_transactions def fetch_transactions

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

@ -1,25 +1,23 @@
class Services::RemotestorageController < Services::BaseController class Services::RemotestorageController < Services::BaseController
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_service_available
before_action :require_feature_enabled before_action :require_feature_enabled
before_action :require_service_available
# Dashboard def dashboard
def show # unless current_user.services_enabled.include?(:remotestorage)
# unless current_user.service_enabled?(:remotestorage)
# redirect_to service_remotestorage_info_path # redirect_to service_remotestorage_info_path
# end # end
# @rs_apps_connected = current_user.remote_storage_authorizations.any?
end end
private private
def require_service_available
http_status :not_found unless Setting.remotestorage_enabled?
end
def require_feature_enabled def require_feature_enabled
unless Flipper.enabled?(:remotestorage, current_user) unless Flipper.enabled?(:remotestorage, current_user)
http_status :forbidden http_status :forbidden
end end
end end
def require_service_available
http_status :not_found unless Setting.remotestorage_enabled?
end
end end

View File

@ -1,51 +0,0 @@
class Services::RsAuthsController < Services::BaseController
before_action :authenticate_user!
before_action :require_feature_enabled
before_action :require_service_available
# before_action :require_service_enabled
before_action :find_rs_auth, only: [:destroy, :launch_app]
def index
@rs_auths = current_user.remote_storage_authorizations
# TODO sort by app name?
end
def destroy
@auth.destroy!
respond_to do |format|
format.html do redirect_to apps_services_storage_url, flash: {
success: 'App authorization revoked'
}
end
format.json { head :no_content }
end
end
def launch_app
user_address = Rails.env.development? ?
"#{current_user.cn}@localhost:3000" :
current_user.address
launch_url = "#{@auth.launch_url}#remotestorage=#{user_address}"
redirect_to launch_url, allow_other_host: true
end
private
def require_feature_enabled
unless Flipper.enabled?(:remotestorage, current_user)
http_status :forbidden
end
end
def require_service_available
http_status :not_found unless Setting.remotestorage_enabled?
end
def find_rs_auth
@auth = current_user.remote_storage_authorizations.find(params[:id])
http_status :not_found unless @auth.present?
end
end

View File

@ -1,49 +1,28 @@
require "securerandom" require 'securerandom'
require "bcrypt"
class SettingsController < ApplicationController class SettingsController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_main_nav_section before_action :set_main_nav_section
before_action :set_settings_section, only: [:show, :update, :update_email, :reset_email_password] before_action :set_settings_section, only: [:show, :update, :update_email]
before_action :set_user, only: [:show, :update, :update_email, :reset_email_password] before_action :set_user, only: [:show, :update, :update_email]
def index def index
redirect_to setting_path(:profile) redirect_to setting_path(:profile)
end end
def show def show
case @settings_section if @settings_section == "experiments"
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_new]
@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])
LdapManager::UpdateDisplayName.call(dn: @user.dn, display_name: @user.display_name) LdapManager::UpdateDisplayName.call(@user.dn, user_params[:display_name])
end
if @user.avatar_new.present?
if store_user_avatar
UserManager::UpdateAvatar.call(user: @user)
else
@validation_errors = @user.errors
render :show, status: :unprocessable_entity and return
end
end
if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key])
UserManager::UpdatePgpKey.call(user: @user)
end end
redirect_to setting_path(@settings_section), flash: { redirect_to setting_path(@settings_section), flash: {
@ -55,9 +34,8 @@ 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?(email_params[:current_password])
if @user.update email: email_params[:email] if @user.update email: email_params[:email]
redirect_to setting_path(:account), flash: { redirect_to setting_path(:account), flash: {
notice: 'Please confirm your new address using the confirmation link we just sent you.' notice: 'Please confirm your new address using the confirmation link we just sent you.'
@ -73,30 +51,6 @@ class SettingsController < ApplicationController
end end
end end
# POST /settings/reset_email_password
def reset_email_password
@user.current_password = security_params[:current_password]
if @user.valid_ldap_authentication?(@user.current_password)
@user.current_password = nil
session[:new_email_password] = generate_email_password
hashed_password = hash_email_password(session[:new_email_password])
LdapManager::UpdateEmailPassword.call(dn: @user.dn, password_hash: hashed_password)
if @user.ldap_entry[:email_maildrop] != @user.address
LdapManager::UpdateEmailMaildrop.call(dn: @user.dn, address: @user.address)
end
redirect_to new_password_services_email_path
else
@validation_errors = {
current_password: [ "Wrong password. Try again!" ]
}
render :show, status: :forbidden
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
@ -104,41 +58,41 @@ 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_id = NostrManager::ValidateId.call(signed_event)
is_valid_sig = NostrManager::VerifySignature.call(signed_event)
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
is_valid_sig = signed_event.verify_signature unless is_valid_id && is_valid_sig && is_correct_content
is_valid_auth = NostrManager::VerifyAuth.call(
event: signed_event,
challenge: session[:shared_secret]
)
unless is_valid_sig && is_valid_auth
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) pubkey_taken = User.all_except(current_user).where(
ou: current_user.ou, nostr_pubkey: signed_event[:pubkey]
).any?
if user_with_pubkey.present? && (user_with_pubkey != current_user) if pubkey_taken
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) current_user.update! nostr_pubkey: signed_event[:pubkey]
session[:shared_secret] = nil session[:shared_secret] = nil
flash[:success] = "Public key verification successful" flash[:success] = "Public key verification successful"
http_status :ok http_status :ok
rescue
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end end
# DELETE /settings/nostr_pubkey # DELETE /settings/nostr_pubkey
def remove_nostr_pubkey def remove_nostr_pubkey
# TODO require current pubkey or password to delete current_user.update! nostr_pubkey: nil
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: nil)
redirect_to setting_path(:nostr), flash: { redirect_to setting_path(:experiments), flash: {
success: 'Public key removed from account' success: 'Public key removed from account'
} }
end end
@ -151,10 +105,7 @@ class SettingsController < ApplicationController
def set_settings_section def set_settings_section
@settings_section = params[:section] @settings_section = params[:section]
allowed_sections = [ allowed_sections = [:profile, :account, :lightning, :xmpp, :experiments]
:profile, :account, :xmpp, :email,
:lightning, :remotestorage, :nostr
]
unless allowed_sections.include?(@settings_section.to_sym) unless allowed_sections.include?(@settings_section.to_sym)
redirect_to setting_path(:profile) redirect_to setting_path(:profile)
@ -166,53 +117,19 @@ class SettingsController < ApplicationController
end end
def user_params def user_params
params.require(:user).permit( params.require(:user).permit(:display_name, preferences: [
:display_name, :avatar_new, :pgp_pubkey, :lightning_notify_sats_received,
preferences: UserPreferences.pref_keys :xmpp_exchange_contacts_with_invitees
) ])
end end
def email_params def email_params
params.require(:user).permit(:email) params.require(:user).permit(:email, :current_password)
end end
def security_params def nostr_event_params
params.require(:user).permit(:current_password) params.permit(signed_event: [
end :id, :pubkey, :created_at, :kind, :tags, :content, :sig
])
def generate_email_password
characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join
end
def hash_email_password(password)
salt = BCrypt::Engine.generate_salt
BCrypt::Engine.hash_secret(password, salt)
end
def store_user_avatar
io = @user.avatar_new.tempfile
img_data = UserManager::ProcessAvatar.call(io: io)
if img_data.blank?
@user.errors.add(:avatar, "failed to process file")
false
end
tempfile = Tempfile.create
tempfile.binmode
tempfile.write(img_data)
tempfile.rewind
hash = Digest::SHA256.hexdigest(img_data)
ext = @user.avatar_new.content_type == "image/png" ? "png" : "jpg"
filename = "#{hash}.#{ext}"
if filename == @user.avatar.filename.to_s
@user.errors.add(:avatar, "must be a new file/picture")
false
else
key = "users/#{@user.cn}/avatars/#{filename}"
@user.avatar.attach io: tempfile, key: key, filename: filename
@user.save
end
end end
end end

View File

@ -96,13 +96,13 @@ 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(
username: @user.cn, username: @user.cn,
domain: Setting.primary_domain, domain: Setting.primary_domain,
email: @user.email, email: @user.email,
password: @user.password, password: @user.password,
invitation: @invitation invitation: @invitation
}) )
end end
def set_context def set_context

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,43 +0,0 @@
class WebKeyDirectoryController < WellKnownController
before_action :allow_cross_origin_requests
# /.well-known/openpgpkey/hu/:hashed_username(.txt)?l=username
def show
if params[:l].blank?
# TODO store hashed username in db if existing implementations trigger
# this a lot
msg = "WKD request with \"l\" param omitted for hu: #{params[:hashed_username]}"
Sentry.capture_message(msg) if Setting.sentry_enabled?
http_status :bad_request and return
end
@user = User.find_by(cn: params[:l].downcase)
if @user.nil? ||
@user.pgp_pubkey.blank? ||
!@user.pgp_pubkey_contains_user_address?
http_status :not_found and return
end
if params[:hashed_username] != @user.wkd_hash
http_status :unprocessable_entity and return
end
respond_to do |format|
format.text do
response.headers['Content-Type'] = 'text/plain'
render plain: @user.pgp_pubkey
end
format.any do
key = @user.gnupg_key.export
send_data key, filename: "#{@user.wkd_hash}.pem",
type: "application/octet-stream"
end
end
end
def policy
head :ok
end
end

View File

@ -1,23 +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 && resource.match(/acct:\w+/)
@username, @domain = @useraddress.split("@") useraddress = resource.split(":").last
username, org = useraddress.split("@")
unless Rails.env.development? username.downcase!
# Allow different domains (e.g. localhost:3000) in development only unless User.where(cn: username, ou: org).any?
head 404 and return unless @domain == Setting.primary_domain
end
unless @user = User.where(ou: Setting.primary_domain)
.find_by(cn: @username.downcase)
head 404 and return head 404 and return
end end
render json: webfinger.to_json, render json: webfinger(useraddress).to_json,
content_type: "application/jrd+json" content_type: "application/jrd+json"
else else
head 422 and return head 422 and return
@ -26,75 +23,24 @@ class WebfingerController < WellKnownController
private private
def webfinger def webfinger(useraddress)
jrd = { links = [];
subject: "acct:#{@user.address}",
aliases: [],
links: []
}
if @user.avatar.attached? links << remotestorage_link(useraddress) if Setting.remotestorage_enabled
jrd[:links] += avatar_link
end
if Setting.mastodon_enabled && @user.service_enabled?(:mastodon) { "links" => links }
# https://docs.joinmastodon.org/spec/webfinger/
jrd[:aliases] += mastodon_aliases
jrd[:links] += mastodon_links
end
if Setting.remotestorage_enabled && @user.service_enabled?(:remotestorage)
# https://datatracker.ietf.org/doc/draft-dejong-remotestorage/
jrd[:links] << remotestorage_link
end
jrd
end end
def avatar_link def remotestorage_link(useraddress)
[ # TODO use when OAuth routes are available
{ # auth_url = new_rs_oauth_url(useraddress)
rel: "http://webfinger.net/rel/avatar", auth_url = "https://example.com/rs/oauth"
type: @user.avatar.content_type, storage_url = "#{Setting.rs_storage_url}/#{useraddress}"
href: helpers.image_url_for(@user.avatar)
}
]
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
def remotestorage_link
auth_url = new_rs_oauth_url(@username, host: Setting.rs_accounts_domain)
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
@ -103,4 +49,9 @@ class WebfingerController < WellKnownController
} }
} }
end end
def allow_cross_origin_requests
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
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!(lndhub_username: @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.primary_domain,
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

@ -1,6 +1,10 @@
module ApplicationHelper module ApplicationHelper
include Pagy::Frontend include Pagy::Frontend
def sats_to_btc(sats)
sats.to_f / 100000000
end
def main_nav_class(current_section, link_to_section) def main_nav_class(current_section, link_to_section)
if current_section == link_to_section if current_section == link_to_section
"bg-gray-900/50 text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block" "bg-gray-900/50 text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
@ -14,23 +18,4 @@ module ApplicationHelper
def badge(text, color) def badge(text, color)
tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800" tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800"
end end
def markdown_to_html(string)
raw Kramdown::Document.new(string, { input: "GFM" }).to_html
end
def image_url_for(attachment)
return s3_image_url(attachment) if Setting.s3_enabled?
if attachment.record.is_a?(User) && attachment.name == "avatar"
hash, format = attachment.blob.filename.to_s.split(".", 2)
user_avatar_url(
username: attachment.record.cn,
hash: hash,
format: format
)
else
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
end
end
end end

View File

@ -1,7 +0,0 @@
module BtcpayHelper
def btcpay_checkout_url(invoice_id)
"#{Setting.btcpay_public_url}/i/#{invoice_id}"
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,9 +1,8 @@
import { Application } from "@hotwired/stimulus" import { Application } from "@hotwired/stimulus"
import { Dropdown, Modal, Tabs } from "tailwindcss-stimulus-components" import { Modal, Tabs } from "tailwindcss-stimulus-components"
const application = Application.start() const application = Application.start()
application.register('dropdown', Dropdown)
application.register('modal', Modal) application.register('modal', Modal)
application.register('tabs', Tabs) application.register('tabs', Tabs)

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

@ -1,27 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "resetPasswordButton", "currentPasswordField" ]
static values = { validationFailed: Boolean }
connect () {
if (this.validationFailedValue) return;
this.element.querySelectorAll(".initial-hidden").forEach(el => {
el.classList.add("hidden");
})
this.element.querySelectorAll(".initial-visible").forEach(el => {
el.classList.remove("hidden");
})
}
showPasswordReset () {
this.element.querySelectorAll(".initial-visible").forEach(el => {
el.classList.add("hidden");
})
this.element.querySelectorAll(".initial-hidden").forEach(el => {
el.classList.remove("hidden");
})
this.currentPasswordFieldTarget.select();
}
}

View File

@ -1,16 +1,24 @@
import { Controller } from "@hotwired/stimulus" import { Controller } from "@hotwired/stimulus"
import { bech32 } from "bech32"
function hexToBytes (hex) {
let bytes = []
for (let c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16))
}
return bytes
}
// 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 (this.hasPubkeyHexValue && this.pubkeyHexValue.length > 0) {
this.pubkeyBech32InputTarget.value = this.pubkeyBech32
}
if (window.nostr) { if (window.nostr) {
if (this.hasSetPubkeyTarget) { if (this.hasSetPubkeyTarget) {
this.setPubkeyTarget.disabled = false this.setPubkeyTarget.disabled = false
@ -24,15 +32,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", {
@ -49,6 +53,11 @@ export default class extends Controller {
} }
} }
get pubkeyBech32 () {
const words = bech32.toWords(hexToBytes(this.pubkeyHexValue))
return bech32.encode('npub', words)
}
get csrfToken () { get csrfToken () {
const element = document.head.querySelector('meta[name="csrf-token"]') const element = document.head.querySelector('meta[name="csrf-token"]')
return element.getAttribute("content") return element.getAttribute("content")

View File

@ -1,26 +0,0 @@
class BtcpayCheckDonationJob < ApplicationJob
queue_as :default
def perform(donation)
return if donation.completed?
invoice = BtcpayManager::FetchInvoice.call(
invoice_id: donation.btcpay_invoice_id
)
case invoice["status"]
when "Settled"
donation.complete!
NotificationMailer.with(user: donation.user)
.bitcoin_donation_confirmed
.deliver_later
when "Processing"
re_enqueue_job(donation)
end
end
def re_enqueue_job(donation)
self.class.set(wait: 20.seconds).perform_later(donation)
end
end

View File

@ -1,10 +1,10 @@
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", "inetOrgPerson", "extensibleObject"], objectclass: ["top", "account", "person", "extensibleObject"],
cn: username, cn: username,
sn: username, sn: username,
uid: username, uid: username,
@ -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

@ -2,12 +2,12 @@ class CreateLndhubAccountJob < ApplicationJob
queue_as :default queue_as :default
def perform(user) def perform(user)
return if user.lndhub_username.present? && user.lndhub_password.present? return if user.ln_account.present? && user.ln_password.present?
lndhub = LndhubV2.new lndhub = LndhubV2.new
credentials = lndhub.create_account credentials = lndhub.create_account
user.update! lndhub_username: credentials["login"], user.update! ln_account: credentials["login"],
lndhub_password: credentials["password"] ln_password: credentials["password"]
end end
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,6 +2,21 @@ class XmppExchangeContactsJob < ApplicationJob
queue_as :default queue_as :default
def perform(inviter, invitee) def perform(inviter, invitee)
EjabberdManager::ExchangeContacts.call(inviter:, invitee:) return unless inviter.services_enabled.include?("xmpp") &&
invitee.services_enabled.include?("xmpp") &&
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new
ejabberd.add_rosteritem({
"localuser": invitee.cn, "localhost": invitee.ou,
"user": inviter.cn, "host": inviter.ou,
"nick": inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
ejabberd.add_rosteritem({
"localuser": inviter.cn, "localhost": inviter.ou,
"user": invitee.cn, "host": invitee.ou,
"nick": invitee.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
end end
end end

View File

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

View File

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

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