1 Commits

Author SHA1 Message Date
14c5dd22d6 Add strfry doc draft
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-19 14:37:54 +02:00
186 changed files with 1534 additions and 3873 deletions

View File

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

View File

@@ -1,9 +1,6 @@
PRIMARY_DOMAIN=kosmos.org PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.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_PUBLIC_URL='https://btcpay.example.com'
@@ -24,8 +21,7 @@ LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de55648
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea' NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf' 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'

4
.gitignore vendored
View File

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

View File

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

22
Gemfile
View File

@@ -2,13 +2,13 @@ source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" } git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 8.0' gem 'rails', '~> 7.1'
# Use Puma as the app server # Use Puma as the app server
gem 'puma', '~> 6.6' gem 'puma', '~> 4.1'
# View components # View components
gem "view_component" gem "view_component"
# Asset bundler # Separate dependency since Rails 7.0
gem 'propshaft' gem 'sprockets-rails'
# Allows custom JS build tasks to integrate with the asset pipeline # Allows custom JS build tasks to integrate with the asset pipeline
gem 'cssbundling-rails' gem 'cssbundling-rails'
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
@@ -19,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'
# 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'
@@ -39,8 +44,6 @@ gem 'pagy', '~> 6.0', '>= 6.0.2'
gem 'flipper' gem 'flipper'
gem 'flipper-active_record' gem 'flipper-active_record'
gem 'flipper-ui' gem 'flipper-ui'
gem 'gpgme', '~> 2.0.24'
gem 'zbase32', '~> 0.1.1'
# HTTP requests # HTTP requests
gem 'faraday' gem 'faraday'
@@ -48,8 +51,8 @@ gem 'down'
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
# Background/scheduled jobs # Background/scheduled jobs
gem 'solid_queue' gem 'sidekiq', '< 7'
gem "mission_control-jobs" gem 'sidekiq-scheduler'
# Monitoring # Monitoring
gem "sentry-ruby" gem "sentry-ruby"
@@ -60,11 +63,10 @@ gem 'discourse_api'
gem "lnurl" gem "lnurl"
gem 'manifique', '~> 1.1.0' gem 'manifique', '~> 1.1.0'
gem 'nostr', '~> 0.6.0' 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.7.2'
gem 'rspec-rails' gem 'rspec-rails'
gem 'rails-controller-testing' gem 'rails-controller-testing'
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -22,7 +22,7 @@ class Admin::UsersController < Admin::BaseController
@services_enabled = @user.services_enabled @services_enabled = @user.services_enabled
@ldap_avatar = LdapManager::FetchAvatar.call(cn: @user.cn) @avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
end end
# POST /admin/users/:username/invitations # POST /admin/users/:username/invitations
@@ -30,7 +30,7 @@ class Admin::UsersController < Admin::BaseController
amount = params[:amount].to_i amount = params[:amount].to_i
notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user]) notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user])
UserManager::CreateInvitations.call(user: @user, amount: amount, notify: notify_user) CreateInvitations.call(user: @user, amount: amount, notify: notify_user)
redirect_to admin_user_path(@user.cn), flash: { redirect_to admin_user_path(@user.cn), flash: {
success: "Added #{amount} invitations to #{@user.cn}'s account" success: "Added #{amount} invitations to #{@user.cn}'s account"

View File

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

@@ -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,7 +1,7 @@
class LnurlpayController < ApplicationController class LnurlpayController < ApplicationController
before_action :check_service_available before_action :check_service_available
before_action :find_user before_action :find_user
before_action :set_cors_access_control_headers before_action :set_cors_access_control_headers, only: [:invoice]
MIN_SATS = 10 MIN_SATS = 10
MAX_SATS = 1_000_000 MAX_SATS = 1_000_000
@@ -37,7 +37,7 @@ 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

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.service_enabled?(:xmpp)
end end
private private

View File

@@ -9,7 +9,7 @@ class Services::LightningController < ApplicationController
before_action :lndhub_fetch_balance before_action :lndhub_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

View File

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

View File

@@ -21,12 +21,10 @@ class SettingsController < ApplicationController
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.avatar_new = user_params[:avatar]
@user.pgp_pubkey = user_params[:pgp_pubkey]
if @user.save if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name]) if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
@@ -34,16 +32,7 @@ class SettingsController < ApplicationController
end end
if @user.avatar_new.present? if @user.avatar_new.present?
if store_user_avatar LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
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,7 +44,6 @@ class SettingsController < ApplicationController
end end
end end
# POST /settings/update_email
def update_email def update_email
if @user.valid_ldap_authentication?(security_params[:current_password]) if @user.valid_ldap_authentication?(security_params[:current_password])
if @user.update email: email_params[:email] if @user.update email: email_params[:email]
@@ -73,7 +61,6 @@ class SettingsController < ApplicationController
end end
end end
# POST /settings/reset_email_password
def reset_email_password def reset_email_password
@user.current_password = security_params[:current_password] @user.current_password = security_params[:current_password]
@@ -96,7 +83,6 @@ class SettingsController < ApplicationController
end end
end end
# POST /settings/reset_password
def reset_password def reset_password
current_user.send_reset_password_instructions current_user.send_reset_password_instructions
sign_out current_user sign_out current_user
@@ -104,7 +90,6 @@ 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.new(**nostr_event_from_params)
@@ -167,8 +152,7 @@ class SettingsController < ApplicationController
def user_params def user_params
params.require(:user).permit( params.require(:user).permit(
:display_name, :avatar_new, :pgp_pubkey, :display_name, :avatar, preferences: UserPreferences.pref_keys
preferences: UserPreferences.pref_keys
) )
end end
@@ -189,30 +173,4 @@ class SettingsController < ApplicationController
salt = BCrypt::Engine.generate_salt salt = BCrypt::Engine.generate_salt
BCrypt::Engine.hash_secret(password, salt) BCrypt::Engine.hash_secret(password, salt)
end 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

View File

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

View File

@@ -1,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,6 +1,8 @@
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]
@@ -33,10 +35,6 @@ class WebfingerController < WellKnownController
links: [] links: []
} }
if @user.avatar.attached?
jrd[:links] += avatar_link
end
if Setting.mastodon_enabled && @user.service_enabled?(:mastodon) if Setting.mastodon_enabled && @user.service_enabled?(:mastodon)
# https://docs.joinmastodon.org/spec/webfinger/ # https://docs.joinmastodon.org/spec/webfinger/
jrd[:aliases] += mastodon_aliases jrd[:aliases] += mastodon_aliases
@@ -51,16 +49,6 @@ class WebfingerController < WellKnownController
jrd jrd
end end
def avatar_link
[
{
rel: "http://webfinger.net/rel/avatar",
type: @user.avatar.content_type,
href: helpers.image_url_for(@user.avatar)
}
]
end
def mastodon_aliases def mastodon_aliases
[ [
"#{Setting.mastodon_public_url}/@#{@user.cn}", "#{Setting.mastodon_public_url}/@#{@user.cn}",
@@ -88,7 +76,7 @@ class WebfingerController < WellKnownController
end end
def remotestorage_link def remotestorage_link
auth_url = new_rs_oauth_url(@username, host: Setting.rs_accounts_domain) auth_url = new_rs_oauth_url(@username)
storage_url = "#{Setting.rs_storage_url}/#{@username}" storage_url = "#{Setting.rs_storage_url}/#{@username}"
{ {
@@ -103,4 +91,10 @@ class WebfingerController < WellKnownController
} }
} }
end end
def allow_cross_origin_requests
return unless Rails.env.development?
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end end

View File

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

View File

@@ -1,8 +1,5 @@
class WellKnownController < ApplicationController class WellKnownController < ApplicationController
before_action :require_nostr_enabled, only: [ :nostr ] 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?
@@ -10,14 +7,8 @@ class WellKnownController < ApplicationController
relay_url = Setting.nostr_relay_url.presence relay_url = Setting.nostr_relay_url.presence
if params[:name] == "_" if params[:name] == "_"
if domain == Setting.primary_domain # pubkey for the primary domain without a username (e.g. kosmos.org)
# pubkey for the primary domain without a username (e.g. kosmos.org) res = { names: { "_": Setting.nostr_public_key } }
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 res[:relays] = { "_" => [ relay_url ] } if relay_url
else else
@user = User.where(cn: params[:name], ou: domain).first @user = User.where(cn: params[:name], ou: domain).first
@@ -39,9 +30,4 @@ class WellKnownController < ApplicationController
def require_nostr_enabled def require_nostr_enabled
http_status :not_found unless Setting.nostr_enabled? http_status :not_found unless Setting.nostr_enabled?
end end
def allow_cross_origin_requests
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end end

View File

@@ -14,19 +14,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 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

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

@@ -4,7 +4,7 @@ class CreateLdapUserJob < ApplicationJob
def perform(username:, domain:, email:, hashed_pw:, confirmed: false) def perform(username:, domain:, email:, hashed_pw:, confirmed: false)
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,

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

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

View File

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

View File

@@ -1,97 +0,0 @@
require 'digest'
require "image_processing/vips"
class XmppSetAvatarJob < ApplicationJob
queue_as :default
def perform(user:, overwrite: false)
return if Rails.env.development?
@user = user
unless overwrite
current_avatar = get_current_avatar
Rails.logger.info { "User #{user.cn} already has an avatar set" }
return if current_avatar.present?
end
Rails.logger.debug { "Setting XMPP avatar for user #{user.cn}" }
stanzas = build_xep0084_stanzas
stanzas.each do |stanza|
payload = { from: @user.address, to: @user.address, stanza: stanza }
res = ejabberd.send_stanza payload
raise res.inspect if res.status != 200
end
end
private
def ejabberd
@ejabberd ||= EjabberdApiClient.new
end
def get_current_avatar
res = ejabberd.get_vcard2 @user, "PHOTO", "BINVAL"
if res.status == 200
# VCARD PHOTO/BINVAL prop exists
res.body
elsif res.status == 400
# VCARD or PHOTO/BINVAL prop does not exist
nil
else
# Unexpected error, let job fail
raise res.inspect
end
end
def process_avatar
@user.avatar.blob.open do |file|
processed = ImageProcessing::Vips
.source(file)
.resize_to_fill(256, 256)
.convert("png")
.call
processed.read
end
end
# See https://xmpp.org/extensions/xep-0084.html
def build_xep0084_stanzas
img_data = process_avatar
sha1_hash = Digest::SHA1.hexdigest(img_data)
base64_data = Base64.strict_encode64(img_data)
[
"""
<iq type='set' from='#{@user.address}' id='avatar-data-#{rand(101)}'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:avatar:data'>
<item id='#{sha1_hash}'>
<data xmlns='urn:xmpp:avatar:data'>#{base64_data}</data>
</item>
</publish>
</pubsub>
</iq>
""".strip,
"""
<iq type='set' from='#{@user.address}' id='avatar-metadata-#{rand(101)}'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:avatar:metadata'>
<item id='#{sha1_hash}'>
<metadata xmlns='urn:xmpp:avatar:metadata'>
<info bytes='#{img_data.size}'
id='#{sha1_hash}'
height='256'
type='image/png'
width='256'/>
</metadata>
</item>
</publish>
</pubsub>
</iq>
""".strip,
]
end
end

View File

@@ -1,90 +1,3 @@
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default Rails.application.config.action_mailer.default_options
layout 'mailer' layout 'mailer'
private
def send_mail
@template ||= "#{self.class.name.underscore}/#{caller[0][/`([^']*)'/, 1]}"
headers['Message-ID'] = message_id
if @user.pgp_pubkey.present?
mail(to: @user.email, subject: "...", content_type: pgp_content_type) do |format|
format.text { render plain: pgp_content }
end
else
mail(to: @user.email, subject: @subject) do |format|
format.text { render @template }
end
end
end
def from_address
self.class.default[:from]
end
def from_domain
Mail::Address.new(from_address).domain
end
def message_id
@message_id ||= "#{SecureRandom.uuid}@#{from_domain}"
end
def boundary
@boundary ||= SecureRandom.hex(8)
end
def pgp_content_type
"multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"------------#{boundary}\""
end
def pgp_nested_content
message_content = render_to_string(template: @template)
message_content_base64 = Base64.encode64(message_content)
nested_boundary = SecureRandom.hex(8)
<<~NESTED_CONTENT
Content-Type: multipart/mixed; boundary="------------#{nested_boundary}"; protected-headers="v1"
Subject: #{@subject}
From: <#{from_address}>
To: #{@user.display_name || @user.cn} <#{@user.email}>
Message-ID: <#{message_id}>
--------------#{nested_boundary}
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: base64
#{message_content_base64}
--------------#{nested_boundary}--
NESTED_CONTENT
end
def pgp_content
encrypted_content = UserManager::PgpEncrypt.call(user: @user, text: pgp_nested_content)
encrypted_base64 = Base64.encode64(encrypted_content.to_s)
<<~EMAIL_CONTENT
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
--------------#{boundary}
Content-Type: application/pgp-encrypted
Content-Description: PGP/MIME version identification
Version: 1
--------------#{boundary}
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc"
-----BEGIN PGP MESSAGE-----
#{encrypted_base64}
-----END PGP MESSAGE-----
--------------#{boundary}--
EMAIL_CONTENT
end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +0,0 @@
module Settings
module RemoteStorageSettings
extend ActiveSupport::Concern
included do
field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present?
field :rs_accounts_domain, type: :string,
default: ENV["RS_AKKOUNTS_DOMAIN"] || ENV["AKKOUNTS_DOMAIN"]
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence
field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,10 +3,9 @@ require 'nostr'
class User < ApplicationRecord class User < ApplicationRecord
include EmailValidatable include EmailValidatable
attr_accessor :current_password
attr_accessor :display_name attr_accessor :display_name
attr_accessor :avatar_new attr_accessor :avatar_new
attr_accessor :pgp_pubkey attr_accessor :current_password
serialize :preferences, coder: UserPreferences serialize :preferences, coder: UserPreferences
@@ -23,16 +22,10 @@ class User < ApplicationRecord
has_many :zaps has_many :zaps
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user", has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
primary_key: "lndhub_username", foreign_key: "login" primary_key: "ln_account", foreign_key: "login"
has_many :accounts, through: :lndhub_user has_many :accounts, through: :lndhub_user
#
# Attachments
#
has_one_attached :avatar
# #
# Validations # Validations
# #
@@ -56,11 +49,8 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true, validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) } if: -> { defined?(@display_name) }
validate :acceptable_avatar validate :acceptable_avatar
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
# #
# Scopes # Scopes
# #
@@ -73,7 +63,7 @@ class User < ApplicationRecord
# Encrypted database columns # Encrypted database columns
# #
encrypts :lndhub_password has_encrypted :ln_login, :ln_password
# Include default devise modules. Others available are: # Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
@@ -84,10 +74,6 @@ class User < ApplicationRecord
:timeoutable, :timeoutable,
:rememberable :rememberable
#
# Methods
#
def ldap_before_save def ldap_before_save
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
self.ou = dn.split(',') self.ou = dn.split(',')
@@ -170,20 +156,6 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name] @display_name ||= ldap_entry[:display_name]
end end
# TODO Variant keys are currently broken for some reason
# (They use the same key as the main blob, when it should be
# "/variants/#{key)"
# def avatar_variant(size: :medium)
# dimensions = case size
# when :large then [400, 400]
# when :medium then [256, 256]
# when :small then [64, 64]
# else [256, 256]
# end
# format = avatar.content_type == "image/png" ? :png : :jpeg
# avatar.variant(resize_to_fill: dimensions, format: format)
# end
def nostr_pubkey def nostr_pubkey
@nostr_pubkey ||= ldap_entry[:nostr_key] @nostr_pubkey ||= ldap_entry[:nostr_key]
end end
@@ -193,22 +165,8 @@ class User < ApplicationRecord
Nostr::PublicKey.new(nostr_pubkey).to_bech32 Nostr::PublicKey.new(nostr_pubkey).to_bech32
end end
def pgp_pubkey def avatar
@pgp_pubkey ||= ldap_entry[:pgp_key] @avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end
def gnupg_key
return nil unless pgp_pubkey.present?
GPGME::Key.import(pgp_pubkey)
GPGME::Key.get(pgp_fpr)
end
def pgp_pubkey_contains_user_address?
gnupg_key.uids.map(&:email).include?(address)
end
def wkd_hash
ZBase32.encode(Digest::SHA1.digest(cn))
end end
def services_enabled def services_enabled
@@ -222,14 +180,14 @@ class User < ApplicationRecord
def enable_service(service) def enable_service(service)
current_services = services_enabled current_services = services_enabled
new_services = Array(service).map(&:to_s) new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq.sort services = (current_services + new_services).uniq
ldap.replace_attribute(dn, :serviceEnabled, services) ldap.replace_attribute(dn, :serviceEnabled, services)
end end
def disable_service(service) def disable_service(service)
current_services = services_enabled current_services = services_enabled
disabled_services = Array(service).map(&:to_s) disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq.sort services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :serviceEnabled, services) ldap.replace_attribute(dn, :serviceEnabled, services)
end end
@@ -248,7 +206,7 @@ class User < ApplicationRecord
return unless avatar_new.present? return unless avatar_new.present?
if avatar_new.size > 1.megabyte if avatar_new.size > 1.megabyte
errors.add(:avatar, "must be less than 1MB file size") errors.add(:avatar, "file size is too large")
end end
acceptable_types = ["image/jpeg", "image/png"] acceptable_types = ["image/jpeg", "image/png"]
@@ -256,10 +214,4 @@ class User < ApplicationRecord
errors.add(:avatar, "must be a JPEG or PNG file") errors.add(:avatar, "must be a JPEG or PNG file")
end end
end end
def acceptable_pgp_key_format
unless GPGME::Key.valid?(pgp_pubkey)
errors.add(:pgp_pubkey, 'is not a valid armored PGP public key block')
end
end
end end

View File

@@ -1,22 +1,36 @@
# #
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/ # API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
# #
class BtcpayManagerService < RestApiService class BtcpayManagerService < ApplicationService
private private
def base_url def base_url
@base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}" @base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
end end
def auth_token def auth_token
@auth_token ||= Setting.btcpay_auth_token @auth_token ||= Setting.btcpay_auth_token
end end
def headers def headers
{ {
"Content-Type" => "application/json", "Content-Type" => "application/json",
"Accept" => "application/json", "Accept" => "application/json",
"Authorization" => "token #{auth_token}" "Authorization" => "token #{auth_token}"
} }
end end
def endpoint_url(path)
"#{base_url}/#{path.gsub(/^\//, '')}"
end
def get(path, params = {})
res = Faraday.get endpoint_url(path), params, headers
JSON.parse(res.body)
end
def post(path, payload)
res = Faraday.post endpoint_url(path), payload.to_json, headers
JSON.parse(res.body)
end
end end

View File

@@ -0,0 +1,54 @@
class CreateAccount < ApplicationService
def initialize(account:)
@username = account[:username]
@domain = account[:ou] || Setting.primary_domain
@email = account[:email]
@password = account[:password]
@invitation = account[:invitation]
@confirmed = account[:confirmed]
end
def call
user = create_user_in_database
add_ldap_document
create_lndhub_account(user) if Setting.lndhub_enabled
if @invitation.present?
update_invitation(user.id)
end
end
private
def create_user_in_database
User.create!(
cn: @username,
ou: @domain,
email: @email,
password: @password,
password_confirmation: @password,
confirmed_at: @confirmed ? DateTime.now : nil
)
end
def update_invitation(user_id)
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
end
def add_ldap_document
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
CreateLdapUserJob.perform_later(
username: @username,
domain: @domain,
email: @email,
hashed_pw: hashed_pw,
confirmed: @confirmed
)
end
def create_lndhub_account(user)
#TODO enable in development when we have a local lndhub (mock?) API
return if Rails.env.development?
CreateLndhubAccountJob.perform_later(user)
end
end

View File

@@ -0,0 +1,17 @@
class CreateInvitations < ApplicationService
def initialize(user:, amount:, notify: true)
@user = user
@amount = amount
@notify = notify
end
def call
@amount.times do
Invitation.create(user: @user)
end
if @notify
NotificationMailer.with(user: @user).new_invitations_available.deliver_later
end
end
end

View File

@@ -4,13 +4,15 @@ class EjabberdApiClient
end end
def post(endpoint, payload) def post(endpoint, payload)
Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, res = Faraday.post("#{@base_url}/#{endpoint}", payload.to_json,
"Content-Type" => "application/json" "Content-Type" => "application/json")
end
# if res.status != 200
# API endpoints Rails.logger.error "[ejabberd] API request failed:"
# Rails.logger.error res.body
#TODO Send custom event to Sentry
end
end
def add_rosteritem(payload) def add_rosteritem(payload)
post "add_rosteritem", payload post "add_rosteritem", payload
@@ -20,31 +22,8 @@ class EjabberdApiClient
post "send_message", payload post "send_message", payload
end end
def send_stanza(payload)
post "send_stanza", payload
end
def get_vcard2(user, name, subname)
payload = {
user: user.cn, host: user.ou,
name: name, subname: subname
}
post "get_vcard2", payload
end
def private_get(user, element_name, namespace)
payload = {
user: user.cn, host: user.ou,
element: element_name, ns: namespace
}
post "private_get", payload
end
def private_set(user, content) def private_set(user, content)
payload = { payload = { user: user.cn, host: user.ou, element: content }
user: user.cn, host: user.ou,
element: content
}
post "private_set", payload post "private_set", payload
end end
end end

View File

@@ -5,12 +5,12 @@ module LdapManager
end end
def call def call
treebase = ldap_config["base"] treebase = ldap_config["base"]
attributes = %w{ jpegPhoto } attributes = %w{ jpegPhoto }
filter = Net::LDAP::Filter.eq("cn", @cn) filter = Net::LDAP::Filter.eq("cn", @cn)
entry = client.search(base: treebase, filter: filter, attributes: attributes).first entry = client.search(base: treebase, filter: filter, attributes: attributes).first
entry[:jpegPhoto].present? ? entry.jpegPhoto.first : nil entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
end end
end end
end end

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
module LdapManager
class UpdatePgpKey < LdapManagerService
def initialize(dn:, pubkey:)
@dn = dn
@pubkey = pubkey
end
def call
if @pubkey.present?
replace_attribute @dn, :pgpKey, @pubkey
else
delete_attribute @dn, :pgpKey
end
end
end
end

View File

@@ -58,7 +58,7 @@ class LdapService < ApplicationService
attributes = %w[ attributes = %w[
dn cn uid mail displayName admin serviceEnabled dn cn uid mail displayName admin serviceEnabled
mailRoutingAddress mailpassword nostrKey pgpKey mailRoutingAddress mailpassword nostrKey
] ]
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
@@ -73,8 +73,7 @@ class LdapService < ApplicationService
services_enabled: e.try(:serviceEnabled), services_enabled: e.try(:serviceEnabled),
email_maildrop: e.try(:mailRoutingAddress), email_maildrop: e.try(:mailRoutingAddress),
email_password: e.try(:mailpassword), email_password: e.try(:mailpassword),
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil, nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil
pgp_key: e.try(:pgpKey) ? e.pgpKey.first : nil
} }
end end
end end
@@ -102,7 +101,7 @@ class LdapService < ApplicationService
dn = "ou=#{ou},cn=users,#{ldap_suffix}" dn = "ou=#{ou},cn=users,#{ldap_suffix}"
aci = <<-EOS aci = <<-EOS
(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || pgpKey || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";) (target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
EOS EOS
attrs = { attrs = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +0,0 @@
class RestApiService < ApplicationService
private
def base_url
raise NotImplementedError
end
def headers
raise NotImplementedError
end
def endpoint_url(path)
"#{base_url}/#{path.gsub(/^\//, '')}"
end
def get(path, params = {})
res = Faraday.get endpoint_url(path), params, headers
# TODO handle unsuccessful responses with no valid JSON body
JSON.parse(res.body)
end
def post(path, payload)
res = Faraday.post endpoint_url(path), payload.to_json, headers
# TODO handle unsuccessful responses with no valid JSON body
JSON.parse(res.body)
end
end

View File

@@ -1,56 +0,0 @@
module UserManager
class CreateAccount < UserManagerService
def initialize(account:)
@username = account[:username]
@domain = account[:ou] || Setting.primary_domain
@email = account[:email]
@password = account[:password]
@invitation = account[:invitation]
@confirmed = account[:confirmed]
end
def call
user = create_user_in_database
add_ldap_document
create_lndhub_account(user) if Setting.lndhub_enabled
if @invitation.present?
update_invitation(user.id)
end
end
private
def create_user_in_database
User.create!(
cn: @username,
ou: @domain,
email: @email,
password: @password,
password_confirmation: @password,
confirmed_at: @confirmed ? DateTime.now : nil
)
end
def update_invitation(user_id)
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
end
def add_ldap_document
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
CreateLdapUserJob.perform_later(
username: @username,
domain: @domain,
email: @email,
hashed_pw: hashed_pw,
confirmed: @confirmed
)
end
def create_lndhub_account(user)
#TODO enable in development when we have a local lndhub (mock?) API
return if Rails.env.development?
CreateLndhubAccountJob.perform_later(user)
end
end
end

View File

@@ -1,19 +0,0 @@
module UserManager
class CreateInvitations < UserManagerService
def initialize(user:, amount:, notify: true)
@user = user
@amount = amount
@notify = notify
end
def call
@amount.times do
Invitation.create(user: @user)
end
if @notify
NotificationMailer.with(user: @user).new_invitations_available.deliver_later
end
end
end
end

View File

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

View File

@@ -1,19 +0,0 @@
require 'gpgme'
module UserManager
class PgpEncrypt < UserManagerService
def initialize(user:, text:)
@user = user
@text = text
end
def call
crypto = GPGME::Crypto.new
crypto.encrypt(
@text,
recipients: @user.gnupg_key,
always_trust: true
)
end
end
end

View File

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

View File

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

View File

@@ -1,24 +0,0 @@
module UserManager
class UpdatePgpKey < UserManagerService
def initialize(user:)
@user = user
end
def call
if @user.pgp_pubkey.blank?
@user.update! pgp_fpr: nil
else
result = GPGME::Key.import(@user.pgp_pubkey)
if result.imports.present?
@user.update! pgp_fpr: result.imports.first.fpr
else
# TODO notify Sentry, user
raise "Failed to import OpenPGP pubkey"
end
end
LdapManager::UpdatePgpKey.call(dn: @user.dn, pubkey: @user.pgp_pubkey)
end
end
end

View File

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

View File

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

View File

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

View File

@@ -19,11 +19,6 @@
title: "Public key", title: "Public key",
description: "The corresponding public key of the accounts service" description: "The corresponding public key of the accounts service"
) %> ) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_public_key_primary_domain,
title: "Public key for primary domain (NIP-05)",
description: "(optional) A different pubkey to announce for the _@#{Setting.primary_domain} Nostr address"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_relay_url, key: :nostr_relay_url,
title: "Relay URL", title: "Relay URL",

View File

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

View File

@@ -89,75 +89,13 @@
</section> </section>
<section class="sm:flex-1 sm:pt-0"> <section class="sm:flex-1 sm:pt-0">
<h3>Avatar</h3> <% if @avatar.present? %>
<% if @user.avatar.attached? %> <h3>LDAP<h3>
<table class="divided"> <p>
<tbody> <img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
<tr> </p>
<th class="align-top">Image</th>
<td class="align-top">
<%= image_tag image_url_for(@user.avatar), class: "h-20 w-20 rounded-lg" %>
</td>
</tr>
<tr>
<th>Content type</th>
<td>
<%= @user.avatar.content_type %>
</td>
</tr>
<tr>
<th>Size</th>
<td>
<%= number_to_human_size(@user.avatar.blob.byte_size) %>
</td>
</tr>
</tbody>
</table>
<% else %>
<p class="text-gray-500">No avatar uploaded</p>
<% end %> <% end %>
<!-- <h3>Actions</h3> -->
<h3 class="mt-12">LDAP</h3>
<table class="divided">
<tbody>
<tr>
<th>Avatar</th>
<td>
<% if @ldap_avatar.present? %>
JPEG size: <%= number_to_human_size(@ldap_avatar.size) %>
<% else %>
&mdash;
<% end %>
</td>
</tr>
<tr>
<th>Display name</th>
<td><%= @user.display_name || "—" %></td>
</tr>
<tr>
<th class="align-top">PGP key</th>
<td class="align-top leading-5">
<% if @user.pgp_pubkey.present? %>
<span class="font-mono" title="<%= @user.pgp_fpr %>">
<% if @user.pgp_pubkey_contains_user_address? %>
<%= link_to wkd_key_url(hashed_username: @user.wkd_hash, l: @user.cn, format: :txt),
class: "ks-text-link", target: "_blank" do %>
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
<% end %>
<% else %>
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
<% end %>
</span><br />
<% @user.gnupg_key.uids.each do |uid| %>
<%= uid.uid %><br />
<% end %>
<% else %>
&mdash;
<% end %>
</td>
</tr>
</tbody>
</table>
</section> </section>
</div> </div>
@@ -246,7 +184,7 @@
<td>XMPP (ejabberd)</td> <td>XMPP (ejabberd)</td>
<td> <td>
<%= render FormElements::ToggleComponent.new( <%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("ejabberd"), enabled: @services_enabled.include?("xmpp"),
input_enabled: false input_enabled: false
) %> ) %>
</td> </td>
@@ -304,7 +242,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><%= @user.lndhub_username %></td> <td><%= @user.ln_account %></td>
<td><%= number_with_delimiter @lndhub_user.balance %> sats</td> <td><%= number_with_delimiter @lndhub_user.balance %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_incoming %> sats</td> <td><%= number_with_delimiter @lndhub_user.sum_incoming %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_outgoing %> sats</td> <td><%= number_with_delimiter @lndhub_user.sum_outgoing %> sats</td>
@@ -313,7 +251,7 @@
</tbody> </tbody>
</table> </table>
<% else %> <% else %>
<p>No LndHub user found for account <strong class="font-mono"><%= @user.lndhub_username %></strong>. <p>No LndHub user found for account <strong class="font-mono"><%= @user.ln_account %></strong>.
<% end %> <% end %>
</section> </section>
<% end %> <% end %>

View File

@@ -15,8 +15,6 @@
In order to connect an app to your storage account, give it your address: In order to connect an app to your storage account, give it your address:
</p> </p>
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5"> <p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
<img src="/img/logos/icon_remotestorage.svg"
class="inline-block h-6 w-6 mr-1 self-center">
<input type="text" id="user_address" class="grow" <input type="text" id="user_address" class="grow"
value=<%= current_user.address %> disabled="disabled" value=<%= current_user.address %> disabled="disabled"
data-clipboard-target="source" /> data-clipboard-target="source" />
@@ -33,24 +31,6 @@
</p> </p>
</section> </section>
<section>
<h3>Compatible Apps</h3>
<p>
Your Storage account is based on a new open standard called
<a href="https://remotestorage.io" target="_blank">
<img src="/img/logos/icon_remotestorage.svg" class="h-4 w-4 inline">
<strong>remoteStorage</strong>
</a>, which is not yet widely supported. Look
for the remoteStorage icon, or check the Sync settings in apps.
</p>
<p>
If you want your favorite apps to support syncing data with your own
Storage account, let the developers know! All relevant information is
available on the <a href="https://remotestorage.io"
target="_blank" class="ks-text-link">remoteStorage website</a>.
</p>
</section>
<section> <section>
<h3>Recommended Apps</h3> <h3>Recommended Apps</h3>
<div data-controller="tabs" <div data-controller="tabs"

View File

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

View File

@@ -5,7 +5,7 @@
<h3>E-Mail Password</h3> <h3>E-Mail Password</h3>
<%= form_for(@user, url: reset_email_password_settings_path, method: "post") do |f| %> <%= form_for(@user, url: reset_email_password_settings_path, method: "post") do |f| %>
<%= hidden_field_tag :section, "email" %> <%= hidden_field_tag :section, "email" %>
<p class="mb-6"> <p class="mb-8">
Use the following button to generate a new email password: Use the following button to generate a new email password:
</p> </p>
<p class="hidden initial-visible"> <p class="hidden initial-visible">

View File

@@ -20,7 +20,7 @@
</button> </button>
</p> </p>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
Your account's address on the Internet Your user address for Chat and Lightning Network.
</p> </p>
</div> </div>
<%= form_for(@user, url: setting_path(:profile), html: { :method => :put }) do |f| %> <%= form_for(@user, url: setting_path(:profile), html: { :method => :put }) do |f| %>
@@ -31,19 +31,23 @@
<% end %> <% end %>
<% end %> <% end %>
<% if Flipper.enabled?(:avatar_upload, current_user) %>
<label class="block"> <label class="block">
<p class="font-bold mb-1">Avatar</p> <p class="font-bold mb-1">
<p class="text-gray-500">Default profile picture</p> Avatar
</p>
<p class="text-gray-500">
Default profile picture
</p>
<div class="flex items-center gap-6"> <div class="flex items-center gap-6">
<% if @user.avatar.attached? %> <% if current_user.avatar.present? %>
<p class="flex-none"> <p class="flex-none">
<%= image_tag image_url_for(@user.avatar), class: "h-24 w-24 rounded-lg" %> <%= image_tag "data:image/jpeg;base64,#{current_user.avatar}", class: "h-24 w-24 rounded-lg" %>
</p> </p>
<% end %> <% end %>
<div class="grow"> <div class="grow">
<p class="mb-2"> <p class="mb-2">
<%= f.file_field :avatar_new, accept: "image/jpeg,image/png" %> <%= f.file_field :avatar, class: "" %>
</p>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
JPEG or PNG image, not larger than 1 megabyte JPEG or PNG image, not larger than 1 megabyte
</p> </p>
@@ -53,6 +57,7 @@
</div> </div>
</div> </div>
</label> </label>
<% end %>
<p class="mt-8 pt-6 border-t border-gray-200 text-right"> <p class="mt-8 pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %> <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env ruby
require_relative "../config/environment"
require "solid_queue/cli"
SolidQueue::Cli.start(ARGV)

View File

@@ -1,4 +1,9 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
APP_PATH = File.expand_path("../config/application", __dir__) begin
require_relative "../config/boot" load File.expand_path('../spring', __FILE__)
require "rails/commands" rescue LoadError => e
raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

View File

@@ -1,4 +1,9 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
require_relative "../config/boot" begin
require "rake" load File.expand_path('../spring', __FILE__)
rescue LoadError => e
raise unless e.message.include?('spring')
end
require_relative '../config/boot'
require 'rake'
Rake.application.run Rake.application.run

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