35 Commits
Author SHA1 Message Date
raucao ebaca5ba65 Merge branch 'chore/upgrade_rails' into live 2025-04-28 15:58:17 +04:00
raucao b6bcfa2ee3 Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2025-04-18 14:54:01 +04:00
raucao 9082ee45d8 Merge branch 'master' into live 2025-01-02 08:32:15 -05:00
raucao 29264aad98 Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-10-16 13:33:35 +02:00
raucao 259b51a95e Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-09-24 21:40:25 +02:00
raucao fb369530e3 Merge branch 'master' into live
continuous-integration/drone/push Build is failing
2024-09-14 17:18:09 +02:00
raucao 5dc10a4d33 Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-09-14 16:46:27 +02:00
raucao 2297c68046 Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-09-14 16:41:01 +02:00
raucao b82ab45c99 Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-09-14 14:57:35 +02:00
raucao d12c63db26 Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-09-10 16:07:24 +02:00
raucao e6a9ef84ce Merge branch 'master' into live
continuous-integration/drone/push Build is failing
2024-08-19 15:13:46 +02:00
raucao b7e91344a0 Merge branch 'master' into live
continuous-integration/drone/push Build is failing
2024-08-19 14:48:35 +02:00
raucao 0f07e32781 Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-08-17 14:49:29 +02:00
raucao 1311b5ed6a Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-08-17 14:46:00 +02:00
raucao 12f82061e8 Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-08-17 14:42:32 +02:00
raucao a07b4369ab Hide njump link for users without pubkey
continuous-integration/drone/push Build is passing
2024-06-23 17:33:34 +02:00
raucao 2605c06807 Merge branch 'feature/admin_pages' into live
continuous-integration/drone/push Build is passing
2024-06-23 17:30:22 +02:00
raucao 1db768fb15 Merge branch 'feature/admin_pages' into live
continuous-integration/drone/push Build is passing
2024-06-23 17:27:03 +02:00
raucao 8a7403df32 Merge branch 'feature/own_relay' into live
continuous-integration/drone/push Build is passing
2024-06-20 15:52:03 +02:00
raucao f0295fef7a Merge branch 'feature/own_relay' into live
continuous-integration/drone/push Build is passing
2024-06-20 15:28:55 +02:00
raucao 090affd304 Merge branch 'feature/own_relay' into live
continuous-integration/drone/push Build is passing
2024-06-20 14:51:20 +02:00
raucao bafddd436b Merge branch 'feature/own_relay' into live
continuous-integration/drone/push Build is passing
2024-06-20 13:59:59 +02:00
raucao 560f193c4b Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-06-20 13:56:11 +02:00
raucao 8aabbad5bb Merge branch 'feature/strfry_zap_receipts' into live 2024-06-20 13:56:00 +02:00
raucao ba8d21eb7a Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-06-09 13:29:57 +02:00
raucao 53df455d53 Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-06-09 13:17:51 +02:00
raucao 9f1af3a9aa Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-06-07 14:13:53 +02:00
raucao 1d09008ce2 Merge branch 'master' into live
continuous-integration/drone/push Build is passing
2024-06-04 17:45:13 +02:00
raucao 57c5317c38 Merge branch 'feature/170-nostr_zaps' into live
continuous-integration/drone/push Build is passing
2024-05-21 18:29:13 +02:00
raucao 41bd920060 Merge branch 'feature/170-nostr_zaps' into live
continuous-integration/drone/push Build is passing
2024-05-21 18:09:09 +02:00
raucao 0815fa6040 Merge branch 'feature/170-nostr_zaps' into live
continuous-integration/drone/push Build is passing
2024-05-19 17:07:58 +02:00
raucao af0e99aa50 Merge branch 'feature/170-nostr_zaps' into live
continuous-integration/drone/push Build is passing
2024-05-19 16:55:10 +02:00
raucao f05eec5255 Merge branch 'feature/170-nostr_zaps' into live
continuous-integration/drone/push Build is passing
2024-05-19 16:48:28 +02:00
raucao 66ca2dc6b0 Merge branch 'feature/173-nostr_ldap' into live
continuous-integration/drone/push Build is passing
2024-05-19 13:44:24 +02:00
raucao 800183e9da Increase sidekiq concurrency in prod
continuous-integration/drone/push Build is passing
2024-05-19 13:44:01 +02:00
501 changed files with 2519 additions and 3983 deletions
+61
View File
@@ -0,0 +1,61 @@
---
kind: pipeline
type: docker
name: CI build
steps:
- name: restore-cache
image: drillster/drone-volume-cache
volumes:
- name: cache
path: /cache
settings:
restore: true
mount:
- ./vendor/cache
when:
branch:
- master
- name: rspec
image: gitea.kosmos.org/kosmos/akkounts-ci:0.9.1
environment:
RAILS_ENV: test
REDIS_URL: redis://redis:6379/0
RS_REDIS_URL: redis://redis:6379/1
commands:
- bundle config unset deployment
- bundle config set cache_all 'true'
- bundle config set cache_path 'vendor/cache'
- bundle config set with 'development test'
- bundle install --jobs=3 --retry=3
- bundle exec rails db:create
- bundle exec rails db:migrate
- yarn install
- rake css:build
- bundle exec rspec
- name: rebuild-cache
image: drillster/drone-volume-cache
volumes:
- name: cache
path: /cache
settings:
rebuild: true
mount:
- ./vendor/cache
when:
branch:
- master
services:
- name: redis
image: redis
volumes:
- name: cache
host:
path: /var/lib/drone/tmp
---
kind: signature
hmac: f9a8cf97f6596625721365f6238f6f298aa5a7a4de10c3fb61c57202ae9d1ee1
...
+1 -22
View File
@@ -1,23 +1,6 @@
# PRIMARY_DOMAIN=kosmos.org # PRIMARY_DOMAIN=kosmos.org
# AKKOUNTS_DOMAIN=accounts.example.com # AKKOUNTS_DOMAIN=accounts.example.com
# Generate this using `rails secret`
# SECRET_KEY_BASE=
# Generate these using `rails db:encryption:init`
# (Optional, needed for LndHub integration)
# ENCRYPTION_PRIMARY_KEY=
# ENCRYPTION_KEY_DERIVATION_SALT=
# The default backend is SQLite
# DB_ADAPTER=postgresql
# PG_HOST=localhost
# PG_PORT=5432
# PG_DATABASE=akkounts
# PG_DATABASE_QUEUE=akkounts_queue
# PG_USERNAME=akkounts
# PG_PASSWORD=
# SMTP_SERVER=smtp.example.com # SMTP_SERVER=smtp.example.com
# SMTP_PORT=587 # SMTP_PORT=587
# SMTP_LOGIN=accounts # SMTP_LOGIN=accounts
@@ -37,12 +20,8 @@
# LDAP_HOST=localhost # LDAP_HOST=localhost
# LDAP_PORT=389 # LDAP_PORT=389
# LDAP_USE_TLS=false
# LDAP_UID_ATTR=cn
# LDAP_BASE="ou=kosmos.org,cn=users,dc=kosmos,dc=org"
# LDAP_ADMIN_USER="cn=Directory Manager"
# LDAP_ADMIN_PASSWORD=passthebutter # LDAP_ADMIN_PASSWORD=passthebutter
# LDAP_SUFFIX="dc=kosmos,dc=org" # LDAP_SUFFIX='dc=kosmos,dc=org'
# REDIS_URL='redis://localhost:6379/1' # REDIS_URL='redis://localhost:6379/1'
+1 -5
View File
@@ -1,9 +1,6 @@
PRIMARY_DOMAIN=kosmos.org PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.kosmos.org AKKOUNTS_DOMAIN=accounts.kosmos.org
ENCRYPTION_PRIMARY_KEY=YhNLBgCFMAzw5dV3gISxnGrhNDMQwRdn
ENCRYPTION_KEY_DERIVATION_SALT=h28g16MRZ1sghF2jTCos1DiLZXUswinR
REDIS_URL='redis://localhost:6379/0' REDIS_URL='redis://localhost:6379/0'
BTCPAY_PUBLIC_URL='https://btcpay.example.com' BTCPAY_PUBLIC_URL='https://btcpay.example.com'
@@ -24,8 +21,7 @@ LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de55648
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea' NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf' NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
RS_REDIS_URL='redis://localhost:6379/1'
RS_STORAGE_URL='https://storage.kosmos.org' RS_STORAGE_URL='https://storage.kosmos.org'
RS_AKKOUNTS_DOMAIN=localhost RS_REDIS_URL='redis://localhost:6379/1'
WEBHOOKS_ALLOWED_IPS='10.1.1.23' WEBHOOKS_ALLOWED_IPS='10.1.1.23'
-59
View File
@@ -1,59 +0,0 @@
name: CI
on:
push:
branches: [master]
pull_request:
jobs:
test:
name: Test
runs-on: ubuntu-latest
container: gitea.kosmos.org/kosmos/akkounts-ci:0.11.1
services:
redis:
image: redis
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
RAILS_ENV: test
REDIS_URL: redis://redis:6379/0
RS_REDIS_URL: redis://redis:6379/1
steps:
- uses: actions/checkout@v4
- name: Restore bundle cache
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-ruby-3.3.12-${{ hashFiles('Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-ruby-3.3.12-
- name: Install Ruby dependencies
run: |
bundle config unset deployment
bundle config set cache_all 'true'
bundle config set cache_path 'vendor/cache'
bundle config set path 'vendor/bundle'
bundle config set with 'development test'
bundle install --jobs=3 --retry=3
- name: Prepare database
run: |
bundle exec rails db:create
bundle exec rails db:migrate
- name: Install JS dependencies
run: bun install
- name: Build CSS
run: |
bundle exec rake css:build
test -s app/assets/builds/application.css
- name: Run tests
run: bundle exec rspec
-2
View File
@@ -9,5 +9,3 @@ jobs:
steps: steps:
- name: Release Drafter - name: Release Drafter
uses: https://github.com/raucao/gitea-release-drafter@dev uses: https://github.com/raucao/gitea-release-drafter@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3
View File
@@ -34,6 +34,9 @@
/public/packs /public/packs
/public/packs-test /public/packs-test
/node_modules /node_modules
/yarn-error.log
yarn-debug.log*
.yarn-integrity
# Ignore local dotenv config file # Ignore local dotenv config file
.env .env
+1 -1
View File
@@ -1 +1 @@
3.3.12 3.3.0
-73
View File
@@ -1,73 +0,0 @@
# AGENTS.md
Akkounts is a Rails 8 monolith for managing Kosmos/LDAP user accounts.
It authenticates against an LDAP directory via Devise and integrates with
ejabberd, Discourse, Mastodon, remoteStorage, Nostr, LNDHub, and BTCPay.
## Development environment
Development runs in Docker Compose — run all commands against the `web` container.
Start services: `docker compose up` (web, ldap, redis, minio, liquor-cabinet, strfry).
The `web` service runs `bin/dev` (foreman: Puma + Tailwind CSS watcher) and embeds
Solid Queue workers (`SOLID_QUEUE_IN_PUMA=true`).
First-time LDAP setup (after creating the 389ds backend once):
`docker compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be_name="dev"`
then `docker compose run web bin/rails ldap:setup`.
## Common commands (prefix with `docker compose exec web`)
- `bin/rspec` — run the test suite (CI runs `bundle exec rspec`)
- `bin/rails db:prepare` — create/migrate DB (also `db:setup`, `db:migrate`)
- `bin/rails ldap:setup` — reset LDAP dir and seed dev entries
- `bin/rails css:build` — build Tailwind CSS (auto-watched in dev)
- `bin/rails lndhub:generate_wallets`, `invitations:generate_for_all_users[N]`
- `bin/jobs` — Solid Queue CLI
Tests use the test env explicitly: `docker compose exec -e RAILS_ENV=test web bin/rspec`.
There is no enforced linter — a `bin/rubocop` binstub exists but no `.rubocop.yml`
(rubocop is a transitive gem, not in the Gemfile); CI does not lint.
## Stack & conventions
- **Assets/views:** ERB + ViewComponent (`app/components/`), Tailwind built via Bun +
importmap (`config/importmap.rb`); pin JS deps with `bin/importmap pin <pkg> --download`.
- **Auth:** Devise + `devise_ldap_authenticatable`; `authentication_keys = [:cn]`,
custom SSHA512 password builder, admin bind. Admin-only mounts (`/jobs` MissionControl,
`/flipper` Flipper UI) are gated by `authenticate :user, ->(u) { u.is_admin? }`.
- **Services:** `app/services/` uses `ApplicationService.call(**args)` with namespaced
`*ManagerService` bases (e.g. `UserManagerService`) and verb-named operations
(`UserManager::CreateAccount`, `NostrManager::PublishEvent`, `BtcpayManager::*`).
- **Models:** `User` is central; `Setting` uses rails-settings-cached with one concern per
service in `app/models/concerns/settings/`. LNDHub models connect to a separate DB
(`LndhubBase` `establish_connection :lndhub`). LDAP is accessed via `LdapService`/
`LdapManagerService`, not ActiveRecord.
- **Jobs:** `app/jobs/` inherit `ApplicationJob < ActiveJob::Base`; backend is **Solid Queue**
(`config.active_job.queue_adapter = :solid_queue`, separate `queue` DB in prod).
`config/recurring.yml` has no active scheduled jobs. Ignore `config/sidekiq.yml` — it's
stale (sidekiq is not in the Gemfile; the README mention is outdated).
- **Config:** env-var driven (`.env.example`/`.env.development`/`.env.test`); `SERVICES`
constant is loaded from `config/services.yml` in `config/initializers/service_details.rb`.
Custom LDAP schema attributes are applied from `schemas/ldap/*.ldif` by `lib/tasks/ldap.rake`.
## Testing conventions (`spec/`)
`spec/rails_helper.rb` includes FactoryBot, Devise `ControllerHelpers` (controller specs),
Warden `Test::Helpers`, DatabaseCleaner (transactional fixtures **off**), ViewComponent
`TestHelpers` + Capybara matchers, and `ActiveJob::TestHelper` for `type: :job`.
- **Feature specs** (`spec/features/`, `type: :feature`): Capybara **rack-test** driver (no JS).
Log in with Warden: `login_as user, scope: :user`; stub admin with
`allow(Devise::LDAP::Adapter).to receive(:get_ldap_param).with(user.cn, :admin).and_return(["true"])`.
- **Services** (`spec/services/`) are tagged `type: :model`; exercise `#call`/`service.send(:private)`
and assert on DB rows / `enqueued_jobs`.
- **Request/component/mailer/job/model/helper** specs in matching `spec/` subdirs.
Fixtures live in `spec/fixtures/`; factories in `spec/factories/`.
## Notes & gotchas
- Generators (`config/application.rb`): ERB templates, RSpec, no stylesheets, factory_bot
with `_factory` suffix dir `spec/factories`.
- `gitno/` and `extras/strfry/` are scratch/infra (Deno Nostr relay policies) — not app code; ignore.
- Don't add code comments unless asked (matches repo style); `frozen_string_literal` is inconsistent.
- Default dev login: username `admin` / password `admin is admin`.
+7 -6
View File
@@ -1,21 +1,22 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM ruby:3.3.12 FROM ruby:3.3.4
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl unzip \ RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
ldap-utils tini libvips ldap-utils tini libvips
RUN curl -fsSL https://bun.sh/install | bash -s -- bun-v1.3.14 RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
ENV PATH="/root/.bun/bin:$PATH" RUN apt-get update && apt-get install -y nodejs
WORKDIR /akkounts WORKDIR /akkounts
COPY ["Gemfile", "Gemfile.lock", "package.json", "bun.lock", "./"] COPY ["Gemfile", "Gemfile.lock", "package.json", "./"]
RUN bundle install RUN bundle install
RUN gem install foreman RUN gem install foreman
RUN bun install RUN npm install -g yarn
RUN yarn install
ENTRYPOINT ["/usr/bin/tini", "--"] ENTRYPOINT ["/usr/bin/tini", "--"]
EXPOSE 3000 EXPOSE 3000
+15 -13
View File
@@ -2,13 +2,13 @@ source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" } git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 8.1' gem 'rails', '~> 7.2'
# Use Puma as the app server # Use Puma as the app server
gem 'puma', '~> 8.0' gem 'puma', '~> 4.1'
# View components # View components
gem "view_component" gem "view_component"
# Asset bundler # Separate dependency since Rails 7.0
gem 'propshaft' gem 'sprockets-rails'
# Allows custom JS build tasks to integrate with the asset pipeline # Allows custom JS build tasks to integrate with the asset pipeline
gem 'cssbundling-rails' gem 'cssbundling-rails'
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
@@ -18,21 +18,25 @@ gem "turbo-rails"
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails" gem "stimulus-rails"
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.15' gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password # Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1' gem 'bcrypt', '~> 3.1'
# Configuration # Configuration
gem 'dotenv-rails' gem 'dotenv-rails'
# Security
gem 'lockbox'
# Authentication # Authentication
gem 'warden' gem 'warden'
gem 'devise', '~> 5.0' gem 'devise', '~> 4.9.0'
gem 'devise_ldap_authenticatable' gem 'devise_ldap_authenticatable'
gem 'net-ldap' gem 'net-ldap'
# Utilities # Utilities
gem 'aasm'
gem "image_processing", "~> 1.12.2" gem "image_processing", "~> 1.12.2"
gem "rqrcode", "~> 2.0" gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3' gem 'rails-settings-cached', '~> 2.8.3'
@@ -42,7 +46,6 @@ gem 'flipper-active_record'
gem 'flipper-ui' gem 'flipper-ui'
gem 'gpgme', '~> 2.0.24' gem 'gpgme', '~> 2.0.24'
gem 'zbase32', '~> 0.1.1' gem 'zbase32', '~> 0.1.1'
gem 'kramdown'
# HTTP requests # HTTP requests
gem 'faraday' gem 'faraday'
@@ -50,8 +53,8 @@ gem 'down'
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
# Background/scheduled jobs # Background/scheduled jobs
gem 'solid_queue' gem 'sidekiq', '< 7'
gem "mission_control-jobs" gem 'sidekiq-scheduler'
# Monitoring # Monitoring
gem "sentry-ruby" gem "sentry-ruby"
@@ -62,18 +65,17 @@ gem 'discourse_api'
gem "lnurl" gem "lnurl"
gem 'manifique', '~> 1.1.0' gem 'manifique', '~> 1.1.0'
gem 'nostr', '~> 0.6.0' gem 'nostr', '~> 0.6.0'
gem "redis", "~> 5.4"
group :development, :test do group :development, :test do
# Use sqlite3 as the database for Active Record # Use sqlite3 as the database for Active Record
gem 'sqlite3', '>= 2.1' gem 'sqlite3', '~> 1.7.2'
gem 'rspec-rails' gem 'rspec-rails'
gem 'rails-controller-testing' gem 'rails-controller-testing'
end end
group :development do group :development do
# Access an interactive console on exception pages or by calling 'console' anywhere in the code. # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
gem 'web-console', '~> 4.3' gem 'web-console', '~> 4.2'
gem 'listen', '~> 3.2' gem 'listen', '~> 3.2'
gem 'letter_opener' gem 'letter_opener'
gem 'letter_opener_web' gem 'letter_opener_web'
+262 -304
View File
@@ -1,110 +1,103 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
aasm (5.5.0) actioncable (7.2.2.1)
concurrent-ruby (~> 1.0) actionpack (= 7.2.2.1)
action_text-trix (2.1.19) activesupport (= 7.2.2.1)
railties
actioncable (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.1.3.1) actionmailbox (7.2.2.1)
actionpack (= 8.1.3.1) actionpack (= 7.2.2.1)
activejob (= 8.1.3.1) activejob (= 7.2.2.1)
activerecord (= 8.1.3.1) activerecord (= 7.2.2.1)
activestorage (= 8.1.3.1) activestorage (= 7.2.2.1)
activesupport (= 8.1.3.1) activesupport (= 7.2.2.1)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.1.3.1) actionmailer (7.2.2.1)
actionpack (= 8.1.3.1) actionpack (= 7.2.2.1)
actionview (= 8.1.3.1) actionview (= 7.2.2.1)
activejob (= 8.1.3.1) activejob (= 7.2.2.1)
activesupport (= 8.1.3.1) activesupport (= 7.2.2.1)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.1.3.1) actionpack (7.2.2.1)
actionview (= 8.1.3.1) actionview (= 7.2.2.1)
activesupport (= 8.1.3.1) activesupport (= 7.2.2.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) racc
rack (>= 2.2.4, < 3.2)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.1.3.1) actiontext (7.2.2.1)
action_text-trix (~> 2.1.15) actionpack (= 7.2.2.1)
actionpack (= 8.1.3.1) activerecord (= 7.2.2.1)
activerecord (= 8.1.3.1) activestorage (= 7.2.2.1)
activestorage (= 8.1.3.1) activesupport (= 7.2.2.1)
activesupport (= 8.1.3.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.1.3.1) actionview (7.2.2.1)
activesupport (= 8.1.3.1) activesupport (= 7.2.2.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (8.1.3.1) activejob (7.2.2.1)
activesupport (= 8.1.3.1) activesupport (= 7.2.2.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.1.3.1) activemodel (7.2.2.1)
activesupport (= 8.1.3.1) activesupport (= 7.2.2.1)
activerecord (8.1.3.1) activerecord (7.2.2.1)
activemodel (= 8.1.3.1) activemodel (= 7.2.2.1)
activesupport (= 8.1.3.1) activesupport (= 7.2.2.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.1.3.1) activestorage (7.2.2.1)
actionpack (= 8.1.3.1) actionpack (= 7.2.2.1)
activejob (= 8.1.3.1) activejob (= 7.2.2.1)
activerecord (= 8.1.3.1) activerecord (= 7.2.2.1)
activesupport (= 8.1.3.1) activesupport (= 7.2.2.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.1.3.1) activesupport (7.2.2.1)
base64 base64
benchmark (>= 0.3)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5) connection_pool (>= 2.2.5)
drb drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
json
logger (>= 1.4.2) logger (>= 1.4.2)
minitest (>= 5.1) minitest (>= 5.1)
securerandom (>= 0.3) securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1) addressable (2.8.6)
addressable (2.8.7) public_suffix (>= 2.0.2, < 6.0)
public_suffix (>= 2.0.2, < 7.0) ast (2.4.2)
ast (2.4.3) aws-eventstream (1.3.0)
aws-eventstream (1.3.2) aws-partitions (1.886.0)
aws-partitions (1.1092.0) aws-sdk-core (3.191.0)
aws-sdk-core (3.222.2)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.8)
base64
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
logger aws-sdk-kms (1.77.0)
aws-sdk-kms (1.99.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.1)
aws-sigv4 (~> 1.5) aws-sdk-s3 (1.143.0)
aws-sdk-s3 (1.183.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.8)
aws-sigv4 (1.11.0) aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
backport (1.2.0) backport (1.2.0)
base64 (0.3.0) base64 (0.2.0)
bcrypt (3.1.22) bcrypt (3.1.20)
bech32 (1.5.0) bech32 (1.4.2)
thor (>= 1.1.0) thor (>= 1.1.0)
benchmark (0.5.0) benchmark (0.4.0)
bigdecimal (4.1.2) bigdecimal (3.1.9)
bindex (0.8.1) bindex (0.8.1)
bip-schnorr (0.7.0) bip-schnorr (0.7.0)
ecdsa_ext (~> 0.5.0) ecdsa_ext (~> 0.5.0)
@@ -118,16 +111,14 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0) chunky_png (1.4.0)
concurrent-ruby (1.3.8) concurrent-ruby (1.3.5)
connection_pool (3.0.2) connection_pool (2.5.2)
crack (1.0.0) crack (0.4.6)
bigdecimal bigdecimal
rexml rexml
crass (1.0.7) crass (1.0.6)
cssbundling-rails (1.4.3) cssbundling-rails (1.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
database_cleaner (2.1.0) database_cleaner (2.1.0)
database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (>= 2, < 3)
@@ -135,128 +126,121 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.5.1) date (3.4.1)
devise (5.0.4) devise (4.9.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 7.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise_ldap_authenticatable (0.8.7) devise_ldap_authenticatable (0.8.7)
devise (>= 3.4.1) devise (>= 3.4.1)
net-ldap (>= 0.16.0) net-ldap (>= 0.16.0)
diff-lcs (1.6.2) diff-lcs (1.5.1)
discourse_api (2.0.1) discourse_api (2.0.1)
faraday (~> 2.7) faraday (~> 2.7)
faraday-follow_redirects faraday-follow_redirects
faraday-multipart faraday-multipart
rack (>= 1.6) rack (>= 1.6)
dotenv (3.1.8) dotenv (2.8.1)
dotenv-rails (3.1.8) dotenv-rails (2.8.1)
dotenv (= 3.1.8) dotenv (= 2.8.1)
railties (>= 6.1) railties (>= 3.2)
down (5.4.2) down (5.4.1)
addressable (~> 2.8) addressable (~> 2.8)
drb (2.2.3) drb (2.2.1)
e2mmap (0.1.0)
ecdsa (1.2.0) ecdsa (1.2.0)
ecdsa_ext (0.5.1) ecdsa_ext (0.5.1)
ecdsa (~> 1.2.0) ecdsa (~> 1.2.0)
erb (6.0.7)
erubi (1.13.1) erubi (1.13.1)
et-orbi (1.4.1) et-orbi (1.2.7)
tzinfo tzinfo
event_emitter (0.2.6) event_emitter (0.2.6)
eventmachine (1.2.7) eventmachine (1.2.7)
factory_bot (6.5.1) factory_bot (6.4.6)
activesupport (>= 6.1.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.4) factory_bot_rails (6.4.3)
factory_bot (~> 6.5) factory_bot (~> 6.4)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.5.1) faker (3.2.3)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.9.2) faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2) faraday-net_http (>= 2.0, < 3.2)
faraday-follow_redirects (0.3.0) faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3) faraday (>= 1, < 3)
faraday-multipart (1.1.0) faraday-multipart (1.0.4)
multipart-post (~> 2.0) multipart-post (~> 2)
faraday-net_http (3.1.1) faraday-net_http (3.1.0)
net-http net-http
faye-websocket (0.11.3) faye-websocket (0.11.3)
eventmachine (>= 0.12.0) eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1) websocket-driver (>= 0.5.1)
ffi (1.17.2) ffi (1.16.3)
ffi (1.17.2-arm64-darwin) flipper (1.2.2)
ffi (1.17.2-x86_64-linux-gnu)
flipper (1.3.4)
concurrent-ruby (< 2) concurrent-ruby (< 2)
flipper-active_record (1.3.4) flipper-active_record (1.2.2)
activerecord (>= 4.2, < 9) activerecord (>= 4.2, < 8)
flipper (~> 1.3.4) flipper (~> 1.2.2)
flipper-ui (1.3.4) flipper-ui (1.2.2)
erubi (>= 1.0.0, < 2.0.0) erubi (>= 1.0.0, < 2.0.0)
flipper (~> 1.3.4) flipper (~> 1.2.2)
rack (>= 1.4, < 4) rack (>= 1.4, < 4)
rack-protection (>= 1.5.3, < 5.0.0) rack-protection (>= 1.5.3, <= 4.0.0)
rack-session (>= 1.0.2, < 3.0.0) sanitize (< 7)
sanitize (< 8) fugit (1.9.0)
fugit (1.13.0) et-orbi (~> 1, >= 1.2.7)
et-orbi (~> 1.4)
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.4.0) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
gpgme (2.0.24) gpgme (2.0.24)
mini_portile2 (~> 2.7) mini_portile2 (~> 2.7)
hashdiff (1.1.2) hashdiff (1.1.0)
i18n (1.15.2) i18n (1.14.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
importmap-rails (2.2.3) importmap-rails (2.0.1)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activesupport (>= 6.0.0) activesupport (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
io-console (0.9.2) io-console (0.8.0)
irb (1.18.0) irb (1.15.2)
pp (>= 0.6.0) pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jaro_winkler (1.6.0) jaro_winkler (1.5.6)
jbuilder (2.15.1) jbuilder (2.11.5)
actionview (>= 7.0.0) actionview (>= 5.0.0)
activesupport (>= 7.0.0) activesupport (>= 5.0.0)
jmespath (1.6.2) jmespath (1.6.2)
json (2.21.2) json (2.7.1)
kramdown (2.5.1) kramdown (2.4.0)
rexml (>= 3.3.9) rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
language_server-protocol (3.17.0.4) language_server-protocol (3.17.0.3)
launchy (3.1.1) launchy (2.5.2)
addressable (~> 2.8) addressable (~> 2.8)
childprocess (~> 5.0) letter_opener (1.8.1)
logger (~> 1.6) launchy (>= 2.2, < 3)
letter_opener (1.10.0) letter_opener_web (2.0.0)
launchy (>= 2.2, < 4) actionmailer (>= 5.2)
letter_opener_web (3.0.0) letter_opener (~> 1.7)
actionmailer (>= 6.1) railties (>= 5.2)
letter_opener (~> 1.9)
railties (>= 6.1)
rexml rexml
lint_roller (1.1.0) listen (3.8.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
lnurl (1.1.1) lnurl (1.1.0)
bech32 (~> 1.1) bech32 (~> 1.1)
lockbox (2.0.1)
logger (1.7.0) logger (1.7.0)
loofah (2.25.2) loofah (2.24.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.9.1) mail (2.8.1)
logger
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap net-imap
net-pop net-pop
@@ -265,28 +249,17 @@ GEM
faraday (~> 2.9.0) faraday (~> 2.9.0)
faraday-follow_redirects (= 0.3.0) faraday-follow_redirects (= 0.3.0)
nokogiri (~> 1.16.0) nokogiri (~> 1.16.0)
marcel (1.2.1) marcel (1.0.4)
matrix (0.4.2) matrix (0.4.2)
mini_magick (4.13.2) method_source (1.0.0)
mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.9) mini_portile2 (2.8.8)
minitest (6.0.6) minitest (5.25.5)
drb (~> 2.0) multipart-post (2.3.0)
prism (~> 1.5) net-http (0.4.1)
mission_control-jobs (1.1.0)
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 uri
net-imap (0.6.6) net-imap (0.5.7)
date date
net-protocol net-protocol
net-ldap (0.19.0) net-ldap (0.19.0)
@@ -296,7 +269,7 @@ GEM
timeout timeout
net-smtp (0.5.1) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.5) nio4r (2.7.4)
nokogiri (1.16.8) nokogiri (1.16.8)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
@@ -311,32 +284,25 @@ GEM
event_emitter (~> 0.2) event_emitter (~> 0.2)
faye-websocket (~> 0.11) faye-websocket (~> 0.11)
json (~> 2.6) json (~> 2.6)
observer (0.1.2)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostruct (0.6.1) pagy (6.4.3)
pagy (6.5.0) parallel (1.24.0)
parallel (1.27.0) parser (3.3.0.5)
parser (3.3.8.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.9) pg (1.5.4)
pp (0.6.4) pp (0.6.2)
prettyprint prettyprint
prettyprint (0.2.0) prettyprint (0.2.0)
prism (1.9.0) psych (5.2.3)
propshaft (1.3.2)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
psych (5.4.0)
date date
stringio stringio
public_suffix (6.0.1) public_suffix (5.0.4)
puma (8.0.2) puma (4.3.12)
nio4r (~> 2.0) nio4r (~> 2.0)
raabro (1.5.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (2.2.23) rack (2.2.13)
rack-protection (3.2.0) rack-protection (3.2.0)
base64 (>= 0.1.0) base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4) rack (~> 2.2, >= 2.2.4)
@@ -347,189 +313,183 @@ GEM
rackup (1.0.1) rackup (1.0.1)
rack (< 3) rack (< 3)
webrick webrick
rails (8.1.3.1) rails (7.2.2.1)
actioncable (= 8.1.3.1) actioncable (= 7.2.2.1)
actionmailbox (= 8.1.3.1) actionmailbox (= 7.2.2.1)
actionmailer (= 8.1.3.1) actionmailer (= 7.2.2.1)
actionpack (= 8.1.3.1) actionpack (= 7.2.2.1)
actiontext (= 8.1.3.1) actiontext (= 7.2.2.1)
actionview (= 8.1.3.1) actionview (= 7.2.2.1)
activejob (= 8.1.3.1) activejob (= 7.2.2.1)
activemodel (= 8.1.3.1) activemodel (= 7.2.2.1)
activerecord (= 8.1.3.1) activerecord (= 7.2.2.1)
activestorage (= 8.1.3.1) activestorage (= 7.2.2.1)
activesupport (= 8.1.3.1) activesupport (= 7.2.2.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.1.3.1) railties (= 7.2.2.1)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.3.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.7.1) rails-html-sanitizer (1.6.2)
loofah (~> 2.25, >= 2.25.2) loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) nokogiri (>= 1.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) rails-settings-cached (2.8.3)
activerecord (>= 5.0.0) activerecord (>= 5.0.0)
railties (>= 5.0.0) railties (>= 5.0.0)
railties (8.1.3.1) railties (7.2.2.1)
actionpack (= 8.1.3.1) actionpack (= 7.2.2.1)
activesupport (= 8.1.3.1) activesupport (= 7.2.2.1)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.4.2) rake (13.2.1)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.11.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
rbs (3.9.2) rbs (2.8.4)
logger rdoc (6.13.1)
rdoc (7.2.0)
erb
psych (>= 4.0.0) psych (>= 4.0.0)
tsort redis (4.8.1)
redis (5.4.0) regexp_parser (2.9.0)
redis-client (>= 0.22.0) reline (0.6.1)
redis-client (0.24.0)
connection_pool
regexp_parser (2.10.0)
reline (0.7.0)
io-console (~> 0.5) io-console (~> 0.5)
responders (3.2.0) responders (3.1.1)
actionpack (>= 7.0) actionpack (>= 5.2)
railties (>= 7.0) railties (>= 5.2)
reverse_markdown (3.0.0) reverse_markdown (2.1.1)
nokogiri nokogiri
rexml (3.4.1) rexml (3.2.6)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.13.6) rspec-core (3.12.2)
rspec-support (~> 3.13.0) rspec-support (~> 3.12.0)
rspec-expectations (3.13.5) rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.12.0)
rspec-mocks (3.13.8) rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.12.0)
rspec-rails (8.0.4) rspec-rails (6.1.1)
actionpack (>= 7.2) actionpack (>= 6.1)
activesupport (>= 7.2) activesupport (>= 6.1)
railties (>= 7.2) railties (>= 6.1)
rspec-core (>= 3.13.0, < 5.0.0) rspec-core (~> 3.12)
rspec-expectations (>= 3.13.0, < 5.0.0) rspec-expectations (~> 3.12)
rspec-mocks (>= 3.13.0, < 5.0.0) rspec-mocks (~> 3.12)
rspec-support (>= 3.13.0, < 5.0.0) rspec-support (~> 3.12)
rspec-support (3.13.7) rspec-support (3.12.1)
rubocop (1.75.3) rubocop (1.60.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (>= 3.17.0)
lint_roller (~> 1.1.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 1.8, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.30.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.44.1) rubocop-ast (1.30.0)
parser (>= 3.3.7.2) parser (>= 3.2.1.0)
prism (~> 1.4)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.3) ruby-vips (2.2.0)
ffi (~> 1.12) ffi (~> 1.12)
logger rufus-scheduler (3.9.1)
sanitize (7.0.0) fugit (~> 1.1, >= 1.1.6)
sanitize (6.1.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.16.8) nokogiri (>= 1.12.0)
securerandom (0.4.1) securerandom (0.4.1)
sentry-rails (5.23.0) sentry-rails (5.16.1)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.23.0) sentry-ruby (~> 5.16.1)
sentry-ruby (5.23.0) sentry-ruby (5.16.1)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
solargraph (0.54.2) sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
sidekiq-scheduler (5.0.3)
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0)
solargraph (0.50.0)
backport (~> 1.2) backport (~> 1.2)
benchmark (~> 0.4) benchmark
bundler (~> 2.0) bundler (~> 2.0)
diff-lcs (~> 1.4) diff-lcs (~> 1.4)
jaro_winkler (~> 1.6) e2mmap
jaro_winkler (~> 1.5)
kramdown (~> 2.3) kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1) kramdown-parser-gfm (~> 1.1)
logger (~> 1.6)
observer (~> 0.1)
ostruct (~> 0.6)
parser (~> 3.0) parser (~> 3.0)
rbs (~> 3.3) rbs (~> 2.0)
reverse_markdown (~> 3.0) reverse_markdown (~> 2.0)
rubocop (~> 1.38) rubocop (~> 1.38)
thor (~> 1.0) thor (~> 1.0)
tilt (~> 2.0) tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24) yard (~> 0.9, >= 0.9.24)
yard-solargraph (~> 0.1) sprockets (4.2.1)
solid_queue (1.6.0) concurrent-ruby (~> 1.0)
activejob (>= 7.1) rack (>= 2.2.4, < 4)
activerecord (>= 7.1) sprockets-rails (3.4.2)
concurrent-ruby (>= 1.3.1) actionpack (>= 5.2)
fugit (~> 1.11) activesupport (>= 5.2)
railties (>= 7.1) sprockets (>= 3.0.0)
thor (>= 1.3.1) sqlite3 (1.7.2)
sqlite3 (2.6.0)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (2.6.0-arm64-darwin) sqlite3 (1.7.2-arm64-darwin)
sqlite3 (2.6.0-x86_64-linux-gnu) sqlite3 (1.7.2-x86_64-linux)
stimulus-rails (1.3.4) stimulus-rails (1.3.3)
railties (>= 6.0.0)
stringio (3.1.7)
thor (1.3.2)
tilt (2.3.0)
timeout (0.4.3)
turbo-rails (1.5.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
stringio (3.2.0)
thor (1.5.0)
tilt (2.6.0)
timeout (0.6.1)
tsort (0.2.0)
turbo-rails (2.0.23)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4) unicode-display_width (2.5.0)
unicode-emoji (~> 4.0, >= 4.0.4) uri (0.13.0)
unicode-emoji (4.0.4)
uri (1.1.1)
useragent (0.16.11) useragent (0.16.11)
view_component (4.12.0) view_component (3.10.0)
actionview (>= 7.1.0) activesupport (>= 5.2.0, < 8.0)
activesupport (>= 7.1.0) concurrent-ruby (~> 1.0)
concurrent-ruby (~> 1) method_source (~> 1.0)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.3.0) web-console (4.2.1)
actionview (>= 8.0.0) actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 8.0.0) railties (>= 6.0.0)
webmock (3.25.1) webmock (3.19.1)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.2) webrick (1.9.1)
websocket-driver (0.8.2) websocket-driver (0.7.7)
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yard (0.9.37) yard (0.9.34)
yard-solargraph (0.1.0)
yard (~> 0.9)
zbase32 (0.1.1) zbase32 (0.1.1)
zeitwerk (2.8.3) zeitwerk (2.7.2)
PLATFORMS PLATFORMS
arm64-darwin-22 arm64-darwin-22
@@ -537,13 +497,12 @@ PLATFORMS
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
aasm
aws-sdk-s3 aws-sdk-s3
bcrypt (~> 3.1) bcrypt (~> 3.1)
capybara capybara
cssbundling-rails cssbundling-rails
database_cleaner database_cleaner
devise (~> 5.0) devise (~> 4.9.0)
devise_ldap_authenticatable devise_ldap_authenticatable
discourse_api discourse_api
dotenv-rails dotenv-rails
@@ -557,37 +516,36 @@ DEPENDENCIES
gpgme (~> 2.0.24) gpgme (~> 2.0.24)
image_processing (~> 1.12.2) image_processing (~> 1.12.2)
importmap-rails importmap-rails
jbuilder (~> 2.15) jbuilder (~> 2.7)
kramdown
letter_opener letter_opener
letter_opener_web letter_opener_web
listen (~> 3.2) listen (~> 3.2)
lnurl lnurl
lockbox
manifique (~> 1.1.0) manifique (~> 1.1.0)
mission_control-jobs
net-ldap net-ldap
nostr (~> 0.6.0) nostr (~> 0.6.0)
pagy (~> 6.0, >= 6.0.2) pagy (~> 6.0, >= 6.0.2)
pg (~> 1.5) pg (~> 1.5)
propshaft puma (~> 4.1)
puma (~> 8.0) rails (~> 7.2)
rails (~> 8.1)
rails-controller-testing rails-controller-testing
rails-settings-cached (~> 2.8.3) rails-settings-cached (~> 2.8.3)
redis (~> 5.4)
rqrcode (~> 2.0) rqrcode (~> 2.0)
rspec-rails rspec-rails
sentry-rails sentry-rails
sentry-ruby sentry-ruby
sidekiq (< 7)
sidekiq-scheduler
solargraph solargraph
solid_queue sprockets-rails
sqlite3 (>= 2.1) sqlite3 (~> 1.7.2)
stimulus-rails stimulus-rails
turbo-rails turbo-rails
tzinfo-data tzinfo-data
view_component view_component
warden warden
web-console (~> 4.3) web-console (~> 4.2)
webmock webmock
zbase32 (~> 0.1.1) zbase32 (~> 0.1.1)
+1 -1
View File
@@ -1,2 +1,2 @@
web: bin/rails server -b 0.0.0.0 -p 3000 web: bin/rails server -b 0.0.0.0 -p 3000
css: bun run build:css:tailwind --watch css: yarn build:css --watch
+3 -4
View File
@@ -1,4 +1,4 @@
[![Build Status](https://gitea.kosmos.org/kosmos/akkounts/actions/workflows/ci.yml/badge.svg)](https://gitea.kosmos.org/kosmos/akkounts/actions) [![Build Status](https://drone.kosmos.org/api/badges/kosmos/akkounts/status.svg)](https://drone.kosmos.org/kosmos/akkounts)
# Akkounts # Akkounts
@@ -36,7 +36,7 @@ run web`._
Installing dependencies: Installing dependencies:
bundle install bundle install
bun install yarn install
Migrating the local database (after schema changes): Migrating the local database (after schema changes):
@@ -57,7 +57,7 @@ Running the test suite:
Running the test suite with Docker Compose requires overriding the Rails Running the test suite with Docker Compose requires overriding the Rails
environment: environment:
docker-compose exec -e "RAILS_ENV=test" web rspec docker-compose run -e "RAILS_ENV=test" web rspec
### Docker Compose ### Docker Compose
@@ -128,7 +128,6 @@ command:
### Front-end ### Front-end
* [Icons](https://feathericons.com)
* [Tailwind CSS](https://tailwindcss.com/) * [Tailwind CSS](https://tailwindcss.com/)
* [Sass](https://sass-lang.com/documentation) * [Sass](https://sass-lang.com/documentation)
* [Stimulus](https://stimulus.hotwired.dev/handbook/) * [Stimulus](https://stimulus.hotwired.dev/handbook/)
+4
View File
@@ -0,0 +1,4 @@
//= link_tree ../images
//= link_tree ../../javascript .js
//= link_tree ../builds
//= link_tree ../../../vendor/javascript .js
@@ -7,7 +7,7 @@
@import "components/buttons"; @import "components/buttons";
@import "components/dashboard_services"; @import "components/dashboard_services";
@import "components/forms"; @import "components/forms";
@import "components/links";
@import "components/notifications"; @import "components/notifications";
@import "components/pagination"; @import "components/pagination";
@import "components/rich_text";
@import "components/tables"; @import "components/tables";
@@ -6,7 +6,6 @@
body { body {
@apply leading-none bg-cover bg-fixed; @apply leading-none bg-cover bg-fixed;
background-image: linear-gradient(35deg, rgba(255,0,255,0.2) 0, rgba(13,79,153,0.8) 100%), url('/img/bg-1.jpg'); background-image: linear-gradient(35deg, rgba(255,0,255,0.2) 0, rgba(13,79,153,0.8) 100%), url('/img/bg-1.jpg');
color: black;
} }
body#admin { body#admin {
@@ -33,10 +32,6 @@
@apply pt-8 sm:pt-12; @apply pt-8 sm:pt-12;
} }
main section h3:not(:first-child) {
@apply mt-8;
}
main section:first-of-type { main section:first-of-type {
@apply pt-0; @apply pt-0;
} }
@@ -60,11 +55,4 @@
main ul li { main ul li {
@apply leading-6; @apply leading-6;
} }
main a:not(nav > *) {
@apply text-blue-600;
&:hover { @apply underline; }
&:visited { @apply text-indigo-600; }
&:active { @apply text-red-600; }
}
} }
+3 -16
View File
@@ -1,15 +1,5 @@
@layer components { @layer components {
.btn-text-dark { @apply text-black; }
.btn-text-dark:hover { @apply text-black no-underline; }
.btn-text-dark:visited { @apply text-black; }
.btn-text-dark:active { @apply text-black; }
.btn-text-light { @apply text-white; }
.btn-text-light:hover { @apply text-white no-underline; }
.btn-text-light:visited { @apply text-white; }
.btn-text-light:active { @apply text-white; }
.btn { .btn {
@apply btn-text-dark;
@apply inline-block font-semibold rounded-md leading-none cursor-pointer text-center @apply inline-block font-semibold rounded-md leading-none cursor-pointer text-center
transition-colors duration-75 focus:outline-none focus:ring-4; transition-colors duration-75 focus:outline-none focus:ring-4;
} }
@@ -38,20 +28,17 @@
} }
.btn-blue { .btn-blue {
@apply btn-text-light; @apply bg-blue-500 hover:bg-blue-600 text-white
@apply bg-blue-500 hover:bg-blue-600
focus:ring-blue-400 focus:ring-opacity-75; focus:ring-blue-400 focus:ring-opacity-75;
} }
.btn-emerald { .btn-emerald {
@apply btn-text-light; @apply bg-emerald-500 hover:bg-emerald-600 text-white
@apply bg-emerald-500 hover:bg-emerald-600
focus:ring-emerald-400 focus:ring-opacity-75; focus:ring-emerald-400 focus:ring-opacity-75;
} }
.btn-red { .btn-red {
@apply btn-text-light; @apply bg-red-600 hover:bg-red-700 text-white
@apply bg-red-600 hover:bg-red-700
focus:ring-red-500 focus:ring-opacity-75; focus:ring-red-500 focus:ring-opacity-75;
} }
@@ -0,0 +1,8 @@
@layer components {
.ks-text-link {
@apply text-blue-600;
&:hover { @apply underline; }
&:visited { @apply text-indigo-600; }
&:active { @apply text-red-600; }
}
}
@@ -34,7 +34,7 @@
.pagy-nav .page a, .page.gap { .pagy-nav .page a, .page.gap {
@apply bg-white border-gray-300 text-gray-500 hover:bg-gray-100 relative @apply bg-white border-gray-300 text-gray-500 hover:bg-gray-100 relative
inline-flex items-center border px-4 py-2 text-sm font-medium inline-flex items-center border px-4 py-2 text-sm font-medium
no-underline focus:z-20; focus:z-20;
} }
.pagy-nav .page.active { .pagy-nav .page.active {
@@ -1,30 +0,0 @@
@layer base {
main {
.rich-text {
ul {
@apply list-disc list-inside;
li {
@apply my-2;
}
}
ol {
@apply list-decimal list-inside;
li {
@apply my-2;
}
}
li > ul,
li > ol {
@apply ps-6 my-1;
}
hr {
@apply my-8;
}
}
}
}
@@ -2,8 +2,6 @@
module AppCatalog module AppCatalog
class WebAppIconComponent < ViewComponent::Base class WebAppIconComponent < ViewComponent::Base
include ApplicationHelper
def initialize(web_app:) def initialize(web_app:)
if web_app&.icon&.attached? if web_app&.icon&.attached?
@image_url = image_url_for(web_app.icon) @image_url = image_url_for(web_app.icon)
@@ -11,5 +9,13 @@ module AppCatalog
@image_url = image_url_for(web_app.apple_touch_icon) @image_url = image_url_for(web_app.apple_touch_icon)
end end
end end
def image_url_for(attachment)
if Setting.s3_enabled?
s3_image_url(attachment)
else
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
end
end
end end
end end
@@ -1,4 +1,4 @@
<%= link_to @href, class: @class, target: @target, data: { <%= link_to @href, class: @class, data: {
'dropdown-target': "menuItem", 'dropdown-target': "menuItem",
'action': "keydown.up->dropdown#previousItem:prevent keydown.down->dropdown#nextItem:prevent" 'action': "keydown.up->dropdown#previousItem:prevent keydown.down->dropdown#nextItem:prevent"
} do %> } do %>
+1 -2
View File
@@ -1,9 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class DropdownLinkComponent < ViewComponent::Base class DropdownLinkComponent < ViewComponent::Base
def initialize(href:, open_in_new_tab: false, separator: false, add_class: nil) def initialize(href:, separator: false, add_class: nil)
@href = href @href = href
@target = open_in_new_tab ? "_blank" : nil
@class = class_str(separator, add_class) @class = class_str(separator, add_class)
end end
@@ -1,30 +0,0 @@
<div class="inline-block text-left" data-controller="modal" data-action="keydown.esc->modal#close">
<button class="btn-md btn-outline text-red-600" data-action="click->modal#open" title="Edit">
<%= content || "Edit" %>
</button>
<%= render ModalComponent.new(show_close_button: false) do %>
<%= form_with model: [:admin, @editable_content],
html: { autocomplete: "off" } do |form| %>
<%= form.hidden_field :redirect_to, value: @redirect_to %>
<p class="mb-2">
<%= form.label :content, @editable_content.key.capitalize, class: 'font-bold' %>
</p>
<% if @editable_content.rich_text %>
<p>
<%= form.textarea :content, class: "md:w-[56rem] md:h-[28rem]" %>
</p>
<p class="text-right">
<%= form.submit "Save", class: "ml-2 btn-md btn-blue" %>
</p>
<% else %>
<p class="">
<%= form.text_field :content, class: "w-80" %>
</p>
<p>
<%= form.submit "Save", class: "btn-md btn-blue w-full" %>
</p>
<% end %>
<% end %>
<% end %>
</div>
@@ -1,6 +0,0 @@
class EditContentButtonComponent < ViewComponent::Base
def initialize(context:, key:, rich_text: false, redirect_to: nil)
@editable_content = EditableContent.find_or_create_by(context:, key:, rich_text:)
@redirect_to = redirect_to
end
end
@@ -1,11 +0,0 @@
<% if @editable_content.has_content? %>
<% if @editable_content.rich_text %>
<div class="rich-text">
<%= helpers.markdown_to_html @editable_content.content %>
</div>
<% else %>
<%= @editable_content.content %>
<% end %>
<% else %>
<%= @default %>
<% end %>
@@ -1,6 +0,0 @@
class EditableContentComponent < ViewComponent::Base
def initialize(context:, key:, rich_text: false, default: nil)
@editable_content = EditableContent.find_or_create_by(context:, key:, rich_text:)
@default = default
end
end
@@ -1,4 +1,4 @@
<main class="w-full max-w-xl mx-auto px-4 sm:px-6 lg:px-8"> <main class="w-full max-w-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
<div class="bg-white rounded-lg shadow px-6 sm:px-12 py-8 sm:py-12"> <div class="bg-white rounded-lg shadow px-6 sm:px-12 py-8 sm:py-12">
<%= content %> <%= content %>
</div> </div>
@@ -1,4 +1,4 @@
<main class="w-full max-w-6xl mx-auto px-4 md:px-6 lg:px-8"> <main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
<div class="md:min-h-[50vh] bg-white rounded-lg shadow px-6 sm:px-12 py-8 sm:py-12"> <div class="md:min-h-[50vh] bg-white rounded-lg shadow px-6 sm:px-12 py-8 sm:py-12">
<%= content %> <%= content %>
</div> </div>
@@ -1,4 +1,4 @@
<main class="w-full max-w-6xl mx-auto px-4 md:px-6 lg:px-8"> <main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
<div class="bg-white rounded-lg shadow"> <div class="bg-white rounded-lg shadow">
<div class="md:min-h-[50vh] divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x"> <div class="md:min-h-[50vh] divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<aside class="py-6 sm:py-8 lg:col-span-3"> <aside class="py-6 sm:py-8 lg:col-span-3">
@@ -1,4 +1,4 @@
<main class="w-full max-w-6xl mx-auto px-4 md:px-6 lg:px-8"> <main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
<div class="md:min-h-[50vh] bg-white rounded-lg shadow"> <div class="md:min-h-[50vh] bg-white rounded-lg shadow">
<div class="px-6 sm:px-12 pt-2 sm:pt-4"> <div class="px-6 sm:px-12 pt-2 sm:pt-4">
<%= render partial: @tabnav_partial %> <%= render partial: @tabnav_partial %>
+1 -2
View File
@@ -12,8 +12,7 @@
</div> </div>
<%= render DropdownComponent.new do %> <%= render DropdownComponent.new do %>
<%= render DropdownLinkComponent.new( <%= render DropdownLinkComponent.new(
href: launch_app_services_storage_rs_auth_url(@auth), href: launch_app_services_storage_rs_auth_url(@auth)
open_in_new_tab: true
) do %> ) do %>
Launch app Launch app
<% end %> <% end %>
+1 -1
View File
@@ -29,7 +29,7 @@ class SidenavLinkComponent < ViewComponent::Base
def class_names_icon(path) def class_names_icon(path)
if @active if @active
"text-teal-600 group-hover:text-teal-600 flex-shrink-0 -ml-1 mr-3 h-6 w-6" "text-teal-500 group-hover:text-teal-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
elsif @disabled elsif @disabled
"text-gray-300 group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6" "text-gray-300 group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
else else
+3 -14
View File
@@ -4,22 +4,11 @@ class Admin::DonationsController < Admin::BaseController
# GET /donations # GET /donations
def index def index
@username = params[:username].presence @pagy, @donations = pagy(Donation.completed.order('paid_at desc'))
pending_scope = Donation.incomplete.joins(:user).order('paid_at desc')
completed_scope = Donation.completed.joins(:user).order('paid_at desc')
if @username
pending_scope = pending_scope.where(users: { cn: @username })
completed_scope = completed_scope.where(users: { cn: @username })
end
@pending_donations = pending_scope
@pagy, @donations = pagy(completed_scope)
@stats = { @stats = {
overall_sats: completed_scope.sum("amount_sats"), overall_sats: @donations.sum("amount_sats"),
donor_count: completed_scope.distinct.count(:user_id) donor_count: Donation.completed.count(:user_id)
} }
end end
@@ -1,45 +0,0 @@
class Admin::EditableContentsController < Admin::BaseController
before_action :set_content, only: [:show, :edit, :update]
before_action :set_current_section, only: [:index, :show, :edit]
def index
@path = params[:path].presence
scope = EditableContent.order(path: :asc)
scope = scope.where(path: @path) if @path
@pagy, @contents = pagy(scope)
end
def show
end
def edit
end
def update
return_to = params[:editable_content][:redirect_to].presence
if @editable_content.update(content_params)
if return_to
redirect_to return_to
else
render status: :ok
end
else
render :edit, status: :unprocessable_entity
end
end
private
def set_content
@editable_content = EditableContent.find(params[:id])
end
def content_params
params.require(:editable_content).permit(:path, :key, :lang, :content, :rich_text)
end
def set_current_section
@current_section = :content
end
end
@@ -1,28 +1,12 @@
class Admin::InvitationsController < Admin::BaseController class Admin::InvitationsController < Admin::BaseController
before_action :set_current_section
def index def index
@username = params[:username].presence @current_section = :invitations
accepted_scope = Invitation.used.order('used_at desc') @pagy, @invitations_used = pagy(Invitation.used.order('used_at desc'))
unused_scope = Invitation.unused
if @username
accepted_scope = accepted_scope.joins(:user).where(users: { cn: @username })
unused_scope = unused_scope.joins(:user).where(users: { cn: @username })
end
@pagy, @invitations_used = pagy(accepted_scope)
@stats = { @stats = {
available: unused_scope.count, available: Invitation.unused.count,
accepted: accepted_scope.count, accepted: @invitations_used.length,
users_with_referrals: accepted_scope.distinct.count(:user_id) users_with_referrals: Invitation.used.distinct.count(:user_id)
} }
end end
private
def set_current_section
@current_section = :invitations
end
end end
@@ -4,7 +4,7 @@ class Admin::LightningController < Admin::BaseController
def index def index
@current_section = :lightning @current_section = :lightning
@users = User.pluck(:cn, :ou, :lndhub_username) @users = User.pluck(:cn, :ou, :ln_account)
@accounts = LndhubAccount.with_balances.order(balance: :desc).to_a @accounts = LndhubAccount.with_balances.order(balance: :desc).to_a
@ln = {} @ln = {}
@@ -1,23 +0,0 @@
class Admin::Settings::MembershipController < Admin::SettingsController
def show
end
def update
update_settings
redirect_to admin_settings_membership_path, flash: {
success: "Settings saved"
}
end
private
def setting_params
params.require(:setting).permit([
:member_status_contributor,
:member_status_sustainer,
:user_index_show_contributors,
:user_index_show_sustainers
])
end
end
+6 -22
View File
@@ -4,41 +4,25 @@ class Admin::UsersController < Admin::BaseController
# GET /admin/users # GET /admin/users
def index def index
ldap = LdapService.new ldap = LdapService.new
ou = Setting.primary_domain @ou = Setting.primary_domain
@show_contributors = Setting.user_index_show_contributors @pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
@show_sustainers = Setting.user_index_show_sustainers
@contributors = ldap.search_users(:memberStatus, :contributor, :cn) if @show_contributors
@sustainers = ldap.search_users(:memberStatus, :sustainer, :cn) if @show_sustainers
@admins = ldap.search_users(:admin, true, :cn)
@username = params[:username].presence
users_scope = User.where(ou: ou).order(cn: :asc)
users_scope = users_scope.where("cn LIKE ?", "%#{User.sanitize_sql_like(@username.downcase)}%") if @username
@pagy, @users = pagy(users_scope)
@stats = { @stats = {
users_confirmed: User.where(ou: ou).confirmed.count, users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: ou).pending.count users_pending: User.where(ou: @ou).pending.count
} }
@stats[:users_contributing] = @contributors.size if @show_contributors
@stats[:users_paying] = @sustainers.size if @show_sustainers
end end
# GET /admin/users/:username # GET /admin/users/:username
def show def show
@invitees = @user.invitees
@recent_invitees = @user.invitees.order(created_at: :desc).limit(5)
@more_invitees = (@invitees - @recent_invitees).count
if Setting.lndhub_admin_enabled? if Setting.lndhub_admin_enabled?
@lndhub_user = @user.lndhub_user @lndhub_user = @user.lndhub_user
end end
@services_enabled = @user.services_enabled @services_enabled = @user.services_enabled
@ldap_avatar = LdapManager::FetchAvatar.call(cn: @user.cn) @avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
end end
# POST /admin/users/:username/invitations # POST /admin/users/:username/invitations
+1 -1
View File
@@ -39,7 +39,7 @@ class ApplicationController < ActionController::Base
end end
def after_sign_in_path_for(user) def after_sign_in_path_for(user)
session.delete(:user_return_to) || root_path session[:user_return_to] || root_path
end end
def lndhub_authenticate(options={}) def lndhub_authenticate(options={})
-27
View File
@@ -1,27 +0,0 @@
class AvatarsController < ApplicationController
def show
if user = User.find_by(cn: params[:username])
http_status :not_found and return unless user.avatar.attached?
sha256_hash = params[:hash]
format = params[:format]&.to_sym || :png
# size = params[:size]&.to_sym || :original
unless user.avatar.filename.to_s == "#{sha256_hash}.#{format}"
http_status :not_found and return
end
# TODO See note for avatar_variant in user model
# blob = if size == :original
# user.avatar.blob
# else
# user.avatar_variant(size: size)&.blob
# end
data = user.avatar.blob.download
send_data data, type: "image/#{format}", disposition: "inline"
else
http_status :not_found
end
end
end
@@ -11,7 +11,7 @@ class Contributions::DonationsController < ApplicationController
def index def index
@current_section = :contributions @current_section = :contributions
@donations_completed = current_user.donations.completed.order('paid_at desc') @donations_completed = current_user.donations.completed.order('paid_at desc')
@donations_processing = current_user.donations.processing.order('created_at desc') @donations_pending = current_user.donations.processing.order('created_at desc')
if Setting.lndhub_enabled? if Setting.lndhub_enabled?
begin begin
@@ -81,11 +81,14 @@ class Contributions::DonationsController < ApplicationController
case invoice["status"] case invoice["status"]
when "Settled" when "Settled"
@donation.complete! @donation.paid_at = DateTime.now
@donation.payment_status = "settled"
@donation.save!
flash_message = { success: "Thank you!" } flash_message = { success: "Thank you!" }
when "Processing" when "Processing"
unless @donation.processing? unless @donation.processing?
@donation.start_processing! @donation.payment_status = "processing"
@donation.save!
flash_message = { success: "Thank you! We will send you an email when the payment is confirmed." } flash_message = { success: "Thank you! We will send you an email when the payment is confirmed." }
BtcpayCheckDonationJob.set(wait: 20.seconds).perform_later(@donation) BtcpayCheckDonationJob.set(wait: 20.seconds).perform_later(@donation)
end end
@@ -1,16 +0,0 @@
class Contributions::OtherController < ApplicationController
before_action :authenticate_user!
before_action :set_content_editing
# GET /contributions/other
def index
@current_section = :contributions
end
private
def set_content_editing
return unless params[:edit] && current_user.is_admin?
@edit_content = true
end
end
@@ -0,0 +1,8 @@
class Contributions::ProjectsController < ApplicationController
before_action :authenticate_user!
# GET /contributions
def index
@current_section = :contributions
end
end
@@ -55,12 +55,7 @@ class Devise::PasswordsController < DeviseController
protected protected
def after_resetting_password_path_for(resource) def after_resetting_password_path_for(resource)
session.delete(:user_return_to) Devise.sign_in_after_reset_password ? after_sign_in_path_for(resource) : new_session_path(resource_name)
if Devise.sign_in_after_reset_password
root_path
else
new_session_path(resource_name)
end
end end
# The path used after sending reset password instructions # The path used after sending reset password instructions
@@ -8,9 +8,6 @@ class Discourse::SsoController < ApplicationController
sso.email = current_user.email sso.email = current_user.email
sso.username = current_user.cn sso.username = current_user.cn
sso.name = current_user.display_name sso.name = current_user.display_name
if current_user.avatar.attached?
sso.avatar_url = helpers.image_url_for(current_user.avatar)
end
sso.admin = current_user.is_admin? sso.admin = current_user.is_admin?
sso.sso_secret = secret sso.sso_secret = secret
+1 -1
View File
@@ -37,7 +37,7 @@ class LnurlpayController < ApplicationController
pubkey: Setting.lndhub_public_key, pubkey: Setting.lndhub_public_key,
customData: [{ customData: [{
customKey: "696969", customKey: "696969",
customValue: @user.lndhub_username customValue: @user.ln_account
}] }]
} }
end end
-9
View File
@@ -1,9 +0,0 @@
class PagesController < ApplicationController
def privacy
@current_section = :privacy
end
def tos
@current_section = :tos
end
end
+4 -5
View File
@@ -1,5 +1,4 @@
class Rs::OauthController < ApplicationController class Rs::OauthController < ApplicationController
prepend_before_action :assert_redirect_uri, only: [:new, :create]
before_action :require_signed_in_with_username, only: :new before_action :require_signed_in_with_username, only: :new
before_action :authenticate_user!, only: :create before_action :authenticate_user!, only: :create
@@ -17,6 +16,8 @@ class Rs::OauthController < ApplicationController
["In 1 month", 1.month.from_now], ["In 1 month", 1.month.from_now],
["In 1 day", 1.day.from_now]] ["In 1 day", 1.day.from_now]]
http_status :bad_request and return unless @redirect_uri.present?
unless current_user == @user unless current_user == @user
sign_out :user sign_out :user
@@ -63,6 +64,8 @@ class Rs::OauthController < ApplicationController
state = params[:state].presence state = params[:state].presence
expire_at = params[:expire_at].presence expire_at = params[:expire_at].presence
http_status :bad_request and return unless redirect_uri.present?
if permissions.empty? if permissions.empty?
redirect_to(url_with_state("#{redirect_uri}#error=invalid_scope", state), redirect_to(url_with_state("#{redirect_uri}#error=invalid_scope", state),
allow_other_host: true) and return allow_other_host: true) and return
@@ -94,10 +97,6 @@ class Rs::OauthController < ApplicationController
private private
def assert_redirect_uri
http_status :bad_request unless params[:redirect_uri].present?
end
def require_signed_in_with_username def require_signed_in_with_username
unless user_signed_in? unless user_signed_in?
session[:user_return_to] = request.url session[:user_return_to] = request.url
@@ -9,7 +9,7 @@ class Services::LightningController < ApplicationController
before_action :lndhub_fetch_balance before_action :lndhub_fetch_balance
def index def index
@wallet_setup_url = "lndhub://#{current_user.lndhub_username}:#{current_user.lndhub_password}@#{ENV['LNDHUB_PUBLIC_URL']}" @wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
end end
def transactions def transactions
@@ -23,11 +23,7 @@ class Services::RsAuthsController < Services::BaseController
end end
def launch_app def launch_app
user_address = Rails.env.development? ? launch_url = "#{@auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{@auth.token}"
"#{current_user.cn}@localhost:3000" :
current_user.address
launch_url = "#{@auth.launch_url}#remotestorage=#{user_address}"
redirect_to launch_url, allow_other_host: true redirect_to launch_url, allow_other_host: true
end end
+3 -34
View File
@@ -25,7 +25,7 @@ class SettingsController < ApplicationController
def update def update
@user.preferences.merge!(user_params[:preferences] || {}) @user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name] @user.display_name = user_params[:display_name]
@user.avatar_new = user_params[:avatar_new] @user.avatar_new = user_params[:avatar]
@user.pgp_pubkey = user_params[:pgp_pubkey] @user.pgp_pubkey = user_params[:pgp_pubkey]
if @user.save if @user.save
@@ -34,12 +34,7 @@ class SettingsController < ApplicationController
end end
if @user.avatar_new.present? if @user.avatar_new.present?
if store_user_avatar LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
UserManager::UpdateAvatar.call(user: @user)
else
@validation_errors = @user.errors
render :show, status: :unprocessable_entity and return
end
end end
if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key]) if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key])
@@ -167,7 +162,7 @@ class SettingsController < ApplicationController
def user_params def user_params
params.require(:user).permit( params.require(:user).permit(
:display_name, :avatar_new, :pgp_pubkey, :display_name, :avatar, :pgp_pubkey,
preferences: UserPreferences.pref_keys preferences: UserPreferences.pref_keys
) )
end end
@@ -189,30 +184,4 @@ class SettingsController < ApplicationController
salt = BCrypt::Engine.generate_salt salt = BCrypt::Engine.generate_salt
BCrypt::Engine.hash_secret(password, salt) BCrypt::Engine.hash_secret(password, salt)
end end
def store_user_avatar
io = @user.avatar_new.tempfile
img_data = UserManager::ProcessAvatar.call(io: io)
if img_data.blank?
@user.errors.add(:avatar, "failed to process file")
false
end
tempfile = Tempfile.create
tempfile.binmode
tempfile.write(img_data)
tempfile.rewind
hash = Digest::SHA256.hexdigest(img_data)
ext = @user.avatar_new.content_type == "image/png" ? "png" : "jpg"
filename = "#{hash}.#{ext}"
if filename == @user.avatar.filename.to_s
@user.errors.add(:avatar, "must be a new file/picture")
false
else
key = "users/#{@user.cn}/avatars/#{filename}"
@user.avatar.attach io: tempfile, key: key, filename: filename
@user.save
end
end
end end
+18
View File
@@ -0,0 +1,18 @@
class TurboController < ApplicationController
class Responder < ActionController::Responder
def to_turbo_stream
controller.render(options.merge(formats: :html))
rescue ActionView::MissingTemplate => error
if get?
raise error
elsif has_errors? && default_action
render rendering_options.merge(formats: :html, status: :unprocessable_entity)
else
redirect_to navigation_location
end
end
end
self.responder = Responder
respond_to :html, :turbo_stream
end
+1 -1
View File
@@ -6,7 +6,7 @@ class Users::DeviseController < ApplicationController
if get? if get?
raise error raise error
elsif has_errors? && default_action elsif has_errors? && default_action
render error_rendering_options.merge(formats: :html, status: :unprocessable_entity) render rendering_options.merge(formats: :html, status: :unprocessable_entity)
else else
redirect_to navigation_location redirect_to navigation_location
end end
@@ -1,16 +1,8 @@
class WebKeyDirectoryController < WellKnownController class WebKeyDirectoryController < WellKnownController
before_action :allow_cross_origin_requests before_action :allow_cross_origin_requests
# /.well-known/openpgpkey/hu/:hashed_username(.txt)?l=username # /.well-known/openpgpkey/hu/:hashed_username(.txt)
def show 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) @user = User.find_by(cn: params[:l].downcase)
if @user.nil? || if @user.nil? ||
-14
View File
@@ -33,10 +33,6 @@ class WebfingerController < WellKnownController
links: [] links: []
} }
if @user.avatar.attached?
jrd[:links] += avatar_link
end
if Setting.mastodon_enabled && @user.service_enabled?(:mastodon) if Setting.mastodon_enabled && @user.service_enabled?(:mastodon)
# https://docs.joinmastodon.org/spec/webfinger/ # https://docs.joinmastodon.org/spec/webfinger/
jrd[:aliases] += mastodon_aliases jrd[:aliases] += mastodon_aliases
@@ -51,16 +47,6 @@ class WebfingerController < WellKnownController
jrd jrd
end end
def avatar_link
[
{
rel: "http://webfinger.net/rel/avatar",
type: @user.avatar.content_type,
href: helpers.image_url_for(@user.avatar)
}
]
end
def mastodon_aliases def mastodon_aliases
[ [
"#{Setting.mastodon_public_url}/@#{@user.cn}", "#{Setting.mastodon_public_url}/@#{@user.cn}",
+1 -1
View File
@@ -5,7 +5,7 @@ class WebhooksController < ApplicationController
before_action :process_payload before_action :process_payload
def lndhub def lndhub
@user = User.find_by!(lndhub_username: @payload[:user_login]) @user = User.find_by!(ln_account: @payload[:user_login])
if @zap = @user.zaps.find_by(payment_request: @payload[:payment_request]) if @zap = @user.zaps.find_by(payment_request: @payload[:payment_request])
settled_at = Time.parse(@payload[:settled_at]) settled_at = Time.parse(@payload[:settled_at])
-21
View File
@@ -14,25 +14,4 @@ module ApplicationHelper
def badge(text, color) def badge(text, color)
tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800" tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800"
end end
def markdown_to_html(string)
raw Kramdown::Document.new(string, {
input: "GFM"
}).to_html
end
def image_url_for(attachment)
return s3_image_url(attachment) if Setting.s3_enabled?
if attachment.record.is_a?(User) && attachment.name == "avatar"
hash, format = attachment.blob.filename.to_s.split(".", 2)
user_avatar_url(
username: attachment.record.cn,
hash: hash,
format: format
)
else
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
end
end
end end
+3 -1
View File
@@ -10,7 +10,9 @@ class BtcpayCheckDonationJob < ApplicationJob
case invoice["status"] case invoice["status"]
when "Settled" when "Settled"
donation.complete! donation.paid_at = DateTime.now
donation.payment_status = "settled"
donation.save!
NotificationMailer.with(user: donation.user) NotificationMailer.with(user: donation.user)
.bitcoin_donation_confirmed .bitcoin_donation_confirmed
+1 -1
View File
@@ -4,7 +4,7 @@ class CreateLdapUserJob < ApplicationJob
def perform(username:, domain:, email:, hashed_pw:, confirmed: false) def perform(username:, domain:, email:, hashed_pw:, confirmed: false)
dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org" dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org"
attr = { attr = {
objectclass: ["top", "account", "person", "inetOrgPerson", "extensibleObject"], objectclass: ["top", "account", "person", "extensibleObject"],
cn: username, cn: username,
sn: username, sn: username,
uid: username, uid: username,
+3 -3
View File
@@ -2,12 +2,12 @@ class CreateLndhubAccountJob < ApplicationJob
queue_as :default queue_as :default
def perform(user) def perform(user)
return if user.lndhub_username.present? && user.lndhub_password.present? return if user.ln_account.present? && user.ln_password.present?
lndhub = LndhubV2.new lndhub = LndhubV2.new
credentials = lndhub.create_account credentials = lndhub.create_account
user.update! lndhub_username: credentials["login"], user.update! ln_account: credentials["login"],
lndhub_password: credentials["password"] ln_password: credentials["password"]
end end
end end
@@ -3,6 +3,8 @@ class RemoteStorageExpireAuthorizationJob < ApplicationJob
def perform(rs_auth_id) def perform(rs_auth_id)
rs_auth = RemoteStorageAuthorization.find rs_auth_id rs_auth = RemoteStorageAuthorization.find rs_auth_id
return unless rs_auth.expire_at.nil? || rs_auth.expire_at <= DateTime.now
rs_auth.destroy! rs_auth.destroy!
end end
end end
+16 -1
View File
@@ -2,6 +2,21 @@ class XmppExchangeContactsJob < ApplicationJob
queue_as :default queue_as :default
def perform(inviter, invitee) def perform(inviter, invitee)
EjabberdManager::ExchangeContacts.call(inviter:, invitee:) return unless inviter.service_enabled?(:ejabberd) &&
invitee.service_enabled?(:ejabberd) &&
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new
ejabberd.add_rosteritem({
"localuser": invitee.cn, "localhost": invitee.ou,
"user": inviter.cn, "host": inviter.ou,
"nick": inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
ejabberd.add_rosteritem({
"localuser": inviter.cn, "localhost": inviter.ou,
"user": invitee.cn, "host": invitee.ou,
"nick": invitee.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
end end
end end
+2 -1
View File
@@ -2,6 +2,7 @@ class XmppSendMessageJob < ApplicationJob
queue_as :default queue_as :default
def perform(payload) def perform(payload)
EjabberdManager::SendMessage.call(payload:) ejabberd = EjabberdApiClient.new
ejabberd.send_message payload
end end
end end
-7
View File
@@ -1,7 +0,0 @@
class XmppSetAvatarJob < ApplicationJob
queue_as :default
def perform(user:, overwrite: false)
EjabberdManager::SetAvatar.call(user:, overwrite:)
end
end
+20 -1
View File
@@ -2,6 +2,25 @@ class XmppSetDefaultBookmarksJob < ApplicationJob
queue_as :default queue_as :default
def perform(user) def perform(user)
EjabberdManager::SetDefaultBookmarks.call(user:) return unless Setting.xmpp_default_rooms.any?
@user = user
ejabberd = EjabberdApiClient.new
ejabberd.private_set user, storage_content
end
def storage_content
bookmarks = ""
Setting.xmpp_default_rooms.each do |r|
bookmarks << conference_element(
jid: r[/<(.+)>/, 1], name: r[/^(.+)\s/, 1], nick: @user.cn,
autojoin: Setting.xmpp_autojoin_default_rooms
)
end
"<storage xmlns='storage:bookmarks'>#{bookmarks}</storage>"
end
def conference_element(jid:, name:, autojoin: false, nick:)
"<conference jid='#{jid}' name='#{name}' autojoin='#{autojoin.to_s}'><nick>#{nick}</nick></conference>"
end end
end end
@@ -11,9 +11,6 @@ module Settings
field :mastodon_address_domain, type: :string, field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
field :mastodon_auth_token, type: :string,
default: ENV["MASTODON_AUTH_TOKEN"].presence
end end
end end
end end
@@ -1,18 +0,0 @@
module Settings
module MembershipSettings
extend ActiveSupport::Concern
included do
field :member_status_contributor, type: :string,
default: "Contributor"
field :member_status_sustainer, type: :string,
default: "Sustainer"
# Admin panel
field :user_index_show_contributors, type: :boolean,
default: false
field :user_index_show_sustainers, type: :boolean,
default: false
end
end
end
+5 -38
View File
@@ -1,42 +1,22 @@
class Donation < ApplicationRecord class Donation < ApplicationRecord
include AASM # Relations
belongs_to :user belongs_to :user
# Validations
validates_presence_of :user validates_presence_of :user
validates_presence_of :donation_method, validates_presence_of :donation_method,
inclusion: { in: %w[ custom btcpay lndhub ] } inclusion: { in: %w[ custom btcpay lndhub ] }
validates_presence_of :payment_status, allow_nil: true, validates_presence_of :payment_status, allow_nil: true,
inclusion: { in: %w[ pending processing settled ] } inclusion: { in: %w[ processing settled ] }
validates_presence_of :paid_at, allow_nil: true validates_presence_of :paid_at, allow_nil: true
validates_presence_of :amount_sats, allow_nil: true validates_presence_of :amount_sats, allow_nil: true
validates_presence_of :fiat_amount, allow_nil: true validates_presence_of :fiat_amount, allow_nil: true
validates_presence_of :fiat_currency, allow_nil: true, validates_presence_of :fiat_currency, allow_nil: true,
inclusion: { in: %w[ EUR USD ] } inclusion: { in: %w[ EUR USD ] }
scope :pending, -> { where(payment_status: "pending") } #Scopes
scope :processing, -> { where(payment_status: "processing") } scope :processing, -> { where(payment_status: "processing") }
scope :completed, -> { where(payment_status: "settled") } scope :completed, -> { where(payment_status: "settled") }
scope :incomplete, -> { where.not(payment_status: "settled") }
aasm column: :payment_status do
state :pending, initial: true
state :processing
state :settled
event :start_processing do
transitions from: :pending, to: :processing
end
event :complete do
transitions from: :processing, to: :settled, after: [:set_paid_at, :set_sustainer_status]
transitions from: :pending, to: :settled, after: [:set_paid_at, :set_sustainer_status]
end
end
def pending?
payment_status == "pending"
end
def processing? def processing?
payment_status == "processing" payment_status == "processing"
@@ -45,17 +25,4 @@ class Donation < ApplicationRecord
def completed? def completed?
payment_status == "settled" payment_status == "settled"
end end
private
def set_paid_at
update paid_at: DateTime.now if paid_at.nil?
end
def set_sustainer_status
user.add_member_status :sustainer
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error("Failed to set memberStatus: #{e.message}")
end
end end
-12
View File
@@ -1,12 +0,0 @@
class EditableContent < ApplicationRecord
validates :key, presence: true,
uniqueness: { scope: :context }
def has_content?
content.present?
end
def is_empty?
content.blank?
end
end
+5 -5
View File
@@ -6,22 +6,22 @@ class LndhubUser < LndhubBase
foreign_key: "user_id" foreign_key: "user_id"
belongs_to :user, class_name: "User", belongs_to :user, class_name: "User",
primary_key: "lndhub_username", primary_key: "ln_account",
foreign_key: "login" foreign_key: "login"
def balance def balance
accounts.current.order(:id).first.ledgers.sum("account_ledgers.amount").to_i.abs accounts.current.first.ledgers.sum("account_ledgers.amount").to_i.abs
end end
def sum_outgoing def sum_outgoing
accounts.outgoing.order(:id).first.ledgers.sum("account_ledgers.amount").to_i.abs accounts.outgoing.first.ledgers.sum("account_ledgers.amount").to_i.abs
end end
def sum_incoming def sum_incoming
accounts.incoming.order(:id).first.ledgers.sum("account_ledgers.amount").to_i.abs accounts.incoming.first.ledgers.sum("account_ledgers.amount").to_i.abs
end end
def sum_fees def sum_fees
accounts.fees.order(:id).first.ledgers.sum("account_ledgers.amount").to_i.abs accounts.fees.first.ledgers.sum("account_ledgers.amount").to_i.abs
end end
end end
+5 -13
View File
@@ -69,19 +69,11 @@ class RemoteStorageAuthorization < ApplicationRecord
end end
def remove_token_expiry_job def remove_token_expiry_job
job_class = RemoteStorageExpireAuthorizationJob queue = Sidekiq::Queue.new(RemoteStorageExpireAuthorizationJob.queue_name)
job_args = [id] queue.each do |job|
next unless job.display_class == "RemoteStorageExpireAuthorizationJob"
query = SolidQueue::Job.where(class_name: job_class.to_s) job.delete if job.display_args == [id]
end
case ActiveRecord::Base.connection.adapter_name.downcase
when /sqlite/
query.where("json_extract(arguments, '$.arguments') = ?", job_args.to_json)
when /postgres/
query.where("CAST(arguments AS jsonb)->>'arguments' = ?", job_args.to_json)
else
raise "Unsupported database adapter"
end.destroy_all
end end
def find_or_create_web_app def find_or_create_web_app
-1
View File
@@ -16,7 +16,6 @@ class Setting < RailsSettings::Base
include Settings::LightningNetworkSettings include Settings::LightningNetworkSettings
include Settings::MastodonSettings include Settings::MastodonSettings
include Settings::MediaWikiSettings include Settings::MediaWikiSettings
include Settings::MembershipSettings
include Settings::NostrSettings include Settings::NostrSettings
include Settings::OpenCollectiveSettings include Settings::OpenCollectiveSettings
include Settings::RemoteStorageSettings include Settings::RemoteStorageSettings
+23 -76
View File
@@ -4,8 +4,8 @@ class User < ApplicationRecord
include EmailValidatable include EmailValidatable
attr_accessor :current_password attr_accessor :current_password
attr_accessor :display_name
attr_accessor :avatar_new attr_accessor :avatar_new
attr_accessor :display_name
attr_accessor :pgp_pubkey attr_accessor :pgp_pubkey
serialize :preferences, coder: UserPreferences serialize :preferences, coder: UserPreferences
@@ -23,16 +23,10 @@ class User < ApplicationRecord
has_many :zaps has_many :zaps
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user", has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
primary_key: "lndhub_username", foreign_key: "login" primary_key: "ln_account", foreign_key: "login"
has_many :accounts, through: :lndhub_user has_many :accounts, through: :lndhub_user
#
# Attachments
#
has_one_attached :avatar
# #
# Validations # Validations
# #
@@ -56,7 +50,6 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true, validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) } if: -> { defined?(@display_name) }
validate :acceptable_avatar validate :acceptable_avatar
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? } validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
@@ -73,7 +66,7 @@ class User < ApplicationRecord
# Encrypted database columns # Encrypted database columns
# #
encrypts :lndhub_password has_encrypted :ln_login, :ln_password
# Include default devise modules. Others available are: # Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
@@ -84,10 +77,6 @@ class User < ApplicationRecord
:timeoutable, :timeoutable,
:rememberable :rememberable
#
# Methods
#
def ldap_before_save def ldap_before_save
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
self.ou = dn.split(',') self.ou = dn.split(',')
@@ -131,11 +120,11 @@ class User < ApplicationRecord
end end
def is_admin? def is_admin?
@admin ||= if admin = Devise::LDAP::Adapter.get_ldap_param(self.cn, :admin) admin ||= if admin = Devise::LDAP::Adapter.get_ldap_param(self.cn, :admin)
!!admin.first !!admin.first
else else
false false
end end
end end
def address def address
@@ -163,41 +152,13 @@ class User < ApplicationRecord
def ldap_entry(reload: false) def ldap_entry(reload: false)
return @ldap_entry if defined?(@ldap_entry) && !reload return @ldap_entry if defined?(@ldap_entry) && !reload
@ldap_entry = ldap.fetch_users(cn: self.cn).first @ldap_entry = ldap.fetch_users(uid: self.cn, ou: self.ou).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 end
def display_name def display_name
@display_name ||= ldap_entry[:display_name] @display_name ||= ldap_entry[:display_name]
end end
# TODO Variant keys are currently broken for some reason
# (They use the same key as the main blob, when it should be
# "/variants/#{key)"
# def avatar_variant(size: :medium)
# dimensions = case size
# when :large then [400, 400]
# when :medium then [256, 256]
# when :small then [64, 64]
# else [256, 256]
# end
# format = avatar.content_type == "image/png" ? :png : :jpeg
# avatar.variant(resize_to_fill: dimensions, format: format)
# end
def nostr_pubkey def nostr_pubkey
@nostr_pubkey ||= ldap_entry[:nostr_key] @nostr_pubkey ||= ldap_entry[:nostr_key]
end end
@@ -225,6 +186,10 @@ class User < ApplicationRecord
ZBase32.encode(Digest::SHA1.digest(cn)) ZBase32.encode(Digest::SHA1.digest(cn))
end end
def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end
def services_enabled def services_enabled
ldap_entry[:services_enabled] || [] ldap_entry[:services_enabled] || []
end end
@@ -234,39 +199,21 @@ class User < ApplicationRecord
end end
def enable_service(service) def enable_service(service)
add_to_ldap_array :services_enabled, :serviceEnabled, service current_services = services_enabled
ldap_entry(reload: true)[:services_enabled] new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq.sort
ldap.replace_attribute(dn, :serviceEnabled, services)
end end
def disable_service(service) def disable_service(service)
remove_from_ldap_array :services_enabled, :serviceEnabled, service current_services = services_enabled
ldap_entry(reload: true)[:services_enabled] disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq.sort
ldap.replace_attribute(dn, :serviceEnabled, services)
end end
def disable_all_services def disable_all_services
ldap.delete_attribute(dn, :serviceEnabled) ldap.delete_attribute(dn,:service)
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 end
private private
@@ -280,7 +227,7 @@ class User < ApplicationRecord
return unless avatar_new.present? return unless avatar_new.present?
if avatar_new.size > 1.megabyte if avatar_new.size > 1.megabyte
errors.add(:avatar, "must be less than 1MB file size") errors.add(:avatar, "file size is too large")
end end
acceptable_types = ["image/jpeg", "image/png"] acceptable_types = ["image/jpeg", "image/png"]
+28 -14
View File
@@ -1,22 +1,36 @@
# #
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/ # API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
# #
class BtcpayManagerService < RestApiService class BtcpayManagerService < ApplicationService
private private
def base_url def base_url
@base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}" @base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
end end
def auth_token def auth_token
@auth_token ||= Setting.btcpay_auth_token @auth_token ||= Setting.btcpay_auth_token
end end
def headers def headers
{ {
"Content-Type" => "application/json", "Content-Type" => "application/json",
"Accept" => "application/json", "Accept" => "application/json",
"Authorization" => "token #{auth_token}" "Authorization" => "token #{auth_token}"
} }
end end
def endpoint_url(path)
"#{base_url}/#{path.gsub(/^\//, '')}"
end
def get(path, params = {})
res = Faraday.get endpoint_url(path), params, headers
JSON.parse(res.body)
end
def post(path, payload)
res = Faraday.post endpoint_url(path), payload.to_json, headers
JSON.parse(res.body)
end
end end
+29
View File
@@ -0,0 +1,29 @@
class EjabberdApiClient
def initialize
@base_url = Setting.ejabberd_api_url
end
def post(endpoint, payload)
res = Faraday.post("#{@base_url}/#{endpoint}", payload.to_json,
"Content-Type" => "application/json")
if res.status != 200
Rails.logger.error "[ejabberd] API request failed:"
Rails.logger.error res.body
#TODO Send custom event to Sentry
end
end
def add_rosteritem(payload)
post "add_rosteritem", payload
end
def send_message(payload)
post "send_message", payload
end
def private_set(user, content)
payload = { user: user.cn, host: user.ou, element: content }
post "private_set", payload
end
end
@@ -1,25 +0,0 @@
module EjabberdManager
class ExchangeContacts < EjabberdManagerService
def initialize(inviter:, invitee:)
@inviter = inviter
@invitee = invitee
end
def call
return unless @inviter.service_enabled?(:ejabberd) &&
@invitee.service_enabled?(:ejabberd) &&
@inviter.preferences[:xmpp_exchange_contacts_with_invitees]
add_rosteritem({
"localuser": @invitee.cn, "localhost": @invitee.ou,
"user": @inviter.cn, "host": @inviter.ou,
"nick": @inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
add_rosteritem({
"localuser": @inviter.cn, "localhost": @inviter.ou,
"user": @invitee.cn, "host": @invitee.ou,
"nick": @invitee.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
end
end
end
@@ -1,25 +0,0 @@
module EjabberdManager
class GetAvatar < EjabberdManagerService
def initialize(user:)
@user = user
end
def call
res = get_vcard2 @user, "PHOTO", "BINVAL"
if res.status == 200
# VCARD PHOTO/BINVAL prop exists
img_base64 = JSON.parse(res.body)["content"]
ct_res = get_vcard2 @user, "PHOTO", "TYPE"
content_type = JSON.parse(ct_res.body)["content"]
{ content_type:, img_base64: }
elsif res.status == 400
# VCARD or PHOTO/BINVAL prop does not exist
nil
else
# Unexpected error, let job fail
raise res.inspect
end
end
end
end
@@ -1,11 +0,0 @@
module EjabberdManager
class SendMessage < EjabberdManagerService
def initialize(payload:)
@payload = payload
end
def call
send_message @payload
end
end
end
@@ -1,80 +0,0 @@
require 'digest'
require "image_processing/vips"
module EjabberdManager
class SetAvatar < EjabberdManagerService
def initialize(user:, overwrite: false)
@user = user
@overwrite = overwrite
end
def call
unless @overwrite
current_avatar = EjabberdManager::GetAvatar.call(user: @user)
Rails.logger.info { "User #{@user.cn} already has an avatar set" }
return if current_avatar.present?
end
Rails.logger.debug { "Setting XMPP avatar for user #{@user.cn}" }
stanzas = build_xep0084_stanzas
stanzas.each do |stanza|
payload = { from: @user.address, to: @user.address, stanza: stanza }
res = send_stanza payload
raise res.inspect if res.status != 200
end
end
private
def process_avatar
@user.avatar.blob.open do |file|
processed = ImageProcessing::Vips
.source(file)
.resize_to_fill(256, 256)
.convert("png")
.call
processed.read
end
end
# See https://xmpp.org/extensions/xep-0084.html
def build_xep0084_stanzas
img_data = process_avatar
sha1_hash = Digest::SHA1.hexdigest(img_data)
base64_data = Base64.strict_encode64(img_data)
[
"""
<iq type='set' from='#{@user.address}' id='avatar-data-#{rand(101)}'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:avatar:data'>
<item id='#{sha1_hash}'>
<data xmlns='urn:xmpp:avatar:data'>#{base64_data}</data>
</item>
</publish>
</pubsub>
</iq>
""".strip,
"""
<iq type='set' from='#{@user.address}' id='avatar-metadata-#{rand(101)}'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:avatar:metadata'>
<item id='#{sha1_hash}'>
<metadata xmlns='urn:xmpp:avatar:metadata'>
<info bytes='#{img_data.size}'
id='#{sha1_hash}'
height='256'
type='image/png'
width='256'/>
</metadata>
</item>
</publish>
</pubsub>
</iq>
""".strip,
]
end
end
end
@@ -1,31 +0,0 @@
module EjabberdManager
class SetDefaultBookmarks < EjabberdManagerService
def initialize(user:)
@user = user
end
def call
return unless Setting.xmpp_default_rooms.any?
private_set @user, storage_content
end
private
def storage_content
bookmarks = ""
Setting.xmpp_default_rooms.each do |r|
bookmarks << conference_element(
jid: r[/<(.+)>/, 1], name: r[/^(.+)\s/, 1], nick: @user.cn,
autojoin: Setting.xmpp_autojoin_default_rooms
)
end
"<storage xmlns='storage:bookmarks'>#{bookmarks}</storage>"
end
def conference_element(jid:, name:, autojoin: false, nick:)
"<conference jid='#{jid}' name='#{name}' autojoin='#{autojoin.to_s}'><nick>#{nick}</nick></conference>"
end
end
end
-55
View File
@@ -1,55 +0,0 @@
class EjabberdManagerService < RestApiService
private
def base_url
@base_url ||= Setting.ejabberd_api_url
end
def headers
{ "Content-Type" => "application/json" }
end
def parse_responses?
false
end
#
# API endpoints
#
def add_rosteritem(payload)
post "add_rosteritem", payload
end
def send_message(payload)
post "send_message", payload
end
def send_stanza(payload)
post "send_stanza", payload
end
def get_vcard2(user, name, subname)
payload = {
user: user.cn, host: user.ou,
name: name, subname: subname
}
post "get_vcard2", payload
end
def private_get(user, element_name, namespace)
payload = {
user: user.cn, host: user.ou,
element: element_name, ns: namespace
}
post "private_get", payload
end
def private_set(user, content)
payload = {
user: user.cn, host: user.ou,
element: content
}
post "private_set", payload
end
end
+3 -3
View File
@@ -5,12 +5,12 @@ module LdapManager
end end
def call def call
treebase = ldap_config["base"] treebase = ldap_config["base"]
attributes = %w{ jpegPhoto } attributes = %w{ jpegPhoto }
filter = Net::LDAP::Filter.eq("cn", @cn) filter = Net::LDAP::Filter.eq("cn", @cn)
entry = client.search(base: treebase, filter: filter, attributes: attributes).first entry = client.search(base: treebase, filter: filter, attributes: attributes).first
entry[:jpegPhoto].present? ? entry.jpegPhoto.first : nil entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
end end
end end
end end
+13 -28
View File
@@ -2,41 +2,26 @@ require "image_processing/vips"
module LdapManager module LdapManager
class UpdateAvatar < LdapManagerService class UpdateAvatar < LdapManagerService
def initialize(user:) def initialize(dn:, file:)
@user = user @dn = dn
@dn = user.dn @img_data = process(file)
end end
def call def call
unless @user.avatar.attached? replace_attribute @dn, :jpegPhoto, @img_data
Rails.logger.error { "Cannot store empty jpegPhoto for user #{@user.cn}" }
return false
end
img_data = @user.avatar.blob.download
jpg_data = process_avatar
Rails.logger.debug { "Storing new jpegPhoto for user #{@user.cn} in LDAP" }
result = replace_attribute(@dn, :jpegPhoto, jpg_data)
result == 0
end end
private private
def process_avatar def process(file)
@user.avatar.blob.open do |file| processed = ImageProcessing::Vips
processed = ImageProcessing::Vips .resize_to_fill(512, 512)
.source(file) .source(file)
.resize_to_fill(256, 256) .convert("jpeg")
.convert("jpeg") .saver(strip: true)
.saver(strip: true) .call
.call
processed.read Base64.strict_encode64 processed.read
end
rescue Vips::Error => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error { "Image processing failed for LDAP avatar: #{e.message}" }
nil
end end
end end
end end
@@ -6,11 +6,7 @@ module LdapManager
end end
def call def call
if @display_name.present? replace_attribute @dn, :displayName, @display_name
replace_attribute @dn, :displayName, @display_name
else
delete_attribute @dn, :displayName
end
end end
end end
end end
+10 -19
View File
@@ -50,17 +50,19 @@ class LdapService < ApplicationService
end end
def fetch_users(args={}) def fetch_users(args={})
if args[:ou]
treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}"
else
treebase = ldap_config["base"]
end
attributes = %w[ attributes = %w[
dn cn uid mail displayName admin serviceEnabled memberStatus dn cn uid mail displayName admin serviceEnabled
mailRoutingAddress mailpassword nostrKey pgpKey mailRoutingAddress mailpassword nostrKey pgpKey
] ]
filter = Net::LDAP::Filter.eq('objectClass', 'person') & filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
Net::LDAP::Filter.eq("cn", args[:cn] || "*")
entries = client.search( entries = client.search(base: treebase, filter: filter, attributes: attributes)
base: ldap_config["base"], filter: filter,
attributes: attributes
)
entries.sort_by! { |e| e.cn[0] } entries.sort_by! { |e| e.cn[0] }
entries = entries.collect do |e| entries = entries.collect do |e|
{ {
@@ -69,7 +71,6 @@ class LdapService < ApplicationService
display_name: e.try(:displayName) ? e.displayName.first : nil, display_name: e.try(:displayName) ? e.displayName.first : nil,
admin: e.try(:admin) ? 'admin' : nil, admin: e.try(:admin) ? 'admin' : nil,
services_enabled: e.try(:serviceEnabled), services_enabled: e.try(:serviceEnabled),
member_status: e.try(:memberStatus),
email_maildrop: e.try(:mailRoutingAddress), email_maildrop: e.try(:mailRoutingAddress),
email_password: e.try(:mailpassword), email_password: e.try(:mailpassword),
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil, nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil,
@@ -78,20 +79,10 @@ class LdapService < ApplicationService
end end
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 def fetch_organizations
attributes = %w{dn ou description} attributes = %w{dn ou description}
filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit") filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit")
# filter = Net::LDAP::Filter.eq("objectClass", "*")
treebase = "cn=users,#{ldap_suffix}" treebase = "cn=users,#{ldap_suffix}"
entries = client.search(base: treebase, filter: filter, attributes: attributes) entries = client.search(base: treebase, filter: filter, attributes: attributes)
+1 -4
View File
@@ -33,10 +33,7 @@ class Lndhub < ApplicationService
end end
def authenticate(user) def authenticate(user)
credentials = post "auth?type=auth", { credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password }
login: user.lndhub_username,
password: user.lndhub_password
}
self.auth_token = credentials["access_token"] self.auth_token = credentials["access_token"]
self.auth_token self.auth_token
end end
@@ -1,12 +0,0 @@
module MastodonManager
class FetchUser < MastodonManagerService
def initialize(mastodon_id:)
@mastodon_id = mastodon_id
end
def call
user = get "v1/admin/accounts/#{@mastodon_id}"
user.with_indifferent_access
end
end
end
@@ -1,14 +0,0 @@
module MastodonManager
class FindUser < MastodonManagerService
def initialize(username:)
@username = username
end
def call
users = get "v2/admin/accounts?username=#{@username}&origin=local"
users = users.map { |u| u.with_indifferent_access }
# Results may contain partial matches
users.find { |u| u.dig(:username).downcase == @username.downcase }
end
end
end
@@ -1,64 +0,0 @@
module MastodonManager
class SyncAccountProfiles < MastodonManagerService
def initialize(direction: "down", overwrite: false, user: nil)
@direction = direction
@overwrite = overwrite
@user = user
if @direction != "down"
raise NotImplementedError
end
end
def call
if @user
Rails.logger.debug { "Syncing account profile for user #{@user.cn} (direction: #{@direction}, overwrite: #{@overwrite})"}
users = User.where(cn: @user.cn)
else
Rails.logger.debug { "Syncing account profiles (direction: #{@direction}, overwrite: #{@overwrite})"}
users = User
end
users.find_each do |user|
if user.mastodon_id.blank?
mastodon_user = MastodonManager::FindUser.call username: user.cn
if mastodon_user
Rails.logger.debug { "Setting mastodon_id for user #{user.cn}" }
user.update! mastodon_id: mastodon_user.dig(:account, :id).to_i
else
Rails.logger.debug { "No Mastodon user found for username #{user.cn}" }
next
end
end
next if user.avatar.attached? && user.display_name.present?
unless mastodon_user
Rails.logger.debug { "Fetching Mastodon account with ID #{user.mastodon_id} for #{user.cn}" }
mastodon_user = MastodonManager::FetchUser.call mastodon_id: user.mastodon_id
end
if user.display_name.blank?
if mastodon_display_name = mastodon_user.dig(:account, :display_name)
Rails.logger.debug { "Setting display name for user #{user.cn} from Mastodon" }
LdapManager::UpdateDisplayName.call(
dn: user.dn, display_name: mastodon_display_name
)
end
end
if !user.avatar.attached?
if avatar_url = mastodon_user.dig(:account, :avatar_static)
Rails.logger.debug { "Importing Mastodon avatar for user #{user.cn}" }
UserManager::ImportRemoteAvatar.call(
user: user, avatar_url: avatar_url
)
end
end
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error e
end
end
end
end
-22
View File
@@ -1,22 +0,0 @@
#
# API Docs: https://docs.joinmastodon.org/methods/
#
class MastodonManagerService < RestApiService
private
def base_url
@base_url ||= "#{Setting.mastodon_public_url}/api"
end
def auth_token
@auth_token ||= Setting.mastodon_auth_token
end
def headers
{
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "Bearer #{auth_token}"
}
end
end
-29
View File
@@ -1,29 +0,0 @@
class RestApiService < ApplicationService
private
def base_url
raise NotImplementedError
end
def headers
raise NotImplementedError
end
def endpoint_url(path)
"#{base_url}/#{path.gsub(/^\//, '')}"
end
def parse_responses?
true
end
def get(path, params = {})
res = Faraday.get endpoint_url(path), params, headers
parse_responses? ? JSON.parse(res.body) : res
end
def post(path, payload)
res = Faraday.post endpoint_url(path), payload.to_json, headers
parse_responses? ? JSON.parse(res.body) : res
end
end
@@ -1,42 +0,0 @@
module UserManager
class ImportRemoteAvatar < UserManagerService
def initialize(user:, avatar_url:)
@user = user
@avatar_url = avatar_url
end
def call
if import_remote_avatar
UserManager::UpdateAvatar.call(user: @user)
end
end
private
def import_remote_avatar
tempfile = Down.download(@avatar_url)
content_type = tempfile.content_type
unless %w[image/jpeg image/png].include?(content_type)
Rails.logger.warn { "Wrong content type of remote avatar for user #{user.cn}: '#{content_type}'" }
return false
end
img_data = UserManager::ProcessAvatar.call(io: tempfile)
tempfile = Tempfile.create
tempfile.binmode
tempfile.write(img_data)
tempfile.rewind
hash = Digest::SHA256.hexdigest(img_data)
ext = content_type == "image/png" ? "png" : "jpg"
filename = "#{hash}.#{ext}"
key = "users/#{@user.cn}/avatars/#{filename}"
@user.avatar.attach io: tempfile, key: key, filename: filename
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.warn "Importing remote avatar failed: \"#{e.message}\""
false
end
end
end
@@ -1,21 +0,0 @@
module UserManager
class ProcessAvatar < UserManagerService
def initialize(io:)
@io = io
end
def call
processed = ImageProcessing::Vips
.source(@io)
.resize_to_fill(400, 400)
.saver(strip: true)
.call
@io.rewind
processed.read
rescue Vips::Error => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.warn { "Image processing failed for avatar: #{e.message}" }
nil
end
end
end
@@ -1,16 +0,0 @@
module UserManager
class UpdateAvatar < UserManagerService
def initialize(user:)
@user = user
end
def call
LdapManager::UpdateAvatar.call(user: @user)
if Setting.ejabberd_enabled?
return if Rails.env.development?
XmppSetAvatarJob.perform_later(user: @user)
end
end
end
end
@@ -1,11 +0,0 @@
<%= form_with url: path, method: :get, local: true, class: "flex gap-1" do %>
<%= text_field_tag :username, @username, placeholder: 'Filter by username' %>
<%= button_tag type: 'submit', name: nil, title: "Filter", class: 'btn-md btn-icon btn-outline' do %>
<%= render partial: "icons/filter", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
<% end %>
<% if @username %>
<%= link_to path, title: "Remove filter", class: 'btn-md btn-icon btn-outline' do %>
<%= render partial: "icons/x", locals: { custom_class: "text-red-600 h-4 w-4 inline" } %>
<% end %>
<% end %>
<% end %>
@@ -38,7 +38,8 @@
<tr> <tr>
<td><%= web_app.name %></td> <td><%= web_app.name %></td>
<td><%= link_to web_app.url, web_app.url, <td><%= link_to web_app.url, web_app.url,
target: "_blank", rel: "nofollow noopener" %></td> target: "_blank", rel: "nofollow noopener",
class: "ks-text-link" %></td>
<td class="hidden md:table-cell"><%= web_app.remote_storage_authorizations.count %></td> <td class="hidden md:table-cell"><%= web_app.remote_storage_authorizations.count %></td>
<td class="hidden md:table-cell"> <td class="hidden md:table-cell">
<span title="<%= web_app.created_at %>" class="cursor-help"> <span title="<%= web_app.created_at %>" class="cursor-help">
-34
View File
@@ -1,34 +0,0 @@
<table class="divided">
<thead>
<tr>
<th>User</th>
<th class="text-right">Sats</th>
<th class="text-right">Fiat Amount</th>
<th class="pl-2">Public name</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
<% donations.each do |donation| %>
<tr>
<td><%= link_to donation.user.cn, admin_user_path(donation.user.cn) %></td>
<td class="text-right"><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></td>
<td class="text-right"><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %></td>
<td class="pl-2"><%= donation.public_name %></td>
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : donation.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
<td class="text-right">
<%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% if defined?(pagy) %>
<div class="mt-8">
<%== pagy_nav pagy %>
</div>
<% end %>
+34 -21
View File
@@ -5,7 +5,7 @@
<%= render QuickstatsContainerComponent.new do %> <%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new( <%= render QuickstatsItemComponent.new(
type: :number, type: :number,
title: 'Received', title: 'Overall',
value: @stats[:overall_sats], value: @stats[:overall_sats],
unit: 'sats' unit: 'sats'
) %> ) %>
@@ -19,28 +19,41 @@
</section> </section>
<section> <section>
<%= render partial: "admin/username_search_form", <% if @donations.any? %>
locals: { path: admin_donations_path } %> <h3>Recent Donations</h3>
</section> <table class="divided mb-8">
<thead>
<% if @pending_donations.present? %> <tr>
<section> <th>User</th>
<h3>Pending</h3> <th class="text-right">Sats</th>
<%= render partial: "admin/donations/list", locals: { <th class="text-right">Fiat Amount</th>
donations: @pending_donations <th class="pl-2">Public name</th>
} %> <th>Date</th>
</section> <th></th>
<% end %> </tr>
</thead>
<section> <tbody>
<% if @donations.present? %> <% @donations.each do |donation| %>
<h3>Received</h3> <tr>
<%= render partial: "admin/donations/list", locals: { <td><%= link_to donation.user.cn, admin_user_path(donation.user.cn), class: 'ks-text-link' %></td>
donations: @donations, pagy: @pagy <td class="text-right"><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></td>
} %> <td class="text-right"><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %></td>
<td class="pl-2"><%= donation.public_name %></td>
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td>
<td class="text-right">
<%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%== pagy_nav @pagy %>
<% else %> <% else %>
<p> <p>
No donations received yet. No donations yet.
</p> </p>
<% end %> <% end %>
</section> </section>
+2 -10
View File
@@ -6,7 +6,7 @@
<tbody> <tbody>
<tr> <tr>
<th>User</th> <th>User</th>
<td><%= link_to @donation.user.cn, admin_user_path(@donation.user.cn) %></td> <td><%= link_to @donation.user.cn, admin_user_path(@donation.user.cn), class: 'ks-text-link' %></td>
</tr> </tr>
<tr> <tr>
<th>Donation Method</th> <th>Donation Method</th>
@@ -25,15 +25,7 @@
<td><%= @donation.public_name %></td> <td><%= @donation.public_name %></td>
</tr> </tr>
<tr> <tr>
<th>Payment status</th> <th>Date</th>
<td><%= @donation.payment_status %></td>
</tr>
<tr>
<th>Created at</th>
<td><%= @donation.created_at&.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
<tr>
<th>Paid at</th>
<td><%= @donation.paid_at&.strftime("%Y-%m-%d (%H:%M UTC)") %></td> <td><%= @donation.paid_at&.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr> </tr>
</tbody> </tbody>

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