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
466 changed files with 1473 additions and 15072 deletions

View File

@@ -12,27 +12,23 @@ steps:
settings:
restore: true
mount:
- ./vendor/cache
- ./vendor
when:
branch:
- master
- name: rspec
image: gitea.kosmos.org/kosmos/akkounts-ci:0.9.1
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'
- bundle config set cache_path 'vendor/cache'
- bundle config set with 'development test'
- bundle install --jobs=3 --retry=3
- bundle exec rails db:create
- bundle exec rails db:migrate
- yarn install
- rake css:build
- bundle exec rspec
- rake spec
- name: rebuild-cache
image: drillster/drone-volume-cache
volumes:
@@ -41,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,71 +1,11 @@
# PRIMARY_DOMAIN=kosmos.org
# AKKOUNTS_DOMAIN=accounts.example.com
LDAP_HOST=localhost
LDAP_PORT=389
LDAP_ADMIN_PASSWORD=passthebutter
LDAP_SUFFIX="dc=kosmos,dc=org"
# 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
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
# S3_ENABLED=true
# 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
BTCPAY_API_URL='http://localhost:23001/api/v1'
# LDAP_HOST=localhost
# LDAP_PORT=389
# LDAP_ADMIN_PASSWORD=passthebutter
# LDAP_SUFFIX='dc=kosmos,dc=org'
# REDIS_URL='redis://localhost:6379/1'
# WEBHOOKS_ALLOWED_IPS='10.1.1.163'
#
# Service Integrations
# (sorted alphabetically by service name)
#
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
# BTCPAY_API_URL='http://localhost:23001/api/v1'
# BTCPAY_STORE_ID=''
# BTCPAY_AUTH_TOKEN=''
# DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
# DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
# DRONECI_PUBLIC_URL='https://drone.kosmos.org'
# EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
# EJABBERD_API_URL='https://xmpp.kosmos.org/api'
# GITEA_PUBLIC_URL='https://gitea.kosmos.org'
# LNDHUB_API_URL='http://localhost:3023'
# LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
# 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'
# MASTODON_ADDRESS_DOMAIN='https://kosmos.org'
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
# NOSTR_PRIVATE_KEY='123456abcdef...'
# NOSTR_PUBLIC_KEY='123456abcdef...'
# NOSTR_RELAY_URL='wss://nostr.kosmos.org'
# RS_STORAGE_URL='https://storage.kosmos.org'
# RS_REDIS_URL='redis://localhost:6379/2'
LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'

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,27 +1,4 @@
PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.kosmos.org
REDIS_URL='redis://localhost:6379/0'
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
BTCPAY_STORE_ID='123456'
DISCOURSE_PUBLIC_URL='http://discourse.example.com'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
EJABBERD_API_URL='http://xmpp.example.com/api'
MASTODON_PUBLIC_URL='http://example.social'
LNDHUB_API_URL='http://localhost:3026'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
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

2
.gitignore vendored
View File

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

View File

@@ -1 +1 @@
3.3.0
2.7.2

View File

@@ -1,22 +1,22 @@
# syntax=docker/dockerfile:1
FROM ruby:3.3.4
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
ldap-utils tini libvips
FROM ruby:2.7.6
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"]

35
Gemfile
View File

@@ -2,7 +2,7 @@ source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 7.1'
gem 'rails', '~> 7.0.2'
# Use Puma as the app server
gem 'puma', '~> 4.1'
# View components
@@ -22,7 +22,7 @@ gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1'
# gem 'bcrypt', '~> 3.1.7'
# Configuration
gem 'dotenv-rails'
@@ -32,53 +32,34 @@ gem 'lockbox'
# Authentication
gem 'warden'
gem 'devise', '~> 4.9.0'
gem 'devise'
gem 'devise_ldap_authenticatable'
gem 'net-ldap'
# Utilities
gem "image_processing", "~> 1.12.2"
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'
gem 'down'
gem 'aws-sdk-s3', require: false
# Background/scheduled jobs
gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler'
# Monitoring
gem "sentry-ruby"
gem "sentry-rails"
# Services
gem 'discourse_api'
gem "lnurl"
gem 'manifique', '~> 1.1.0'
gem 'nostr', '~> 0.6.0'
group :development, :test do
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.7.2'
gem 'sqlite3', '~> 1.4'
gem 'rspec-rails'
gem 'rails-controller-testing'
gem "byebug", "~> 11.1"
end
group :development do
# 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 'letter_opener'
gem 'letter_opener_web'
gem 'faker'
gem 'solargraph'
end
group :test do
@@ -89,8 +70,8 @@ group :test do
end
group :production do
gem 'pg', '~> 1.5'
# Use postgresql as the database for Active Record
gem 'pg', '~> 1.2.3'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

View File

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

View File

@@ -14,60 +14,52 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
Docker Desktop)
3. Run `docker compose up --build` and wait until all services have started
(389ds might take an extra minute to be ready). This will take a while when
running for the first time, so you might want to do something else in the
meantime.
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
_Note: when using Docker Compose, prefix the following commands with `docker-compose
run web`._
Installing dependencies:
bundle 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
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
Running the background workers (requires Redis) _(automatic with Docker Compose)_:
Running the background workers (requires Redis):
bundle exec sidekiq -C config/sidekiq.yml
Running the test suite:
Running all specs:
bundle exec rspec
Running the test suite with Docker Compose requires overriding the Rails
environment:
### Docker (Compose)
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
Services/containers are configured in `docker-compose.yml`.
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
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
@@ -84,54 +76,17 @@ 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
("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
to start over with a fresh installation, delete both that volume as well as the
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](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
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.
## 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
@@ -148,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 {
@@ -32,23 +28,12 @@
focus:ring-blue-400 focus:ring-opacity-75;
}
.btn-emerald {
@apply bg-emerald-500 hover:bg-emerald-600 text-white
focus:ring-emerald-400 focus:ring-opacity-75;
}
.btn-red {
@apply bg-red-600 hover:bg-red-700 text-white
focus:ring-red-500 focus:ring-opacity-75;
}
.btn-outline-purple {
@apply border-2 border-purple-500 hover:bg-purple-100
focus:ring-purple-400 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) 20%, 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,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,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,14 +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),
placeholder: @placeholder,
data: {
:'default-value' => Setting.get_field(@key)[:default]
},
class: "w-full" %>
<% end %>

View File

@@ -1,21 +0,0 @@
# frozen_string_literal: true
module FormElements
class FieldsetResettableSettingComponent < ViewComponent::Base
def initialize(tag: "li", key:, type: :text, title:, description: nil, placeholder: nil)
@tag = tag
@positioning = :vertical
@title = title
@description = description
@key = key.to_sym
@type = type
@resettable = is_resettable?(@key)
@placeholder = placeholder
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,35 +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>
<% if @description.present? %>
<p class="text-gray-500"><%= @description %></p>
<% end %>
</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: nil)
@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
@description = 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,5 +1,5 @@
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
<div class="md:min-h-[50vh] bg-white rounded-lg shadow">
<div class="bg-white rounded-lg shadow">
<div class="px-6 sm:px-12 pt-2 sm:pt-4">
<%= render partial: @tabnav_partial %>
</div>

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="relative m-4 max-h-screen w-auto max-w-full
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

@@ -34,8 +34,6 @@ class NotificationComponent < ViewComponent::Base
'alert-octagon'
when 'alert'
'alert-octagon'
when 'warning'
'alert-octagon'
else
'info'
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,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 %>
<% if @icon.present? %>
<%= 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>
<% end %>

View File

@@ -1,13 +1,10 @@
# frozen_string_literal: true
class SidenavLinkComponent < ViewComponent::Base
def initialize(name:, level: 1, path:, icon: nil, text_icon: nil,
active: false, disabled: false)
def initialize(name:, path:, icon:, active: false, disabled: false)
@name = name
@level = level
@path = path
@icon = icon
@text_icon = text_icon
@active = active
@disabled = disabled
@link_class = class_names_link(path)
@@ -15,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,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,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

@@ -3,16 +3,13 @@ class Admin::DonationsController < Admin::BaseController
before_action :set_current_section, only: [:index, :show, :new, :edit]
# GET /donations
# GET /donations.json
def index
@pagy, @donations = pagy(Donation.completed.order('paid_at desc'))
@stats = {
overall_sats: @donations.sum("amount_sats"),
donor_count: Donation.completed.count(:user_id)
}
@donations = Donation.all
end
# GET /donations/1
# GET /donations/1.json
def show
end
@@ -26,41 +23,43 @@ class Admin::DonationsController < Admin::BaseController
end
# POST /donations
# POST /donations.json
def create
@donation = Donation.new(donation_params)
if @donation.paid_at == nil
@donation.errors.add(:paid_at, message: "is required")
render :new, status: :unprocessable_entity and return
end
if @donation.save
redirect_to admin_donation_url(@donation), flash: {
success: 'Donation was successfully created.'
}
else
render :new, status: :unprocessable_entity
respond_to do |format|
if @donation.save
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 }
format.json { render json: @donation.errors, status: :unprocessable_entity }
end
end
end
# PUT /donations/1
# PATCH/PUT /donations/1
# PATCH/PUT /donations/1.json
def update
if @donation.update(donation_params)
redirect_to admin_donation_url(@donation), flash: {
success: 'Donation was successfully updated.'
}
else
render :edit, status: :unprocessable_entity
respond_to do |format|
if @donation.update(donation_params)
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 }
format.json { render json: @donation.errors, status: :unprocessable_entity }
end
end
end
# DELETE /donations/1
# DELETE /donations/1.json
def destroy
@donation.destroy
redirect_to admin_donations_url, flash: {
success: 'Donation was successfully destroyed.'
}
respond_to do |format|
format.html { redirect_to admin_donations_url, notice: 'Donation was successfully destroyed.' }
format.json { head :no_content }
end
end
private
@@ -71,10 +70,7 @@ class Admin::DonationsController < Admin::BaseController
# Only allow a list of trusted parameters through.
def donation_params
params.require(:donation).permit(
:user_id, :donation_method,
:amount_sats, :fiat_amount, :fiat_currency,
:public_name, :paid_at)
params.require(:donation).permit(:user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :paid_at)
end
def set_current_section

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,20 +0,0 @@
class Admin::Settings::RegistrationsController < Admin::SettingsController
def show
end
def update
update_settings
redirect_to admin_settings_registrations_path, flash: {
success: "Settings saved"
}
end
private
def setting_params
params.require(:setting).permit([
:reserved_usernames, default_services: []
])
end
end

View File

@@ -1,32 +0,0 @@
class Admin::Settings::ServicesController < Admin::SettingsController
before_action :set_service, only: [:show, :update]
def index
redirect_to admin_settings_service_path("btcpay")
end
def show
end
def update
update_settings
redirect_to admin_settings_service_path(@service), flash: {
success: "Settings saved"
}
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

View File

@@ -1,49 +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 clean_param(key).nil? ||
(Setting.send(key).to_s == clean_param(key))
changed_keys.push(key)
setting = Setting.new(var: key)
setting.value = clean_param(key)
unless setting.valid?
@errors.merge!(setting.errors)
end
end
if @errors.any?
render :show and return
end
changed_keys.each do |key|
Setting.send("#{key}=", clean_param(key))
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
def clean_param(key)
if Setting.get_field(key)[:type] == :string
setting_params[key].strip
else
setting_params[key]
end
end
end

View File

@@ -1,62 +0,0 @@
class Admin::UsersController < Admin::BaseController
before_action :set_user, except: [:index]
before_action :set_current_section
# GET /admin/users
def index
ldap = LdapService.new
@ou = Setting.primary_domain
@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
# GET /admin/users/:username
def show
if Setting.lndhub_admin_enabled?
@lndhub_user = @user.lndhub_user
end
@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
private
def set_user
@user = User.find_by(cn: params[:username], ou: Setting.primary_domain)
http_status :not_found unless @user
end
def set_current_section
@current_section = :users
end
end

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

@@ -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
@@ -37,35 +25,4 @@ class ApplicationController < ActionController::Base
format.any { head status }
end
end
def after_sign_in_path_for(user)
session[:user_return_to] || root_path
end
def lndhub_authenticate(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 lndhub_fetch_balance
@balance = LndhubManager::FetchUserBalance.call(auth_token: @ln_auth_token)
rescue AuthError
lndhub_authenticate(force_reauth: true)
raise if @fetch_balance_retried
@fetch_balance_retried = true
lndhub_fetch_balance
end
def nostr_event_from_params
params.permit!
params[:signed_event].to_h.symbolize_keys
end
end

View File

@@ -1,129 +1,10 @@
class Contributions::DonationsController < ApplicationController
include BtcpayHelper
before_action :require_user_signed_in
before_action :authenticate_user!
before_action :set_donation_methods, only: [:index, :create]
before_action :require_donation_method_enabled, only: [:create]
before_action :validate_donation_params, only: [:create]
before_action :set_donation, only: [:confirm_btcpay]
# GET /contributions/donations
# GET /donations
# GET /donations.json
def index
@donations = current_user.donations.completed
@current_section = :contributions
@donations_completed = current_user.donations.completed.order('paid_at desc')
@donations_pending = current_user.donations.processing.order('created_at desc')
if Setting.lndhub_enabled?
begin
lndhub_authenticate
lndhub_fetch_balance
rescue
@balance = 0
end
end
end
# POST /contributions/donations
def create
if params[:currency] == "sats"
fiat_amount = nil
fiat_currency = nil
amount_sats = params[:amount]
else
fiat_amount = params[:amount].to_i
fiat_currency = params[:currency]
amount_sats = nil
end
@donation = current_user.donations.create!(
donation_method: params[:donation_method],
payment_method: nil,
paid_at: nil,
amount_sats: amount_sats,
fiat_amount: (fiat_amount.nil? ? nil : fiat_amount * 100), # store in cents
fiat_currency: fiat_currency,
public_name: params[:public_name]
)
case params[:donation_method]
when "btcpay"
res = BtcpayManager::CreateInvoice.call(
amount: fiat_amount || (amount_sats.to_f / 100000000),
currency: fiat_currency || "BTC",
redirect_url: confirm_btcpay_contributions_donation_url(@donation)
)
@donation.update! btcpay_invoice_id: res["id"]
redirect_to btcpay_checkout_url(res["id"]), allow_other_host: true
else
redirect_to contributions_donations_url, flash: {
error: "Donation method currently not available"
}
end
end
def confirm_btcpay
redirect_to contributions_donations_url and return if @donation.completed?
invoice = BtcpayManager::FetchInvoice.call(invoice_id: @donation.btcpay_invoice_id)
if @donation.amount_sats.present?
# TODO make default fiat currency configurable and/or determine from user's
# i18n browser settings
@donation.fiat_currency = "EUR"
exchange_rate = BtcpayManager::FetchExchangeRate.call(fiat_currency: @donation.fiat_currency)
@donation.fiat_amount = (((@donation.amount_sats.to_f / 100000000) * exchange_rate) * 100).to_i
else
amt_str = invoice["paymentMethods"].first["amount"]
@donation.amount_sats = amt_str.tr(".","").sub(/0*$/, "").to_i
end
case invoice["status"]
when "Settled"
@donation.paid_at = DateTime.now
@donation.payment_status = "settled"
@donation.save!
flash_message = { success: "Thank you!" }
when "Processing"
unless @donation.processing?
@donation.payment_status = "processing"
@donation.save!
flash_message = { success: "Thank you! We will send you an email when the payment is confirmed." }
BtcpayCheckDonationJob.set(wait: 20.seconds).perform_later(@donation)
end
when "Expired"
flash_message = { warning: "The payment request for this donation has expired" }
else
flash_message = { warning: "Could not determine status of payment" }
end
redirect_to contributions_donations_url, flash: flash_message
end
private
def set_donation
@donation = current_user.donations.find_by(id: params[:id])
http_status :not_found unless @donation.present?
end
def set_donation_methods
@donation_methods = []
@donation_methods.push :btcpay if Setting.btcpay_enabled?
@donation_methods.push :lndhub if Setting.lndhub_enabled?
@donation_methods.push :opencollective if Setting.opencollective_enabled?
end
def require_donation_method_enabled
http_status :forbidden unless @donation_methods.include?(
params[:donation_method].to_sym
)
end
def validate_donation_params
if !%w[EUR USD sats].include?(params[:currency]) || (params[:amount].to_i <= 0)
http_status :unprocessable_entity
end
end
end

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,161 +1,75 @@
class LnurlpayController < ApplicationController
before_action :check_service_available
before_action :find_user
before_action :set_cors_access_control_headers
before_action :find_user_by_address
MIN_SATS = 10
MAX_SATS = 1_000_000
MAX_COMMENT_CHARS = 100
# GET /.well-known/lnurlp/:username
def index
res = {
render json: {
status: "OK",
callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice",
callback: "https://accounts.kosmos.org/lnurlpay/#{@user.address}/invoice",
tag: "payRequest",
maxSendable: MAX_SATS * 1000, # msat
minSendable: MIN_SATS * 1000, # msat
metadata: metadata(@user.address),
commentAllowed: MAX_COMMENT_CHARS
}
if Setting.nostr_enabled?
res[:allowsNostr] = true
res[:nostrPubkey] = Setting.nostr_public_key
end
render json: res
end
# GET /.well-known/keysend/:username
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
# GET /lnurlpay/:username/invoice
def invoice
amount = params[:amount].to_i / 1000 # msats to sats
amount = params[:amount].to_i / 1000 # msats
address = params[:address]
comment = params[:comment] || ""
address = @user.address
if !valid_amount?(amount)
render json: { status: "ERROR", reason: "Invalid amount" }
return
end
if params[:nostr].present? && Setting.nostr_enabled?
handle_zap_request amount, params[:nostr], params[:lnurl]
else
handle_pay_request address, amount, comment
if !valid_comment?(comment)
render json: { status: "ERROR", reason: "Comment too long" }
return
end
memo = "Sats for #{address}"
memo = "#{memo}: \"#{comment}\"" if comment.present?
payment_request = @user.ln_create_invoice({
amount: amount, # we create invoices in sats
memo: memo,
description_hash: Digest::SHA2.hexdigest(metadata(address)),
})
render json: {
status: "OK",
successAction: {
tag: "message",
message: "Sats received. Thank you!"
},
routes: [],
pr: payment_request
}
end
private
def set_cors_access_control_headers
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Headers'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
def find_user_by_address
address = params[:address].split("@")
@user = User.where(cn: address.first, ou: address.last).first
http_status :not_found if @user.nil?
end
def check_service_available
http_status :not_found unless Setting.lndhub_enabled?
end
def metadata(address)
"[[\"text/identifier\", \"#{address}\"], [\"text/plain\", \"Send sats, receive thanks.\"]]"
end
def find_user
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first
http_status :not_found if @user.nil?
end
def valid_amount?(amount_in_sats)
amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
end
def metadata(address)
"[[\"text/identifier\",\"#{address}\"],[\"text/plain\",\"Sats for #{address}\"]]"
end
def valid_comment?(comment)
comment.length <= MAX_COMMENT_CHARS
end
def valid_amount?(amount_in_sats)
amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
end
def valid_comment?(comment)
comment.length <= MAX_COMMENT_CHARS
end
def handle_pay_request(address, amount, comment)
if !valid_comment?(comment)
render json: { status: "ERROR", reason: "Comment too long" }
return
end
desc = "To #{address}"
desc = "#{desc}: \"#{comment}\"" if comment.present?
invoice = LndhubManager::CreateUserInvoice.call(
user: @user, payload: {
amount: amount, # sats
description: desc,
description_hash: Digest::SHA256.hexdigest(metadata(address)),
}
)
render json: {
status: "OK",
successAction: {
tag: "message",
message: "Sats received. Thank you!"
},
routes: [],
pr: invoice["payment_request"]
}
end
def nostr_event_from_payload(nostr_param)
event_obj = JSON.parse(nostr_param).transform_keys(&:to_sym)
Nostr::Event.new(**event_obj)
rescue => e
return nil
end
def valid_zap_request?(amount, event, lnurl)
NostrManager::VerifyZapRequest.call(
amount: amount, event: event, lnurl: lnurl
)
end
def handle_zap_request(amount, nostr_param, lnurl_param)
event = nostr_event_from_payload(nostr_param)
unless event.present? && valid_zap_request?(amount*1000, event, lnurl_param)
render json: { status: "ERROR", reason: "Invalid zap request" }
return
end
# TODO might want to use the existing invoice and zap record if there are
# multiple calls with the same zap request
desc = "Zap for #{@user.address}"
desc = "#{desc}: \"#{event.content}\"" if event.content.present?
invoice = LndhubManager::CreateUserInvoice.call(
user: @user, payload: {
amount: amount, # sats
description: desc,
description_hash: Digest::SHA256.hexdigest(event.to_json),
}
)
@user.zaps.create! request: event,
payment_request: invoice["payment_request"],
amount: amount
render json: { status: "OK", pr: invoice["payment_request"] }
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.service_enabled?(:ejabberd)
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,103 +0,0 @@
require "rqrcode"
require "lnurl"
class Services::LightningController < ApplicationController
before_action :set_current_section
before_action :require_service_available
before_action :authenticate_user!
before_action :lndhub_authenticate
before_action :lndhub_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 set_current_section
@current_section = :services
end
def require_service_available
http_status :not_found unless Setting.lndhub_enabled?
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.service_enabled?(:mastodon)
end
private
def require_service_available
http_status :not_found unless Setting.mastodon_enabled?
end
end

View File

@@ -1,25 +0,0 @@
class Services::RemotestorageController < Services::BaseController
before_action :authenticate_user!
before_action :require_service_available
before_action :require_feature_enabled
# Dashboard
def show
# unless current_user.service_enabled?(:remotestorage)
# redirect_to service_remotestorage_info_path
# end
# @rs_apps_connected = current_user.remote_storage_authorizations.any?
end
private
def require_service_available
http_status :not_found unless Setting.remotestorage_enabled?
end
def require_feature_enabled
unless Flipper.enabled?(:remotestorage, current_user)
http_status :forbidden
end
end
end

View File

@@ -1,47 +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, only: [:destroy, :launch_app]
def index
@rs_auths = current_user.remote_storage_authorizations
# TODO sort by app name?
end
def destroy
@auth.destroy!
respond_to do |format|
format.html do redirect_to apps_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

@@ -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,176 +1,13 @@
require "securerandom"
require "bcrypt"
class SettingsController < ApplicationController
before_action :authenticate_user!
before_action :set_main_nav_section
before_action :set_settings_section, only: [:show, :update, :update_email, :reset_email_password]
before_action :set_user, only: [:show, :update, :update_email, :reset_email_password]
before_action :require_user_signed_in
before_action :set_current_section
def index
redirect_to setting_path(:profile)
end
def show
case @settings_section
when "lightning"
@notifications_enabled = @user.preferences[:lightning_notify_sats_received] != "disabled" ||
@user.preferences[:lightning_notify_zap_received] != "disabled"
when "nostr"
session[:shared_secret] ||= SecureRandom.base64(12)
end
end
def update
@user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name]
@user.avatar_new = user_params[:avatar]
if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
LdapManager::UpdateDisplayName.call(dn: @user.dn, display_name: @user.display_name)
end
if @user.avatar_new.present?
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
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?(security_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_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
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.new(**nostr_event_from_params)
is_valid_sig = signed_event.verify_signature
is_valid_auth = NostrManager::VerifyAuth.call(
event: signed_event,
challenge: session[:shared_secret]
)
unless is_valid_sig && is_valid_auth
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
if user_with_pubkey.present? && (user_with_pubkey != current_user)
flash[:alert] = "Public key already in use for a different account"
http_status :unprocessable_entity and return
end
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event.pubkey)
session[:shared_secret] = nil
flash[:success] = "Public key verification successful"
http_status :ok
end
# DELETE /settings/nostr_pubkey
def remove_nostr_pubkey
# TODO require current pubkey or password to delete
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: nil)
redirect_to setting_path(:nostr), 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, :xmpp, :email,
:lightning, :remotestorage, :nostr
]
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, :avatar, preferences: UserPreferences.pref_keys
)
end
def email_params
params.require(:user).permit(:email)
end
def security_params
params.require(:user).permit(:current_password)
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
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
@@ -96,13 +96,13 @@ class SignupController < ApplicationController
session[:new_user] = nil
session[:validation_error] = nil
CreateAccount.call(account: {
CreateAccount.call(
username: @user.cn,
domain: Setting.primary_domain,
domain: "kosmos.org",
email: @user.email,
password: @user.password,
invitation: @invitation
})
)
end
def set_context

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

@@ -1,62 +0,0 @@
# frozen_string_literal: true
class Users::SessionsController < Devise::SessionsController
# before_action :configure_sign_in_params, only: [:create]
# GET /resource/sign_in
def new
session[:shared_secret] = SecureRandom.base64(12)
super
end
# POST /resource/sign_in
# def create
# super
# end
# DELETE /resource/sign_out
# def destroy
# super
# end
# POST /users/nostr_login
def nostr_login
signed_event = Nostr::Event.new(**nostr_event_from_params)
is_valid_sig = signed_event.verify_signature
is_valid_auth = NostrManager::VerifyAuth.call(
event: signed_event,
challenge: session[:shared_secret]
)
session[:shared_secret] = nil
unless is_valid_sig && is_valid_auth
flash[:alert] = "Login verification failed"
http_status :unauthorized and return
end
user = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
if user.present?
set_flash_message!(:notice, :signed_in)
sign_in("user", user)
render json: { redirect_url: after_sign_in_path_for(user) }, status: :ok
else
flash[:alert] = "Failed to find your account. Nostr login may be disabled."
http_status :unauthorized
end
end
protected
def set_flash_message(key, kind, options = {})
# Hide flash message after redirecting from a signin route while logged in
super unless key == :alert && kind == "already_authenticated"
end
# If you have extra params to permit, append them to the sanitizer.
# def configure_sign_in_params
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
# 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,92 +0,0 @@
class WebfingerController < WellKnownController
before_action :allow_cross_origin_requests, only: [:show]
def show
resource = params[:resource]
if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1)
@username, @domain = @useraddress.split("@")
unless Rails.env.development?
# Allow different domains (e.g. localhost:3000) in development only
head 404 and return unless @domain == Setting.primary_domain
end
unless @user = User.where(ou: Setting.primary_domain)
.find_by(cn: @username.downcase)
head 404 and return
end
render json: webfinger.to_json,
content_type: "application/jrd+json"
else
head 422 and return
end
end
private
def webfinger
jrd = {
subject: "acct:#{@user.address}",
aliases: [],
links: []
}
if Setting.mastodon_enabled && @user.service_enabled?(:mastodon)
# https://docs.joinmastodon.org/spec/webfinger/
jrd[:aliases] += mastodon_aliases
jrd[:links] += mastodon_links
end
if Setting.remotestorage_enabled && @user.service_enabled?(:remotestorage)
# https://datatracker.ietf.org/doc/draft-dejong-remotestorage/
jrd[:links] << remotestorage_link
end
jrd
end
def mastodon_aliases
[
"#{Setting.mastodon_public_url}/@#{@user.cn}",
"#{Setting.mastodon_public_url}/users/#{@user.cn}"
]
end
def mastodon_links
[
{
rel: "http://webfinger.net/rel/profile-page",
type: "text/html",
href: "#{Setting.mastodon_public_url}/@#{@user.cn}"
},
{
rel: "self",
type: "application/activity+json",
href: "#{Setting.mastodon_public_url}/users/#{@user.cn}"
},
{
rel: "http://ostatus.org/schema/1.0/subscribe",
template: "#{Setting.mastodon_public_url}/authorize_interaction?uri={uri}"
}
]
end
def remotestorage_link
auth_url = new_rs_oauth_url(@username, host: Setting.accounts_domain)
storage_url = "#{Setting.rs_storage_url}/#{@username}"
{
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
end

View File

@@ -1,77 +0,0 @@
class WebhooksController < ApplicationController
skip_forgery_protection
before_action :authorize_request
before_action :process_payload
def lndhub
@user = User.find_by!(ln_account: @payload[:user_login])
if @zap = @user.zaps.find_by(payment_request: @payload[:payment_request])
settled_at = Time.parse(@payload[:settled_at])
zap_receipt = NostrManager::CreateZapReceipt.call(
zap: @zap,
paid_at: settled_at.to_i,
preimage: @payload[:preimage]
)
@zap.update! settled_at: settled_at, receipt: zap_receipt.to_h
NostrManager::PublishZapReceipt.call(zap: @zap)
end
send_notifications
head :ok
end
private
def authorize_request
if !ENV['WEBHOOKS_ALLOWED_IPS'].split(',').include?(request.remote_ip)
head :forbidden and return
end
end
def process_payload
@payload = JSON.parse(request.body.read, symbolize_names: true)
unless @payload[:type] == "incoming" &&
@payload[:state] == "settled"
head :no_content and return
end
rescue
head :unprocessable_entity and return
end
def send_notifications
return if @payload[:amount] < @user.preferences[:lightning_notify_min_sats]
if @user.preferences[:lightning_notify_only_with_message]
return if @payload[:memo].blank?
end
target = @zap.present? ? @user.preferences[:lightning_notify_zap_received] :
@user.preferences[:lightning_notify_sats_received]
case target
when "xmpp"
notify_xmpp
when "email"
notify_email
end
end
# TODO refactor into mailer-like generic class/service
def notify_xmpp
XmppSendMessageJob.perform_later({
type: "normal",
from: Setting.xmpp_notifications_from_address,
to: @user.address,
subject: "Sats received!",
body: "#{helpers.number_with_delimiter @payload[:amount]} sats received in your Lightning wallet:\n> #{@payload[:memo]}"
})
end
def notify_email
NotificationMailer.with(user: @user, amount_sats: @payload[:amount])
.lightning_sats_received.deliver_later
end
end

View File

@@ -1,47 +0,0 @@
class WellKnownController < ApplicationController
before_action :require_nostr_enabled, only: [ :nostr ]
before_action :allow_cross_origin_requests, only: [ :nostr ]
layout false
def nostr
http_status :unprocessable_entity and return if params[:name].blank?
domain = request.headers["X-Forwarded-Host"].presence || Setting.primary_domain
relay_url = Setting.nostr_relay_url.presence
if params[:name] == "_"
if domain == Setting.primary_domain
# pubkey for the primary domain without a username (e.g. kosmos.org)
res = { names: { "_": Setting.nostr_public_key_primary_domain.presence || Setting.nostr_public_key } }
else
# pubkey for the akkounts domain without a username (e.g. accounts.kosmos.org)
res = { names: { "_": Setting.nostr_public_key } }
end
res[:relays] = { "_" => [ relay_url ] } if relay_url
else
@user = User.where(cn: params[:name], ou: domain).first
http_status :not_found and return if @user.nil? || @user.nostr_pubkey.blank?
res = { names: { @user.cn => @user.nostr_pubkey } }
res[:relays] = { @user.nostr_pubkey => [ relay_url ] } if relay_url
end
respond_to do |format|
format.json do
render json: res.to_json
end
end
end
private
def require_nostr_enabled
http_status :not_found unless Setting.nostr_enabled?
end
def allow_cross_origin_requests
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end

View File

@@ -1 +0,0 @@
class AuthError < StandardError; end

View File

@@ -1,5 +1,7 @@
module ApplicationHelper
include Pagy::Frontend
def sats_to_btc(sats)
sats.to_f / 100000000
end
def main_nav_class(current_section, link_to_section)
if current_section == link_to_section
@@ -8,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

@@ -1,7 +0,0 @@
module BtcpayHelper
def btcpay_checkout_url(invoice_id)
"#{Setting.btcpay_public_url}/i/#{invoice_id}"
end
end

View File

@@ -0,0 +1,2 @@
module DashboardHelper
end

View File

@@ -0,0 +1,2 @@
module DonationsHelper
end

View File

@@ -0,0 +1,2 @@
module InvitationsHelper
end

View File

@@ -0,0 +1,2 @@
module LdapUsersHelper
end

View File

@@ -0,0 +1,2 @@
module LnurlpayHelper
end

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