1 Commits

Author SHA1 Message Date
Râu Cao
462dd24da3 WIP contribution nav
All checks were successful
continuous-integration/drone/push Build is passing
2023-06-12 14:32:59 +02:00
234 changed files with 1070 additions and 6925 deletions

View File

@@ -12,24 +12,20 @@ steps:
settings: settings:
restore: true restore: true
mount: mount:
- ./vendor/cache - ./vendor
when: when:
branch: branch:
- master - master
- name: rspec - name: rspec
image: gitea.kosmos.org/kosmos/akkounts-ci:0.9.1 image: guildeducation/rails:2.7.2-14.20.0
environment: environment:
RAILS_ENV: test RAILS_ENV: test
REDIS_URL: redis://redis:6379/0
RS_REDIS_URL: redis://redis:6379/1
commands: commands:
- bundle config unset deployment - bundle config unset deployment
- bundle config set cache_all 'true' - bundle config set cache_all 'true'
- bundle config set cache_path 'vendor/cache' - bundle config set cache_path 'vendor/cache'
- bundle config set with 'development test' - bundle config set with 'development test'
- bundle install --jobs=3 --retry=3 - bundle install --jobs=3 --retry=3
- bundle exec rails db:create
- bundle exec rails db:migrate
- yarn install - yarn install
- rake css:build - rake css:build
- bundle exec rspec - bundle exec rspec
@@ -41,15 +37,11 @@ steps:
settings: settings:
rebuild: true rebuild: true
mount: mount:
- ./vendor/cache - ./vendor
when: when:
branch: branch:
- master - master
services:
- name: redis
image: redis
volumes: volumes:
- name: cache - name: cache
host: host:

View File

@@ -1,64 +1,42 @@
# PRIMARY_DOMAIN=kosmos.org AKKOUNTS_DOMAIN=accounts.example.com
# AKKOUNTS_DOMAIN=accounts.example.com
# SMTP_SERVER=smtp.example.com SMTP_SERVER=smtp.example.com
# SMTP_PORT=587 SMTP_PORT=587
# SMTP_LOGIN=accounts SMTP_LOGIN=accounts
# SMTP_PASSWORD=123abc SMTP_PASSWORD=123abc
# SMTP_FROM_ADDRESS=accounts@example.com SMTP_FROM_ADDRESS=accounts@example.com
# SMTP_DOMAIN=example.com SMTP_DOMAIN=example.com
# SMTP_AUTH_METHOD=plain SMTP_AUTH_METHOD=plain
# SMTP_ENABLE_STARTTLS=auto SMTP_ENABLE_STARTTLS=auto
# S3_ENABLED=true REDIS_URL='redis://localhost:6379/1'
# S3_ENDPOINT=https://s3.kosmos.org
# S3_REGION=garage
# S3_BUCKET=akkounts-production
# S3_ALIAS_HOST=https://accounts.web.s3.kosmos.org
# S3_ACCESS_KEY=123456abcdefg
# S3_SECRET_KEY=123456789123456789123456789
# LDAP_HOST=localhost LDAP_HOST=localhost
# LDAP_PORT=389 LDAP_PORT=389
# 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' WEBHOOKS_ALLOWED_IPS='10.1.1.163'
# WEBHOOKS_ALLOWED_IPS='10.1.1.163' DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
# GITEA_PUBLIC_URL='https://gitea.kosmos.org'
# Service Integrations MASTODON_PUBLIC_URL='https://kosmos.social'
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
RS_STORAGE_URL='https://storage.kosmos.org'
# BTCPAY_API_URL='http://localhost:23001/api/v1' EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
# BTCPAY_STORE_ID='' EJABBERD_API_URL='https://xmpp.kosmos.org/api'
# BTCPAY_AUTH_TOKEN=''
# DISCOURSE_PUBLIC_URL='https://community.kosmos.org' BTCPAY_API_URL='http://localhost:23001/api/v1'
# DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
# DRONECI_PUBLIC_URL='https://drone.kosmos.org' LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
# EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin' LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
# EJABBERD_API_URL='https://xmpp.kosmos.org/api' LNDHUB_ADMIN_UI=true
LNDHUB_PG_HOST=localhost
# GITEA_PUBLIC_URL='https://gitea.kosmos.org' LNDHUB_PG_PORT=5432
LNDHUB_PG_DATABASE=lndhub
# LNDHUB_API_URL='http://localhost:3023' LNDHUB_PG_USERNAME=lndhub
# LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' LNDHUB_PG_PASSWORD=''
# LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
# LNDHUB_ADMIN_UI=true
# LNDHUB_ADMIN_TOKEN=123456789
# LNDHUB_PG_HOST=localhost
# LNDHUB_PG_PORT=5432
# LNDHUB_PG_DATABASE=lndhub
# LNDHUB_PG_USERNAME=lndhub
# LNDHUB_PG_PASSWORD=''
# MASTODON_PUBLIC_URL='https://kosmos.social'
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
# RS_STORAGE_URL='https://storage.kosmos.org'
# RS_REDIS_URL='redis://localhost:6379/2'

View File

@@ -1,20 +1,14 @@
PRIMARY_DOMAIN=kosmos.org
REDIS_URL='redis://localhost:6379/0'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
BTCPAY_STORE_ID='123456'
DISCOURSE_PUBLIC_URL='http://discourse.example.com' DISCOURSE_PUBLIC_URL='http://discourse.example.com'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw' DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
EJABBERD_API_URL='http://xmpp.example.com/api' EJABBERD_API_URL='http://xmpp.example.com/api'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
LNDHUB_API_URL='http://localhost:3026' LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946' LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
RS_STORAGE_URL='https://storage.kosmos.org' RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/1'
WEBHOOKS_ALLOWED_IPS='10.1.1.23' WEBHOOKS_ALLOWED_IPS='10.1.1.23'

View File

@@ -7,7 +7,6 @@ version-resolver:
minor: minor:
labels: labels:
- 'release/minor' - 'release/minor'
- 'feature'
patch: patch:
labels: labels:
- 'release/patch' - 'release/patch'

2
.gitignore vendored
View File

@@ -23,7 +23,6 @@
!/tmp/pids/ !/tmp/pids/
!/tmp/pids/.keep !/tmp/pids/.keep
/storage
/public/assets /public/assets
.byebug_history .byebug_history
@@ -40,7 +39,6 @@ yarn-debug.log*
# Ignore local dotenv config file # Ignore local dotenv config file
.env .env
.env.development
# Ignore redis dumps from sidekiq # Ignore redis dumps from sidekiq
dump.rdb dump.rdb

View File

@@ -1 +1 @@
3.3.0 2.7.2

View File

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

26
Gemfile
View File

@@ -2,7 +2,7 @@ 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', '~> 7.1' gem 'rails', '~> 7.0.2'
# Use Puma as the app server # Use Puma as the app server
gem 'puma', '~> 4.1' gem 'puma', '~> 4.1'
# View components # View components
@@ -22,7 +22,7 @@ gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production # Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0' # gem 'redis', '~> 4.0'
# Use Active Model has_secure_password # Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1' # gem 'bcrypt', '~> 3.1.7'
# Configuration # Configuration
gem 'dotenv-rails' gem 'dotenv-rails'
@@ -37,7 +37,6 @@ gem 'devise_ldap_authenticatable'
gem 'net-ldap' gem 'net-ldap'
# Utilities # Utilities
gem "image_processing", "~> 1.12.2"
gem "rqrcode", "~> 2.0" gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3' gem 'rails-settings-cached', '~> 2.8.3'
gem 'pagy', '~> 6.0', '>= 6.0.2' gem 'pagy', '~> 6.0', '>= 6.0.2'
@@ -47,33 +46,28 @@ gem 'flipper-ui'
# HTTP requests # HTTP requests
gem 'faraday' gem 'faraday'
gem 'down'
gem 'aws-sdk-s3', require: false
# Background/scheduled jobs # Background/scheduled jobs
gem 'sidekiq', '< 7' gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler' gem 'sidekiq-scheduler'
# Service integrations
gem 'discourse_api'
# Monitoring # Monitoring
gem "sentry-ruby" gem "sentry-ruby"
gem "sentry-rails" gem "sentry-rails"
# Services
gem 'discourse_api'
gem "lnurl"
gem 'manifique'
gem 'nostr'
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', '~> 1.7.2' gem 'sqlite3', '~> 1.4'
gem 'rspec-rails' gem 'rspec-rails'
gem 'rails-controller-testing' gem "byebug", "~> 11.1"
end end
group :development do group :development do
# Access an interactive console on exception pages or by calling 'console' anywhere in the code. # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
gem 'web-console', '~> 4.2' gem 'web-console', '>= 3.3.0'
gem 'listen', '~> 3.2' gem 'listen', '~> 3.2'
gem 'letter_opener' gem 'letter_opener'
gem 'letter_opener_web' gem 'letter_opener_web'
@@ -89,8 +83,8 @@ group :test do
end end
group :production do group :production do
gem 'pg', '~> 1.5' # Use postgresql as the database for Active Record
gem 'pg', '~> 1.2.3'
end end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

View File

@@ -1,136 +1,104 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.1.3) actioncable (7.0.4)
actionpack (= 7.1.3) actionpack (= 7.0.4)
activesupport (= 7.1.3) activesupport (= 7.0.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) actionmailbox (7.0.4)
actionmailbox (7.1.3) actionpack (= 7.0.4)
actionpack (= 7.1.3) activejob (= 7.0.4)
activejob (= 7.1.3) activerecord (= 7.0.4)
activerecord (= 7.1.3) activestorage (= 7.0.4)
activestorage (= 7.1.3) activesupport (= 7.0.4)
activesupport (= 7.1.3)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.1.3) actionmailer (7.0.4)
actionpack (= 7.1.3) actionpack (= 7.0.4)
actionview (= 7.1.3) actionview (= 7.0.4)
activejob (= 7.1.3) activejob (= 7.0.4)
activesupport (= 7.1.3) activesupport (= 7.0.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.0)
actionpack (7.1.3) actionpack (7.0.4)
actionview (= 7.1.3) actionview (= 7.0.4)
activesupport (= 7.1.3) activesupport (= 7.0.4)
nokogiri (>= 1.8.5) rack (~> 2.0, >= 2.2.0)
racc
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.1.3) actiontext (7.0.4)
actionpack (= 7.1.3) actionpack (= 7.0.4)
activerecord (= 7.1.3) activerecord (= 7.0.4)
activestorage (= 7.1.3) activestorage (= 7.0.4)
activesupport (= 7.1.3) activesupport (= 7.0.4)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.1.3) actionview (7.0.4)
activesupport (= 7.1.3) activesupport (= 7.0.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.4)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.1.3) activejob (7.0.4)
activesupport (= 7.1.3) activesupport (= 7.0.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.3) activemodel (7.0.4)
activesupport (= 7.1.3) activesupport (= 7.0.4)
activerecord (7.1.3) activerecord (7.0.4)
activemodel (= 7.1.3) activemodel (= 7.0.4)
activesupport (= 7.1.3) activesupport (= 7.0.4)
timeout (>= 0.4.0) activestorage (7.0.4)
activestorage (7.1.3) actionpack (= 7.0.4)
actionpack (= 7.1.3) activejob (= 7.0.4)
activejob (= 7.1.3) activerecord (= 7.0.4)
activerecord (= 7.1.3) activesupport (= 7.0.4)
activesupport (= 7.1.3)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.3) mini_mime (>= 1.1.0)
base64 activesupport (7.0.4)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.6) addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.886.0)
aws-sdk-core (3.191.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.77.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.143.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
backport (1.2.0) backport (1.2.0)
base64 (0.2.0) bcrypt (3.1.18)
bcrypt (3.1.20) benchmark (0.2.1)
bech32 (1.4.2)
thor (>= 1.1.0)
benchmark (0.3.0)
bigdecimal (3.1.6)
bindex (0.8.1) bindex (0.8.1)
bip-schnorr (0.7.0)
ecdsa_ext (~> 0.5.0)
builder (3.2.4) builder (3.2.4)
capybara (3.40.0) byebug (11.1.3)
capybara (3.38.0)
addressable addressable
matrix matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.11) nokogiri (~> 1.8)
rack (>= 1.6.0) rack (>= 1.6.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
chunky_png (1.4.0) chunky_png (1.4.0)
concurrent-ruby (1.2.3) concurrent-ruby (1.1.10)
connection_pool (2.4.1) connection_pool (2.3.0)
crack (0.4.6) crack (0.4.5)
bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
cssbundling-rails (1.4.0) cssbundling-rails (1.1.1)
railties (>= 6.0.0) railties (>= 6.0.0)
database_cleaner (2.0.2) database_cleaner (2.0.1)
database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (~> 2.0.0)
database_cleaner-active_record (2.1.0) database_cleaner-active_record (2.0.1)
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.3.4) devise (4.9.0)
devise (4.9.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@@ -139,8 +107,8 @@ GEM
devise_ldap_authenticatable (0.8.7) devise_ldap_authenticatable (0.8.7)
devise (>= 3.4.1) devise (>= 3.4.1)
net-ldap (>= 0.16.0) net-ldap (>= 0.16.0)
diff-lcs (1.5.1) diff-lcs (1.5.0)
discourse_api (2.0.1) discourse_api (2.0.0)
faraday (~> 2.7) faraday (~> 2.7)
faraday-follow_redirects faraday-follow_redirects
faraday-multipart faraday-multipart
@@ -149,81 +117,59 @@ GEM
dotenv-rails (2.8.1) dotenv-rails (2.8.1)
dotenv (= 2.8.1) dotenv (= 2.8.1)
railties (>= 3.2) railties (>= 3.2)
down (5.4.1)
addressable (~> 2.8)
drb (2.2.0)
ruby2_keywords
e2mmap (0.1.0) e2mmap (0.1.0)
ecdsa (1.2.0) erubi (1.11.0)
ecdsa_ext (0.5.0)
ecdsa (~> 1.2.0)
erubi (1.12.0)
et-orbi (1.2.7) et-orbi (1.2.7)
tzinfo tzinfo
event_emitter (0.2.6) factory_bot (6.2.1)
eventmachine (1.2.7)
factory_bot (6.4.6)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.3) factory_bot_rails (6.2.0)
factory_bot (~> 6.4) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.2.3) faker (3.0.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.9.0) faraday (2.7.1)
faraday-net_http (>= 2.0, < 3.2) faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-follow_redirects (0.3.0) faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3) faraday (>= 1, < 3)
faraday-multipart (1.0.4) faraday-multipart (1.0.4)
multipart-post (~> 2) multipart-post (~> 2)
faraday-net_http (3.1.0) faraday-net_http (3.0.2)
net-http ffi (1.15.5)
faye-websocket (0.11.3) flipper (0.28.0)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.16.3)
flipper (1.2.2)
concurrent-ruby (< 2) concurrent-ruby (< 2)
flipper-active_record (1.2.2) flipper-active_record (0.28.0)
activerecord (>= 4.2, < 8) activerecord (>= 4.2, < 8)
flipper (~> 1.2.2) flipper (~> 0.28.0)
flipper-ui (1.2.2) flipper-ui (0.28.0)
erubi (>= 1.0.0, < 2.0.0) erubi (>= 1.0.0, < 2.0.0)
flipper (~> 1.2.2) flipper (~> 0.28.0)
rack (>= 1.4, < 4) rack (>= 1.4, < 3)
rack-protection (>= 1.5.3, <= 4.0.0) rack-protection (>= 1.5.3, <= 4.0.0)
sanitize (< 7) sanitize (< 7)
fugit (1.9.0) fugit (1.7.2)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.2.1) globalid (1.0.0)
activesupport (>= 6.1) activesupport (>= 5.0)
hashdiff (1.1.0) hashdiff (1.0.1)
i18n (1.14.1) i18n (1.12.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) importmap-rails (1.1.5)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (2.0.1)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
io-console (0.7.2) jaro_winkler (1.5.4)
irb (1.11.1)
rdoc
reline (>= 0.4.2)
jaro_winkler (1.5.6)
jbuilder (2.11.5) jbuilder (2.11.5)
actionview (>= 5.0.0) actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
jmespath (1.6.2) json (2.6.3)
json (2.7.1)
kramdown (2.4.0) kramdown (2.4.0)
rexml 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.3) launchy (2.5.0)
launchy (2.5.2) addressable (~> 2.7)
addressable (~> 2.8)
letter_opener (1.8.1) letter_opener (1.8.1)
launchy (>= 2.2, < 3) launchy (>= 2.2, < 3)
letter_opener_web (2.0.0) letter_opener_web (2.0.0)
@@ -231,268 +177,220 @@ GEM
letter_opener (~> 1.7) letter_opener (~> 1.7)
railties (>= 5.2) railties (>= 5.2)
rexml rexml
listen (3.8.0) listen (3.7.1)
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.0) lockbox (1.1.0)
bech32 (~> 1.1) loofah (2.19.0)
lockbox (1.3.2)
loofah (2.22.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.5.9)
mail (2.8.1) mail (2.7.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
manifique (1.0.1)
faraday (~> 2.9.0)
faraday-follow_redirects (= 0.3.0)
nokogiri (~> 1.16.0)
marcel (1.0.2) marcel (1.0.2)
matrix (0.4.2) matrix (0.4.2)
method_source (1.0.0) method_source (1.0.0)
mini_magick (4.12.0) mini_mime (1.1.2)
mini_mime (1.1.5) mini_portile2 (2.8.0)
mini_portile2 (2.8.5) minitest (5.16.3)
minitest (5.21.2)
multipart-post (2.3.0) multipart-post (2.3.0)
mutex_m (0.2.0) net-imap (0.3.1)
net-http (0.4.1)
uri
net-imap (0.4.9.1)
date
net-protocol net-protocol
net-ldap (0.19.0) net-ldap (0.17.1)
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.1.3)
timeout timeout
net-smtp (0.4.0.1) net-smtp (0.3.3)
net-protocol net-protocol
nio4r (2.7.0) nio4r (2.5.8)
nokogiri (1.16.0) nokogiri (1.13.9)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.0-arm64-darwin) nokogiri (1.13.9-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.0-x86_64-linux)
racc (~> 1.4)
nostr (0.5.0)
bech32 (~> 1.4)
bip-schnorr (~> 0.6)
ecdsa (~> 1.2)
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
json (~> 2.6)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pagy (6.4.3) pagy (6.0.2)
parallel (1.24.0) parallel (1.22.1)
parser (3.3.0.5) parser (3.2.1.1)
ast (~> 2.4.1) ast (~> 2.4.1)
racc pg (1.2.3)
pg (1.5.4) public_suffix (5.0.0)
psych (5.1.2)
stringio
public_suffix (5.0.4)
puma (4.3.12) puma (4.3.12)
nio4r (~> 2.0) nio4r (~> 2.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.7.3) racc (1.6.0)
rack (2.2.8) rack (2.2.4)
rack-protection (3.2.0) rack-protection (3.0.6)
base64 (>= 0.1.0) rack
rack (~> 2.2, >= 2.2.4) rack-test (2.0.2)
rack-session (1.0.2)
rack (< 3)
rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rackup (1.0.0) rails (7.0.4)
rack (< 3) actioncable (= 7.0.4)
webrick actionmailbox (= 7.0.4)
rails (7.1.3) actionmailer (= 7.0.4)
actioncable (= 7.1.3) actionpack (= 7.0.4)
actionmailbox (= 7.1.3) actiontext (= 7.0.4)
actionmailer (= 7.1.3) actionview (= 7.0.4)
actionpack (= 7.1.3) activejob (= 7.0.4)
actiontext (= 7.1.3) activemodel (= 7.0.4)
actionview (= 7.1.3) activerecord (= 7.0.4)
activejob (= 7.1.3) activestorage (= 7.0.4)
activemodel (= 7.1.3) activesupport (= 7.0.4)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.3) railties (= 7.0.4)
rails-controller-testing (1.0.5) rails-dom-testing (2.0.3)
actionpack (>= 5.0.1.rc1) activesupport (>= 4.2.0)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.4.3)
loofah (~> 2.21) loofah (~> 2.3)
nokogiri (~> 1.14)
rails-settings-cached (2.8.3) rails-settings-cached (2.8.3)
activerecord (>= 5.0.0) activerecord (>= 5.0.0)
railties (>= 5.0.0) railties (>= 5.0.0)
railties (7.1.3) railties (7.0.4)
actionpack (= 7.1.3) actionpack (= 7.0.4)
activesupport (= 7.1.3) activesupport (= 7.0.4)
irb method_source
rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0)
zeitwerk (~> 2.6) zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.1.0) rake (13.0.6)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
rbs (2.8.4) redis (5.0.5)
rdoc (6.6.2) redis-client (>= 0.9.0)
psych (>= 4.0.0) redis-client (0.11.2)
redis (4.8.1) connection_pool
regexp_parser (2.9.0) regexp_parser (2.6.1)
reline (0.4.2) responders (3.1.0)
io-console (~> 0.5)
responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
reverse_markdown (2.1.1) reverse_markdown (2.1.1)
nokogiri nokogiri
rexml (3.2.6) rexml (3.2.5)
rqrcode (2.2.0) rqrcode (2.1.2)
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.12.2) rspec-core (3.12.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-expectations (3.12.3) rspec-expectations (3.12.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-mocks (3.12.6) rspec-mocks (3.12.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-rails (6.1.1) rspec-rails (6.0.1)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
rspec-core (~> 3.12) rspec-core (~> 3.11)
rspec-expectations (~> 3.12) rspec-expectations (~> 3.11)
rspec-mocks (~> 3.12) rspec-mocks (~> 3.11)
rspec-support (~> 3.12) rspec-support (~> 3.11)
rspec-support (3.12.1) rspec-support (3.12.0)
rubocop (1.60.2) rubocop (1.48.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.26.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0) rubocop-ast (1.28.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.0)
ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rufus-scheduler (3.9.1) rufus-scheduler (3.8.2)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
sanitize (6.1.0) sanitize (6.0.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
sentry-rails (5.16.1) sentry-rails (5.8.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.16.1) sentry-ruby (~> 5.8.0)
sentry-ruby (5.16.1) sentry-ruby (5.8.0)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (6.5.12) sidekiq (6.5.5)
connection_pool (>= 2.2.5, < 3) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
redis (>= 4.5.0, < 5) redis (>= 4.5.0)
sidekiq-scheduler (5.0.3) sidekiq-scheduler (4.0.3)
redis (>= 4.2.0)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8) sidekiq (>= 4, < 7)
tilt (>= 1.4.0) tilt (>= 1.4.0)
solargraph (0.50.0) solargraph (0.48.0)
backport (~> 1.2) backport (~> 1.2)
benchmark benchmark
bundler (~> 2.0) bundler (>= 1.17.2)
diff-lcs (~> 1.4) diff-lcs (~> 1.4)
e2mmap e2mmap
jaro_winkler (~> 1.5) jaro_winkler (~> 1.5)
kramdown (~> 2.3) kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1) kramdown-parser-gfm (~> 1.1)
parser (~> 3.0) parser (~> 3.0)
rbs (~> 2.0) reverse_markdown (>= 1.0.5, < 3)
reverse_markdown (~> 2.0) rubocop (>= 0.52)
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)
sprockets (4.2.1) sprockets (4.1.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4) rack (> 1, < 3)
sprockets-rails (3.4.2) sprockets-rails (3.4.2)
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.7.2) sqlite3 (1.5.4)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (1.7.2-arm64-darwin) sqlite3 (1.5.4-x86_64-linux)
sqlite3 (1.7.2-x86_64-linux) stimulus-rails (1.2.1)
stimulus-rails (1.3.3)
railties (>= 6.0.0) railties (>= 6.0.0)
stringio (3.1.0) thor (1.2.1)
thor (1.3.0) tilt (2.0.11)
tilt (2.3.0) timeout (0.3.0)
timeout (0.4.1) turbo-rails (1.3.2)
turbo-rails (1.5.0)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0) activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
tzinfo (2.0.6) tzinfo (2.0.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0) unicode-display_width (2.4.2)
uri (0.13.0) view_component (2.78.0)
view_component (3.10.0) activesupport (>= 5.0.0, < 8.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
method_source (~> 1.0) method_source (~> 1.0)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.0)
actionview (>= 6.0.0) actionview (>= 6.0.0)
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webmock (3.19.1) webmock (3.18.1)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.1) webrick (1.7.0)
websocket-driver (0.7.6) websocket-driver (0.7.5)
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.34) yard (0.9.28)
zeitwerk (2.6.12) webrick (~> 1.7.0)
zeitwerk (2.6.6)
PLATFORMS PLATFORMS
arm64-darwin-22
ruby ruby
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
aws-sdk-s3 byebug (~> 11.1)
bcrypt (~> 3.1)
capybara capybara
cssbundling-rails cssbundling-rails
database_cleaner database_cleaner
@@ -500,29 +398,23 @@ DEPENDENCIES
devise_ldap_authenticatable devise_ldap_authenticatable
discourse_api discourse_api
dotenv-rails dotenv-rails
down
factory_bot_rails factory_bot_rails
faker faker
faraday faraday
flipper flipper
flipper-active_record flipper-active_record
flipper-ui flipper-ui
image_processing (~> 1.12.2)
importmap-rails importmap-rails
jbuilder (~> 2.7) jbuilder (~> 2.7)
letter_opener letter_opener
letter_opener_web letter_opener_web
listen (~> 3.2) listen (~> 3.2)
lnurl
lockbox lockbox
manifique
net-ldap net-ldap
nostr
pagy (~> 6.0, >= 6.0.2) pagy (~> 6.0, >= 6.0.2)
pg (~> 1.5) pg (~> 1.2.3)
puma (~> 4.1) puma (~> 4.1)
rails (~> 7.1) rails (~> 7.0.2)
rails-controller-testing
rails-settings-cached (~> 2.8.3) rails-settings-cached (~> 2.8.3)
rqrcode (~> 2.0) rqrcode (~> 2.0)
rspec-rails rspec-rails
@@ -532,14 +424,14 @@ DEPENDENCIES
sidekiq-scheduler sidekiq-scheduler
solargraph solargraph
sprockets-rails sprockets-rails
sqlite3 (~> 1.7.2) sqlite3 (~> 1.4)
stimulus-rails stimulus-rails
turbo-rails turbo-rails
tzinfo-data tzinfo-data
view_component view_component
warden warden
web-console (~> 4.2) web-console (>= 3.3.0)
webmock webmock
BUNDLED WITH BUNDLED WITH
2.5.5 2.3.7

View File

@@ -14,10 +14,9 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in 1. Make sure [Docker Compose is installed][1] and Docker is running (included in
Docker Desktop) Docker Desktop)
3. Run `docker compose up --build` and wait until all services have started 2. Uncomment the `redis`, `web`, and `sidekiq` sections in `docker-compose.yml`
(389ds might take an extra minute to be ready). This will take a while when 3. Run `docker compose up` and wait until 389ds announces its successful start
running for the first time, so you might want to do something else in the in the log output
meantime.
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"` 4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
5. `docker compose run web rails ldap:setup` 5. `docker compose run web rails ldap:setup`
6. `docker compose run web rails db:setup` 6. `docker compose run web rails db:setup`
@@ -30,44 +29,36 @@ have the password "user is user".
### Rails app ### Rails app
_Note: when using Docker Compose, prefix the following commands with `docker-compose
run web`._
Installing dependencies: Installing dependencies:
bundle install bundle install
yarn install yarn install
Migrating the local database (after schema changes): Setting up local database (SQLite):
bundle exec rails db:create
bundle exec rails db:migrate bundle exec rails db:migrate
Running the dev server, and auto-building CSS files on change _(automatic with Docker Compose)_: Running the dev server and auto-building CSS files on change:
bin/dev bin/dev
Running the background workers (requires Redis) _(automatic with Docker Compose)_: Running the background workers (requires Redis):
bundle exec sidekiq -C config/sidekiq.yml bundle exec sidekiq -C config/sidekiq.yml
Running the test suite: Running all specs:
bundle exec rspec bundle exec rspec
Running the test suite with Docker Compose requires overriding the Rails ### Docker (Compose)
environment:
docker-compose run -e "RAILS_ENV=test" web rspec There is a working Docker Compose config file, which allows you to spin up both
an app server for Rails as well as a local 389ds (LDAP) server.
### Docker Compose By default, `docker-compose up` will only start the LDAP server, listening on
port 389 on your machine. Uncomment other services in `docker-compose.yml` if
Services/containers are configured in `docker-compose.yml`. you want to use them.
You can run services selectively, for example if you want to run the Rails app
and test suite on the host machine. Just add the service names of the
containers you want to run to the `up` command, like so:
docker-compose up ldap redis
#### LDAP server #### LDAP server
@@ -84,31 +75,8 @@ Now you can seed the back-end with data using this Rails task:
The setup task will first delete any existing entries in the directory tree The setup task will first delete any existing entries in the directory tree
("dc=kosmos,dc=org"), and then create our development entries. ("dc=kosmos,dc=org"), and then create our development entries.
Note that all 389ds data is stored in the `389ds-data` volume. So if you want Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
to start over with a fresh installation, delete both that volume as well as the with a fresh installation, delete both that directory as well as the container.
container.
#### Minio / remoteStorage
If you want to run remoteStorage accounts locally, you will have to create the
respective bucket first. With the `minio` container running (run by default
when using Docker Compose), follow these steps:
* `docker compose up web redis minio liquor-cabinet`
* Head to http://localhost:9001 and log in with user `minioadmin`, password
`minioadmin`
* Create a new bucket called `remotestorage` (or whatever you
change the `S3_BUCKET` config to)
* Create a new key with ID "dev-key" and secret "123456789" (or whatever you
change `S3_ACCESS_KEY` and `S3_SECRET_KEY` to). Leave the policy field empty,
as it will automatically allow access to the bucket you created.
### Adding npm modules to use with Stimulus controllers
The following command downloads the specified npm module to `vendor/javascript`
and adds an entry for it to `config/importmap.rb`.
bin/importmap pin bech32 --download
### Solargraph ### Solargraph
@@ -130,8 +98,6 @@ command:
* [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/)
* [Tailwind Stimulus Components](https://github.com/excid3/tailwindcss-stimulus-components)
### Testing ### Testing

View File

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

View File

@@ -2,7 +2,6 @@
@import "tailwindcss/components"; @import "tailwindcss/components";
@import "tailwindcss/utilities"; @import "tailwindcss/utilities";
@import "components/animations";
@import "components/base"; @import "components/base";
@import "components/buttons"; @import "components/buttons";
@import "components/dashboard_services"; @import "components/dashboard_services";

View File

@@ -1,16 +0,0 @@
@keyframes scaleIn {
from {
transform: scale(0.5);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.animate-scale-in {
animation-name: scaleIn;
animation-duration: 0.15s;
animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1);
}

View File

@@ -24,10 +24,6 @@
@apply text-xl mb-6; @apply text-xl mb-6;
} }
h4 {
@apply font-bold mb-4 leading-6;
}
main section { main section {
@apply pt-8 sm:pt-12; @apply pt-8 sm:pt-12;
} }

View File

@@ -15,11 +15,7 @@
} }
.btn-icon { .btn-icon {
@apply py-2 px-3; @apply px-3;
}
.btn-outline {
@apply py-2 border-2 border-gray-100 hover:bg-gray-100;
} }
.btn-gray { .btn-gray {
@@ -36,9 +32,4 @@
@apply bg-red-600 hover:bg-red-700 text-white @apply bg-red-600 hover:bg-red-700 text-white
focus:ring-red-500 focus:ring-opacity-75; focus:ring-red-500 focus:ring-opacity-75;
} }
.btn:disabled {
@apply bg-gray-100 hover:bg-gray-200 text-gray-400
focus:ring-gray-300 focus:ring-opacity-75;
}
} }

View File

@@ -15,10 +15,6 @@
@apply border-b-red-600; @apply border-b-red-600;
} }
.field_with_errors {
@apply inline-block;
}
.error-msg { .error-msg {
@apply text-red-700; @apply text-red-700;
} }

View File

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

View File

@@ -1,21 +0,0 @@
# frozen_string_literal: true
module AppCatalog
class WebAppIconComponent < ViewComponent::Base
def initialize(web_app:)
if web_app&.icon&.attached?
@image_url = image_url_for(web_app.icon)
elsif web_app&.apple_touch_icon&.attached?
@image_url = image_url_for(web_app.apple_touch_icon)
end
end
def image_url_for(attachment)
if Setting.s3_enabled?
s3_image_url(attachment)
else
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
end
end
end
end

View File

@@ -1,15 +0,0 @@
<div class="flex">
<div class="<%= @icon_container_class %>">
<%= image_tag(@icon_path, class: 'h-full w-full') %>
</div>
<div class="flex-1 px-4">
<h4 class="sm:pt-2 mb-2 text-lg font-bold"><%= @name %></h4>
<p class="leading-snug"><%= @description %></p>
<p class="leading-snug flex flex-wrap gap-3">
<% @links.each do |link| %>
<a href="<%= link[1] %>" target="_blank"
class="flex-0 btn-sm btn-gray"><%= link[0] %></a>
<% end %>
</p>
</div>
</div>

View File

@@ -1,19 +0,0 @@
# frozen_string_literal: true
class AppInfoComponent < ViewComponent::Base
def initialize(name:, description:, icon_path: , icon_fill_box: false, links: [])
@name = name
@description = description
@icon_path = icon_path
@icon_container_class = icon_container_class(icon_fill_box)
@links = links
end
def icon_container_class(icon_fill_box)
str = "flex-0 h-16 w-16 sm:h-28 sm:w-28 bg-white rounded-3xl overflow-hidden"
unless icon_fill_box
str += " p-2 border border-gray-200"
end
str
end
end

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,4 @@
<%= tag.public_send(@tag, class: "mb-6 last:mb-0", data: { <%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
:'field-name' => @field_name
}) do %>
<% if @positioning == :vertical %> <% if @positioning == :vertical %>
<label class="block"> <label class="block">
<p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>"> <p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>">
@@ -11,21 +9,7 @@
<%= @descripton %> <%= @descripton %>
</p> </p>
<% end %> <% end %>
<%= content %>
<%= tag.p class: "flex gap-x-1", data: {
controller: @resettable ? "settings--resettable-field" : nil,
} do %>
<%= content %>
<% if @resettable %>
<button type="button"
class="relative grow-0 shrink-0 btn-md btn-outline text-red-700"
title="Reset to default value"
data-settings--resettable-field-target="resetButton"
data-action="settings--resettable-field#resetField">
Reset
</button>
<% end %>
<% end %>
</label> </label>
<% elsif @positioning == :horizontal %> <% elsif @positioning == :horizontal %>
<label class="block flex items-center justify-between"> <label class="block flex items-center justify-between">

View File

@@ -2,15 +2,11 @@
module FormElements module FormElements
class FieldsetComponent < ViewComponent::Base class FieldsetComponent < ViewComponent::Base
def initialize(tag: "li", positioning: :vertical, def initialize(tag: "li", positioning: :vertical, title:, description: nil)
title:, description: nil,
field_name: nil, resettable: false)
@tag = tag @tag = tag
@positioning = positioning @positioning = positioning
@title = title @title = title
@descripton = description @descripton = description
@field_name = field_name
@resettable = resettable
end end
end end
end end

View File

@@ -1,13 +0,0 @@
<%= render FormElements::FieldsetComponent.new(
title: @title,
description: @description,
field_name: "setting_#{@key.to_s}",
resettable: @resettable
) do %>
<%= method("#{@type}_field").call :setting, @key,
value: Setting.public_send(@key),
data: {
:'default-value' => Setting.get_field(@key)[:default]
},
class: "w-full" %>
<% end %>

View File

@@ -1,20 +0,0 @@
# frozen_string_literal: true
module FormElements
class FieldsetResettableSettingComponent < ViewComponent::Base
def initialize(tag: "li", key:, type: :text, title:, description: nil)
@tag = tag
@positioning = :vertical
@title = title
@description = description
@key = key.to_sym
@type = type
@resettable = is_resettable?(@key)
end
def is_resettable?(key)
default_value = Setting.get_field(key)[:default]
default_value.present? && (default_value != Setting.send(key))
end
end
end

View File

@@ -5,9 +5,7 @@
} : nil do %> } : nil do %>
<div class="flex flex-col"> <div class="flex flex-col">
<label class="font-bold mb-1"><%= @title %></label> <label class="font-bold mb-1"><%= @title %></label>
<% if @description.present? %>
<p class="text-gray-500"><%= @descripton %></p> <p class="text-gray-500"><%= @descripton %></p>
<% end %>
</div> </div>
<div class="relative ml-4 inline-flex flex-shrink-0"> <div class="relative ml-4 inline-flex flex-shrink-0">
<%= render FormElements::ToggleComponent.new( <%= render FormElements::ToggleComponent.new(

View File

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

View File

@@ -0,0 +1,3 @@
<%= link_to @path, class: @link_class do %>
<%= @name %>
<% end %>

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
class HeaderTabLinkComponent < ViewComponent::Base
def initialize(name:, path:, active: false, disabled: false)
@name = name
@path = path
@active = active
@disabled = disabled
@link_class = class_names_link(path)
end
def class_names_link(path)
common = "block md:inline-block px-5 py-2 rounded-md font-medium text-base md:text-xl"
if @active
"#{common} bg-gray-900/50 text-white"
else
"#{common} text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white"
end
end
end

View File

@@ -0,0 +1,12 @@
<header class="py-10">
<div class="max-w-6xl md:flex md:gap-x-10 mx-auto px-4 sm:px-6 lg:px-8">
<% if @title.present? %>
<h1 class="text-3xl font-bold text-white">
<%= @title %>
</h1>
<% end %>
<nav class="md:grow flex gap-x-4 <%= @title.present? ? "justify-end" : "justify-start" %>" aria-label="Tabs">
<%= render partial: @tabnav_partial %>
</nav>
</div>
</header>

View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
class HeaderWithTabsComponent < ViewComponent::Base
def initialize(title: nil, tabnav_partial:)
@title = title
@tabnav_partial = tabnav_partial
end
end

View File

@@ -1,30 +0,0 @@
<div tabindex="-1" class="relative z-10">
<!-- Modal Background -->
<div class="hidden fixed inset-0 bg-black bg-opacity-80 overflow-y-auto flex items-center justify-center"
data-modal-target="background"
data-action="click->modal#closeBackground"
data-transition-enter="transition-all ease-in-out duration-100"
data-transition-enter-from="bg-opacity-0"
data-transition-enter-to="bg-opacity-80"
data-transition-leave="transition-all ease-in-out duration-100"
data-transition-leave-from="bg-opacity-80"
data-transition-leave-to="bg-opacity-0">
<!-- Modal Container -->
<div data-modal-target="container"
class="max-h-screen w-auto max-w-lg relative
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
<!-- Modal Card -->
<div class="m-1 bg-white rounded shadow">
<div class="p-8">
<%= content %>
<% if @show_close_button %>
<div class="flex justify-end items-center flex-wrap mt-6">
<button class="btn-md btn-blue" data-action="click->modal#close:prevent">Close</button>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>

View File

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

View File

@@ -1,6 +0,0 @@
<%= render ModalComponent.new do %>
<% if @descripton.present? %>
<p class="mb-6"><%= @description %></p>
<% end %>
<p><%= raw @qr_code_svg %></p>
<% end %>

View File

@@ -1,24 +0,0 @@
require "rqrcode"
class QrCodeModalComponent < ViewComponent::Base
def initialize(qr_content:, description: nil)
@description = description
@qr_code_svg = qr_code_svg(qr_content)
end
private
def qr_code_svg(content)
qr_code = RQRCode::QRCode.new(content)
qr_code.as_svg(
color: "000",
shape_rendering: "crispEdges",
module_size: 6,
standalone: true,
use_path: true,
svg_attributes: {
class: 'inline-block'
}
)
end
end

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class SidenavLinkComponent < ViewComponent::Base class SidenavLinkComponent < ViewComponent::Base
def initialize(name:, level: 1, path:, icon: nil, text_icon: nil, def initialize(name:, level: 1, path:, icon:, active: false, disabled: false)
active: false, disabled: false)
@name = name @name = name
@level = level @level = level
@path = path @path = path
@icon = icon @icon = icon
@text_icon = text_icon
@active = active @active = active
@disabled = disabled @disabled = disabled
@link_class = class_names_link(path) @link_class = class_names_link(path)

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
class Admin::Settings::RegistrationsController < Admin::SettingsController class Admin::Settings::RegistrationsController < Admin::SettingsController
def show def index
end end
def update def create
update_settings update_settings
redirect_to admin_settings_registrations_path, flash: { redirect_to admin_settings_registrations_path, flash: {

View File

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

View File

@@ -20,7 +20,7 @@ class Admin::SettingsController < Admin::BaseController
end end
if @errors.any? if @errors.any?
render :show and return render :index and return
end end
changed_keys.each do |key| changed_keys.each do |key|

View File

@@ -1,11 +1,11 @@
class Admin::UsersController < Admin::BaseController class Admin::UsersController < Admin::BaseController
before_action :set_user, except: [:index] before_action :set_user, only: [:show]
before_action :set_current_section before_action :set_current_section
# GET /admin/users
def index def index
ldap = LdapService.new ldap = LdapService.new
@ou = Setting.primary_domain @ou = params[:ou] || "kosmos.org"
@orgs = ldap.fetch_organizations
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc)) @pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
@stats = { @stats = {
@@ -14,46 +14,19 @@ class Admin::UsersController < Admin::BaseController
} }
end end
# GET /admin/users/:username
def show def show
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
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
end
# POST /admin/users/:username/invitations
def create_invitations
amount = params[:amount].to_i
notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user])
CreateInvitations.call(user: @user, amount: amount, notify: notify_user)
redirect_to admin_user_path(@user.cn), flash: {
success: "Added #{amount} invitations to #{@user.cn}'s account"
}
end
# DELETE /admin/users/:username/invitations
def delete_invitations
invitations = @user.invitations.unused
amount = invitations.count
invitations.destroy_all
redirect_to admin_user_path(@user.cn), flash: {
success: "Removed #{amount} invitations from #{@user.cn}'s account"
}
end end
private private
def set_user def set_user
@user = User.find_by(cn: params[:username], ou: Setting.primary_domain) address = params[:address].split("@")
http_status :not_found unless @user @user = User.where(cn: address.first, ou: address.last).first
end end
def set_current_section def set_current_section

View File

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

View File

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

View File

@@ -37,8 +37,4 @@ class ApplicationController < ActionController::Base
format.any { head status } format.any { head status }
end end
end end
def after_sign_in_path_for(user)
session[:user_return_to] || root_path
end
end end

View File

@@ -1,6 +1,6 @@
class LnurlpayController < ApplicationController class LnurlpayController < ApplicationController
before_action :check_service_available before_action :check_feature_enabled
before_action :find_user before_action :find_user_by_address
MIN_SATS = 10 MIN_SATS = 10
MAX_SATS = 1_000_000 MAX_SATS = 1_000_000
@@ -9,7 +9,7 @@ class LnurlpayController < ApplicationController
def index def index
render json: { render json: {
status: "OK", status: "OK",
callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice", callback: "https://accounts.kosmos.org/lnurlpay/#{@user.address}/invoice",
tag: "payRequest", tag: "payRequest",
maxSendable: MAX_SATS * 1000, # msat maxSendable: MAX_SATS * 1000, # msat
minSendable: MIN_SATS * 1000, # msat minSendable: MIN_SATS * 1000, # msat
@@ -34,8 +34,8 @@ class LnurlpayController < ApplicationController
def invoice def invoice
amount = params[:amount].to_i / 1000 # msats amount = params[:amount].to_i / 1000 # msats
address = params[:address]
comment = params[:comment] || "" comment = params[:comment] || ""
address = @user.address
if !valid_amount?(amount) if !valid_amount?(amount)
render json: { status: "ERROR", reason: "Invalid amount" } render json: { status: "ERROR", reason: "Invalid amount" }
@@ -69,8 +69,9 @@ class LnurlpayController < ApplicationController
private private
def find_user def find_user_by_address
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first address = params[:address].split("@")
@user = User.where(cn: address.first, ou: address.last).first
http_status :not_found if @user.nil? http_status :not_found if @user.nil?
end end
@@ -88,7 +89,7 @@ class LnurlpayController < ApplicationController
private private
def check_service_available def check_feature_enabled
http_status :not_found unless Setting.lndhub_enabled? http_status :not_found unless Setting.lndhub_enabled?
end end
end end

View File

@@ -1,131 +0,0 @@
class Rs::OauthController < ApplicationController
before_action :require_signed_in_with_username, only: :new
before_action :authenticate_user!, only: :create
def new
@user = User.where(cn: params[:username].downcase, ou: Setting.primary_domain).first
@scopes = parse_scopes params[:scope]
@redirect_uri = params[:redirect_uri]
@client_id = params[:client_id]
@state = params[:state]
@root_access_requested = (@scopes & [":r",":rw"]).any?
@denial_url = url_with_state("#{@redirect_uri}#error=access_denied", @state)
@expire_at_dates = [["Never", nil],
["In 1 month", 1.month.from_now],
["In 1 day", 1.day.from_now]]
http_status :bad_request and return unless @redirect_uri.present?
unless current_user == @user
sign_out :user
redirect_to new_rs_oauth_url(@user.cn,
scope: params[:scope],
redirect_uri: params[:redirect_uri],
client_id: params[:client_id],
state: params[:state])
return
end
unless @client_id.present?
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_request", @state),
allow_other_host: true) and return
end
if @scopes.empty?
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_scope", @state),
allow_other_host: true) and return
end
unless hostname_of(@client_id) == hostname_of(@redirect_uri)
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_client", @state),
allow_other_host: true) and return
end
@client_id.gsub!(/http(s)?:\/\//, "")
if auth = current_user.remote_storage_authorizations.valid.where(permissions: @scopes, client_id: @client_id).first
redirect_to(url_with_state("#{@redirect_uri}#access_token=#{auth.token}", @state),
allow_other_host: true) and return
end
end
def create
unless current_user.id.to_s == params[:user_id]
Rails.logger.info("NO MATCH: #{params[:user_id]}, #{current_user.id}")
http_status :forbidden and return
end
permissions = parse_scopes params[:scope]
redirect_uri = params[:redirect_uri].presence
client_id = params[:client_id].presence
state = params[:state].presence
expire_at = params[:expire_at].presence
http_status :bad_request and return unless redirect_uri.present?
if permissions.empty?
redirect_to(url_with_state("#{redirect_uri}#error=invalid_scope", state),
allow_other_host: true) and return
end
unless client_id.present?
redirect_to(url_with_state("#{redirect_uri}#error=invalid_request", state),
allow_other_host: true) and return
end
unless hostname_of(client_id) == hostname_of(redirect_uri)
redirect_to(url_with_state("#{redirect_uri}#error=invalid_client", state),
allow_other_host: true) and return
end
client_id.gsub!(/http(s)?:\/\//, "")
auth = current_user.remote_storage_authorizations.create!(
permissions: permissions,
client_id: client_id,
redirect_uri: redirect_uri,
app_name: client_id,
expire_at: expire_at
)
redirect_to url_with_state("#{redirect_uri}#access_token=#{auth.token}", state),
allow_other_host: true
end
private
def require_signed_in_with_username
unless user_signed_in?
session[:user_return_to] = request.url
redirect_to new_user_session_path(cn: params[:username], ou: Setting.primary_domain)
end
end
def hostname_of(uri)
uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0]
end
def parse_scopes(scope_string)
return [] if scope_string.blank?
scopes = scope_string.
gsub(/\[|\]/, "").
gsub(/\,/, " ").
gsub(/\/:/, ":").
split(/\s/).map(&:strip).
reject(&:empty?)
scopes = [":r"] if scopes.include?("*:r")
scopes = [":rw"] if scopes.include?("*:rw")
scopes
end
def url_with_state(url, state)
state ? "#{url}&state=#{CGI.escape(state)}" : url
end
end

View File

@@ -1,9 +0,0 @@
class Services::BaseController < ApplicationController
before_action :set_current_section
private
def set_current_section
@current_section = :services
end
end

View File

@@ -1,14 +0,0 @@
class Services::ChatController < Services::BaseController
before_action :authenticate_user!
before_action :require_service_available
def show
@service_enabled = current_user.services_enabled.include?(:xmpp)
end
private
def require_service_available
http_status :not_found unless Setting.ejabberd_enabled?
end
end

View File

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

View File

@@ -1,5 +1,4 @@
require "rqrcode" require "rqrcode"
require "lnurl"
class Services::LightningController < ApplicationController class Services::LightningController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
@@ -8,51 +7,25 @@ class Services::LightningController < ApplicationController
before_action :fetch_balance before_action :fetch_balance
def index def index
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}" @wallet_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
qrcode = RQRCode::QRCode.new(@wallet_url)
@svg = qrcode.as_svg(
color: "000",
shape_rendering: "crispEdges",
module_size: 6,
standalone: true,
use_path: true,
svg_attributes: {
class: 'inline-block'
}
)
end end
def transactions def transactions
@transactions = fetch_transactions @transactions = fetch_transactions
end end
def qr_lnurlp
lnurlp_url = "https://kosmos.org/.well-known/lnurlp/#{current_user.cn}"
lnurlp_bech32 = Lnurl.new(lnurlp_url).to_bech32
qr_code = RQRCode::QRCode.new("lightning:" + lnurlp_bech32)
respond_to do |format|
format.svg do
qr_svg = qr_code.as_svg(
color: "000",
shape_rendering: "crispEdges",
module_size: 6,
standalone: true,
use_path: true,
svg_attributes: {
class: 'inline-block'
}
)
send_data(
qr_svg,
filename: "bitcoin-lightning-#{current_user.address}.svg",
type: "image/svg+xml"
)
end
format.png do
qr_png = qr_code.as_png(
fill: "white",
color: "black",
size: 1024,
)
send_data(
qr_png,
filename: "bitcoin-lightning-#{current_user.address}.png",
type: "image/png"
)
end
end
end
private private
def authenticate_with_lndhub(options={}) def authenticate_with_lndhub(options={})

View File

@@ -1,14 +0,0 @@
class Services::MastodonController < Services::BaseController
before_action :authenticate_user!
before_action :require_service_available
def show
@service_enabled = current_user.services_enabled.include?(:mastodon)
end
private
def require_service_available
http_status :not_found unless Setting.mastodon_enabled?
end
end

View File

@@ -1,26 +1,30 @@
class Services::RemotestorageController < Services::BaseController class Services::RemotestorageController < ApplicationController
before_action :authenticate_user! before_action :require_user_signed_in
before_action :require_service_available before_action :require_service_enabled
before_action :require_feature_enabled before_action :require_feature_enabled
before_action :set_current_section
# Dashboard def dashboard
def show
# unless current_user.services_enabled.include?(:remotestorage) # unless current_user.services_enabled.include?(:remotestorage)
# redirect_to service_remotestorage_info_path # redirect_to service_remotestorage_info_path
# end # end
@rs_auths = current_user.remote_storage_authorizations
# TODO sort by app name
end end
private private
def require_service_available
http_status :not_found unless Setting.remotestorage_enabled?
end
def require_feature_enabled def require_feature_enabled
unless Flipper.enabled?(:remotestorage, current_user) unless Flipper.enabled?(:remotestorage, current_user)
http_status :forbidden http_status :forbidden
end end
end end
def require_service_enabled
unless Setting.remotestorage_enabled?
http_status :not_found
end
end
def set_current_section
@current_section = :services
end
end end

View File

@@ -1,42 +0,0 @@
class Services::RsAuthsController < Services::BaseController
before_action :authenticate_user!
before_action :require_feature_enabled
before_action :require_service_available
# before_action :require_service_enabled
before_action :find_rs_auth
def destroy
@auth.destroy!
respond_to do |format|
format.html do redirect_to services_storage_url, flash: {
success: 'App authorization revoked'
}
end
format.json { head :no_content }
end
end
def launch_app
launch_url = "#{@auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{@auth.token}"
redirect_to launch_url, allow_other_host: true
end
private
def require_feature_enabled
unless Flipper.enabled?(:remotestorage, current_user)
http_status :forbidden
end
end
def require_service_available
http_status :not_found unless Setting.remotestorage_enabled?
end
def find_rs_auth
@auth = current_user.remote_storage_authorizations.find(params[:id])
http_status :not_found unless @auth.present?
end
end

View File

@@ -1,34 +1,23 @@
require "securerandom"
require "bcrypt"
class SettingsController < ApplicationController class SettingsController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_main_nav_section before_action :set_main_nav_section
before_action :set_settings_section, only: [:show, :update, :update_email, :reset_email_password] before_action :set_settings_section, only: [:show, :update, :update_email]
before_action :set_user, only: [:show, :update, :update_email, :reset_email_password] before_action :set_user, only: [:show, :update, :update_email]
def index def index
redirect_to setting_path(:profile) redirect_to setting_path(:profile)
end end
def show def show
if @settings_section == "experiments"
session[:shared_secret] ||= SecureRandom.base64(12)
end
end end
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]
if @user.save if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name]) if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
LdapManager::UpdateDisplayName.call(dn: @user.dn, display_name: @user.display_name) LdapManager::UpdateDisplayName.call(@user.dn, user_params[:display_name])
end
if @user.avatar_new.present?
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
end end
redirect_to setting_path(@settings_section), flash: { redirect_to setting_path(@settings_section), flash: {
@@ -41,7 +30,7 @@ class SettingsController < ApplicationController
end end
def update_email def update_email
if @user.valid_ldap_authentication?(security_params[:current_password]) if @user.valid_ldap_authentication?(email_params[:current_password])
if @user.update email: email_params[:email] if @user.update email: email_params[:email]
redirect_to setting_path(:account), flash: { redirect_to setting_path(:account), flash: {
notice: 'Please confirm your new address using the confirmation link we just sent you.' notice: 'Please confirm your new address using the confirmation link we just sent you.'
@@ -57,28 +46,6 @@ class SettingsController < ApplicationController
end end
end end
def reset_email_password
@user.current_password = security_params[:current_password]
if @user.valid_ldap_authentication?(@user.current_password)
@user.current_password = nil
session[:new_email_password] = generate_email_password
hashed_password = hash_email_password(session[:new_email_password])
LdapManager::UpdateEmailPassword.call(dn: @user.dn, password_hash: hashed_password)
if @user.ldap_entry[:email_maildrop] != @user.address
LdapManager::UpdateEmailMaildrop.call(dn: @user.dn, address: @user.address)
end
redirect_to new_password_services_email_path
else
@validation_errors = {
current_password: [ "Wrong password. Try again!" ]
}
render :show, status: :forbidden
end
end
def reset_password def reset_password
current_user.send_reset_password_instructions current_user.send_reset_password_instructions
sign_out current_user sign_out current_user
@@ -86,45 +53,6 @@ class SettingsController < ApplicationController
redirect_to check_your_email_path, notice: msg redirect_to check_your_email_path, notice: msg
end end
def set_nostr_pubkey
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
is_valid_id = NostrManager::ValidateId.call(event: signed_event)
is_valid_sig = NostrManager::VerifySignature.call(event: signed_event)
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
unless is_valid_id && is_valid_sig && is_correct_content
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end
pubkey_taken = User.all_except(current_user).where(
ou: current_user.ou, nostr_pubkey: signed_event[:pubkey]
).any?
if pubkey_taken
flash[:alert] = "Public key already in use for a different account"
http_status :unprocessable_entity and return
end
current_user.update! nostr_pubkey: signed_event[:pubkey]
session[:shared_secret] = nil
flash[:success] = "Public key verification successful"
http_status :ok
rescue
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end
# DELETE /settings/nostr_pubkey
def remove_nostr_pubkey
current_user.update! nostr_pubkey: nil
redirect_to setting_path(:experiments), flash: {
success: 'Public key removed from account'
}
end
private private
def set_main_nav_section def set_main_nav_section
@@ -133,10 +61,7 @@ class SettingsController < ApplicationController
def set_settings_section def set_settings_section
@settings_section = params[:section] @settings_section = params[:section]
allowed_sections = [ allowed_sections = [:profile, :account, :lightning, :xmpp]
:profile, :account, :xmpp, :email, :lightning, :remotestorage,
:experiments
]
unless allowed_sections.include?(@settings_section.to_sym) unless allowed_sections.include?(@settings_section.to_sym)
redirect_to setting_path(:profile) redirect_to setting_path(:profile)
@@ -148,34 +73,13 @@ class SettingsController < ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:display_name, :avatar, preferences: [ params.require(:user).permit(:display_name, preferences: [
:lightning_notify_sats_received, :lightning_notify_sats_received,
:remotestorage_notify_auth_created,
:xmpp_exchange_contacts_with_invitees :xmpp_exchange_contacts_with_invitees
]) ])
end end
def email_params def email_params
params.require(:user).permit(:email) params.require(:user).permit(:email, :current_password)
end
def security_params
params.require(:user).permit(:current_password)
end
def nostr_event_params
params.permit(signed_event: [
:id, :pubkey, :created_at, :kind, :tags, :content, :sig
])
end
def generate_email_password
characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join
end
def hash_email_password(password)
salt = BCrypt::Engine.generate_salt
BCrypt::Engine.hash_secret(password, salt)
end end
end end

View File

@@ -88,7 +88,7 @@ class SignupController < ApplicationController
if session[:new_user].present? if session[:new_user].present?
@user = User.new(session[:new_user]) @user = User.new(session[:new_user])
else else
@user = User.new(ou: Setting.primary_domain) @user = User.new(ou: "kosmos.org")
end end
end end
@@ -96,13 +96,13 @@ class SignupController < ApplicationController
session[:new_user] = nil session[:new_user] = nil
session[:validation_error] = nil session[:validation_error] = nil
CreateAccount.call(account: { CreateAccount.call(
username: @user.cn, username: @user.cn,
domain: Setting.primary_domain, domain: "kosmos.org",
email: @user.email, email: @user.email,
password: @user.password, password: @user.password,
invitation: @invitation invitation: @invitation
}) )
end end
def set_context def set_context

View File

@@ -6,19 +6,15 @@ class WebfingerController < ApplicationController
def show def show
resource = params[:resource] resource = params[:resource]
if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1) if resource && resource.match(/acct:\w+/)
@username, @org = @useraddress.split("@") useraddress = resource.split(":").last
username, org = useraddress.split("@")
unless Rails.env.development? username.downcase!
# Allow different domains (e.g. localhost:3000) in development only unless User.where(cn: username, ou: org).any?
head 404 and return unless @org == Setting.primary_domain
end
unless User.where(cn: @username.downcase, ou: Setting.primary_domain).any?
head 404 and return head 404 and return
end end
render json: webfinger.to_json, render json: webfinger(useraddress).to_json,
content_type: "application/jrd+json" content_type: "application/jrd+json"
else else
head 422 and return head 422 and return
@@ -27,18 +23,19 @@ class WebfingerController < ApplicationController
private private
def webfinger def webfinger(useraddress)
links = []; links = [];
# TODO check if storage service is enabled for user, not just globally links << remotestorage_link(useraddress) if Setting.remotestorage_enabled
links << remotestorage_link if Setting.remotestorage_enabled
{ "links" => links } { "links" => links }
end end
def remotestorage_link def remotestorage_link(useraddress)
auth_url = new_rs_oauth_url(@username) # TODO use when OAuth routes are available
storage_url = "#{Setting.rs_storage_url}/#{@username}" # auth_url = new_rs_oauth_url(useraddress)
auth_url = "https://example.com/rs/oauth"
storage_url = "#{Setting.rs_storage_url}/#{useraddress}"
{ {
"rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage", "rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage",
@@ -54,8 +51,7 @@ class WebfingerController < ApplicationController
end end
def allow_cross_origin_requests def allow_cross_origin_requests
return unless Rails.env.development? headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Origin'] = "*" headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
headers['Access-Control-Allow-Methods'] = "GET"
end end
end end

View File

@@ -30,7 +30,7 @@ class WebhooksController < ApplicationController
def notify_xmpp(address, amt_sats, memo) def notify_xmpp(address, amt_sats, memo)
payload = { payload = {
type: "normal", type: "normal",
from: Setting.xmpp_notifications_from_address, from: "kosmos.org", # TODO domain config
to: address, to: address,
subject: "Sats received!", subject: "Sats received!",
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}" body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"

View File

@@ -1,16 +0,0 @@
class WellKnownController < ApplicationController
def nostr
http_status :unprocessable_entity and return if params[:name].blank?
domain = request.headers["X-Forwarded-Host"].presence || Setting.primary_domain
@user = User.where(cn: params[:name], ou: domain).first
http_status :not_found and return if @user.nil? || @user.nostr_pubkey.blank?
respond_to do |format|
format.json do
render json: {
names: { "#{@user.cn}": @user.nostr_pubkey }
}.to_json
end
end
end
end

View File

@@ -1,11 +0,0 @@
module OauthHelper
def scope_name(scope)
scope.gsub(/(\:.+)/, '')
end
def scope_permissions(scope)
scope.match(/\:r$/) ? "r" : "rw"
end
end

View File

@@ -1,12 +1,7 @@
import { Application } from "@hotwired/stimulus" import { Application } from "@hotwired/stimulus"
import { Dropdown, Modal, Tabs } from "tailwindcss-stimulus-components"
const application = Application.start() const application = Application.start()
application.register('dropdown', Dropdown)
application.register('modal', Modal)
application.register('tabs', Tabs)
// Configure Stimulus development experience // Configure Stimulus development experience
application.debug = false application.debug = false
window.Stimulus = application window.Stimulus = application

View File

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

View File

@@ -1,47 +0,0 @@
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="settings--nostr-pubkey"
export default class extends Controller {
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
static values = { userAddress: String, pubkeyHex: String, sharedSecret: String }
connect () {
if (window.nostr) {
if (this.hasSetPubkeyTarget) {
this.setPubkeyTarget.disabled = false
}
} else {
this.noExtensionTarget.classList.remove("hidden")
}
}
async setPubkey () {
this.setPubkeyTarget.disabled = true
try {
const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: `Connect my public key to ${this.userAddressValue} (confirmation ${this.sharedSecretValue})`
})
const res = await fetch("/settings/set_nostr_pubkey", {
method: "POST", credentials: "include", headers: {
"Accept": "application/json", 'Content-Type': 'application/json',
"X-CSRF-Token": this.csrfToken
}, body: JSON.stringify({ signed_event: signedEvent })
});
window.location.reload()
} catch (error) {
console.warn('Unable to verify pubkey:', error.message)
this.setPubkeyTarget.disabled = false
}
}
get csrfToken () {
const element = document.head.querySelector('meta[name="csrf-token"]')
return element.getAttribute("content")
}
}

View File

@@ -1,10 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "resetButton" ]
resetField () {
const inputEl = this.element.querySelector('input')
inputEl.value = inputEl.dataset.defaultValue
}
}

View File

@@ -1,10 +0,0 @@
class RemoteStorageExpireAuthorizationJob < ApplicationJob
queue_as :remotestorage
def perform(rs_auth_id)
rs_auth = RemoteStorageAuthorization.find rs_auth_id
return unless rs_auth.expire_at.nil? || rs_auth.expire_at <= DateTime.now
rs_auth.destroy!
end
end

View File

@@ -5,22 +5,4 @@ class NotificationMailer < ApplicationMailer
@subject = "Sats received" @subject = "Sats received"
mail to: @user.email, subject: @subject mail to: @user.email, subject: @subject
end end
def remotestorage_auth_created
@user = params[:user]
@auth = params[:auth]
@permissions = @auth.permissions.map do |p|
access = p.split(":")[1] == 'r' ? 'read' : 'read/write'
directory = p.split(':')[0] == '' ? 'all folders and files' : p.split(':')[0]
"#{access} #{directory}"
end
@subject = "New app connected to your storage"
mail to: @user.email, subject: @subject
end
def new_invitations_available
@user = params[:user]
@subject = "New invitations added to your account"
mail to: @user.email, subject: @subject
end
end end

View File

@@ -1,5 +0,0 @@
module AppCatalog
def self.table_name_prefix
"app_catalog_"
end
end

View File

@@ -1,16 +0,0 @@
class AppCatalog::WebApp < ApplicationRecord
store :metadata, coder: JSON
has_many :remote_storage_authorizations, dependent: :destroy
has_one_attached :icon
has_one_attached :apple_touch_icon
validates :url, presence: true, uniqueness: true
validates :url, format: { with: URI.regexp },
if: Proc.new { |a| a.url.present? }
def update_metadata
AppCatalogManager::UpdateMetadata.call(app: self)
end
end

View File

@@ -1,114 +0,0 @@
class RemoteStorageAuthorization < ApplicationRecord
belongs_to :user
belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true
serialize :permissions unless Rails.env.production?
validates_presence_of :permissions
validates_presence_of :client_id
scope :valid, -> { where(expire_at: nil).or(where(expire_at: (DateTime.now)..)) }
scope :expired, -> { where(expire_at: ..(DateTime.now)) }
after_initialize do |a|
a.permissions = [] if a.permissions == nil
end
before_create :generate_token
before_create :store_token_in_redis
before_create :find_or_create_web_app
after_create :schedule_token_expiry
after_create :notify_user
before_destroy :delete_token_from_redis
after_destroy :remove_token_expiry_job
def url
uri = URI.parse self.redirect_uri
"#{uri.scheme}://#{client_id}"
end
def launch_url
return url unless web_app && web_app.metadata[:start_url].present?
start_url = web_app.metadata[:start_url]
if start_url.match("^https?:\/\/")
return start_url.start_with?(url) ? start_url : url
else
path = start_url.gsub(/^\.\.\//, "").gsub(/^\.\//, "").gsub(/^\//, "")
"#{url}/#{path}"
end
end
def delete_token_from_redis
key = "authorizations:#{user.cn}:#{token}"
redis.srem? key, redis.smembers(key)
rescue => e
Rails.logger.error e
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
private
def redis
@redis ||= Redis.new(url: Setting.rs_redis_url)
end
def generate_token(length=16)
self.token = SecureRandom.hex(length) if self.token.blank?
end
def store_token_in_redis
redis.sadd "authorizations:#{user.cn}:#{token}", permissions
end
def schedule_token_expiry
return unless expire_at.present?
RemoteStorageExpireAuthorizationJob.set(wait_until: expire_at)
.perform_later(id)
end
def remove_token_expiry_job
queue = Sidekiq::Queue.new(RemoteStorageExpireAuthorizationJob.queue_name)
queue.each do |job|
next unless job.display_class == "RemoteStorageExpireAuthorizationJob"
job.delete if job.display_args == [id]
end
end
def find_or_create_web_app
if looks_like_hosted_origin?
web_app = AppCatalog::WebApp.find_or_create_by!(url: self.url)
web_app.update_metadata unless web_app.name.present?
self.web_app = web_app
self.app_name = web_app.name.presence || client_id
else
self.app_name = client_id
end
end
def looks_like_hosted_origin?
uri = URI.parse self.redirect_uri
!!(uri.host =~ /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/)
rescue URI::InvalidURIError
false
end
def notify_user
notify = user.preferences[:remotestorage_notify_auth_created]
case notify
when "xmpp"
router = Router.new
payload = {
type: "normal", to: user.address,
from: Setting.xmpp_notifications_from_address,
body: "You have just granted '#{self.client_id}' access to your Kosmos Storage. Visit your Storage dashboard to check on your connected apps and revoke permissions anytime: #{router.services_storage_url}"
}
XmppSendMessageJob.perform_later(payload)
when "email"
NotificationMailer.with(user: user, auth: self)
.remotestorage_auth_created.deliver_later
end
end
end

View File

@@ -2,9 +2,6 @@
class Setting < RailsSettings::Base class Setting < RailsSettings::Base
cache_prefix { "v1" } cache_prefix { "v1" }
field :primary_domain, type: :string,
default: ENV["PRIMARY_DOMAIN"].presence
field :accounts_domain, type: :string, field :accounts_domain, type: :string,
default: ENV["AKKOUNTS_DOMAIN"].presence default: ENV["AKKOUNTS_DOMAIN"].presence
@@ -12,12 +9,9 @@ class Setting < RailsSettings::Base
# Internal services # Internal services
# #
field :redis_url, type: :string, field :redis_url, type: :string, readonly: true,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0" default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
field :s3_enabled, type: :boolean,
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
# #
# Registrations # Registrations
# #
@@ -32,67 +26,38 @@ class Setting < RailsSettings::Base
field :xmpp_default_rooms, type: :array, default: [] field :xmpp_default_rooms, type: :array, default: []
field :xmpp_autojoin_default_rooms, type: :boolean, default: false field :xmpp_autojoin_default_rooms, type: :boolean, default: false
field :xmpp_notifications_from_address, type: :string, default: primary_domain
# #
# Sentry # Sentry
# #
field :sentry_enabled, type: :boolean, readonly: true, field :sentry_enabled, type: :boolean, readonly: true,
default: ENV["SENTRY_DSN"].present? default: (ENV["SENTRY_DSN"].present?.to_s || false)
#
# BTCPay Server
#
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
# #
# Discourse # Discourse
# #
field :discourse_public_url, type: :string, field :discourse_public_url, type: :string, readonly: true,
default: ENV["DISCOURSE_PUBLIC_URL"].presence default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean, field :discourse_enabled, type: :boolean,
default: ENV["DISCOURSE_PUBLIC_URL"].present? default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
field :discourse_connect_secret, type: :string, field :discourse_connect_secret, type: :string, readonly: true,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence default: ENV["DISCOURSE_CONNECT_SECRET"].presence
#
# Drone CI
#
field :droneci_public_url, type: :string,
default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean,
default: ENV["DRONECI_PUBLIC_URL"].present?
# #
# ejabberd # ejabberd
# #
field :ejabberd_enabled, type: :boolean, field :ejabberd_enabled, type: :boolean,
default: ENV["EJABBERD_API_URL"].present? default: (ENV["EJABBERD_API_URL"].present?.to_s || false)
field :ejabberd_api_url, type: :string, field :ejabberd_api_url, type: :string, readonly: true,
default: ENV["EJABBERD_API_URL"].presence default: ENV["EJABBERD_API_URL"].presence
field :ejabberd_admin_url, type: :string, field :ejabberd_admin_url, type: :string, readonly: true,
default: ENV["EJABBERD_ADMIN_URL"].presence default: ENV["EJABBERD_ADMIN_URL"].presence
field :ejabberd_buddy_roster, type: :string, field :ejabberd_buddy_roster, type: :string,
@@ -102,56 +67,50 @@ class Setting < RailsSettings::Base
# Gitea # Gitea
# #
field :gitea_public_url, type: :string, field :gitea_public_url, type: :string, readonly: true,
default: ENV["GITEA_PUBLIC_URL"].presence default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean, field :gitea_enabled, type: :boolean,
default: ENV["GITEA_PUBLIC_URL"].present? default: (ENV["GITEA_PUBLIC_URL"].present?.to_s || false)
# #
# Lightning Network # Lightning Network
# #
field :lndhub_api_url, type: :string, field :lndhub_api_url, type: :string, readonly: true,
default: ENV["LNDHUB_API_URL"].presence default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean, field :lndhub_enabled, type: :boolean,
default: ENV["LNDHUB_API_URL"].present? default: (ENV["LNDHUB_API_URL"].present?.to_s || false)
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean, field :lndhub_admin_enabled, type: :boolean,
default: ENV["LNDHUB_ADMIN_UI"] || false default: (ENV["LNDHUB_ADMIN_UI"] || false)
field :lndhub_public_key, type: :string, field :lndhub_public_key, type: :string, readonly: true,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "") default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean, field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present? } default: -> { self.lndhub_public_key.present?.to_s || false }
# #
# Mastodon # Mastodon
# #
field :mastodon_public_url, type: :string, field :mastodon_public_url, type: :string, readonly: true,
default: ENV["MASTODON_PUBLIC_URL"].presence default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean, field :mastodon_enabled, type: :boolean,
default: ENV["MASTODON_PUBLIC_URL"].present? default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false)
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
# #
# MediaWiki # MediaWiki
# #
field :mediawiki_public_url, type: :string, field :mediawiki_public_url, type: :string, readonly: true,
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean, field :mediawiki_enabled, type: :boolean,
default: ENV["MEDIAWIKI_PUBLIC_URL"].present? default: (ENV["MEDIAWIKI_PUBLIC_URL"].present?.to_s || false)
# #
# Nostr # Nostr
@@ -164,37 +123,8 @@ class Setting < RailsSettings::Base
# #
field :remotestorage_enabled, type: :boolean, field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present? default: (ENV["RS_STORAGE_URL"].present?.to_s || false)
field :rs_storage_url, type: :string, field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence default: ENV["RS_STORAGE_URL"].presence
field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
#
# E-Mail Service
#
field :email_enabled, type: :boolean,
default: ENV["EMAIL_SMTP_HOST"].present?
# field :email_smtp_host, type: :string,
# default: ENV["EMAIL_SMTP_HOST"].presence
#
# field :email_smtp_port, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || 587
#
# field :email_smtp_enable_starttls, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || true
#
# field :email_auth_method, type: :string,
# default: ENV["EMAIL_AUTH_METHOD"].presence || "plain"
#
# field :email_imap_host, type: :string,
# default: ENV["EMAIL_IMAP_HOST"].presence
#
# field :email_imap_port, type: :string,
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
end end

View File

@@ -1,18 +1,11 @@
require 'nostr'
class User < ApplicationRecord class User < ApplicationRecord
include EmailValidatable include EmailValidatable
attr_accessor :display_name attr_accessor :display_name
attr_accessor :avatar_new
attr_accessor :current_password
serialize :preferences, coder: UserPreferences serialize :preferences, UserPreferences
#
# Relations # Relations
#
has_many :invitations, dependent: :destroy has_many :invitations, dependent: :destroy
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id' has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user has_one :inviter, through: :invitation, source: :user
@@ -25,13 +18,7 @@ class User < ApplicationRecord
has_many :accounts, through: :lndhub_user has_many :accounts, through: :lndhub_user
has_many :remote_storage_authorizations validates_uniqueness_of :cn
#
# Validations
#
validates_uniqueness_of :cn, scope: :ou
validates_length_of :cn, minimum: 3 validates_length_of :cn, minimum: 3
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/, validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
if: Proc.new{ |u| u.cn.present? }, if: Proc.new{ |u| u.cn.present? },
@@ -41,8 +28,7 @@ class User < ApplicationRecord
message: "is invalid. Usernames need to start with a letter." message: "is invalid. Usernames need to start with a letter."
# FIXME This needs a server restart to apply values # FIXME This needs a server restart to apply values
validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i, validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i,
message: "has already been taken", message: "has already been taken"
unless: Proc.new{ |u| u.persisted? }
validates_uniqueness_of :email validates_uniqueness_of :email
validates :email, email: true validates :email, email: true
@@ -50,21 +36,8 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true, validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) } if: -> { defined?(@display_name) }
validates_uniqueness_of :nostr_pubkey, allow_blank: true scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :pending, -> { where(confirmed_at: nil) }
validate :acceptable_avatar
#
# Scopes
#
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :pending, -> { where(confirmed_at: nil) }
scope :all_except, -> (user) { where.not(id: user) }
#
# Encrypted database columns
#
has_encrypted :ln_login, :ln_password has_encrypted :ln_login, :ln_password
@@ -92,14 +65,12 @@ class User < ApplicationRecord
def devise_after_confirmation def devise_after_confirmation
if ldap_entry[:mail] != self.email if ldap_entry[:mail] != self.email
# E-Mail update confirmed # E-Mail update confirmed
LdapManager::UpdateEmail.call(dn: self.dn, address: self.email) LdapManager::UpdateEmail.call(self.dn, self.email)
else else
# E-Mail from signup confirmed (i.e. account activation) # E-Mail from signup confirmed (i.e. account activation)
# TODO Make configurable, only activate globally enabled services
enable_service %w[ discourse gitea mediawiki xmpp ] enable_service %w[ discourse gitea mediawiki xmpp ]
# TODO enable in development when we have easy setup of ejabberd etc. #TODO enable in development when we have easy setup of ejabberd etc.
return if Rails.env.development? || !Setting.ejabberd_enabled? return if Rails.env.development? || !Setting.ejabberd_enabled?
XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present? XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present?
@@ -133,11 +104,6 @@ class User < ApplicationRecord
"#{self.cn}@#{self.ou}" "#{self.cn}@#{self.ou}"
end end
def mastodon_address
return nil unless Setting.mastodon_enabled?
"#{self.cn}@#{Setting.mastodon_address_domain}"
end
def valid_attribute?(attribute_name) def valid_attribute?(attribute_name)
self.valid? self.valid?
self.errors[attribute_name].blank? self.errors[attribute_name].blank?
@@ -163,10 +129,6 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name] @display_name ||= ldap_entry[:display_name]
end end
def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end
def services_enabled def services_enabled
ldap_entry[:service] || [] ldap_entry[:service] || []
end end
@@ -189,28 +151,10 @@ class User < ApplicationRecord
ldap.delete_attribute(dn,:service) ldap.delete_attribute(dn,:service)
end end
def nostr_pubkey_bech32
return nil unless nostr_pubkey.present?
Nostr::PublicKey.new(nostr_pubkey).to_bech32
end
private private
def ldap def ldap
return @ldap_service if defined?(@ldap_service) return @ldap_service if defined?(@ldap_service)
@ldap_service = LdapService.new @ldap_service = LdapService.new
end end
def acceptable_avatar
return unless avatar_new.present?
if avatar_new.size > 1.megabyte
errors.add(:avatar, "file size is too large")
end
acceptable_types = ["image/jpeg", "image/png"]
unless acceptable_types.include?(avatar_new.content_type)
errors.add(:avatar, "must be a JPEG or PNG file")
end
end
end end

View File

@@ -1,63 +0,0 @@
require "manifique"
require "down"
module AppCatalogManager
class UpdateMetadata < AppCatalogManagerService
def initialize(app:)
@app = app
end
def call
agent = Manifique::Agent.new(url: @app.url)
metadata = agent.fetch_metadata
@app.name = metadata.name
[:name, :short_name, :description, :theme_color, :background_color,
:display, :start_url, :scope, :share_target, :icons].each do |prop|
@app.metadata[prop] = metadata.send(prop) if prop
end
@app.save!
# TODO move icon downloads to separate, async job
if icon = metadata.select_icon(sizes: "256x256") ||
icon = metadata.select_icon(sizes: "192x192")
attach_remote_image(:icon, icon)
# TODO elsif get whatever is available
end
if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon")
attach_remote_image(:apple_touch_icon, apple_touch_icon)
end
rescue Manifique::Error => e
msg = "Fetching web app manifest failed for #{e.url}: #{e.type}"
Rails.logger.warn(msg)
Sentry.capture_message(msg) if Setting.sentry_enabled?
false
end
def attach_remote_image(attachment_name, icon)
if icon['src'].start_with?("http")
download_url = icon['src']
else
download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}"
end
filename = "#{attachment_name}-#{Time.now.to_i}.png"
key = "web_apps/#{@app.id}/icons/#{filename}"
begin
tempfile = Down.download(download_url)
@app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
rescue Down::NotFound
msg = "Download of \"#{attachment_name}\" failed: NotFound error for #{download_url}"
Rails.logger.warn(msg)
Sentry.capture_message(msg)
rescue => e
Rails.logger.warn "Saving attachment \"#{attachment_name}\" failed: \"#{e.message}\""
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
end
end
end

View File

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

View File

@@ -1,7 +1,7 @@
class ApplicationService class ApplicationService
# This enables executing a service's `#call` method directly via # This enables executing a service's `#call` method directly via
# `MyService.call(args)`, without creating a class instance it first. # `MyService.call(args)`, without creating a class instance it first.
def self.call(**args, &block) def self.call(*args, &block)
new(**args, &block).call new(*args, &block).call
end end
end end

32
app/services/btc_pay.rb Normal file
View File

@@ -0,0 +1,32 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcPay
def initialize
@base_url = ENV["BTCPAY_API_URL"]
@store_id = Rails.application.credentials.btcpay[:store_id]
@auth_token = Rails.application.credentials.btcpay[:auth_token]
end
def onchain_wallet_balance
res = get "stores/#{@store_id}/payment-methods/onchain/BTC/wallet"
{
balance: res["balance"].to_f,
unconfirmed_balance: res["unconfirmedBalance"].to_f,
confirmed_balance: res["confirmedBalance"].to_f
}
end
private
def get(endpoint)
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{@auth_token}"
})
JSON.parse(res.body)
end
end

View File

@@ -1,11 +0,0 @@
module BtcpayManager
class FetchLightningWalletBalance < BtcpayManagerService
def call
res = get "stores/#{store_id}/lightning/BTC/balance"
{
confirmed_balance: res["offchain"]["local"].to_i / 1000 # msats to sats
}
end
end
end

View File

@@ -1,13 +0,0 @@
module BtcpayManager
class FetchOnchainWalletBalance < BtcpayManagerService
def call
res = get "stores/#{store_id}/payment-methods/onchain/BTC/wallet"
{
balance: (res["balance"].to_f * 100000000).to_i, # BTC to sats
unconfirmed_balance: (res["unconfirmedBalance"].to_f * 100000000).to_i,
confirmed_balance: (res["confirmedBalance"].to_f * 100000000).to_i
}
end
end
end

View File

@@ -1,24 +0,0 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcpayManagerService < ApplicationService
attr_reader :base_url, :store_id, :auth_token
def initialize
@base_url = Setting.btcpay_api_url
@store_id = Setting.btcpay_store_id
@auth_token = Setting.btcpay_auth_token
end
private
def get(endpoint)
res = Faraday.get("#{base_url}/#{endpoint}", {}, {
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{auth_token}"
})
JSON.parse(res.body)
end
end

View File

@@ -1,11 +1,11 @@
class CreateAccount < ApplicationService class CreateAccount < ApplicationService
def initialize(account:) def initialize(args)
@username = account[:username] @username = args[:username]
@domain = account[:ou] || Setting.primary_domain @domain = args[:ou] || "kosmos.org"
@email = account[:email] @email = args[:email]
@password = account[:password] @password = args[:password]
@invitation = account[:invitation] @invitation = args[:invitation]
@confirmed = account[:confirmed] @confirmed = args[:confirmed]
end end
def call def call

View File

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

View File

@@ -1,16 +0,0 @@
module LdapManager
class FetchAvatar < LdapManagerService
def initialize(cn:)
@cn = cn
end
def call
treebase = ldap_config["base"]
attributes = %w{ jpegPhoto }
filter = Net::LDAP::Filter.eq("cn", @cn)
entry = ldap_client.search(base: treebase, filter: filter, attributes: attributes).first
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
end
end
end

View File

@@ -1,27 +0,0 @@
require "image_processing/vips"
module LdapManager
class UpdateAvatar < LdapManagerService
def initialize(dn:, file:)
@dn = dn
@img_data = process(file)
end
def call
replace_attribute @dn, :jpegPhoto, @img_data
end
private
def process(file)
processed = ImageProcessing::Vips
.resize_to_fill(512, 512)
.source(file)
.convert("jpeg")
.saver(strip: true)
.call
Base64.strict_encode64 processed.read
end
end
end

View File

@@ -1,6 +1,6 @@
module LdapManager module LdapManager
class UpdateDisplayName < LdapManagerService class UpdateDisplayName < LdapManagerService
def initialize(dn:, display_name:) def initialize(dn, display_name)
@dn = dn @dn = dn
@display_name = display_name @display_name = display_name
end end

View File

@@ -1,6 +1,6 @@
module LdapManager module LdapManager
class UpdateEmail < LdapManagerService class UpdateEmail < LdapManagerService
def initialize(dn:, address:) def initialize(dn, address)
@dn = dn @dn = dn
@address = address @address = address
end end

View File

@@ -1,12 +0,0 @@
module LdapManager
class UpdateEmailMaildrop < LdapManagerService
def initialize(dn:, address:)
@dn = dn
@address = address
end
def call
replace_attribute @dn, :mailRoutingAddress, @address
end
end
end

View File

@@ -1,12 +0,0 @@
module LdapManager
class UpdateEmailPassword < LdapManagerService
def initialize(dn:, password_hash:)
@dn = dn
@password_hash = password_hash
end
def call
replace_attribute @dn, :mailpassword, @password_hash
end
end
end

View File

@@ -1,5 +1,2 @@
class LdapManagerService < LdapService class LdapManagerService < LdapService
def suffix
@suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end
end end

View File

@@ -50,11 +50,8 @@ class LdapService < ApplicationService
treebase = ldap_config["base"] treebase = ldap_config["base"]
end end
attributes = %w[ attributes = %w{dn cn uid mail displayName admin service}
dn cn uid mail displayName admin service filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
mailRoutingAddress mailpassword
]
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes) entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] } entries.sort_by! { |e| e.cn[0] }
@@ -64,9 +61,7 @@ class LdapService < ApplicationService
mail: e.try(:mail) ? e.mail.first : nil, mail: e.try(:mail) ? e.mail.first : nil,
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,
service: e.try(:service), service: e.try(:service)
email_maildrop: e.try(:mailRoutingAddress),
email_password: e.try(:mailpassword)
} }
end end
end end

View File

@@ -14,7 +14,7 @@ class LndhubV2 < Lndhub
end end
def create_account(payload={}) def create_account(payload={})
post "v2/users", payload, admin_token: Setting.lndhub_admin_token post "v2/users", payload, admin_token: Rails.application.credentials.lndhub[:admin_token]
end end
def create_invoice(payload) def create_invoice(payload)

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
require "nostr"
class NostrManagerService < ApplicationService
end

View File

@@ -1,7 +0,0 @@
class Router
include Rails.application.routes.url_helpers
def self.default_url_options
ActionMailer::Base.default_url_options
end
end

View File

@@ -1,56 +0,0 @@
<%= render HeaderComponent.new(title: "App Catalog") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_app_catalog') do %>
<section>
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Known Web Apps',
value: @stats[:known_apps],
) %>
<%# <%= render QuickstatsItemComponent.new(
<%# type: :number,
<%# title: 'Accepted',
<%# value: @stats[:accepted],
<%# ) %>
<%# <%= render QuickstatsItemComponent.new(
<%# type: :number,
<%# title: 'Users with referrals',
<%# value: @stats[:users_with_referrals],
<%# meta: "/ #{User.count}"
<%# ) %>
<% end %>
</section>
<% if @web_apps.any? %>
<section>
<h3>Web Apps</h3>
<table class="divided mb-8">
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th class="hidden md:table-cell">RS Auths</th>
<th class="hidden md:table-cell">Created at</th>
</tr>
</thead>
<tbody>
<% @web_apps.each do |web_app| %>
<tr>
<td><%= web_app.name %></td>
<td><%= link_to web_app.url, web_app.url,
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">
<span title="<%= web_app.created_at %>" class="cursor-help">
<%= time_ago_in_words web_app.created_at, include_seconds: false %> ago
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
<%== pagy_nav @pagy %>
</section>
<% end %>
<% end %>

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