75 Commits

Author SHA1 Message Date
393f85e45c WIP Add member/contributor status to users
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-27 13:32:58 +04:00
4bf6985b87 Fix wrong matcher for custom LDAP attribute
All checks were successful
continuous-integration/drone/push Build is passing
389ds doesn't like case-insensitive matches for 7-bit ASCII strings
2025-05-23 14:08:41 +04:00
308cac5a39 Merge pull request 'Add Mastodon API client, service for syncing avatars and display names' (#225) from feature/mastodon_api into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #225
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-23 08:48:15 +00:00
7f766473ab Fix typo
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-22 13:21:37 +04:00
c1bac2625c Only log exception to stdout
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-05-21 16:42:49 +04:00
c5c6765d67 Log LDAP exceptions
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-21 16:29:52 +04:00
171524fb83 Use production link
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-18 14:58:55 +04:00
3538067da6 Use production link
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2025-05-18 14:58:34 +04:00
c374bcd3bc Merge branch 'master' into feature/mastodon_api
Some checks are pending
continuous-integration/drone/push Build is running
2025-05-18 14:56:42 +04:00
655009ad7a Add example link for PGP pubkey
Some checks are pending
continuous-integration/drone/push Build is running
2025-05-18 14:56:29 +04:00
71c9bd29ab Merge branch 'master' into feature/mastodon_api 2025-05-18 14:46:28 +04:00
e66d134550 Log missing l param for WKD requests, return 400
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-18 14:46:04 +04:00
11167e3e43 Merge branch 'master' into feature/mastodon_api 2025-05-18 14:37:47 +04:00
ebbd87368c Handle l param missing for WKD request
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-18 14:37:22 +04:00
7b0ebb761f Allow display name to be removed
All checks were successful
continuous-integration/drone/push Build is passing
When form field is empty
2025-05-18 14:26:09 +04:00
fb03427d59 Allow syncing a single Mastodon profile
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-17 18:56:34 +04:00
ad138f715c Update doc 2025-05-17 18:56:34 +04:00
6730aae2dc Only update other avatars in one place
Prevent future mistakes
2025-05-17 18:56:33 +04:00
a71aa3fda2 Don't queue job when service isn't enabled 2025-05-17 18:56:33 +04:00
92e6b1395a Add avatar to admin user page 2025-05-17 18:56:33 +04:00
37c59b7b0c Sync Mastodon IDs/profiles to local accounts
Add a new service to import some data from Mastodon accounts:

* Find users by username, store Mastodon account ID in local db when
  found
* Import display name (don't overwrite existing)
* Import avatar (don't overwrite existing)
2025-05-17 18:56:30 +04:00
c291765777 Add mastodon_id to users 2025-05-17 16:44:13 +04:00
f0cfde560b Add Mastodon API service class, auth token config
Add a new REST API service class to keep things DRY
2025-05-17 14:18:16 +04:00
c43e43d89c Open RS apps in new tab
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-16 17:30:11 +04:00
dbbf116c52 Fix RS storage-first auth work in dev, remove token
All checks were successful
continuous-integration/drone/push Build is passing
See https://github.com/remotestorage/remotestorage.js/issues/900
2025-05-16 15:59:40 +04:00
208b1f04ae Fix web app icon component
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-16 15:38:03 +04:00
8049f81b73 Merge pull request 'Set XMPP avatar when new avatar is uploaded' (#224) from feature/ejabberd_pep into master
Some checks are pending
continuous-integration/drone/push Build is running
Reviewed-on: #224
2025-05-16 11:37:29 +00:00
5f276ff349 Queue XmppSetAvatarJob when new avatar is uploaded
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Failing after 10m9s
And let job do nothing in development for now
2025-05-15 22:04:25 +04:00
5916969447 Add job for setting avatar via XMPP 2025-05-15 20:05:53 +04:00
382c5ad10e Return response for ejabberd API calls 2025-05-15 12:53:58 +04:00
8b3243af6b Sort API methods alphabetically
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:19:09 +04:00
fc36fbf10c Add get_vcard2 to ejabberd client
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:16:53 +04:00
06d2705c4c Add private_get to ejabberd service
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:01:10 +04:00
03be2e09e6 Merge pull request 'User avatars' (#223) from feature/user_avatars into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #223
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-14 14:58:15 +00:00
582d339c0a Remove feature gate for avatar upload
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2025-05-14 18:55:26 +04:00
a098ea43bb Add avatar URL to Webfinger when available
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-14 15:39:50 +04:00
417e346074 Do not use ActiveStorage variants, process original avatar
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Variants are currently broken. So we process the original file with the
most common avatar dimensions and stripping metadata, then hash and
upload only that version.
2025-05-14 14:42:03 +04:00
1884f082ee Add note about variants not working when not generated ad-hoc
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-12 18:07:10 +04:00
51a3652fc8 Fix S3 keys/paths for user avatars
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Also fixes the avatars controller to work with all back-ends
2025-05-12 16:39:53 +04:00
46b908839d Add avatar URL to Discourse Connect
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Discourse should download and set the avatar if the user doesn't have
one set yet.
2025-05-12 15:04:56 +04:00
512f0ccca1 Add controller for rendering avatars on simple URL 2025-05-12 15:04:01 +04:00
17ffbde03a WIP Store avatars as ActiveStorage attachments
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Also push to LDAP as jpegPhoto
2025-05-11 18:43:21 +04:00
9e2210c45b Store avatars as binary instead of base64
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-10 20:58:36 +04:00
6d7d722c5d Add inetOrgPerson objectclass to user entries
refs #174
2025-05-08 16:52:54 +04:00
ae5d63c613 Merge pull request 'Move remaining credentials from Rails credentials store to ENV' (#221) from chore/215-configs into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #221
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-06 17:16:32 +00:00
93aa26f430 Remove lockbox column
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2025-05-06 20:14:25 +04:00
50110c12b9 Remove lockbox gem
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-06 20:01:01 +04:00
95843aee6d Remove credentials files
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-06 19:50:27 +04:00
84ed4b2de2 Remove old ln columns from users table
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-06 19:47:58 +04:00
931624cf95 Add encryption credentials to test env
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-06 18:14:26 +04:00
eae370b737 Migrate from lockbox to ActiveRecord encryption (1/2)
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-06 18:09:27 +04:00
15a9fdec3e Make RS auth work by default in dev with Docker Compose 2025-05-06 18:07:52 +04:00
3d8619532b Refactor LDAP config
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
* Move credentials to ENV vars in prod
* Use same configs in dev and prod
* Make UID attribute and admin DN configurable
2025-05-06 15:32:59 +04:00
d56edb34f1 Remove SMTP credentials from Rails credentials
Already unused
2025-05-06 15:08:46 +04:00
a97bbf61a8 Fix postgresql query for deleting auth expiry job
All checks were successful
continuous-integration/drone/push Build is passing
Solid Queue uses a text column, instead of a jsonb, so we need to cast
it as jsonb on the fly.
2025-05-05 17:37:58 +04:00
5a523fd220 Merge pull request 'Refactor database configs' (#220) from chore/db_configs into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #220
2025-05-05 12:54:22 +00:00
889c9ae824 Refactor database configs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
* Move postgres credentials to ENV vars
* Allow postgres in development
* Allow SQlite in production
* Refactor optional lndhub db config

Co-authored-by: Greg Karékinian <greg@karekinian.com>
2025-05-05 15:25:25 +04:00
e686cf42e8 Merge pull request 'Switch from Sidekiq to Solid Queue' (#219) from dev/sidekiq_to_solidqueue into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #219
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-05 11:24:56 +00:00
906468d156 Allow to immediately expire auth via job
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
When running the job before its schedule
2025-05-05 12:46:46 +04:00
ee5c6d86d0 Port RS auth job removal to Solid Queue
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-05 11:07:30 +04:00
d1eea85b04 Add Redis gem explicitly, remove sidekiq require
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-04 18:14:49 +04:00
ecd814641a Remove Sidekiq initializer
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-04 17:44:37 +04:00
b1dd5800b2 Update lockfile
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-04 17:42:31 +04:00
0cad4cdcfe WIP Switch from Sidekiq to Solid Queue
Some checks failed
continuous-integration/drone/push Build is running
continuous-integration/drone/pr Build is failing
2025-05-04 17:40:33 +04:00
b61906059c Merge pull request 'Upgrade Rails to 8.0' (#216) from chore/upgrade_rails into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #216
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-04-30 08:36:16 +00:00
aef779a59c Switch from Sprockets to Propshaft
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 1s
2025-04-29 17:11:21 +04:00
1ddecab2c3 Upgrade Rails to 8.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-04-28 17:49:54 +04:00
74b4bc3875 Upgrade Rails to 7.2
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-28 00:17:25 +04:00
646c95ecc2 Fix local/development RS auth URL
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-27 16:09:32 +04:00
fb054ae455 Add task for generating ctags
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-26 12:37:10 +04:00
536052e9bf Merge pull request 'Upgrade strfry/deno, port strfry policies to @nostrify/policies' (#214) from chore/upgrade_strfry_deno into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #214
2025-04-18 10:51:35 +00:00
b29a0abb0b Document strfry integration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2025-04-16 17:34:10 +04:00
29ff486683 Port strfry policies to @nostrify/policies
Use packages from JSR and adapt code for new policy APIs
2025-04-15 19:01:22 +04:00
e53b9dd186 Upgrade strfry docker image
Contains latest strfry (1.0.4) and deno (2.2.10)
2025-04-15 19:00:52 +04:00
a2921297fe Fix seeds
The CreateAccount service has moved to a namespace
2025-04-11 16:14:44 +04:00
123 changed files with 2603 additions and 1122 deletions

View File

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

View File

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

4
.gitignore vendored
View File

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

20
Gemfile
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,8 @@
</div>
<%= render DropdownComponent.new do %>
<%= 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 %>
Launch app
<% end %>

View File

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

View File

@@ -4,13 +4,19 @@ class Admin::UsersController < Admin::BaseController
# GET /admin/users
def index
ldap = LdapService.new
@ou = Setting.primary_domain
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
ldap = LdapService.new
ou = Setting.primary_domain
@admins = ldap.search_users(:admin, true, :cn)
@contributors = ldap.search_users(:memberStatus, :contributor, :cn)
@sustainers = ldap.search_users(:memberStatus, :sustainer, :cn)
@pagy, @users = pagy(User.where(ou: ou).order(cn: :asc))
@stats = {
users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: @ou).pending.count
users_confirmed: User.where(ou: ou).confirmed.count,
users_pending: User.where(ou: ou).pending.count,
users_contributing: @contributors.size,
users_paying: @sustainers.size
}
end
@@ -22,7 +28,7 @@ class Admin::UsersController < Admin::BaseController
@services_enabled = @user.services_enabled
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
@ldap_avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
end
# POST /admin/users/:username/invitations

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,97 @@
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

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

View File

@@ -0,0 +1,10 @@
module Settings
module MemberSettings
extend ActiveSupport::Concern
included do
field :member_default_status, type: :string,
default: ENV["MEMBER_DEFAULT_STATUS"].presence
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,22 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcpayManagerService < ApplicationService
class BtcpayManagerService < RestApiService
private
def base_url
@base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
end
def base_url
@base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
end
def auth_token
@auth_token ||= Setting.btcpay_auth_token
end
def auth_token
@auth_token ||= Setting.btcpay_auth_token
end
def headers
{
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{auth_token}"
}
end
def 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
def headers
{
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{auth_token}"
}
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
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

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

@@ -0,0 +1,64 @@
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

@@ -0,0 +1,22 @@
#
# 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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,21 @@
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

@@ -0,0 +1,15 @@
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

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

View File

@@ -13,6 +13,16 @@
title: 'Pending',
value: @stats[:users_pending],
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Contributors',
value: @stats[:users_contributing],
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Sustainers',
value: @stats[:users_paying],
) %>
<% end %>
</section>
@@ -29,8 +39,12 @@
<% @users.each do |user| %>
<tr>
<td><%= link_to(user.cn, admin_user_path(user.cn), class: 'ks-text-link') %></td>
<td><%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %></td>
<td><%= user.is_admin? ? badge("admin", :red) : "" %></td>
<td>
<%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %>
<%= @contributors.include?(user.cn) ? badge("contributor", :green) : "" %>
<%= @sustainers.include?(user.cn) ? badge("sustainer", :green) : "" %>
</td>
<td><%= @admins.include?(user.cn) ? badge("admin", :red) : "" %></td>
</tr>
<% end %>
</tbody>

View File

@@ -32,6 +32,13 @@
<th>Roles</th>
<td><%= @user.is_admin? ? badge("admin", :red) : "—" %></td>
</tr>
<tr>
<th>Status</th>
<td>
<%= @user.is_contributing_member? ? badge("contributor", :green) : "" %>
<%= @user.is_paying_member? ? badge("sustainer", :green) : "" %>
</td>
</tr>
<tr>
<th>Invited by</th>
<td>
@@ -89,14 +96,42 @@
</section>
<section class="sm:flex-1 sm:pt-0">
<h3>LDAP</h3>
<h3>Avatar</h3>
<% if @user.avatar.attached? %>
<table class="divided">
<tbody>
<tr>
<th class="align-top">Image</th>
<td class="align-top">
<%= image_tag image_url_for(@user.avatar), class: "h-20 w-20 rounded-lg" %>
</td>
</tr>
<tr>
<th>Content type</th>
<td>
<%= @user.avatar.content_type %>
</td>
</tr>
<tr>
<th>Size</th>
<td>
<%= number_to_human_size(@user.avatar.blob.byte_size) %>
</td>
</tr>
</tbody>
</table>
<% else %>
<p class="text-gray-500">No avatar uploaded</p>
<% end %>
<h3 class="mt-12">LDAP</h3>
<table class="divided">
<tbody>
<tr>
<th>Avatar</th>
<td>
<% if @avatar.present? %>
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
<% if @ldap_avatar.present? %>
JPEG size: <%= number_to_human_size(@ldap_avatar.size) %>
<% else %>
&mdash;
<% end %>
@@ -276,7 +311,7 @@
</thead>
<tbody>
<tr>
<td><%= @user.ln_account %></td>
<td><%= @user.lndhub_username %></td>
<td><%= number_with_delimiter @lndhub_user.balance %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_incoming %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_outgoing %> sats</td>
@@ -285,7 +320,7 @@
</tbody>
</table>
<% else %>
<p>No LndHub user found for account <strong class="font-mono"><%= @user.ln_account %></strong>.
<p>No LndHub user found for account <strong class="font-mono"><%= @user.lndhub_username %></strong>.
<% end %>
</section>
<% end %>

View File

@@ -14,8 +14,9 @@
<p class="mb-6">
In order to connect an app to your storage account, give it your address:
</p>
<p data-controller="clipboard" class="flex items-center gap-1 sm:w-2/5">
<img src="/img/logos/icon_remotestorage.svg" class="inline-block h-6 w-6 mr-1">
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
<img src="/img/logos/icon_remotestorage.svg"
class="inline-block h-6 w-6 mr-1 self-center">
<input type="text" id="user_address" class="grow"
value=<%= current_user.address %> disabled="disabled"
data-clipboard-target="source" />

View File

@@ -52,9 +52,11 @@
<section class="!pt-8 sm:!pt-12">
<h3>OpenPGP</h3>
<ul role="list">
<% example_link = link_to "example", "https://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: "Your OpenPGP public key in ASCII Armor format"
description: raw("Your OpenPGP public key in ASCII Armor format (#{example_link})")
) do %>
<%= f.text_area :pgp_pubkey,
value: @user.pgp_pubkey,

View File

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

6
bin/jobs Executable file
View File

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

View File

@@ -1,9 +1,4 @@
#!/usr/bin/env ruby
begin
load File.expand_path('../spring', __FILE__)
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'
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../config/boot"
require "rails/commands"

View File

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

8
bin/rubocop Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"
# explicit rubocop config increases performance slightly while avoiding config confusion.
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
load Gem.bin_path("rubocop", "rubocop")

View File

@@ -1,36 +1,34 @@
#!/usr/bin/env ruby
require 'fileutils'
require "fileutils"
# path to your application root.
APP_ROOT = File.expand_path('..', __dir__)
APP_ROOT = File.expand_path("..", __dir__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
system(*args, exception: true)
end
FileUtils.chdir APP_ROOT do
# This script is a way to setup or update your development environment automatically.
# This script is idempotent, so that you can run it at anytime and get an expectable outcome.
# This script is a way to set up or update your development environment automatically.
# This script is idempotent, so that you can run it at any time and get an expectable outcome.
# Add necessary setup steps to this file.
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
# Install JavaScript dependencies
# system('bin/yarn')
puts "== Installing dependencies =="
system("bundle check") || system!("bundle install")
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
# FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
# unless File.exist?("config/database.yml")
# FileUtils.cp "config/database.yml.sample", "config/database.yml"
# end
puts "\n== Preparing database =="
system! 'bin/rails db:prepare'
system! "bin/rails db:prepare"
puts "\n== Removing old logs and tempfiles =="
system! 'bin/rails log:clear tmp:clear'
system! "bin/rails log:clear tmp:clear"
puts "\n== Restarting application server =="
system! 'bin/rails restart'
unless ARGV.include?("--skip-server")
puts "\n== Starting development server =="
STDOUT.flush # flush the output before exec(2) so that it displays
exec "bin/dev"
end
end

View File

@@ -1,4 +1,4 @@
require_relative 'boot'
require_relative "boot"
require "rails"
# Pick the frameworks you want:
@@ -12,7 +12,6 @@ require "action_mailbox/engine"
# require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
require "sprockets/railtie"
# require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
@@ -22,12 +21,20 @@ Bundler.require(*Rails.groups)
module Akkounts
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0
config.load_defaults 8.0
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w[assets tasks])
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
# Don't generate system test files.
config.generators.system_tests = nil
@@ -40,7 +47,15 @@ module Akkounts
g.stylesheets false
end
config.active_job.queue_adapter = :sidekiq
config.active_job.queue_adapter = :solid_queue
config.mission_control.jobs.http_basic_auth_enabled = false
config.action_mailer.deliver_later_queue_name = nil # use "default" queue
# The default includes webp, which requires webp support everywhere
config.active_storage.web_image_content_types = %w[image/png image/jpeg image/gif]
config.active_record.encryption.primary_key = ENV["ENCRYPTION_PRIMARY_KEY"]
config.active_record.encryption.key_derivation_salt = ENV["ENCRYPTION_KEY_DERIVATION_SALT"]
end
end

View File

@@ -1 +0,0 @@
tmI5vm7qZhaigr52jEBVWkRdj+EE+9OmPh3vWXC7kA/OHuuucpr7SodychuMkQDPLM0BLk88LFsqvRIR+mqnLWpRC+P9aeUFE6ohxSWzcAd7Y4sgxUD8zpCRPndrwTw0hxXXj1WZSYeWn4BoAB34aV+gYen2MajZF3a95hJGtS5yjgWxvLVkQQKqRDfykkfX6fCS0BPo5X7sT7m4xwCATD/D4219wajm5W3TIdkriHtwt28ZLspaRWA5e0UkzKf8+/Gaj2CrW7UWcvew8R93zQ5RA2/Sp3sDTVN+kLz9I9Q095lQC0ywCAEFYHeKmc2tjrzqRaAAWu06xmWLqGIg21G+A/UU9lUJOkIpxQACWoOfS2IoXR1nXhgXMopkz3aCBXDxKw554v4H2QyOceOsuRf2C685ibMqzQkKMmJ4tcbiOJL77DUc08JTjB8Dq4Ohr8sMzXbV/hATevjYoRP0XarLekqhLv90ZLuIVY16DwB0CzACeNBKeKbeLqJF51upRRWgi+gTbYpV04yUwnXdyssF8mydWocgihrTryBi8F6PsuhBGcaYdP+0yibnGxDCC4x2rupbBfMj2OIX7pYzgtIHB3Eo954Y+bCoggqbE/Qrb9VVXNMgtKgLt8EGWU2tg6wl9QicitIq87uLDAade93zTn6rmcKPywjMDo6jbVIs653ZdUhiKdHGdpnJccbgQ/iLSPB1umNnCeaEX5jM+K9zBvl7ZMCdSk1YIQ==--ekKumqLiSlVJNwMe--K/ecXmmMT1x+WnIXMbHBDw==

View File

@@ -1 +0,0 @@
6b101c9addbfa5f959b5859f756bc9d7

View File

@@ -1 +0,0 @@
vqH5By5qFLImVjdlWj+7FwGg8APKnr/AEd7WqekG7L0vNA32WGBpwS1uGzs02LIcATRwGj8DyJxiBOB/w9z8cwoO+t6Woi5hAnOSCQwFWKLT0dZq7jgtT8pxK0Yu/Nf91PEFN1rc/8ZFy2KKVpbtMbMPyivT38e/ctBZD/lHrWkndvLXYvFVhqWjUnDOGbhwl/U0RZgqBBjvlm3B0JkQfiN8VXPlCJL2Cd8kd0+MpRCRTgtcxA==--OdVXnDP7OhzJxCsP--+8SI6IFIeXyDxXb+WpqhIQ==

View File

@@ -1,21 +1,37 @@
default: &default
adapter: sqlite3
adapter: <%= ENV["DB_ADAPTER"] || "sqlite3" %>
pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %>
timeout: 5000
<% if ENV["DB_ADAPTER"] == "postgresql" %>
host: <%= ENV["PG_HOST"] || 'localhost' %>
port: <%= ENV["PG_PORT"] || 5432 %>
username: <%= ENV["PG_USERNAME"] || 'akkounts' %>
password: <%= ENV["PG_PASSWORD"] %>
<% end %>
<% if ENV["LNDHUB_PG_HOST"].present? %>
lndhub: &lndhub
adapter: postgresql
database_tasks: false
host: <%= ENV["LNDHUB_PG_HOST"] %>
port: <%= ENV["LNDHUB_PG_PORT"] || 5432 %>
database: <%= ENV["LNDHUB_PG_DATABASE"] || 'lndhub' %>
username: <%= ENV["LNDHUB_PG_USERNAME"] || 'lndhub' %>
password: <%= ENV["LNDHUB_PG_PASSWORD"] %>
<% end %>
development:
primary:
<<: *default
database: db/development.sqlite3
lndhub:
database: <%= ENV["DB_ADAPTER"] == "postgresql" ? ENV["PG_DATABASE"] : "db/development.sqlite3" %>
queue:
<<: *default
adapter: postgresql
database_tasks: false
host: <%= ENV["LNDHUB_PG_HOST"] || 'localhost' %>
port: <%= ENV["LNDHUB_PG_PORT"] || 5432 %>
database: <%= ENV["LNDHUB_PG_DATABASE"] || 'lndhub' %>
username: <%= ENV["LNDHUB_PG_USERNAME"] || 'lndhub' %>
password: <%= ENV["LNDHUB_PG_PASSWORD"] %>
database: <%= ENV["DB_ADAPTER"] == "postgresql" ? ENV["PG_DATABASE_QUEUE"] : "db/development_queue.sqlite3" %>
migrations_paths: db/queue_migrate
<% if ENV["LNDHUB_PG_HOST"].present? %>
lndhub:
<<: *lndhub
<% end %>
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
@@ -32,18 +48,12 @@ test:
production:
primary:
<<: *default
adapter: postgresql
database: akkounts
port: 5432
host: <%= Rails.application.credentials.postgres[:host] rescue nil %>
username: <%= Rails.application.credentials.postgres[:username] rescue nil %>
password: <%= Rails.application.credentials.postgres[:password] rescue nil %>
lndhub:
database: <%= ENV["DB_ADAPTER"] == "postgresql" ? ENV["PG_DATABASE"] : "db/production.sqlite3" %>
queue:
<<: *default
adapter: postgresql
database_tasks: false
host: <%= ENV["LNDHUB_PG_HOST"] || 'localhost' %>
port: <%= ENV["LNDHUB_PG_PORT"] || 5432 %>
database: <%= ENV["LNDHUB_PG_DATABASE"] || 'lndhub' %>
username: <%= ENV["LNDHUB_PG_USERNAME"] || 'lndhub' %>
password: <%= ENV["LNDHUB_PG_PASSWORD"] %>
database: <%= ENV["DB_ADAPTER"] == "postgresql" ? ENV["PG_DATABASE_QUEUE"] : "db/production_queue.sqlite3" %>
migrations_paths: db/queue_migrate
<% if ENV["LNDHUB_PG_HOST"].present? %>
lndhub:
<<: *lndhub
<% end %>

View File

@@ -1,5 +1,5 @@
# Load the Rails application.
require_relative 'application'
require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!

View File

@@ -1,10 +1,10 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Make code changes take effect immediately without server restart.
config.enable_reloading = true
# Do not eager load code on boot.
config.eager_load = false
@@ -12,16 +12,15 @@ Rails.application.configure do
# Show full error reports.
config.consider_all_requests_local = true
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join('tmp', 'caching-dev.txt').exist?
# Enable server timing.
config.server_timing = true
# Enable/disable Action Controller caching. By default Action Controller caching is disabled.
# Run rails dev:cache to toggle Action Controller caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.to_i}"
}
config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
else
config.action_controller.perform_caching = false
@@ -31,41 +30,56 @@ Rails.application.configure do
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
# Make template changes take effect immediately.
config.action_mailer.perform_caching = false
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = true
# Append comments with runtime information tags to SQL queries in logs.
config.active_record.query_log_tags_enabled = true
# Highlight code that enqueued background job in logs.
config.active_job.verbose_enqueue_logs = true
# Solid Queue database
config.solid_queue.connects_to = { database: { writing: :queue } }
# Suppress logger output for asset requests.
config.assets.quiet = true
# config.assets.quiet = true
# Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
# config.i18n.raise_on_missing_translations = true
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
# Annotate rendered view with file names.
config.action_view.annotate_rendered_view_with_filenames = true
# Don't actually send emails, cache them for viewing via letter opener
config.action_mailer.delivery_method = :letter_opener
# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
# Don't care if the mailer can't send
# Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
# Notice if the mailer can't send
config.action_mailer.raise_delivery_errors = true
# Base URL to be used by email template link helpers
config.action_mailer.default_url_options = {
host: "localhost:3000",
host: "localhost:3000", # TODO port: 3000
protocol: "http"
}

View File

@@ -1,61 +1,61 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
config.enable_reloading = false
# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
# Eager load code on boot for better performance and memory savings (ignored by Rake tasks).
config.eager_load = true
# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true
# Full error reports are disabled.
config.consider_all_requests_local = false
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass
# Turn on fragment caching in view templates.
config.action_controller.perform_caching = true
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# Cache assets for far-future expiry since they are all digest stamped.
config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" }
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
# config.asset_host = "http://assets.example.com"
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Mount Action Cable outside main process or domain.
# config.action_cable.mount_path = nil
# config.action_cable.url = 'wss://example.com/cable'
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
# config.assume_ssl = true
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
config.log_level = :debug
# Skip http-to-https redirect for the default health check endpoint.
# config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
# Prepend all log lines with the following tags.
# Log to STDOUT with the current request id as a default log tag.
config.log_tags = [ :request_id ]
config.logger = ActiveSupport::TaggedLogging.logger(STDOUT)
# Use a different cache store in production.
# Change to "debug" to log everything (including potentially personally-identifiable information!)
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
# Prevent health checks from clogging up the logs.
config.silence_healthcheck_path = "/up"
# Don't log any deprecations.
config.active_support.report_deprecations = false
# Replace the default in-process memory cache store with a durable alternative.
# config.cache_store = :mem_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment).
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "akkounts_production"
# Solid Queue database
config.solid_queue.connects_to = { database: { writing: :queue } }
# E-mail settings, adapted from https://github.com/mastodon/mastodon
@@ -106,6 +106,10 @@ Rails.application.configure do
config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym
# Disable caching for Action Mailer templates even if Action Controller
# caching is enabled.
config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
config.action_mailer.raise_delivery_errors = true
@@ -120,43 +124,18 @@ Rails.application.configure do
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups.
# require 'syslog/logger'
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
# Inserts middleware to perform automatic connection switching.
# The `database_selector` hash is used to pass options to the DatabaseSelector
# middleware. The `delay` is used to determine how long to wait after a write
# to send a subsequent read to the primary.
# Only use :id for inspections in production.
config.active_record.attributes_for_inspect = [ :id ]
# Enable DNS rebinding protection and other `Host` header attacks.
# config.hosts = [
# "example.com", # Allow requests from example.com
# /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
# ]
#
# The `database_resolver` class is used by the middleware to determine which
# database is appropriate to use based on the time delay.
#
# The `database_resolver_context` class is used by the middleware to set
# timestamps for the last write to the primary. The resolver uses the context
# class timestamps to determine how long to wait before reading from the
# replica.
#
# By default Rails will store a last write timestamp in the session. The
# DatabaseSelector middleware is designed as such you can define your own
# strategy for connection switching and pass that into the middleware through
# these configuration options.
# config.active_record.database_selector = { delay: 2.seconds }
# config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
# config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
# Skip DNS rebinding protection for the default health check endpoint.
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end

View File

@@ -6,31 +6,33 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
config.cache_classes = false
config.action_view.cache_template_loading = true
# While tests run files are not watched, reloading is not necessary.
config.enable_reloading = false
# Do not eager load code on boot. This avoids loading your whole application
# just for the purpose of running a single test. If you are using a tool that
# preloads Rails for running tests, you may have to set it to true.
config.eager_load = false
# Eager loading loads your entire application. When running a single test locally,
# this is usually not necessary, and can slow down your test suite. However, it's
# recommended that you enable it in continuous integration systems to ensure eager
# loading is working properly before deploying your code.
config.eager_load = ENV["CI"].present?
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{1.hour.to_i}"
}
# Configure public file server for tests with cache-control for performance.
config.public_file_server.headers = { "cache-control" => "public, max-age=3600" }
# Show full error reports and disable caching.
config.consider_all_requests_local = true
# Show full error reports.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.cache_store = :null_store
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = :none
# Render exception templates for rescuable exceptions and raise for other exceptions.
config.action_dispatch.show_exceptions = :rescuable
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
config.active_job.queue_adapter = :test
# Disable caching for Action Mailer templates even if Action Controller
# caching is enabled.
config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
@@ -38,27 +40,28 @@ Rails.application.configure do
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
config.action_mailer.default_options = {
from: "accounts@kosmos.org",
message_id: -> { "<#{Mail.random_tag}@kosmos.org>" },
}
config.action_mailer.default_url_options = {
host: "accounts.kosmos.org",
protocol: "https"
}
config.action_mailer.default_options = {
from: "accounts@kosmos.org",
message_id: -> { "<#{Mail.random_tag}@kosmos.org>" },
}
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
config.active_job.queue_adapter = :test
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
config.active_storage.service = :s3
else
config.active_storage.service = :local
end
# Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
end

View File

@@ -1,14 +1,9 @@
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = '1.0'
Rails.application.config.assets.version = "1.0"
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Add Yarn node_modules folder to the asset load path.
Rails.application.config.assets.paths << Rails.root.join('node_modules')
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )

View File

@@ -1,30 +1,25 @@
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
# Define an application-wide content security policy.
# See the Securing Rails Applications Guide for more information:
# https://guides.rubyonrails.org/security.html#content-security-policy-header
# Rails.application.config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # If you are using webpack-dev-server then specify webpack-dev-server host
# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# Rails.application.configure do
# config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# end
#
# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# config.content_security_policy_nonce_directives = %w(script-src style-src)
#
# # Report violations without enforcing the policy.
# # config.content_security_policy_report_only = true
# end
# If you are using UJS then enable automatic nonce generation
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
# Set the nonce only to specific directives
# Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true

View File

@@ -45,7 +45,7 @@ Devise.setup do |config|
# Configure the e-mail address which will be shown in Devise::Mailer,
# note that it will be overwritten if you use your own mailer class
# with default "from" parameter.
config.mailer_sender = 'accounts@kosmos.org'
config.mailer_sender = ENV["SMTP_FROM_ADDRESS"] || 'accounts@localhost'
# Configure the class responsible to send e-mails.
# config.mailer = 'Devise::Mailer'

View File

@@ -0,0 +1,11 @@
# See https://alvincrespo.hashnode.dev/rails-8s-lazy-route-loading-devise
# TODO remove when Devise is fixed
require 'devise'
Devise # make sure it's already loaded
module Devise
def self.mappings
Rails.application.try(:reload_routes_unless_loaded)
@@mappings
end
end

View File

@@ -1,4 +1,8 @@
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
Rails.application.config.filter_parameters += [:password]
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
:password, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
]

View File

@@ -4,13 +4,13 @@
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, '\1en'
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.plural /^(ox)$/i, "\\1en"
# inflect.singular /^(ox)en/i, "\\1"
# inflect.irregular "person", "people"
# inflect.uncountable %w( fish sheep )
# end
# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym 'RESTful'
# inflect.acronym "RESTful"
# end

View File

@@ -1 +0,0 @@
Rails.application.routes.default_url_options[:host] = ENV['APP_DOMAIN']

View File

@@ -0,0 +1,13 @@
# Be sure to restart your server when you modify this file.
# Define an application-wide HTTP permissions policy. For further
# information see: https://developers.google.com/web/updates/2018/06/feature-policy
# Rails.application.config.permissions_policy do |policy|
# policy.camera :none
# policy.gyroscope :none
# policy.microphone :none
# policy.usb :none
# policy.fullscreen :self
# policy.payment :self, "https://secure.example.com"
# end

View File

@@ -1,5 +0,0 @@
require_relative "../../app/models/setting"
Sidekiq.configure_server do |config|
config.redis = { url: Setting.redis_url }
end

View File

@@ -28,11 +28,11 @@ authorizations: &AUTHORIZATIONS
development:
host: <%= ENV["LDAP_HOST"] || "localhost" %>
port: <%= ENV["LDAP_PORT"] || "389" %>
attribute: cn
base: <%= ENV["LDAP_BASE"] || "ou=kosmos.org,cn=users,dc=kosmos,dc=org" %>
admin_user: "cn=Directory Manager"
admin_password: <%= ENV["LDAP_ADMIN_PASSWORD"] %>
ssl: <%= ENV["LDAP_USE_TLS"] || "false" %>
attribute: <%= ENV["LDAP_UID_ATTR"] || "cn" %>
base: <%= ENV["LDAP_BASE"] || "ou=kosmos.org,cn=users,dc=kosmos,dc=org" %>
admin_user: <%= ENV["LDAP_ADMIN_USER"] || "cn=Directory Manager" %>
admin_password: <%= ENV["LDAP_ADMIN_PASSWORD"] %>
# <<: *AUTHORIZATIONS
test:
@@ -46,11 +46,11 @@ test:
# <<: *AUTHORIZATIONS
production:
host: ldap.kosmos.local
port: 389
attribute: cn
base: ou=kosmos.org,cn=users,dc=kosmos,dc=org
admin_user: <%= Rails.application.credentials.ldap[:username] rescue nil %>
admin_password: <%= Rails.application.credentials.ldap[:password] rescue nil %>
# ssl: false
host: <%= ENV["LDAP_HOST"] || "localhost" %>
port: <%= ENV["LDAP_PORT"] || "389" %>
ssl: <%= ENV["LDAP_USE_TLS"] || "false" %>
attribute: <%= ENV["LDAP_UID_ATTR"] || "cn" %>
base: <%= ENV["LDAP_BASE"] || "ou=kosmos.org,cn=users,dc=kosmos,dc=org" %>
admin_user: <%= ENV["LDAP_ADMIN_USER"] || "cn=Directory Manager" %>
admin_password: <%= ENV["LDAP_ADMIN_PASSWORD"] %>
# <<: *AUTHORIZATIONS

View File

@@ -1,38 +1,43 @@
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
# This configuration file will be evaluated by Puma. The top-level methods that
# are invoked here are part of Puma's configuration DSL. For more information
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
# Puma starts a configurable number of processes (workers) and each process
# serves each request in a thread from an internal thread pool.
#
# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You
# should only set this value when you want to run 2 or more workers. The
# default is already 1.
#
# The ideal number of threads per worker depends both on how much time the
# application spends waiting for IO operations and on how much you wish to
# prioritize throughput over latency.
#
# As a rule of thumb, increasing the number of threads will increase how much
# traffic a given process can handle (throughput), but due to CRuby's
# Global VM Lock (GVL) it has diminishing returns and will degrade the
# response time (latency) of the application.
#
# The default is set to 3 threads as it's deemed a decent compromise between
# throughput and latency for the average Rails application.
#
# Any libraries that use a connection pool or another resource pool should
# be configured to provide at least as many connections as the number of
# threads. This includes Active Record's `pool` parameter in `database.yml`.
max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
min_threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads min_threads_count, max_threads_count
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
port ENV.fetch("PORT", 3000)
environment ENV.fetch("RAILS_ENV") { "development" }
# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
# preload_app!
# Allow puma to be restarted by `rails restart` command.
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
# Run the Solid Queue supervisor inside of Puma for single-server deployments
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]
# Specify the PID file. Defaults to tmp/pids/server.pid in development.
# In other environments, only set the PID file if requested.
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]

21
config/queue.yml Normal file
View File

@@ -0,0 +1,21 @@
default: &default
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "*"
threads: 3
processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
polling_interval: 0.1
development:
<<: *default
workers:
- queues: "*"
threads: 1
test:
<<: *default
production:
<<: *default

10
config/recurring.yml Normal file
View File

@@ -0,0 +1,10 @@
# production:
# periodic_cleanup:
# class: CleanSoftDeletedRecordsJob
# queue: background
# args: [ 1000, { batch_size: 500 } ]
# schedule: every hour
# periodic_command:
# command: "SoftDeletedRecord.due.delete_all"
# priority: 2
# schedule: at 5am every day

View File

@@ -1,5 +1,3 @@
require 'sidekiq/web'
Rails.application.routes.draw do
devise_for :users, controllers: {
confirmations: 'users/confirmations',
@@ -17,6 +15,9 @@ Rails.application.routes.draw do
match 'signup/:step', to: 'signup#steps', as: :signup_steps, via: [:get, :post]
post 'signup_validate', to: 'signup#validate'
get "users/:username/avatars/:hash", to: "avatars#show", as: :user_avatar
namespace :contributions do
root to: 'donations#index'
resources :donations, only: ['index', 'create'] do
@@ -123,7 +124,7 @@ Rails.application.routes.draw do
end
authenticate :user, ->(user) { user.is_admin? } do
mount Sidekiq::Web, at: '/sidekiq'
mount MissionControl::Jobs::Engine, at: "/jobs"
mount Flipper::UI.app(Flipper), at: '/flipper'
end

View File

@@ -0,0 +1,22 @@
# This migration comes from active_storage (originally 20190112182829)
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
def up
return unless table_exists?(:active_storage_blobs)
unless column_exists?(:active_storage_blobs, :service_name)
add_column :active_storage_blobs, :service_name, :string
if configured_service = ActiveStorage::Blob.service.name
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
end
change_column :active_storage_blobs, :service_name, :string, null: false
end
end
def down
return unless table_exists?(:active_storage_blobs)
remove_column :active_storage_blobs, :service_name
end
end

View File

@@ -0,0 +1,27 @@
# This migration comes from active_storage (originally 20191206030411)
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
def change
return unless table_exists?(:active_storage_blobs)
# Use Active Record's configured type for primary key
create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t|
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
t.string :variation_digest, null: false
t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_key_type
config = Rails.configuration.generators
config.options[config.orm][:primary_key_type] || :primary_key
end
def blobs_primary_key_type
pkey_name = connection.primary_key(:active_storage_blobs)
pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
pkey_column.bigint? ? :bigint : pkey_column.type
end
end

View File

@@ -0,0 +1,8 @@
# This migration comes from active_storage (originally 20211119233751)
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
def change
return unless table_exists?(:active_storage_blobs)
change_column_null(:active_storage_blobs, :checksum, true)
end
end

View File

@@ -0,0 +1,6 @@
class AddLndhubPasswordToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :lndhub_username, :string
add_column :users, :lndhub_password, :text
end
end

View File

@@ -0,0 +1,11 @@
class MigrateLockboxData < ActiveRecord::Migration[8.0]
def up
User.find_each do |user|
ln_account = user.ln_account
ln_password = user.ln_password
user.lndhub_username = ln_account
user.lndhub_password = ln_password
user.save!
end
end
end

View File

@@ -0,0 +1,6 @@
class RemoveLnFieldsFromUsers < ActiveRecord::Migration[8.0]
def change
remove_column :users, :ln_account
remove_column :users, :ln_password_ciphertext
end
end

View File

@@ -0,0 +1,5 @@
class AddMastodonIdToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :mastodon_id, :bigint
end
end

141
db/queue_schema.rb Normal file
View File

@@ -0,0 +1,141 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 1) do
create_table "solid_queue_blocked_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.string "concurrency_key", null: false
t.datetime "expires_at", null: false
t.datetime "created_at", null: false
t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release"
t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance"
t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
end
create_table "solid_queue_claimed_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.bigint "process_id"
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
end
create_table "solid_queue_failed_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.text "error"
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true
end
create_table "solid_queue_jobs", force: :cascade do |t|
t.string "queue_name", null: false
t.string "class_name", null: false
t.text "arguments"
t.integer "priority", default: 0, null: false
t.string "active_job_id"
t.datetime "scheduled_at"
t.datetime "finished_at"
t.string "concurrency_key"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id"
t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name"
t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at"
t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering"
t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting"
end
create_table "solid_queue_pauses", force: :cascade do |t|
t.string "queue_name", null: false
t.datetime "created_at", null: false
t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true
end
create_table "solid_queue_processes", force: :cascade do |t|
t.string "kind", null: false
t.datetime "last_heartbeat_at", null: false
t.bigint "supervisor_id"
t.integer "pid", null: false
t.string "hostname"
t.text "metadata"
t.datetime "created_at", null: false
t.string "name", null: false
t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at"
t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id"
end
create_table "solid_queue_ready_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true
t.index ["priority", "job_id"], name: "index_solid_queue_poll_all"
t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue"
end
create_table "solid_queue_recurring_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "task_key", null: false
t.datetime "run_at", null: false
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
end
create_table "solid_queue_recurring_tasks", force: :cascade do |t|
t.string "key", null: false
t.string "schedule", null: false
t.string "command", limit: 2048
t.string "class_name"
t.text "arguments"
t.string "queue_name"
t.integer "priority", default: 0
t.boolean "static", default: true, null: false
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true
t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static"
end
create_table "solid_queue_scheduled_executions", force: :cascade do |t|
t.bigint "job_id", null: false
t.string "queue_name", null: false
t.integer "priority", default: 0, null: false
t.datetime "scheduled_at", null: false
t.datetime "created_at", null: false
t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all"
end
create_table "solid_queue_semaphores", force: :cascade do |t|
t.string "key", null: false
t.integer "value", default: 1, null: false
t.datetime "expires_at", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at"
t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value"
t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
end
add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_09_22_205634) do
ActiveRecord::Schema[8.0].define(version: 2025_05_17_105755) do
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
@@ -127,12 +127,13 @@ ActiveRecord::Schema[7.1].define(version: 2024_09_22_205634) do
t.datetime "confirmed_at", precision: nil
t.datetime "confirmation_sent_at", precision: nil
t.string "unconfirmed_email"
t.text "ln_password_ciphertext"
t.string "ln_account"
t.datetime "remember_created_at"
t.string "remember_token"
t.text "preferences"
t.string "pgp_fpr"
t.string "lndhub_username"
t.text "lndhub_password"
t.bigint "mastodon_id"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end

View File

@@ -7,7 +7,7 @@ Sidekiq::Testing.inline! do
puts "Create user: admin"
CreateAccount.call(account: {
UserManager::CreateAccount.call(account: {
username: "admin", domain: "kosmos.org", email: "admin@example.com",
password: "admin is admin", confirmed: true
})
@@ -20,7 +20,7 @@ Sidekiq::Testing.inline! do
email = Faker::Internet.unique.email
next if username.length < 3
CreateAccount.call(account: {
UserManager::CreateAccount.call(account: {
username: username, domain: "kosmos.org", email: email,
password: "user is user", confirmed: true
})

View File

@@ -37,43 +37,25 @@ services:
- "3000:3000"
environment:
RAILS_ENV: development
SOLID_QUEUE_IN_PUMA: true
LAUNCHY_DRY_RUN: true
BROWSER: /dev/null
ENCRYPTION_PRIMARY_KEY: YhNLBgCFMAzw5dV3gISxnGrhNDMQwRdn
ENCRYPTION_KEY_DERIVATION_SALT: h28g16MRZ1sghF2jTCos1DiLZXUswinR
PRIMARY_DOMAIN: kosmos.org
AKKOUNTS_DOMAIN: accounts.kosmos.org
LDAP_HOST: ldap
LDAP_PORT: 3389
LDAP_ADMIN_PASSWORD: passthebutter
LDAP_USE_TLS: "false"
REDIS_URL: redis://redis:6379/0
ACTIVE_STORAGE_PATH: "/akkounts/tmp/attachments"
ACTIVE_STORAGE_PATH: /akkounts/tmp/attachments
RS_REDIS_URL: redis://redis:6379/1
RS_STORAGE_URL: "http://localhost:4567"
RS_STORAGE_URL: http://localhost:4567
RS_AKKOUNTS_DOMAIN: localhost
S3_ENABLED: false
NOSTR_PUBLIC_KEY: bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf
NOSTR_PRIVATE_KEY: 7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea
NOSTR_RELAY_URL: "ws://strfry:7777"
depends_on:
- ldap
- redis
sidekiq:
build: .
command: bash -c "bundle exec sidekiq -C config/sidekiq.yml"
volumes:
- .:/akkounts
networks:
- internal_network
environment:
RAILS_ENV: development
PRIMARY_DOMAIN: kosmos.org
LDAP_HOST: ldap
LDAP_PORT: 3389
LDAP_ADMIN_PASSWORD: passthebutter
LDAP_USE_TLS: "false"
LAUNCHY_DRY_RUN: true
BROWSER: /dev/null
REDIS_URL: redis://redis:6379/0
RS_REDIS_URL: redis://redis:6379/1
RS_STORAGE_URL: "http://localhost:4567"
S3_ENABLED: false
NOSTR_RELAY_URL: ws://strfry:7777
depends_on:
- ldap
- redis
@@ -111,7 +93,7 @@ services:
- redis
strfry:
image: gitea.kosmos.org/kosmos/strfry-deno:1.1.1
image: gitea.kosmos.org/kosmos/strfry-deno:2.0.0
volumes:
- ./docker/strfry/strfry.conf:/etc/strfry.conf
- ./extras/strfry:/opt/strfry

13
docs/dev/mastodon.md Normal file
View File

@@ -0,0 +1,13 @@
# Mastodon
## API access
(Optional)
* Log in to your Mastodon instance with an admin account
* Create a new OAuth application (Settings -> Development)
* Select `admin:read` access in the permissions
* After confirming, click on the application name in the list, and copy the
access token
* Configure it as `MASTODON_AUTH_TOKEN` via environment variable, or on
Admin -> Settings -> Services -> Mastodon

57
docs/dev/nostr.md Normal file
View File

@@ -0,0 +1,57 @@
# Nostr
## strfry
The `extras/strfry` directory contains code to integrate [strfry][1] with
akkounts, so that notes published to the relay have to be authored by (or in
some cases just related to) local users who have verified their Nostr public
key.
### Requirements
[Deno](https://deno.com/) needs to be installed on the machine that you run
strfry on.
We provide a Docker image with recent strfry and Deno builds:
https://gitea.kosmos.org/kosmos/-/packages/container/strfry-deno/
### Configuration
You can use either environment variables (see e.g. the `strfry` service in
`docker-compose-yml`) or a local `.env` file in the same working directory
that you place the extra files in (e.g. `/opt/strfry`).
In your `strfry.conf`, configure `strfry-policy.ts` as the write policy, like so:
```
writePolicy {
plugin = "/opt/strfry/strfry-policy.ts"
}
```
All dependencies will be downloaded and cached automatically when the plugin is
called for the first time.
### Manual tasks
You can sync all notes authored by local users (any account that has verified
their Nostr pubkey with akkounts) from a remote [strfry][1] relay via negentropy
sync:
deno run -A /opt/strfry/strfry-sync.ts wss://nostr.kosmos.org
Or, in the running container when using Docker Compose:
docker compose exec strfry deno run -A /opt/strfry/strfry-sync.ts wss://nostr.kosmos.org
The `strfry` service container also exposes the local relay on your local host
on port 4777.
[nak](https://github.com/fiatjaf/nak) is a helpful tool for manual Nostr tasks.
Here's how you can grab a note by its event ID from a remote relay and publish
it to your local strfry for example:
nak req -i 0fb010192685b86b0810b3de3706fbbf3b8c1db30b14533094a2b9700c820cdc nostr.kosmos.org | nak event ws://localhost:4777
[1]: https://github.com/hoytech/strfry

320
extras/strfry/deno.lock generated
View File

@@ -1,101 +1,231 @@
{
"version": "3",
"packages": {
"specifiers": {
"jsr:@nostr/tools@^2.3.1": "jsr:@nostr/tools@2.3.1",
"npm:@noble/ciphers@^0.5.1": "npm:@noble/ciphers@0.5.3",
"npm:@noble/curves@1.2.0": "npm:@noble/curves@1.2.0",
"npm:@noble/hashes@1.3.1": "npm:@noble/hashes@1.3.1",
"npm:@scure/base@1.1.1": "npm:@scure/base@1.1.1",
"npm:ldapts": "npm:ldapts@7.0.12"
"version": "4",
"specifiers": {
"jsr:@nostr/tools@*": "2.3.1",
"jsr:@nostr/tools@^2.3.1": "2.3.1",
"jsr:@nostrify/nostrify@0.36": "0.36.2",
"jsr:@nostrify/policies@*": "0.36.1",
"jsr:@nostrify/strfry@*": "0.2.1",
"jsr:@nostrify/types@0.35": "0.35.0",
"jsr:@nostrify/types@0.36": "0.36.0",
"jsr:@std/bytes@^1.0.5": "1.0.5",
"jsr:@std/encoding@~0.224.1": "0.224.3",
"jsr:@std/json@^1.0.1": "1.0.1",
"jsr:@std/streams@^1.0.7": "1.0.9",
"jsr:@std/streams@^1.0.8": "1.0.9",
"npm:@noble/ciphers@~0.5.1": "0.5.3",
"npm:@noble/curves@1.2.0": "1.2.0",
"npm:@noble/hashes@1.3.1": "1.3.1",
"npm:@scure/base@1.1.1": "1.1.1",
"npm:@scure/bip32@^1.4.0": "1.6.2",
"npm:@scure/bip39@^1.3.0": "1.5.4",
"npm:ldapts@*": "7.0.12",
"npm:lru-cache@^10.2.0": "10.4.3",
"npm:nostr-tools@^2.7.0": "2.12.0",
"npm:websocket-ts@^2.1.5": "2.2.1",
"npm:zod@^3.23.8": "3.24.2"
},
"jsr": {
"@nostr/tools@2.3.1": {
"integrity": "af01dc45cb28784c584d7a0699707196f397bcc53946efa582a01b11ddde4d61",
"dependencies": [
"npm:@noble/ciphers",
"npm:@noble/curves",
"npm:@noble/hashes",
"npm:@scure/base"
]
},
"jsr": {
"@nostr/tools@2.3.1": {
"integrity": "af01dc45cb28784c584d7a0699707196f397bcc53946efa582a01b11ddde4d61",
"dependencies": [
"npm:@noble/ciphers@^0.5.1",
"npm:@noble/curves@1.2.0",
"npm:@noble/hashes@1.3.1",
"npm:@scure/base@1.1.1"
]
}
"@nostrify/nostrify@0.36.2": {
"integrity": "cc4787ca170b623a2e5dfed1baa4426077daa6143af728ea7dd325d58f4d04d6",
"dependencies": [
"jsr:@nostrify/types@0.35",
"jsr:@std/encoding",
"npm:@scure/bip32",
"npm:@scure/bip39",
"npm:lru-cache",
"npm:nostr-tools",
"npm:websocket-ts",
"npm:zod"
]
},
"npm": {
"@noble/ciphers@0.5.3": {
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
"dependencies": {}
},
"@noble/curves@1.2.0": {
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"dependencies": {
"@noble/hashes": "@noble/hashes@1.3.2"
}
},
"@noble/hashes@1.3.1": {
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"dependencies": {}
},
"@noble/hashes@1.3.2": {
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"dependencies": {}
},
"@scure/base@1.1.1": {
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"dependencies": {}
},
"@types/asn1@0.2.4": {
"integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==",
"dependencies": {
"@types/node": "@types/node@18.16.19"
}
},
"@types/node@18.16.19": {
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
"dependencies": {}
},
"@types/uuid@9.0.8": {
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"dependencies": {}
},
"asn1@0.2.6": {
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dependencies": {
"safer-buffer": "safer-buffer@2.1.2"
}
},
"debug@4.3.5": {
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
"dependencies": {
"ms": "ms@2.1.2"
}
},
"ldapts@7.0.12": {
"integrity": "sha512-orwgIejUi/ZyGah9y8jWZmFUg8Ci5M8WAv0oZjSf3MVuk1sRBdor9Qy1ttGHbYpWj96HXKFunQ8AYZ8WWGp17g==",
"dependencies": {
"@types/asn1": "@types/asn1@0.2.4",
"@types/uuid": "@types/uuid@9.0.8",
"asn1": "asn1@0.2.6",
"debug": "debug@4.3.5",
"strict-event-emitter-types": "strict-event-emitter-types@2.0.0",
"uuid": "uuid@9.0.1"
}
},
"ms@2.1.2": {
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dependencies": {}
},
"safer-buffer@2.1.2": {
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dependencies": {}
},
"strict-event-emitter-types@2.0.0": {
"integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==",
"dependencies": {}
},
"uuid@9.0.1": {
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"dependencies": {}
}
"@nostrify/policies@0.36.1": {
"integrity": "6d59af115a687fcd18b6caebab0e4f50ee6cdb0aafa2aacd0aec2065021275b4",
"dependencies": [
"jsr:@nostrify/nostrify",
"jsr:@nostrify/types@0.35",
"npm:nostr-tools"
]
},
"@nostrify/strfry@0.2.1": {
"integrity": "be437b13f49e6564e557da23072bf642723a603568f672543a64d9fda6663432",
"dependencies": [
"jsr:@nostrify/types@0.36",
"jsr:@std/json",
"jsr:@std/streams@^1.0.8"
]
},
"@nostrify/types@0.35.0": {
"integrity": "b8d515563d467072694557d5626fa1600f74e83197eef45dd86a9a99c64f7fe6"
},
"@nostrify/types@0.36.0": {
"integrity": "b3413467debcbd298d217483df4e2aae6c335a34765c90ac7811cf7c637600e7"
},
"@std/bytes@1.0.5": {
"integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e"
},
"@std/encoding@0.224.3": {
"integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf"
},
"@std/json@1.0.1": {
"integrity": "1f0f70737e8827f9acca086282e903677bc1bb0c8ffcd1f21bca60039563049f",
"dependencies": [
"jsr:@std/streams@^1.0.7"
]
},
"@std/streams@1.0.9": {
"integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035",
"dependencies": [
"jsr:@std/bytes"
]
}
},
"npm": {
"@noble/ciphers@0.5.3": {
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="
},
"@noble/curves@1.1.0": {
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"dependencies": [
"@noble/hashes@1.3.1"
]
},
"@noble/curves@1.2.0": {
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"dependencies": [
"@noble/hashes@1.3.2"
]
},
"@noble/curves@1.8.2": {
"integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==",
"dependencies": [
"@noble/hashes@1.7.2"
]
},
"@noble/hashes@1.3.1": {
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="
},
"@noble/hashes@1.3.2": {
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="
},
"@noble/hashes@1.7.2": {
"integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ=="
},
"@scure/base@1.1.1": {
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
},
"@scure/base@1.2.4": {
"integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ=="
},
"@scure/bip32@1.3.1": {
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"dependencies": [
"@noble/curves@1.1.0",
"@noble/hashes@1.3.2",
"@scure/base@1.1.1"
]
},
"@scure/bip32@1.6.2": {
"integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==",
"dependencies": [
"@noble/curves@1.8.2",
"@noble/hashes@1.7.2",
"@scure/base@1.2.4"
]
},
"@scure/bip39@1.2.1": {
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"dependencies": [
"@noble/hashes@1.3.2",
"@scure/base@1.1.1"
]
},
"@scure/bip39@1.5.4": {
"integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==",
"dependencies": [
"@noble/hashes@1.7.2",
"@scure/base@1.2.4"
]
},
"@types/asn1@0.2.4": {
"integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==",
"dependencies": [
"@types/node"
]
},
"@types/node@18.16.19": {
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA=="
},
"@types/uuid@9.0.8": {
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
},
"asn1@0.2.6": {
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dependencies": [
"safer-buffer"
]
},
"debug@4.3.5": {
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
"dependencies": [
"ms"
]
},
"ldapts@7.0.12": {
"integrity": "sha512-orwgIejUi/ZyGah9y8jWZmFUg8Ci5M8WAv0oZjSf3MVuk1sRBdor9Qy1ttGHbYpWj96HXKFunQ8AYZ8WWGp17g==",
"dependencies": [
"@types/asn1",
"@types/uuid",
"asn1",
"debug",
"strict-event-emitter-types",
"uuid"
]
},
"lru-cache@10.4.3": {
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
},
"ms@2.1.2": {
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nostr-tools@2.12.0": {
"integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==",
"dependencies": [
"@noble/ciphers",
"@noble/curves@1.2.0",
"@noble/hashes@1.3.1",
"@scure/base@1.1.1",
"@scure/bip32@1.3.1",
"@scure/bip39@1.2.1",
"nostr-wasm"
]
},
"nostr-wasm@0.1.0": {
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="
},
"safer-buffer@2.1.2": {
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"strict-event-emitter-types@2.0.0": {
"integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA=="
},
"uuid@9.0.1": {
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
},
"websocket-ts@2.2.1": {
"integrity": "sha512-YKPDfxlK5qOheLZ2bTIiktZO1bpfGdNCPJmTEaPW7G9UXI1GKjDdeacOrsULUS000OPNxDVOyAuKLuIWPqWM0Q=="
},
"zod@3.24.2": {
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="
}
},
"remote": {

View File

@@ -1,8 +1,8 @@
import type { IterablePubkeys, Policy } from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
import { NostrEvent, NostrRelayInfo, NostrRelayOK, NPolicy } from 'jsr:@nostrify/types@^0.35.0';
import { nip57 } from 'jsr:@nostr/tools';
import { Client } from 'npm:ldapts';
import { nip57 } from '@nostr/tools';
interface LdapConfig {
export interface LdapConfig {
url: string;
bindDN: string;
password: string;
@@ -10,68 +10,74 @@ interface LdapConfig {
whitelistPubkeys?: IterablePubkeys;
}
const ldapPolicy: Policy<LdapConfig> = async (msg, opts) => {
const client = new Client({ url: opts.url });
const { kind, tags } = msg.event;
let { pubkey } = msg.event;
let out = { id: msg.event.id }
export class LdapPolicy implements NPolicy {
constructor(private opts: LdapConfig) {}
if (opts.whitelistPubkeys.includes(pubkey)) {
out['action'] = 'accept';
out['msg'] = '';
return out;
}
// deno-lint-ignore require-await
async call(event: NostrEvent): Promise<NostrRelayOK> {
const client = new Client({ url: this.opts.url });
const { id, kind, tags } = event;
let { pubkey } = event;
// Zap receipt
if (kind === 9735) {
const descriptionTag = tags.find(([t, v]) => t === 'description' && v);
const invalidZapRequestMsg = 'Zap receipts must contain a valid zap request from a relay member';
if (typeof descriptionTag === 'undefined') {
out['action'] = 'reject';
out['msg'] = invalidZapRequestMsg;
return out;
if (this.opts.whitelistPubkeys.includes(pubkey)) {
return ['OK', id, true, ''];
}
const zapRequestJSON = descriptionTag[1];
const validationResult = nip57.validateZapRequest(zapRequestJSON);
// Zap receipt
if (kind === 9735) {
const descriptionTag = tags.find(([t, v]) => t === 'description' && v);
const invalidZapRequestMsg = 'Zap receipts must contain a valid zap request from a relay member';
// TODO
// The zap receipt event's pubkey MUST be the same as the recipient's lnurl provider's nostrPubkey (retrieved in step 1 of the protocol flow).
// The invoiceAmount contained in the bolt11 tag of the zap receipt MUST equal the amount tag of the zap request (if present).
if (typeof descriptionTag === 'undefined') {
return ['OK', id, false, invalidZapRequestMsg];
}
if (validationResult === null) {
pubkey = JSON.parse(zapRequestJSON).pubkey;
} else {
out['action'] = 'reject';
out['msg'] = invalidZapRequestMsg;
return out;
const zapRequestJSON = descriptionTag[1];
const validationResult = nip57.validateZapRequest(zapRequestJSON);
// TODO
// The zap receipt event's pubkey MUST be the same as the recipient's lnurl provider's nostrPubkey (retrieved in step 1 of the protocol flow).
// The invoiceAmount contained in the bolt11 tag of the zap receipt MUST equal the amount tag of the zap request (if present).
if (validationResult === null) {
pubkey = JSON.parse(zapRequestJSON).pubkey;
} else {
return ['OK', id, false, invalidZapRequestMsg];
}
}
const out = { accept: true, msg: ''};
try {
await client.bind(this.opts.bindDN, this.opts.password);
const { searchEntries } = await client.search(this.opts.searchDN, {
filter: `(nostrKey=${pubkey})`,
attributes: ['nostrKey']
});
const memberKey = searchEntries[0]?.nostrKey;
if (memberKey === pubkey) {
out['accept'] = true;
} else {
out['accept'] = false;
out['msg'] = 'Only members can publish notes on this relay';
}
} catch (e) {
out['accept'] = false;
out['msg'] = 'Auth service temporarily unavailable';
console.warn(`[ldap-policy] Auth service temporarily unavailable: ${e.message}`)
} finally {
await client.unbind();
return ['OK', id, out['accept'], out['msg']];
}
}
try {
await client.bind(opts.bindDN, opts.password);
const { searchEntries } = await client.search(opts.searchDN, {
filter: `(nostrKey=${pubkey})`,
attributes: ['nostrKey']
});
const memberKey = searchEntries[0]?.nostrKey;
if (memberKey === pubkey) {
out['action'] = 'accept';
out['msg'] = '';
} else {
out['action'] = 'reject';
out['msg'] = 'Only members can publish notes on this relay';
}
} catch (ex) {
out['action'] = 'reject';
out['msg'] = 'Auth service temporarily unavailable';
} finally {
await client.unbind();
return out;
get info(): NostrRelayInfo {
return {
limitation: {
restricted_writes: true,
},
};
}
};
export default ldapPolicy;
}

View File

@@ -1,20 +1,20 @@
#!/bin/sh
//bin/true; exec deno run -A "$0" "$@"
//bin/true; exec deno run --unstable-kv -A "$0" "$@"
import {
antiDuplicationPolicy,
hellthreadPolicy,
pipeline,
rateLimitPolicy,
AntiDuplicationPolicy,
HellthreadPolicy,
PipePolicy,
readStdin,
writeStdout,
} from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
import ldapPolicy from './ldap-policy.ts';
} from 'jsr:@nostrify/policies';
import { strfry } from 'jsr:@nostrify/strfry';
import { LdapConfig, LdapPolicy } from './ldap-policy.ts';
import { load } from "https://deno.land/std@0.224.0/dotenv/mod.ts";
const dirname = new URL('.', import.meta.url).pathname;
await load({ envPath: `${dirname}/.env`, export: true });
const ldapConfig = {
const ldapConfig: LdapConfig = {
url: Deno.env.get("LDAP_URL"),
bindDN: Deno.env.get("LDAP_BIND_DN"),
password: Deno.env.get("LDAP_PASSWORD"),
@@ -22,13 +22,10 @@ const ldapConfig = {
whitelistPubkeys: Deno.env.get("WHITELIST_PUBKEYS")?.split(',')
}
for await (const msg of readStdin()) {
const result = await pipeline(msg, [
[hellthreadPolicy, { limit: 10 }],
[antiDuplicationPolicy, { ttl: 60000, minLength: 50 }],
[rateLimitPolicy, { whitelist: ['127.0.0.1'] }],
[ldapPolicy, ldapConfig],
]);
const policy = new PipePolicy([
new HellthreadPolicy({ limit: 10 }),
new AntiDuplicationPolicy({ kv: await Deno.openKv(), expireIn: 60000, minLength: 50 }),
new LdapPolicy(ldapConfig)
]);
writeStdout(result);
}
await strfry(policy);

29
lib/tasks/ctags.rake Normal file
View File

@@ -0,0 +1,29 @@
module Kosmos
class Ctags
def self.generate_app_tags
excludes = %w[.git gitno log tmp public].join(" --exclude ")
cmd = "ctags -R --languages=ruby --exclude #{excludes} ."
system cmd
end
def self.generate_bundler_tags
runtime = ::Bundler::Runtime.new Dir.pwd, ::Bundler.definition
paths = runtime.specs.map(&:full_gem_path)
generate_tags(paths, "gems.tags")
end
def self.generate_tags(paths, tag_file)
paths = paths.join(' ').strip
cmd = "find #{paths} -ignore_readdir_race -type f -name '*.rb' 2>/dev/null | ctags -f #{tag_file} -L -"
system cmd
end
end
end
namespace :ctags do
desc 'generate ctags'
task :create do
Kosmos::Ctags.generate_app_tags
Kosmos::Ctags.generate_bundler_tags
end
end

View File

@@ -21,7 +21,7 @@ namespace :ldap do
desc "Add custom attributes to schema"
task add_custom_attributes: :environment do |t, args|
%w[ admin service_enabled nostr_key pgp_key ].each do |name|
%w[ admin service_enabled member_status nostr_key pgp_key ].each do |name|
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "add")
Rake::Task['ldap:modify_ldap_schema'].reenable
end
@@ -29,7 +29,7 @@ namespace :ldap do
desc "Delete custom attributes from schema"
task delete_custom_attributes: :environment do |t, args|
%w[ admin service_enabled nostr_key pgp_key ].each do |name|
%w[ admin service_enabled member_status nostr_key pgp_key ].each do |name|
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "delete")
Rake::Task['ldap:modify_ldap_schema'].reenable
end

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