32 Commits

Author SHA1 Message Date
Râu Cao
7f5b8c22b7 Make empty donations page prettier
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2023-01-10 14:09:15 +08:00
Râu Cao
44ec856091 Make invitation page prettier when it's empty 2023-01-10 14:09:15 +08:00
Râu Cao
6ac9f68566 Add coming-soon note to disabled settings nav items 2023-01-10 14:09:15 +08:00
Râu Cao
69bd9dcf78 Allow to copy invitation URLs via button 2023-01-10 14:09:14 +08:00
Râu Cao
2c6e6caab6 Add more info about project contributions 2023-01-10 14:09:14 +08:00
Râu Cao
a551e8d2ef Add Zeus to recommended wallet apps 2023-01-10 14:09:14 +08:00
Râu Cao
9166d887c4 Replace vanilla JS with new clipboard code 2023-01-10 14:09:14 +08:00
Râu Cao
d3313a202b Add a clipboard controller and wire up the copy button 2023-01-10 14:09:13 +08:00
Râu Cao
029e6b011d WIP Profile settings page
Show the user's user address, and provide a button for copying it to the
clipboard
2023-01-10 14:09:13 +08:00
Râu Cao
f1ef257c97 Rename settings page 2023-01-10 14:09:13 +08:00
Râu Cao
3f1c4f17a7 Add inviter and time to admin invitations list 2023-01-10 14:09:12 +08:00
Râu Cao
8c5852fefe Fix devise not rendering errors as flash messages
https://github.com/heartcombo/devise/issues/5446

closes #63
2023-01-10 14:09:12 +08:00
Râu Cao
317dba0b2d Set a minimum height for content with sidenav 2023-01-10 14:09:12 +08:00
Râu Cao
ab08c9ecf5 Improve button style 2023-01-10 14:09:12 +08:00
Râu Cao
49dd7bd96d Refactor settings routes and menu
Use sub controllers/routes for the sections
2023-01-10 14:09:11 +08:00
Râu Cao
c650b73ff9 Fix web container start when offline 2023-01-10 14:09:11 +08:00
Râu Cao
ea6173c60f Use tabnav component for wallet view 2023-01-10 14:09:11 +08:00
Râu Cao
cd46daa6ba Wording 2023-01-10 14:09:11 +08:00
Râu Cao
df3f91e2d0 Change donations to contrbutions, add tabbed nav
Introduces components for tabbed navigation and adds a tab menu and item
for non-financial contributions to the donations/contributions page.
2023-01-10 14:09:11 +08:00
Râu Cao
7c4106d7a2 Use more appropriate icon in sidenav 2023-01-10 14:09:10 +08:00
Râu Cao
cb79884c53 Improve README, add quick start instructions 2023-01-10 14:09:10 +08:00
Râu Cao
9266fdcfc2 Remove pid dir from git 2023-01-10 14:09:10 +08:00
Râu Cao
b04d822586 Comment encryption option in admin ldap users controller
Refactor to use the service later
2023-01-10 14:09:10 +08:00
Râu Cao
ba7b10fbc8 Add db/user seeds 2023-01-10 14:09:09 +08:00
Râu Cao
3b51670850 Rename ldap seed task to setup 2023-01-10 14:09:09 +08:00
Râu Cao
5b2a9e8c0c Don't start phpldapadmin by default 2023-01-10 14:09:09 +08:00
Râu Cao
ae239d584f Remove obsolete method 2023-01-10 14:09:09 +08:00
Râu Cao
e6d65ee582 Add flag for creating pre-confirmed users 2023-01-10 14:09:08 +08:00
Râu Cao
08b3ec499e Define patch version for Ruby base image
No need to re-download new images for every patch version
2023-01-10 14:09:08 +08:00
Râu Cao
6c17fbbbeb Delete admin role manually on reset 2023-01-10 14:09:08 +08:00
Râu Cao
76877645ce Add missing ACI and role to LDAP seeds 2023-01-10 14:09:08 +08:00
Râu Cao
7d143fabb8 Add note about resetting LDAP server 2023-01-10 14:09:07 +08:00
284 changed files with 1086 additions and 8566 deletions

View File

@@ -12,7 +12,7 @@ steps:
settings:
restore: true
mount:
- ./vendor/cache
- ./vendor
when:
branch:
- master
@@ -20,8 +20,6 @@ steps:
image: guildeducation/rails:2.7.2-14.20.0
environment:
RAILS_ENV: test
REDIS_URL: redis://redis:6379/0
RS_REDIS_URL: redis://redis:6379/1
commands:
- bundle config unset deployment
- bundle config set cache_all 'true'
@@ -30,7 +28,7 @@ steps:
- bundle install --jobs=3 --retry=3
- yarn install
- rake css:build
- bundle exec rspec
- rake spec
- name: rebuild-cache
image: drillster/drone-volume-cache
volumes:
@@ -39,15 +37,11 @@ steps:
settings:
rebuild: true
mount:
- ./vendor/cache
- ./vendor
when:
branch:
- master
services:
- name: redis
image: redis
volumes:
- name: cache
host:

View File

@@ -1,46 +1,11 @@
PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.example.com
SMTP_SERVER=smtp.example.com
SMTP_PORT=587
SMTP_LOGIN=accounts
SMTP_PASSWORD=123abc
SMTP_FROM_ADDRESS=accounts@example.com
SMTP_DOMAIN=example.com
SMTP_AUTH_METHOD=plain
SMTP_ENABLE_STARTTLS=auto
LDAP_HOST=localhost
LDAP_PORT=389
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'
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
DRONECI_PUBLIC_URL='https://drone.kosmos.org'
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
MASTODON_PUBLIC_URL='https://kosmos.social'
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/2'
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
BTCPAY_API_URL='http://localhost:23001/api/v1'
LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
LNDHUB_ADMIN_UI=true
LNDHUB_PG_HOST=localhost
LNDHUB_PG_PORT=5432
LNDHUB_PG_DATABASE=lndhub
LNDHUB_PG_USERNAME=lndhub
LNDHUB_PG_PASSWORD=''

4
.env.production Normal file
View File

@@ -0,0 +1,4 @@
EJABBERD_API_URL='https://xmpp.kosmos.org:5443/api'
BTCPAY_API_URL='http://10.1.1.163:23001/api/v1'
LNDHUB_API_URL='http://10.1.1.163:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'

View File

@@ -1,19 +1,4 @@
PRIMARY_DOMAIN=kosmos.org
REDIS_URL='redis://localhost:6379/0'
DISCOURSE_PUBLIC_URL='http://discourse.example.com'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
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:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/1'
WEBHOOKS_ALLOWED_IPS='10.1.1.23'

View File

@@ -1,14 +0,0 @@
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
version-resolver:
major:
labels:
- 'release/major'
minor:
labels:
- 'release/minor'
- 'feature'
patch:
labels:
- 'release/patch'
default: patch

View File

@@ -1,11 +0,0 @@
name: Release Drafter
on:
pull_request:
types: [closed]
jobs:
release_drafter_job:
name: Update release notes draft
runs-on: ubuntu-latest
steps:
- name: Release Drafter
uses: https://github.com/raucao/gitea-release-drafter@dev

1
.gitignore vendored
View File

@@ -39,7 +39,6 @@ yarn-debug.log*
# Ignore local dotenv config file
.env
.env.development
# Ignore redis dumps from sidekiq
dump.rdb

View File

@@ -1,21 +1,22 @@
# syntax=docker/dockerfile:1
FROM ruby:2.7.6
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
ldap-utils tini
RUN apt-get update -qq && apt-get install -y curl ldap-utils
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get install -y nodejs
WORKDIR /akkounts
COPY ["Gemfile", "Gemfile.lock", "package.json", "./"]
COPY Gemfile /akkounts/Gemfile
COPY Gemfile.lock /akkounts/Gemfile.lock
COPY package.json /akkounts/package.json
RUN bundle install
RUN gem install foreman
RUN npm install -g yarn
RUN yarn install
ENTRYPOINT ["/usr/bin/tini", "--"]
# Add a script to be executed every time the container starts.
COPY docker/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
# Configure the main process to run when running the image
CMD ["bin", "dev"]

18
Gemfile
View File

@@ -32,17 +32,12 @@ gem 'lockbox'
# Authentication
gem 'warden'
gem 'devise', '~> 4.9.0'
gem 'devise'
gem 'devise_ldap_authenticatable'
gem 'net-ldap'
# Utilities
gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3'
gem 'pagy', '~> 6.0', '>= 6.0.2'
gem 'flipper'
gem 'flipper-active_record'
gem 'flipper-ui'
# HTTP requests
gem 'faraday'
@@ -51,20 +46,10 @@ gem 'faraday'
gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler'
# Monitoring
gem "sentry-ruby"
gem "sentry-rails"
# Services
gem 'discourse_api'
gem "lnurl"
gem 'nostr', git: 'https://gitea.kosmos.org/kosmos/nostr-gem.git', branch: 'feature/ruby_2.7_compat'
group :development, :test do
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4'
gem 'rspec-rails'
gem 'rails-controller-testing'
gem "byebug", "~> 11.1"
end
@@ -75,7 +60,6 @@ group :development do
gem 'letter_opener'
gem 'letter_opener_web'
gem 'faker'
gem 'solargraph'
end
group :test do

View File

@@ -1,98 +1,78 @@
GIT
remote: https://gitea.kosmos.org/kosmos/nostr-gem.git
revision: 596529d9eb50d13b3f385245636698fccf37b442
branch: feature/ruby_2.7_compat
specs:
nostr (0.4.0)
bech32 (~> 1.3)
bip-schnorr (~> 0.4)
ecdsa (~> 1.2)
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
json (~> 2.6)
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.5)
actionpack (= 7.0.5)
activesupport (= 7.0.5)
actioncable (7.0.4)
actionpack (= 7.0.4)
activesupport (= 7.0.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.5)
actionpack (= 7.0.5)
activejob (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
actionmailbox (7.0.4)
actionpack (= 7.0.4)
activejob (= 7.0.4)
activerecord (= 7.0.4)
activestorage (= 7.0.4)
activesupport (= 7.0.4)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.5)
actionpack (= 7.0.5)
actionview (= 7.0.5)
activejob (= 7.0.5)
activesupport (= 7.0.5)
actionmailer (7.0.4)
actionpack (= 7.0.4)
actionview (= 7.0.4)
activejob (= 7.0.4)
activesupport (= 7.0.4)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.5)
actionview (= 7.0.5)
activesupport (= 7.0.5)
rack (~> 2.0, >= 2.2.4)
actionpack (7.0.4)
actionview (= 7.0.4)
activesupport (= 7.0.4)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.5)
actionpack (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
actiontext (7.0.4)
actionpack (= 7.0.4)
activerecord (= 7.0.4)
activestorage (= 7.0.4)
activesupport (= 7.0.4)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.5)
activesupport (= 7.0.5)
actionview (7.0.4)
activesupport (= 7.0.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.5)
activesupport (= 7.0.5)
activejob (7.0.4)
activesupport (= 7.0.4)
globalid (>= 0.3.6)
activemodel (7.0.5)
activesupport (= 7.0.5)
activerecord (7.0.5)
activemodel (= 7.0.5)
activesupport (= 7.0.5)
activestorage (7.0.5)
actionpack (= 7.0.5)
activejob (= 7.0.5)
activerecord (= 7.0.5)
activesupport (= 7.0.5)
activemodel (7.0.4)
activesupport (= 7.0.4)
activerecord (7.0.4)
activemodel (= 7.0.4)
activesupport (= 7.0.4)
activestorage (7.0.4)
actionpack (= 7.0.4)
activejob (= 7.0.4)
activerecord (= 7.0.4)
activesupport (= 7.0.4)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.5)
activesupport (7.0.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.4)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
backport (1.2.0)
bcrypt (3.1.18)
bech32 (1.3.0)
thor (>= 1.1.0)
benchmark (0.2.1)
bindex (0.8.1)
bip-schnorr (0.6.0)
ecdsa_ext (~> 0.5.0)
builder (3.2.4)
byebug (11.1.3)
capybara (3.39.2)
capybara (3.38.0)
addressable
matrix
mini_mime (>= 0.1.3)
@@ -102,21 +82,20 @@ GEM
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
chunky_png (1.4.0)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
concurrent-ruby (1.1.10)
connection_pool (2.3.0)
crack (0.4.5)
rexml
crass (1.0.6)
cssbundling-rails (1.1.2)
cssbundling-rails (1.1.1)
railties (>= 6.0.0)
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.1.0)
database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0)
database_cleaner-active_record (2.0.1)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.3)
devise (4.9.2)
devise (4.8.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -126,76 +105,41 @@ GEM
devise (>= 3.4.1)
net-ldap (>= 0.16.0)
diff-lcs (1.5.0)
discourse_api (2.0.1)
faraday (~> 2.7)
faraday-follow_redirects
faraday-multipart
rack (>= 1.6)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
e2mmap (0.1.0)
ecdsa (1.2.0)
ecdsa_ext (0.5.0)
ecdsa (~> 1.2.0)
erubi (1.12.0)
erubi (1.11.0)
et-orbi (1.2.7)
tzinfo
event_emitter (0.2.6)
eventmachine (1.2.7)
factory_bot (6.2.1)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faker (3.2.0)
faker (3.0.0)
i18n (>= 1.8.11, < 2)
faraday (2.7.6)
faraday (2.7.1)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.0.2)
faye-websocket (0.11.2)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.15.5)
flipper (0.28.0)
concurrent-ruby (< 2)
flipper-active_record (0.28.0)
activerecord (>= 4.2, < 8)
flipper (~> 0.28.0)
flipper-ui (0.28.0)
erubi (>= 1.0.0, < 2.0.0)
flipper (~> 0.28.0)
rack (>= 1.4, < 3)
rack-protection (>= 1.5.3, <= 4.0.0)
sanitize (< 7)
fugit (1.8.1)
fugit (1.7.2)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
globalid (1.1.0)
globalid (1.0.0)
activesupport (>= 5.0)
hashdiff (1.0.1)
i18n (1.14.1)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
importmap-rails (1.1.6)
importmap-rails (1.1.5)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
jaro_winkler (1.5.6)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.6.3)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
launchy (2.5.2)
addressable (~> 2.8)
launchy (2.5.0)
addressable (~> 2.7)
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
letter_opener_web (2.0.0)
@@ -203,198 +147,141 @@ GEM
letter_opener (~> 1.7)
railties (>= 5.2)
rexml
listen (3.8.0)
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
lnurl (1.0.1)
bech32 (~> 1.1)
lockbox (1.2.0)
loofah (2.21.3)
lockbox (1.1.0)
loofah (2.19.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.2)
matrix (0.4.2)
method_source (1.0.0)
mini_mime (1.1.2)
minitest (5.18.0)
multipart-post (2.3.0)
net-imap (0.3.6)
date
mini_portile2 (2.8.0)
minitest (5.16.3)
net-imap (0.3.1)
net-protocol
net-ldap (0.18.0)
net-ldap (0.17.1)
net-pop (0.1.2)
net-protocol
net-protocol (0.2.1)
net-protocol (0.1.3)
timeout
net-smtp (0.3.3)
net-protocol
nio4r (2.5.9)
nokogiri (1.15.2-arm64-darwin)
nio4r (2.5.8)
nokogiri (1.13.9)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.15.2-x86_64-linux)
nokogiri (1.13.9-x86_64-linux)
racc (~> 1.4)
orm_adapter (0.5.0)
pagy (6.0.4)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
pg (1.2.3)
public_suffix (5.0.1)
public_suffix (5.0.0)
puma (4.3.12)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.7.1)
rack (2.2.7)
rack-protection (3.0.6)
rack
rack-test (2.1.0)
racc (1.6.0)
rack (2.2.4)
rack-test (2.0.2)
rack (>= 1.3)
rails (7.0.5)
actioncable (= 7.0.5)
actionmailbox (= 7.0.5)
actionmailer (= 7.0.5)
actionpack (= 7.0.5)
actiontext (= 7.0.5)
actionview (= 7.0.5)
activejob (= 7.0.5)
activemodel (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
rails (7.0.4)
actioncable (= 7.0.4)
actionmailbox (= 7.0.4)
actionmailer (= 7.0.4)
actionpack (= 7.0.4)
actiontext (= 7.0.4)
actionview (= 7.0.4)
activejob (= 7.0.4)
activemodel (= 7.0.4)
activerecord (= 7.0.4)
activestorage (= 7.0.4)
activesupport (= 7.0.4)
bundler (>= 1.15.0)
railties (= 7.0.5)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
railties (= 7.0.4)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
rails-settings-cached (2.8.3)
activerecord (>= 5.0.0)
railties (>= 5.0.0)
railties (7.0.5)
actionpack (= 7.0.5)
activesupport (= 7.0.5)
rails-html-sanitizer (1.4.3)
loofah (~> 2.3)
railties (7.0.4)
actionpack (= 7.0.4)
activesupport (= 7.0.4)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbs (2.8.4)
redis (4.8.1)
regexp_parser (2.8.1)
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
reverse_markdown (2.1.1)
nokogiri
redis (5.0.5)
redis-client (>= 0.9.0)
redis-client (0.11.2)
connection_pool
regexp_parser (2.6.1)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.5)
rqrcode (2.2.0)
rqrcode (2.1.2)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec-core (3.12.2)
rspec-core (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)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.5)
rspec-mocks (3.12.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.3)
rspec-rails (6.0.1)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-core (~> 3.11)
rspec-expectations (~> 3.11)
rspec-mocks (~> 3.11)
rspec-support (~> 3.11)
rspec-support (3.12.0)
rubocop (1.52.1)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.2.3)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rufus-scheduler (3.9.1)
rufus-scheduler (3.8.2)
fugit (~> 1.1, >= 1.1.6)
sanitize (6.0.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
sentry-rails (5.9.0)
railties (>= 5.0)
sentry-ruby (~> 5.9.0)
sentry-ruby (5.9.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (6.5.9)
connection_pool (>= 2.2.5, < 3)
sidekiq (6.5.5)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
sidekiq-scheduler (5.0.3)
redis (>= 4.5.0)
sidekiq-scheduler (4.0.3)
redis (>= 4.2.0)
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
sidekiq (>= 4, < 7)
tilt (>= 1.4.0)
solargraph (0.49.0)
backport (~> 1.2)
benchmark
bundler (~> 2.0)
diff-lcs (~> 1.4)
e2mmap
jaro_winkler (~> 1.5)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1)
parser (~> 3.0)
rbs (~> 2.0)
reverse_markdown (~> 2.0)
rubocop (~> 1.38)
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
sprockets (4.2.0)
sprockets (4.1.1)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
rack (> 1, < 3)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.6.3-arm64-darwin)
sqlite3 (1.6.3-x86_64-linux)
sqlite3 (1.5.4)
mini_portile2 (~> 2.8.0)
sqlite3 (1.5.4-x86_64-linux)
stimulus-rails (1.2.1)
railties (>= 6.0.0)
thor (1.2.2)
tilt (2.2.0)
timeout (0.3.2)
turbo-rails (1.4.0)
thor (1.2.1)
tilt (2.0.11)
timeout (0.3.0)
turbo-rails (1.3.2)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
tzinfo (2.0.6)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2)
view_component (3.2.0)
activesupport (>= 5.2.0, < 8.0)
view_component (2.78.0)
activesupport (>= 5.0.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
warden (1.2.9)
@@ -413,11 +300,10 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.34)
zeitwerk (2.6.8)
zeitwerk (2.6.6)
PLATFORMS
arm64-darwin-22
ruby
x86_64-linux
DEPENDENCIES
@@ -425,38 +311,26 @@ DEPENDENCIES
capybara
cssbundling-rails
database_cleaner
devise (~> 4.9.0)
devise
devise_ldap_authenticatable
discourse_api
dotenv-rails
factory_bot_rails
faker
faraday
flipper
flipper-active_record
flipper-ui
importmap-rails
jbuilder (~> 2.7)
letter_opener
letter_opener_web
listen (~> 3.2)
lnurl
lockbox
net-ldap
nostr!
pagy (~> 6.0, >= 6.0.2)
pg (~> 1.2.3)
puma (~> 4.1)
rails (~> 7.0.2)
rails-controller-testing
rails-settings-cached (~> 2.8.3)
rqrcode (~> 2.0)
rspec-rails
sentry-rails
sentry-ruby
sidekiq (< 7)
sidekiq-scheduler
solargraph
sprockets-rails
sqlite3 (~> 1.4)
stimulus-rails

View File

@@ -14,16 +14,18 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
Docker Desktop)
2. Uncomment the `web` section in `docker-compose.yml`
3. Run `docker compose up` and wait until 389ds announces its successful start
in the log output
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`
6. `docker compose run web rails db:setup`
5. `docker compose run web rails db:setup`
After these steps, you should have a working Rails app with a handful of test
users running on [http://localhost:3000](http://localhost:3000).
Log in with username "admin" and password "admin is admin". All users listed on
[http://localhost:3000/admin/users](http://localhost:3000/admin/users)
[http://localhost:3000/admin/ldap_users](http://localhost:3000/admin/ldap_users)
have the password "user is user".
### Rails app
@@ -52,14 +54,12 @@ Running all specs:
### Docker (Compose)
There is a working Docker Compose config file, which define a number of services including
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.
For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`,
listening on port 389 on your machine.
You can pick and choose your services adding them by name (listed in `docker-compose.yml`) at
the end of the docker compose command. eg. `docker compose up ldap redis`
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
you want to use them.
#### LDAP server
@@ -79,35 +79,14 @@ The setup task will first delete any existing entries in the directory tree
Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
with a fresh installation, delete both that directory as well as the container.
### 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](https://solargraph.org/) is a Ruby language server, which you may
use with your editor to add features like auto-completion and syntax
validation. You can add inline documentation for bundled gems with this
command:
bundle exec yard gems
## Documentation
### Rails
* [Ruby on Rails](https://guides.rubyonrails.org/)
* [Pagination](https://ddnexus.github.io/pagy/)
* [Sass](https://sass-lang.com/documentation)
### Front-end
* [Tailwind CSS](https://tailwindcss.com/)
* [Sass](https://sass-lang.com/documentation)
* [Stimulus](https://stimulus.hotwired.dev/handbook/)
* [Tailwind Stimulus Components](https://github.com/excid3/tailwindcss-stimulus-components)
### Testing
@@ -124,10 +103,6 @@ command:
* [Sidekiq](https://github.com/mperham/sidekiq/wiki/)
* [ActiveJob](https://github.com/mperham/sidekiq/wiki/Active-Job)
### Feature Flags
* [Flipper](https://www.flippercloud.io/docs/get-started/self-hosted)
## License
[GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/)

View File

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

View File

@@ -2,12 +2,9 @@
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "components/animations";
@import "components/base";
@import "components/buttons";
@import "components/dashboard_services";
@import "components/forms";
@import "components/links";
@import "components/notifications";
@import "components/pagination";
@import "components/tables";

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;
}
h4 {
@apply font-bold mb-4 leading-6;
}
main section {
@apply pt-8 sm:pt-12;
}
@@ -40,18 +36,10 @@
@apply mb-4 leading-6;
}
main p:last-child {
@apply mb-0;
}
main ul {
@apply mb-6;
}
main ul:last-child {
@apply mb-0;
}
main ul li {
@apply leading-6;
}

View File

@@ -1,6 +1,6 @@
@layer components {
.btn {
@apply inline-block font-semibold rounded-md leading-none cursor-pointer text-center
@apply font-semibold rounded-md leading-none cursor-pointer text-center
transition-colors duration-75 focus:outline-none focus:ring-4;
}
@@ -15,11 +15,7 @@
}
.btn-icon {
@apply py-2 px-3;
}
.btn-outline {
@apply py-2 border-2 border-gray-100 hover:bg-gray-100;
@apply px-3;
}
.btn-gray {
@@ -37,8 +33,7 @@
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;
input[type=text]:disabled {
@apply text-gray-700;
}
}

View File

@@ -1,5 +0,0 @@
@layer components {
.services > div > a {
background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 0, rgba(255,255,255,0.88) 100%);
}
}

View File

@@ -1,20 +1,11 @@
@layer components {
input[type=text], input[type=email], input[type=password],
input[type=number], select, textarea {
@apply rounded-md bg-gray-100 focus:bg-white
input[type=number], select {
@apply mt-1 rounded-md bg-gray-100 focus:bg-white
border-transparent focus:border-transparent focus:ring-2
focus:ring-blue-600 focus:ring-opacity-75;
}
input[type=text]:disabled,
input[type=email]:disabled {
@apply text-gray-700;
}
input.field_with_errors {
@apply border-b-red-600;
}
.field_with_errors {
@apply inline-block;
}

View File

@@ -5,4 +5,10 @@
&:visited { @apply text-indigo-600; }
&:active { @apply text-red-600; }
}
.devise-links {
a {
@apply ks-text-link;
}
}
}

View File

@@ -1,45 +0,0 @@
@layer components {
.pagy-nav.pagination {
@apply isolate inline-flex -space-x-px rounded-md shadow-sm;
}
.pagy-nav .page:not(.prev):not(.next) {
@apply hidden sm:inline-block;
}
.pagy-nav .page.next a {
@apply relative inline-flex items-center rounded-r-md border
border-gray-300 bg-white px-3 py-2 text-sm font-medium
text-gray-500 hover:bg-gray-100 focus:z-20;
}
.pagy-nav .page.prev a {
@apply relative inline-flex items-center rounded-l-md border
border-gray-300 bg-white px-3 py-2 text-sm font-medium
text-gray-500 hover:bg-gray-100 focus:z-20;
}
.pagy-nav .page.next.disabled {
@apply relative inline-flex items-center rounded-r-md border
border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium
text-gray-400 focus:z-20;
}
.pagy-nav .page.prev.disabled {
@apply relative inline-flex items-center rounded-l-md border
border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium
text-gray-400 focus:z-20;
}
.pagy-nav .page a, .page.gap {
@apply bg-white border-gray-300 text-gray-500 hover:bg-gray-100 relative
inline-flex items-center border px-4 py-2 text-sm font-medium
focus:z-20;
}
.pagy-nav .page.active {
@apply z-10 border-indigo-500 bg-indigo-50 text-indigo-600 relative
inline-flex items-center border px-4 py-2 text-sm font-medium
focus:z-20;
}
}

View File

@@ -7,30 +7,16 @@
@apply text-left;
}
table thead th {
table th {
@apply pb-3.5 text-sm font-normal uppercase text-gray-500;
}
table tbody th {
@apply text-left font-normal text-gray-500;
}
table th:not(:last-of-type),
table td:not(:last-of-type) {
@apply pr-2;
}
table td, tbody th {
table td {
@apply py-2;
}
table.divided {
@apply divide-y divide-gray-300;
}
table.divided tbody {
@apply divide-y divide-gray-200;
}
table.divided td, table.divided tbody th {
@apply py-3;
}
}

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,45 +0,0 @@
<%= tag.public_send(@tag, class: "mb-6 last:mb-0", data: {
:'field-name' => @field_name
}) do %>
<% if @positioning == :vertical %>
<label class="block">
<p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>">
<%= @title %>
</p>
<% if @descripton.present? %>
<p class="text-gray-500">
<%= @descripton %>
</p>
<% end %>
<%= 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>
<% elsif @positioning == :horizontal %>
<label class="block flex items-center justify-between">
<div class="flex flex-col">
<label class="font-bold mb-1"><%= @title %></label>
<% if @descripton.present? %>
<p class="text-gray-500"><%= @descripton %></p>
<% end %>
</div>
<div class="relative ml-4 inline-flex flex-shrink-0">
<%= content %>
</div>
</label>
<% else %>
<p>Invalid <code>positioning<code> argument for <code>FieldsetComponent</code>.</p>
<% end %>
<% end %>

View File

@@ -1,16 +0,0 @@
# frozen_string_literal: true
module FormElements
class FieldsetComponent < ViewComponent::Base
def initialize(tag: "li", positioning: :vertical,
title:, description: nil,
field_name: nil, resettable: false)
@tag = tag
@positioning = positioning
@title = title
@descripton = description
@field_name = field_name
@resettable = resettable
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
@descripton = 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

@@ -1,33 +0,0 @@
<%= tag.public_send @tag, class: "flex items-center justify-between mb-6 last:mb-0",
data: @form_enabled ? {
controller: "settings--toggle",
:'settings--toggle-switch-enabled-value' => @enabled.to_s
} : nil do %>
<div class="flex flex-col">
<label class="font-bold mb-1"><%= @title %></label>
<p class="text-gray-500"><%= @descripton %></p>
</div>
<div class="relative ml-4 inline-flex flex-shrink-0">
<%= render FormElements::ToggleComponent.new(
enabled: @enabled,
input_enabled: @input_enabled,
class_names: @form_enabled ? "hidden" : nil,
data: {
:'settings--toggle-target' => "button",
action: "settings--toggle#toggleSwitch"
}) %>
<% if @form_enabled %>
<% if @attribute.present? %>
<%= @form.check_box @attribute, {
checked: @enabled,
data: { :'settings--toggle-target' => "checkbox" }
}, "true", "false" %>
<% else %>
<input name="<%= @field_name %>" type="hidden" value="false" autocomplete="off">
<%= check_box_tag @field_name, "true", @enabled, {
data: { :'settings--toggle-target' => "checkbox" }
} %>
<% end %>
<% end %>
</div>
<% end %>

View File

@@ -1,19 +0,0 @@
# frozen_string_literal: true
module FormElements
class FieldsetToggleComponent < ViewComponent::Base
def initialize(tag: "li", form: nil, attribute: nil, field_name: nil,
enabled: false, input_enabled: true, title:, description:)
@tag = tag
@form = form
@attribute = attribute
@field_name = field_name
@form_enabled = @form.present? || @field_name.present?
@enabled = enabled
@input_enabled = input_enabled
@title = title
@descripton = description
@button_text = @enabled ? "Switch off" : "Switch on"
end
end
end

View File

@@ -1,15 +0,0 @@
<%= button_tag type: "button", name: "toggle", data: @data,
role: "switch", aria: { checked: @enabled.to_s },
tabindex: @tabindex, disabled: !@input_enabled,
class: "#{ @enabled ? 'bg-blue-600' : 'bg-gray-200' }
#{ @class_names.present? ? @class_names : '' }
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer
rounded-full border-2 border-transparent transition-colors
duration-200 ease-in-out focus:outline-none focus:ring-2
focus:ring-blue-600 focus:ring-offset-2" do %>
<span class="sr-only"><%= @button_text %></span>
<span aria-hidden="true" data-settings--toggle-target="switch"
class="<%= @enabled ? 'translate-x-5' : 'translate-x-0' %>
pointer-events-none inline-block h-5 w-5 transform rounded-full
bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
<% end %>

View File

@@ -1,13 +0,0 @@
# frozen_string_literal: true
module FormElements
class ToggleComponent < ViewComponent::Base
def initialize(enabled:, input_enabled: true, data: nil, class_names: nil, tabindex: nil)
@enabled = !!enabled
@input_enabled = input_enabled
@data = data
@class_names = class_names
@tabindex = tabindex
end
end
end

View File

@@ -1,28 +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 %>
<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>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,2 +0,0 @@
class ModalComponent < ViewComponent::Base
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,3 +0,0 @@
<dl class="grid grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-12">
<%= content %>
</dl>

View File

@@ -1,4 +0,0 @@
# frozen_string_literal: true
class QuickstatsContainerComponent < ViewComponent::Base
end

View File

@@ -1,18 +0,0 @@
<div class="">
<dt class="mb-2 text-gray-500">
<%= @title %>
</dt>
<dd>
<% if @type == :number %>
<span class="text-2xl"><%= number_with_delimiter @value %></span>
<% else %>
<span class="text-2xl"><%= @value %></span>
<% end %>
<% if @unit %>
<span><%= @unit %></span>
<% end %>
<% if @meta %>
<span class="text-gray-500"><%= @meta %></span>
<% end %>
</dd>
</div>

View File

@@ -1,13 +0,0 @@
# frozen_string_literal: true
class QuickstatsItemComponent < ViewComponent::Base
def initialize(type:, title:, value:, unit: nil, meta: nil, icon_name: nil, icon_color_class: nil)
@type = type
@title = title
@value = value
@unit = unit
@meta = meta
@icon_name = icon_name
@icon_color_class = icon_color_class
end
end

View File

@@ -1,9 +1,8 @@
# frozen_string_literal: true
class SidenavLinkComponent < ViewComponent::Base
def initialize(name:, level: 1, path:, icon:, active: false, disabled: false)
def initialize(name:, path:, icon:, active: false, disabled: false)
@name = name
@level = level
@path = path
@icon = icon
@active = active
@@ -13,15 +12,12 @@ class SidenavLinkComponent < ViewComponent::Base
end
def class_names_link(path)
px = @level == 1 ? "px-4" : "pl-8 pr-4"
base = "#{px} py-2 group border-l-4 flex items-center text-base font-medium"
if @active
"#{base} bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
"bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700 group border-l-4 px-4 py-2 flex items-center text-base font-medium"
elsif @disabled
"#{base} border-transparent text-gray-400 hover:bg-gray-50"
"border-transparent text-gray-400 hover:bg-gray-50 group border-l-4 px-4 py-2 flex items-center text-base font-medium"
else
"#{base} border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900"
"border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-4 py-2 flex items-center text-base font-medium"
end
end

View File

@@ -7,14 +7,10 @@
<div class="md:col-span-4 mt-4 md:mt-0">
<p class="font-mono md:text-right mb-0 p-4 border border-gray-300 rounded-lg overflow-hidden">
<% if @balance %>
<span class="text-2xl"><%= number_with_delimiter @balance %></span>
<span class="text-xl">sats</span>
<br>
<span class="text-xl"><%= number_with_delimiter @balance %> sats</span><br>
<span class="text-sm text-gray-500">Available balance</span>
<% else %>
<span class="text-2xl">n/a</span>
<span class="text-xl">sats</span>
<br>
<span class="text-xl">n/a sats</span><br>
<span class="text-sm text-gray-500">Balance unavailable</span>
<% end %>
</p>

View File

@@ -1,5 +1,5 @@
class AccountController < ApplicationController
before_action :authenticate_user!
before_action :require_user_signed_in
def index
@current_section = :account

View File

@@ -1,5 +1,4 @@
class Admin::BaseController < ApplicationController
include Pagy::Backend
before_action :authenticate_user!
before_action :authorize_admin
@@ -8,4 +7,5 @@ class Admin::BaseController < ApplicationController
def set_context
@context = :admin
end
end

View File

@@ -5,12 +5,7 @@ class Admin::DonationsController < Admin::BaseController
# GET /donations
# GET /donations.json
def index
@pagy, @donations = pagy(Donation.all.order('created_at desc'))
@stats = {
overall_sats: @donations.all.sum("amount_sats"),
donor_count: Donation.distinct.count(:user_id)
}
@donations = Donation.all
end
# GET /donations/1
@@ -34,14 +29,10 @@ class Admin::DonationsController < Admin::BaseController
respond_to do |format|
if @donation.save
format.html do
redirect_to admin_donation_url(@donation), flash: {
success: 'Donation was successfully created.'
}
end
format.html { redirect_to admin_donation_url(@donation), notice: 'Donation was successfully created.' }
format.json { render :show, status: :created, location: @donation }
else
format.html { render :new, status: :unprocessable_entity }
format.html { render :new }
format.json { render json: @donation.errors, status: :unprocessable_entity }
end
end
@@ -52,14 +43,10 @@ class Admin::DonationsController < Admin::BaseController
def update
respond_to do |format|
if @donation.update(donation_params)
format.html do
redirect_to admin_donation_url(@donation), flash: {
success: 'Donation was successfully updated.'
}
end
format.html { redirect_to admin_donation_url(@donation), notice: 'Donation was successfully updated.' }
format.json { render :show, status: :ok, location: @donation }
else
format.html { render :edit, status: :unprocessable_entity }
format.html { render :edit }
format.json { render json: @donation.errors, status: :unprocessable_entity }
end
end
@@ -70,10 +57,7 @@ class Admin::DonationsController < Admin::BaseController
def destroy
@donation.destroy
respond_to do |format|
format.html do redirect_to admin_donations_url, flash: {
success: 'Donation was successfully destroyed.'
}
end
format.html { redirect_to admin_donations_url, notice: 'Donation was successfully destroyed.' }
format.json { head :no_content }
end
end

View File

@@ -1,12 +1,8 @@
class Admin::InvitationsController < Admin::BaseController
def index
@current_section = :invitations
@pagy, @invitations_used = pagy(Invitation.used.order('used_at desc'))
@stats = {
available: Invitation.unused.count,
accepted: @invitations_used.length,
users_with_referrals: Invitation.used.distinct.count(:user_id)
}
@invitations_unused_count = Invitation.unused.count
@users_with_referrals_count = Invitation.used.distinct.count(:user_id)
@invitations_used = Invitation.used.order('used_at desc')
end
end

View File

@@ -0,0 +1,45 @@
class Admin::LdapUsersController < Admin::BaseController
before_action :set_current_section
def index
attributes = %w{dn cn uid mail admin}
filter = Net::LDAP::Filter.eq("uid", "*")
@ou = params[:ou] || "kosmos.org"
treebase = "ou=#{@ou},cn=users,dc=kosmos,dc=org"
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] }
@entries = entries.collect do |e|
{
uid: e.uid.first,
mail: e.try(:mail) ? e.mail.first : nil,
admin: e.try(:admin) ? 'admin' : nil
# password: e.userpassword.first
}
end
# ldap_client.get_operation_result
end
private
def ldap_client
ldap_client ||= Net::LDAP.new host: ldap_config['host'],
port: ldap_config['port'],
# encryption: ldap_config['ssl'],
auth: {
method: :simple,
username: ldap_config['admin_user'],
password: ldap_config['admin_password']
}
end
def ldap_config
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
end
def set_current_section
@current_section = :ldap_users
end
end

View File

@@ -1,21 +0,0 @@
class Admin::LightningController < Admin::BaseController
before_action :check_feature_enabled
def index
@current_section = :lightning
@users = User.pluck(:cn, :ou, :ln_account)
@accounts = LndhubAccount.with_balances.order(balance: :desc).to_a
@ln = {}
@ln[:current_balance] = LndhubAccount.current.joins(:ledgers).sum("account_ledgers.amount")
@ln[:users_with_sats] = @accounts.length
end
def check_feature_enabled
if !Setting.lndhub_admin_enabled?
flash[:alert] = "Lightning Admin UI not enabled"
redirect_to admin_root_path and return
end
end
end

View File

@@ -1,12 +0,0 @@
class Admin::Settings::RegistrationsController < Admin::SettingsController
def index
end
def create
update_settings
redirect_to admin_settings_registrations_path, flash: {
success: "Settings saved"
}
end
end

View File

@@ -1,19 +0,0 @@
class Admin::Settings::ServicesController < Admin::SettingsController
def index
@service = params[:s]
if @service.blank?
redirect_to admin_settings_services_path(params: { s: "discourse" })
end
end
def create
service = params.require(:service)
update_settings
redirect_to admin_settings_services_path(params: { s: service }), flash: {
success: "Settings saved"
}
end
end

View File

@@ -1,40 +0,0 @@
class Admin::SettingsController < Admin::BaseController
before_action :set_current_section
def index
end
def update_settings
@errors = ActiveModel::Errors.new(Setting.new)
changed_keys = []
setting_params.keys.each do |key|
next if setting_params[key].nil? ||
(Setting.send(key).to_s == setting_params[key].strip)
changed_keys.push(key)
setting = Setting.new(var: key)
setting.value = setting_params[key].strip
unless setting.valid?
@errors.merge!(setting.errors)
end
end
if @errors.any?
render :index and return
end
changed_keys.each do |key|
Setting.send("#{key}=", setting_params[key].strip)
end
end
private
def set_current_section
@current_section = :settings
end
def setting_params
params.require(:setting).permit(Setting.editable_keys.map(&:to_sym))
end
end

View File

@@ -1,35 +0,0 @@
class Admin::UsersController < Admin::BaseController
before_action :set_user, only: [:show]
before_action :set_current_section
def index
ldap = LdapService.new
@ou = params[:ou] || Setting.primary_domain
@orgs = ldap.fetch_organizations
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
@stats = {
users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: @ou).pending.count
}
end
def show
if Setting.lndhub_admin_enabled?
@lndhub_user = @user.lndhub_user
end
@services_enabled = @user.services_enabled
end
private
def set_user
address = params[:address].split("@")
@user = User.where(cn: address.first, ou: address.last).first
end
def set_current_section
@current_section = :users
end
end

View File

@@ -3,18 +3,6 @@ class ApplicationController < ActionController::Base
render :text => exception, :status => 500
end
before_action :sentry_set_user
def sentry_set_user
return unless Setting.sentry_enabled
if user_signed_in?
Sentry.set_user(id: current_user.id, username: current_user.cn)
else
Sentry.set_user({})
end
end
def require_user_signed_in
unless user_signed_in?
redirect_to welcome_path and return

View File

@@ -1,5 +1,5 @@
class Contributions::DonationsController < ApplicationController
before_action :authenticate_user!
before_action :require_user_signed_in
# GET /donations
# GET /donations.json

View File

@@ -1,5 +1,5 @@
class Contributions::ProjectsController < ApplicationController
before_action :authenticate_user!
before_action :require_user_signed_in
# GET /contributions
def index

View File

@@ -2,6 +2,6 @@ class DashboardController < ApplicationController
before_action :require_user_signed_in
def index
@current_section = :services
@current_section = :dashboard
end
end

View File

@@ -1,17 +0,0 @@
class Discourse::SsoController < ApplicationController
before_action :authenticate_user!
def connect
secret = Setting.discourse_connect_secret
sso = DiscourseApi::SingleSignOn.parse(request.query_string, secret)
sso.external_id = current_user.id
sso.email = current_user.email
sso.username = current_user.cn
sso.name = current_user.display_name
sso.admin = current_user.is_admin?
sso.sso_secret = secret
redirect_to sso.to_url("#{Setting.discourse_public_url}/session/sso_login"),
allow_other_host: true
end
end

View File

@@ -1,11 +1,11 @@
class InvitationsController < ApplicationController
before_action :authenticate_user!, except: ["show"]
before_action :require_user_signed_in, except: ["show"]
before_action :require_user_signed_out, only: ["show"]
# GET /invitations
def index
@invitations_unused = current_user.invitations.unused
@invitations_used = current_user.invitations.used.order('used_at desc')
@invitations_used = current_user.invitations.used
@current_section = :invitations
end
@@ -27,10 +27,7 @@ class InvitationsController < ApplicationController
respond_to do |format|
if @invitation.save
format.html do redirect_to @invitation, flash: {
success: 'Invitation was successfully created.'
}
end
format.html { redirect_to @invitation, notice: 'Invitation was successfully created.' }
format.json { render :show, status: :created, location: @invitation }
else
format.html { render :new }

View File

@@ -1,5 +1,4 @@
class LnurlpayController < ApplicationController
before_action :check_feature_enabled
before_action :find_user_by_address
MIN_SATS = 10
@@ -18,20 +17,6 @@ class LnurlpayController < ApplicationController
}
end
def keysend
http_status :not_found and return unless Setting.lndhub_keysend_enabled?
render json: {
status: "OK",
tag: "keysend",
pubkey: Setting.lndhub_public_key,
customData: [{
customKey: "696969",
customValue: @user.ln_account
}]
}
end
def invoice
amount = params[:amount].to_i / 1000 # msats
address = params[:address]
@@ -47,7 +32,7 @@ class LnurlpayController < ApplicationController
return
end
memo = "To #{address}"
memo = "Sats for #{address}"
memo = "#{memo}: \"#{comment}\"" if comment.present?
payment_request = @user.ln_create_invoice({
@@ -87,9 +72,4 @@ class LnurlpayController < ApplicationController
comment.length <= MAX_COMMENT_CHARS
end
private
def check_feature_enabled
http_status :not_found unless Setting.lndhub_enabled?
end
end

View File

@@ -1,145 +0,0 @@
class Rs::OauthController < ApplicationController
before_action :require_signed_in_with_username, only: :new
before_action :authenticate_user!, only: :create
def new
username, org = params[:useraddress].split("@")
@user = User.where(cn: username.downcase, ou: org).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.address,
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, #TODO use user-defined name
expire_at: expire_at
)
redirect_to url_with_state("#{redirect_uri}#access_token=#{auth.token}", state),
allow_other_host: true
end
# GET /rs/oauth/token/:id/launch_app
def launch_app
auth = current_user.remote_storage_authorizations.find(params[:id])
redirect_to app_auth_url(auth), allow_other_host: true
end
private
def require_signed_in_with_username
unless user_signed_in?
username, org = params[:useraddress].split("@")
redirect_to new_user_session_path(cn: username, ou: org)
end
end
def app_auth_url(auth)
url = "#{auth.url}#remotestorage=#{current_user.address}"
url += "&access_token=#{auth.token}"
url
end
def hostname_of(uri)
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,122 +0,0 @@
require "rqrcode"
require "lnurl"
class Services::LightningController < ApplicationController
before_action :authenticate_user!
before_action :authenticate_with_lndhub
before_action :set_current_section
before_action :fetch_balance
def index
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
end
def transactions
@transactions = fetch_transactions
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
def authenticate_with_lndhub(options={})
if session[:ln_auth_token].present? && !options[:force_reauth]
@ln_auth_token = session[:ln_auth_token]
else
lndhub = Lndhub.new
auth_token = lndhub.authenticate(current_user)
session[:ln_auth_token] = auth_token
@ln_auth_token = auth_token
end
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
def set_current_section
@current_section = :services
end
def fetch_balance
lndhub = Lndhub.new
data = lndhub.balance @ln_auth_token
@balance = data["BTC"]["AvailableBalance"] rescue nil
rescue AuthError
authenticate_with_lndhub(force_reauth: true)
raise if @fetch_balance_retried
@fetch_balance_retried = true
fetch_balance
end
def fetch_transactions
lndhub = Lndhub.new
txs = lndhub.gettxs @ln_auth_token
invoices = lndhub.getuserinvoices(@ln_auth_token).select{|i| i["ispaid"]}
process_transactions(txs + invoices)
rescue AuthError
authenticate_with_lndhub(force_reauth: true)
raise if @fetch_transactions_retried
@fetch_transactions_retried = true
fetch_transactions
end
def process_transactions(txs)
txs.collect do |tx|
if tx["type"] == "bitcoind_tx"
tx["amount_sats"] = (tx["amount"] * 100000000).to_i
tx["datetime"] = Time.at(tx["time"].to_i)
tx["title"] = "Received"
tx["description"] = "On-chain topup"
tx["received"] = true
else
tx["amount_sats"] = tx["value"] || tx["amt"]
tx["fee"] = tx["type"] == "paid_invoice" ? tx["fee"] : nil
tx["datetime"] = Time.at(tx["timestamp"].to_i)
tx["title"] = tx["type"] == "paid_invoice" ? "Sent" : "Received"
tx["description"] = tx["memo"] || tx["description"]
tx["received"] = tx["type"] == "user_invoice"
end
end
# Handle an edge case where lndhub.go includes a failed payment in the
# list, which wasn't actually booked
txs.reject!{ |tx| tx["type"] == "paid_invoice" && tx["payment_preimage"].blank? }
txs.sort{ |a,b| b["datetime"] <=> a["datetime"] }
end
end

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,23 +0,0 @@
class Services::RemotestorageController < Services::BaseController
before_action :authenticate_user!
before_action :require_feature_enabled
before_action :require_service_available
def dashboard
# unless current_user.services_enabled.include?(:remotestorage)
# redirect_to service_remotestorage_info_path
# end
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
end

View File

@@ -0,0 +1,13 @@
class Settings::AccountController < SettingsController
def index
end
def reset_password
current_user.send_reset_password_instructions
sign_out current_user
msg = "We have sent you an email with a link to reset your password."
redirect_to check_your_email_path, notice: msg
end
end

View File

@@ -0,0 +1,11 @@
class Settings::ProfileController < SettingsController
def index
@user = current_user
end
def update
end
end

View File

@@ -1,135 +1,13 @@
require 'securerandom'
class SettingsController < ApplicationController
before_action :authenticate_user!
before_action :set_main_nav_section
before_action :set_settings_section, only: [:show, :update, :update_email]
before_action :set_user, only: [:show, :update, :update_email]
before_action :require_user_signed_in
before_action :set_current_section
def index
redirect_to setting_path(:profile)
end
def show
if @settings_section == "experiments"
session[:shared_secret] ||= SecureRandom.base64(12)
end
end
def update
@user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name]
if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
LdapManager::UpdateDisplayName.call(@user.dn, user_params[:display_name])
end
redirect_to setting_path(@settings_section), flash: {
success: 'Settings saved.'
}
else
@validation_errors = @user.errors
render :show, status: :unprocessable_entity
end
end
def update_email
if @user.valid_ldap_authentication?(email_params[:current_password])
if @user.update email: email_params[:email]
redirect_to setting_path(:account), flash: {
notice: 'Please confirm your new address using the confirmation link we just sent you.'
}
else
@validation_errors = @user.errors
render :show, status: :unprocessable_entity
end
else
redirect_to setting_path(:account), flash: {
error: 'Password did not match your current password. Try again.'
}
end
end
def reset_password
current_user.send_reset_password_instructions
sign_out current_user
msg = "We have sent you an email with a link to reset your password."
redirect_to check_your_email_path, notice: msg
end
def set_nostr_pubkey
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
is_valid_id = NostrManager::ValidateId.call(signed_event)
is_valid_sig = NostrManager::VerifySignature.call(signed_event)
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
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
def set_main_nav_section
@current_section = :settings
end
def set_settings_section
@settings_section = params[:section]
allowed_sections = [:profile, :account, :lightning, :xmpp, :experiments]
unless allowed_sections.include?(@settings_section.to_sym)
redirect_to setting_path(:profile)
end
end
def set_user
@user = current_user
end
def user_params
params.require(:user).permit(:display_name, preferences: [
:lightning_notify_sats_received,
:xmpp_exchange_contacts_with_invitees
])
end
def email_params
params.require(:user).permit(:email, :current_password)
end
def nostr_event_params
params.permit(signed_event: [
:id, :pubkey, :created_at, :kind, :tags, :content, :sig
])
end
def set_current_section
@current_section = :settings
end
end

View File

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

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
class Users::ConfirmationsController < Devise::ConfirmationsController
# GET /resource/confirmation?confirmation_token=abcdef
def show
self.resource = resource_class.confirm_by_token(params[:confirmation_token])
yield resource if block_given?
if resource.errors.empty?
set_flash_message!(:success, :confirmed)
resource.devise_after_confirmation
respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) }
else
respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new }
end
end
end

View File

@@ -0,0 +1,81 @@
require "rqrcode"
class WalletController < ApplicationController
before_action :require_user_signed_in
before_action :authenticate_with_lndhub
before_action :set_current_section
before_action :fetch_balance
def index
@wallet_url = "lndhub://#{current_user.ln_login}:#{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
def transactions
@transactions = fetch_transactions
end
private
def authenticate_with_lndhub
if session["ln_auth_token"].present?
@ln_auth_token = session["ln_auth_token"]
else
lndhub = Lndhub.new
auth_token = lndhub.authenticate(current_user)
session["ln_auth_token"] = auth_token
@ln_auth_token = auth_token
end
rescue
# TODO add exception tracking
end
def set_current_section
@current_section = :wallet
end
def fetch_balance
lndhub = Lndhub.new
data = lndhub.balance @ln_auth_token
@balance = data["BTC"]["AvailableBalance"] rescue nil
end
def fetch_transactions
lndhub = Lndhub.new
txs = lndhub.gettxs @ln_auth_token
invoices = lndhub.getuserinvoices(@ln_auth_token).select{|i| i["ispaid"]}
process_transactions(txs + invoices)
end
def process_transactions(txs)
txs.collect do |tx|
if tx["type"] == "bitcoind_tx"
tx["amount_sats"] = (tx["amount"] * 100000000).to_i
tx["datetime"] = Time.at(tx["time"].to_i)
tx["title"] = "Received"
tx["description"] = "On-chain topup"
tx["received"] = true
else
tx["amount_sats"] = tx["value"] || tx["amt"]
tx["datetime"] = Time.at(tx["timestamp"].to_i)
tx["title"] = tx["type"] == "paid_invoice" ? "Sent" : "Received"
tx["description"] = tx["memo"] || tx["description"]
tx["received"] = tx["type"] == "user_invoice"
end
end
txs.sort{ |a,b| b["datetime"] <=> a["datetime"] }
end
end

View File

@@ -1,57 +0,0 @@
class WebfingerController < ApplicationController
before_action :allow_cross_origin_requests, only: [:show]
layout false
def show
resource = params[:resource]
if resource && resource.match(/acct:\w+/)
useraddress = resource.split(":").last
username, org = useraddress.split("@")
username.downcase!
unless User.where(cn: username, ou: org).any?
head 404 and return
end
render json: webfinger(useraddress).to_json,
content_type: "application/jrd+json"
else
head 422 and return
end
end
private
def webfinger(useraddress)
links = [];
links << remotestorage_link(useraddress) if Setting.remotestorage_enabled
{ "links" => links }
end
def remotestorage_link(useraddress)
# TODO use when OAuth routes are available
# 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",
"href" => storage_url,
"properties" => {
"http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13",
"http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url,
"http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter
"http://tools.ietf.org/html/rfc7233": "GET", # content range requests
"http://remotestorage.io/spec/web-authoring": nil
}
}
end
def allow_cross_origin_requests
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
end
end

View File

@@ -1,46 +0,0 @@
class WebhooksController < ApplicationController
skip_forgery_protection
before_action :authorize_request
def lndhub
begin
payload = JSON.parse(request.body.read, symbolize_names: true)
head :no_content and return unless payload[:type] == "incoming"
rescue
head :unprocessable_entity and return
end
user = User.find_by!(ln_account: payload[:user_login])
notify = user.preferences[:lightning_notify_sats_received]
case notify
when "xmpp"
notify_xmpp(user.address, payload[:amount], payload[:memo])
when "email"
NotificationMailer.with(user: user, amount_sats: payload[:amount])
.lightning_sats_received.deliver_later
end
head :ok
end
private
# TODO refactor into mailer-like generic class/service
def notify_xmpp(address, amt_sats, memo)
payload = {
type: "normal",
from: Setting.primary_domain,
to: address,
subject: "Sats received!",
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"
}
XmppSendMessageJob.perform_later(payload)
end
def authorize_request
if !ENV['WEBHOOKS_ALLOWED_IPS'].split(',').include?(request.remote_ip)
head :forbidden and return
end
end
end

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 +0,0 @@
class AuthError < StandardError; end

View File

@@ -1,6 +1,4 @@
module ApplicationHelper
include Pagy::Frontend
def sats_to_btc(sats)
sats.to_f / 100000000
end
@@ -12,10 +10,5 @@ module ApplicationHelper
"text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
end
end
# Colors available: gray, red, yellow, green, blue, purple, pink
# (Add more colors by adding classes to the safelist in tailwind.config.js)
def badge(text, color)
tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800"
end
end

View File

@@ -0,0 +1,2 @@
module LdapUsersHelper
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,2 +0,0 @@
module UsersHelper
end

View File

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

View File

@@ -4,10 +4,6 @@ export default class extends Controller {
static targets = ["buttons", "countdown"]
connect() {
// Devise timeoutable ends up adding a second flash message without content
// TODO investigate bug
if (this.element.textContent.trim() == "true") return;
const timeoutSeconds = parseInt(this.data.get("timeout"));
setTimeout(() => {

View File

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

View File

@@ -1,65 +0,0 @@
import { Controller } from "@hotwired/stimulus"
import { bech32 } from "bech32"
function hexToBytes (hex) {
let bytes = []
for (let c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16))
}
return bytes
}
// Connects to data-controller="settings--nostr-pubkey"
export default class extends Controller {
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
static values = { userAddress: String, pubkeyHex: String, sharedSecret: String }
connect () {
if (this.hasPubkeyHexValue && this.pubkeyHexValue.length > 0) {
this.pubkeyBech32InputTarget.value = this.pubkeyBech32
}
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 pubkeyBech32 () {
const words = bech32.toWords(hexToBytes(this.pubkeyHexValue))
return bech32.encode('npub', words)
}
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,30 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "button", "switch", "checkbox" ]
static values = { switchEnabled: Boolean }
connect () {
this.buttonTarget.classList.remove("hidden")
this.checkboxTarget.classList.add("hidden")
}
toggleSwitch () {
this.switchEnabledValue = !this.switchEnabledValue
this.checkboxTarget.checked = this.switchEnabledValue
if (this.switchEnabledValue) {
this.buttonTarget.setAttribute("aria-checked", "true");
this.buttonTarget.classList.remove("bg-gray-200")
this.buttonTarget.classList.add("bg-blue-600")
this.switchTarget.classList.remove("translate-x-0")
this.switchTarget.classList.add("translate-x-5")
} else {
this.buttonTarget.setAttribute("aria-checked", "false");
this.buttonTarget.classList.remove("bg-blue-600")
this.buttonTarget.classList.add("bg-gray-200")
this.switchTarget.classList.remove("translate-x-5")
this.switchTarget.classList.add("translate-x-0")
}
}
}

View File

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

View File

@@ -0,0 +1,13 @@
class CreateLndhubWalletJob < ApplicationJob
queue_as :default
def perform(user)
return if user.ln_login.present? && user.ln_password.present?
lndhub = Lndhub.new
credentials = lndhub.create({ partnerid: user.ou, accounttype: "user" })
user.update! ln_login: credentials["login"],
ln_password: credentials["password"]
end
end

View File

@@ -0,0 +1,18 @@
class ExchangeXmppContactsJob < ApplicationJob
queue_as :default
def perform(inviter, username, domain)
ejabberd = EjabberdApiClient.new
ejabberd.add_rosteritem({
"localuser": username, "localhost": domain,
"user": inviter.cn, "host": inviter.ou,
"nick": inviter.cn, "group": "Friends", "subs": "both"
})
ejabberd.add_rosteritem({
"localuser": inviter.cn, "localhost": inviter.ou,
"user": username, "host": domain,
"nick": username, "group": "Friends", "subs": "both"
})
end
end

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

@@ -1,22 +0,0 @@
class XmppExchangeContactsJob < ApplicationJob
queue_as :default
def perform(inviter, invitee)
return unless inviter.services_enabled.include?("xmpp") &&
invitee.services_enabled.include?("xmpp") &&
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new
ejabberd.add_rosteritem({
"localuser": invitee.cn, "localhost": invitee.ou,
"user": inviter.cn, "host": inviter.ou,
"nick": inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
ejabberd.add_rosteritem({
"localuser": inviter.cn, "localhost": inviter.ou,
"user": invitee.cn, "host": invitee.ou,
"nick": invitee.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
end
end

View File

@@ -1,8 +0,0 @@
class XmppSendMessageJob < ApplicationJob
queue_as :default
def perform(payload)
ejabberd = EjabberdApiClient.new
ejabberd.send_message payload
end
end

View File

@@ -1,26 +0,0 @@
class XmppSetDefaultBookmarksJob < ApplicationJob
queue_as :default
def perform(user)
return unless Setting.xmpp_default_rooms.any?
@user = user
ejabberd = EjabberdApiClient.new
ejabberd.private_set user, storage_content
end
def storage_content
bookmarks = ""
Setting.xmpp_default_rooms.each do |r|
bookmarks << conference_element(
jid: r[/<(.+)>/, 1], name: r[/^(.+)\s/, 1], nick: @user.cn,
autojoin: Setting.xmpp_autojoin_default_rooms
)
end
"<storage xmlns='storage:bookmarks'>#{bookmarks}</storage>"
end
def conference_element(jid:, name:, autojoin: false, nick:)
"<conference jid='#{jid}' name='#{name}' autojoin='#{autojoin.to_s}'><nick>#{nick}</nick></conference>"
end
end

View File

@@ -1,3 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
end

View File

@@ -1,23 +0,0 @@
# A custom mailer that can be used from the Rails console for one-off emails
# today, and later connected from an admin panel mailing page.
#
# Assign any template variables you want to use:
#
# user = User.first
#
# Create the email body from a custom email template file:
#
# body = ERB.new(File.read('./tmp/mailer-1.txt.erb')).result binding
#
# Send email via Sidekiq:
#
# CustomMailer.with(user: user, subject: "Important announcement", body: body).custom_message.deliver_later
#
class CustomMailer < ApplicationMailer
def custom_message
@user = params[:user]
@subject = params[:subject]
@body = params[:body]
mail(to: @user.email, subject: @subject)
end
end

View File

@@ -1,34 +0,0 @@
# frozen_string_literal: true
if defined?(ActionMailer)
class Devise::Mailer < Devise.parent_mailer.constantize
include Devise::Mailers::Helpers
def confirmation_instructions(record, token, opts = {})
@token = token
if record.pending_reconfirmation?
devise_mail(record, :reconfirmation_instructions, opts)
else
devise_mail(record, :confirmation_instructions, opts)
end
end
def reset_password_instructions(record, token, opts = {})
@token = token
devise_mail(record, :reset_password_instructions, opts)
end
def unlock_instructions(record, token, opts = {})
@token = token
devise_mail(record, :unlock_instructions, opts)
end
def email_changed(record, opts = {})
devise_mail(record, :email_changed, opts)
end
def password_change(record, opts = {})
devise_mail(record, :password_change, opts)
end
end
end

View File

@@ -1,8 +0,0 @@
class NotificationMailer < ApplicationMailer
def lightning_sats_received
@user = params[:user]
@amount_sats = params[:amount_sats]
@subject = "Sats received"
mail to: @user.email, subject: @subject
end
end

View File

@@ -3,9 +3,7 @@ class Donation < ApplicationRecord
belongs_to :user
# Validations
validates_presence_of :user
validates_presence_of :amount_sats
validates_presence_of :paid_at
# Hooks
# TODO before_create :store_fiat_value

View File

@@ -1,7 +1,6 @@
class Invitation < ApplicationRecord
# Relations
belongs_to :user
belongs_to :invitee, class_name: "User", foreign_key: 'invited_user_id', optional: true
# Validations
validates_presence_of :user

View File

@@ -1,21 +0,0 @@
class LndhubAccount < LndhubBase
self.table_name = "accounts"
self.inheritance_column = :_type_disabled
has_many :ledgers, class_name: "LndhubAccountLedger",
foreign_key: "account_id"
belongs_to :user, class_name: "LndhubUser",
foreign_key: "user_id"
scope :current, -> { where(type: "current") }
scope :outgoing, -> { where(type: "outgoing") }
scope :incoming, -> { where(type: "incoming") }
scope :fees, -> { where(type: "fees") }
scope :with_balances, -> {
current.joins(:user).joins(:ledgers)
.group("accounts.id", "users.login")
.select("accounts.id, users.login, SUM(account_ledgers.amount) AS balance")
}
end

View File

@@ -1,3 +0,0 @@
class LndhubAccountLedger < LndhubBase
self.table_name = "account_ledgers"
end

View File

@@ -1,4 +0,0 @@
class LndhubBase < ActiveRecord::Base
self.abstract_class = true
establish_connection :lndhub
end

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