Compare commits
262 Commits
v0.2.0
...
7f5b8c22b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f5b8c22b7
|
||
|
|
44ec856091
|
||
|
|
6ac9f68566
|
||
|
|
69bd9dcf78
|
||
|
|
2c6e6caab6
|
||
|
|
a551e8d2ef
|
||
|
|
9166d887c4
|
||
|
|
d3313a202b
|
||
|
|
029e6b011d
|
||
|
|
f1ef257c97
|
||
|
|
3f1c4f17a7
|
||
|
|
8c5852fefe
|
||
|
|
317dba0b2d
|
||
|
|
ab08c9ecf5
|
||
|
|
49dd7bd96d
|
||
|
|
c650b73ff9
|
||
|
|
ea6173c60f
|
||
|
|
cd46daa6ba
|
||
|
|
df3f91e2d0
|
||
|
|
7c4106d7a2
|
||
|
|
cb79884c53
|
||
|
|
9266fdcfc2
|
||
|
|
b04d822586
|
||
|
|
ba7b10fbc8
|
||
|
|
3b51670850
|
||
|
|
5b2a9e8c0c
|
||
|
|
ae239d584f
|
||
|
|
e6d65ee582
|
||
|
|
08b3ec499e
|
||
|
|
6c17fbbbeb
|
||
|
|
76877645ce
|
||
|
|
7d143fabb8
|
||
|
|
efafd38f68
|
||
|
|
537e1a4774
|
||
|
|
c3b9ff8b4a
|
||
|
|
93d56f79d5
|
||
|
|
1a30345f46
|
||
|
|
778babcc05
|
||
|
|
fa3b53d3b3
|
||
|
|
0ca85656b7
|
||
|
|
f7183f68d5
|
||
| 87027b514b | |||
|
|
16ad621365
|
||
| 33e87d6472 | |||
|
03dc6c7a9c
|
|||
|
897b5bf4ea
|
|||
|
caea2d0121
|
|||
|
e1ff5c479e
|
|||
|
9b3386de30
|
|||
|
f2287c1186
|
|||
| b29197cf4e | |||
|
5c48055ac8
|
|||
|
5ead3476b7
|
|||
| fbf163740a | |||
|
|
1fc1457e97 | ||
| 1f57bbd9c2 | |||
|
2a2793ae44
|
|||
|
8773bf5f9e
|
|||
|
d9970c126a
|
|||
|
4e0d4bf86d
|
|||
|
333bcbfe7e
|
|||
| 875af6d14c | |||
| 8f87a03060 | |||
|
7838fe5f34
|
|||
|
512798d122
|
|||
|
384c28aaaa
|
|||
|
8e5d6dabdc
|
|||
|
ade9261c2c
|
|||
|
bd2a161306
|
|||
|
78c243c985
|
|||
|
cf62bfc5c2
|
|||
|
10f179a095
|
|||
|
f7d0a0ba85
|
|||
| 83e4dfa18f | |||
|
4c70600d1f
|
|||
|
9903683536
|
|||
|
4c51b9c966
|
|||
| 6790e8383d | |||
|
ed886d8182
|
|||
|
ca940ec35d
|
|||
|
5751c0338a
|
|||
|
b9ec363f36
|
|||
|
417768a30c
|
|||
|
9824dcd2c6
|
|||
|
5a784b5fa6
|
|||
|
f36f6866a7
|
|||
|
1fecfe57de
|
|||
|
3165714957
|
|||
|
4ccf43cf4a
|
|||
|
c0e79918ea
|
|||
|
2b00eebb73
|
|||
|
86cdb1202b
|
|||
|
6a469d6a75
|
|||
|
7d66b75216
|
|||
|
8102fa1230
|
|||
|
835152c656
|
|||
|
7c5bd9aa34
|
|||
|
b329b557c4
|
|||
|
2e301c3019
|
|||
|
4f2b35ccb9
|
|||
| a2889705ed | |||
|
7cb0111449
|
|||
| 773ea24c5d | |||
|
cd3e4161b8
|
|||
|
5a658ce580
|
|||
|
6e9b38f04b
|
|||
|
a71a9dfad0
|
|||
|
1c4e444c0b
|
|||
|
565a3c3276
|
|||
|
9fdbf27a60
|
|||
|
1a9b47ceee
|
|||
|
908809bc48
|
|||
|
9636671d57
|
|||
|
51cddd94f5
|
|||
|
123e7aa2a1
|
|||
|
3596955642
|
|||
|
562b16cf89
|
|||
|
830c634f88
|
|||
|
2a793e9201
|
|||
|
e571ed9429
|
|||
|
a67f3e466b
|
|||
|
ff3013f917
|
|||
|
0fa6c1a211
|
|||
|
30b2646b85
|
|||
|
f8b86b0a22
|
|||
| b71a2fa643 | |||
|
eda1f3999f
|
|||
|
c06e58a0fb
|
|||
|
c33637003e
|
|||
|
836bd0a977
|
|||
|
8578fbdad9
|
|||
|
878eac083c
|
|||
|
05da7f5dac
|
|||
|
87e3b1a76c
|
|||
|
32f02cc18a
|
|||
|
1b17cfb396
|
|||
| e5aa5a665c | |||
| d37b68a6e5 | |||
|
56936916ff
|
|||
|
c93a460cff
|
|||
| f5ceda35c1 | |||
|
eb0439d6dc
|
|||
|
c3dde3506e
|
|||
| f22ffe373c | |||
|
bc20e89617
|
|||
| 0f0f296a5e | |||
|
78aea5d608
|
|||
|
f1d3e3d8ec
|
|||
| 2706c76890 | |||
| 17f5eb56cd | |||
| aa6b677b13 | |||
|
9abdab2274
|
|||
|
dd49d1208f
|
|||
|
db9118cb7c
|
|||
|
89913ba60b
|
|||
|
8cf631fd94
|
|||
| d0b359039b | |||
|
84cf523049
|
|||
| a7390ba00b | |||
|
67d148d117
|
|||
|
83ad6f4eef
|
|||
|
2e31268698
|
|||
|
f3b22c02ef
|
|||
| dbe65b4b5a | |||
|
2871fc0f53
|
|||
|
968689a512
|
|||
|
ab29f618f4
|
|||
| 94975a1b30 | |||
|
cd8880d9dc
|
|||
|
f59182b9c1
|
|||
|
941cb4a571
|
|||
|
f534898d8b
|
|||
| 18c7c54403 | |||
|
12a9d4674b
|
|||
|
1af8e068c5
|
|||
| 669b163814 | |||
| 46c7affd1f | |||
| 7ab107b689 | |||
| 5aee1a4100 | |||
|
1578fb9976
|
|||
| 8e64a7cf78 | |||
|
8b5bd66598
|
|||
| ac8552362c | |||
|
99c86c42c5
|
|||
|
d0267cb760
|
|||
| 25ddab9241 | |||
|
bf76ac55ee
|
|||
|
40e5c3609e
|
|||
|
1078c034ad
|
|||
|
bfa38ad7b2
|
|||
|
4f20cd0d0a
|
|||
|
e2ee33a1da
|
|||
|
8662a4c8c1
|
|||
|
dbc811b840
|
|||
|
884070a3cb
|
|||
|
3c350155de
|
|||
|
21c6ebc137
|
|||
|
0a1052fcb7
|
|||
|
f94227f9f3
|
|||
| 088961dfec | |||
|
31cf353d3a
|
|||
|
4eb40abc9c
|
|||
|
682c78c7c3
|
|||
|
f9726ad9be
|
|||
|
89188f5081
|
|||
| 6a6ff84ff2 | |||
|
b6949acc96
|
|||
|
814633034f
|
|||
| 260dedb6cf | |||
|
656c887811
|
|||
|
7e9af716ac
|
|||
|
58cc6811f9
|
|||
|
8ad85636d9
|
|||
|
35e2c8cd30
|
|||
| 4526c941b8 | |||
| 4f5ebd5330 | |||
|
d7e4c6f3ae
|
|||
|
14caefe2d1
|
|||
|
0110f27ada
|
|||
|
dc7cf107c2
|
|||
| 4fbfaadb44 | |||
|
a01cb9ae21
|
|||
|
698e4381c2
|
|||
|
8997349186
|
|||
|
92bfc33bf0
|
|||
|
c6eb21faad
|
|||
| 2d9bc90b16 | |||
|
a0c579e319
|
|||
|
f289ee9365
|
|||
| 46a7345ce9 | |||
|
e12d02a988
|
|||
| 5e8618f25a | |||
|
2bdf08a523
|
|||
|
9ddd36c414
|
|||
|
9372ea7343
|
|||
|
c62ce00184
|
|||
|
4d8cd740ba
|
|||
|
9858572a2f
|
|||
|
51edf55ae9
|
|||
| 75485ce8e9 | |||
|
fcbfcc4007
|
|||
|
cdcb7b3aef
|
|||
| bcf5172956 | |||
|
26c6c5a3b2
|
|||
|
4a65573934
|
|||
|
5e2d5c3b28
|
|||
|
2f70bae523
|
|||
|
40f3e8327a
|
|||
|
f3d6e29e4e
|
|||
| 8903ae2624 | |||
|
26e9073674
|
|||
| 73a89c2601 | |||
|
7d4dee17b7
|
|||
| 602ca6ee94 | |||
|
69fc1ca57e
|
|||
|
ee72a32c7e
|
|||
|
8a0d89ef60
|
|||
|
54af949c7d
|
|||
|
6dac732a7f
|
|||
|
e8c1a6066a
|
|||
| 44fadb12d6 | |||
|
533452469b
|
26
.drone.yml
26
.drone.yml
@@ -1,3 +1,4 @@
|
|||||||
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
name: CI build
|
name: CI build
|
||||||
@@ -11,19 +12,23 @@ steps:
|
|||||||
settings:
|
settings:
|
||||||
restore: true
|
restore: true
|
||||||
mount:
|
mount:
|
||||||
- vendor
|
- ./vendor
|
||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
- master
|
- master
|
||||||
- name: rspec
|
- name: rspec
|
||||||
image: guildeducation/rails:2.7.1-12.19.0
|
image: guildeducation/rails:2.7.2-14.20.0
|
||||||
|
environment:
|
||||||
|
RAILS_ENV: test
|
||||||
commands:
|
commands:
|
||||||
- bundle install --jobs=3 --retry=3 --deployment
|
- 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
|
||||||
- yarn install
|
- yarn install
|
||||||
- bundle exec rspec
|
- rake css:build
|
||||||
when:
|
- rake spec
|
||||||
branch:
|
|
||||||
- master
|
|
||||||
- name: rebuild-cache
|
- name: rebuild-cache
|
||||||
image: drillster/drone-volume-cache
|
image: drillster/drone-volume-cache
|
||||||
volumes:
|
volumes:
|
||||||
@@ -32,7 +37,7 @@ steps:
|
|||||||
settings:
|
settings:
|
||||||
rebuild: true
|
rebuild: true
|
||||||
mount:
|
mount:
|
||||||
- vendor
|
- ./vendor
|
||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
- master
|
- master
|
||||||
@@ -41,3 +46,8 @@ volumes:
|
|||||||
- name: cache
|
- name: cache
|
||||||
host:
|
host:
|
||||||
path: /var/lib/drone/tmp
|
path: /var/lib/drone/tmp
|
||||||
|
---
|
||||||
|
kind: signature
|
||||||
|
hmac: f9a8cf97f6596625721365f6238f6f298aa5a7a4de10c3fb61c57202ae9d1ee1
|
||||||
|
|
||||||
|
...
|
||||||
|
|||||||
17
.env.example
17
.env.example
@@ -1,8 +1,11 @@
|
|||||||
LDAP_HOST=192.168.33.10
|
LDAP_HOST=localhost
|
||||||
LDAP_PORT=389
|
LDAP_PORT=389
|
||||||
#
|
LDAP_ADMIN_PASSWORD=passthebutter
|
||||||
# Production LDAP server:
|
LDAP_SUFFIX="dc=kosmos,dc=org"
|
||||||
#
|
|
||||||
# LDAP_HOST=ldap.kosmos.org
|
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
||||||
# LDAP_PORT=636
|
|
||||||
# LDAP_USE_TLS=true
|
BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||||
|
|
||||||
|
LNDHUB_API_URL='http://localhost:3023'
|
||||||
|
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||||
|
|||||||
4
.env.production
Normal file
4
.env.production
Normal 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'
|
||||||
4
.env.test
Normal file
4
.env.test
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
EJABBERD_API_URL='http://xmpp.example.com/api'
|
||||||
|
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
||||||
|
LNDHUB_API_URL='http://localhost:3023'
|
||||||
|
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -39,3 +39,9 @@ yarn-debug.log*
|
|||||||
|
|
||||||
# Ignore local dotenv config file
|
# Ignore local dotenv config file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Ignore redis dumps from sidekiq
|
||||||
|
dump.rdb
|
||||||
|
|
||||||
|
/app/assets/builds/*
|
||||||
|
!/app/assets/builds/.keep
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.6.1
|
2.7.2
|
||||||
|
|||||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
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 /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
|
||||||
|
|
||||||
|
# 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"]
|
||||||
48
Gemfile
48
Gemfile
@@ -2,15 +2,21 @@ source 'https://rubygems.org'
|
|||||||
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
||||||
|
|
||||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||||
gem 'rails', '~> 6.0.3', '>= 6.0.3.4'
|
gem 'rails', '~> 7.0.2'
|
||||||
# Use Puma as the app server
|
# Use Puma as the app server
|
||||||
gem 'puma', '~> 4.1'
|
gem 'puma', '~> 4.1'
|
||||||
# Use SCSS for stylesheets
|
# View components
|
||||||
gem 'sass-rails', '>= 6'
|
gem "view_component"
|
||||||
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
|
# Separate dependency since Rails 7.0
|
||||||
gem 'webpacker', '~> 4.0'
|
gem 'sprockets-rails'
|
||||||
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
|
# Allows custom JS build tasks to integrate with the asset pipeline
|
||||||
gem 'turbolinks', '~> 5'
|
gem 'cssbundling-rails'
|
||||||
|
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
|
||||||
|
gem "importmap-rails"
|
||||||
|
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
|
||||||
|
gem "turbo-rails"
|
||||||
|
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
|
||||||
|
gem "stimulus-rails"
|
||||||
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
||||||
gem 'jbuilder', '~> 2.7'
|
gem 'jbuilder', '~> 2.7'
|
||||||
# Use Redis adapter to run Action Cable in production
|
# Use Redis adapter to run Action Cable in production
|
||||||
@@ -18,39 +24,49 @@ gem 'jbuilder', '~> 2.7'
|
|||||||
# Use Active Model has_secure_password
|
# Use Active Model has_secure_password
|
||||||
# gem 'bcrypt', '~> 3.1.7'
|
# gem 'bcrypt', '~> 3.1.7'
|
||||||
|
|
||||||
# Reduces boot times through caching; required in config/boot.rb
|
# Configuration
|
||||||
gem 'bootsnap', '>= 1.4.2', require: false
|
gem 'dotenv-rails'
|
||||||
|
|
||||||
gem 'dotenv-rails', groups: [:development, :test]
|
# Security
|
||||||
|
gem 'lockbox'
|
||||||
|
|
||||||
|
# Authentication
|
||||||
gem 'warden'
|
gem 'warden'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise_ldap_authenticatable'
|
gem 'devise_ldap_authenticatable'
|
||||||
gem 'net-ldap'
|
gem 'net-ldap'
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
gem "rqrcode", "~> 2.0"
|
||||||
|
|
||||||
|
# HTTP requests
|
||||||
|
gem 'faraday'
|
||||||
|
|
||||||
|
# Background/scheduled jobs
|
||||||
|
gem 'sidekiq', '< 7'
|
||||||
|
gem 'sidekiq-scheduler'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# Use sqlite3 as the database for Active Record
|
# Use sqlite3 as the database for Active Record
|
||||||
gem 'sqlite3', '~> 1.4'
|
gem 'sqlite3', '~> 1.4'
|
||||||
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
gem 'rspec-rails'
|
||||||
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
|
gem "byebug", "~> 11.1"
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
|
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
|
||||||
gem 'web-console', '>= 3.3.0'
|
gem 'web-console', '>= 3.3.0'
|
||||||
gem 'listen', '~> 3.2'
|
gem 'listen', '~> 3.2'
|
||||||
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
|
||||||
gem 'spring'
|
|
||||||
gem 'spring-watcher-listen', '~> 2.0.0'
|
|
||||||
gem 'letter_opener'
|
gem 'letter_opener'
|
||||||
gem 'letter_opener_web'
|
gem 'letter_opener_web'
|
||||||
|
gem 'faker'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'rspec-rails'
|
|
||||||
gem 'factory_bot_rails'
|
gem 'factory_bot_rails'
|
||||||
gem 'capybara'
|
gem 'capybara'
|
||||||
gem 'database_cleaner'
|
gem 'database_cleaner'
|
||||||
|
gem 'webmock'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :production do
|
group :production do
|
||||||
|
|||||||
425
Gemfile.lock
425
Gemfile.lock
@@ -1,81 +1,101 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.0.3.4)
|
actioncable (7.0.4)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 7.0.4)
|
||||||
|
activesupport (= 7.0.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.0.3.4)
|
actionmailbox (7.0.4)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 7.0.4)
|
||||||
activejob (= 6.0.3.4)
|
activejob (= 7.0.4)
|
||||||
activerecord (= 6.0.3.4)
|
activerecord (= 7.0.4)
|
||||||
activestorage (= 6.0.3.4)
|
activestorage (= 7.0.4)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 7.0.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.0.3.4)
|
net-imap
|
||||||
actionpack (= 6.0.3.4)
|
net-pop
|
||||||
actionview (= 6.0.3.4)
|
net-smtp
|
||||||
activejob (= 6.0.3.4)
|
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)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
|
net-imap
|
||||||
|
net-pop
|
||||||
|
net-smtp
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.0.3.4)
|
actionpack (7.0.4)
|
||||||
actionview (= 6.0.3.4)
|
actionview (= 7.0.4)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 7.0.4)
|
||||||
rack (~> 2.0, >= 2.0.8)
|
rack (~> 2.0, >= 2.2.0)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.0.3.4)
|
actiontext (7.0.4)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 7.0.4)
|
||||||
activerecord (= 6.0.3.4)
|
activerecord (= 7.0.4)
|
||||||
activestorage (= 6.0.3.4)
|
activestorage (= 7.0.4)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 7.0.4)
|
||||||
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.0.3.4)
|
actionview (7.0.4)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 7.0.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
activejob (6.0.3.4)
|
activejob (7.0.4)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 7.0.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.0.3.4)
|
activemodel (7.0.4)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 7.0.4)
|
||||||
activerecord (6.0.3.4)
|
activerecord (7.0.4)
|
||||||
activemodel (= 6.0.3.4)
|
activemodel (= 7.0.4)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 7.0.4)
|
||||||
activestorage (6.0.3.4)
|
activestorage (7.0.4)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 7.0.4)
|
||||||
activejob (= 6.0.3.4)
|
activejob (= 7.0.4)
|
||||||
activerecord (= 6.0.3.4)
|
activerecord (= 7.0.4)
|
||||||
marcel (~> 0.3.1)
|
activesupport (= 7.0.4)
|
||||||
activesupport (6.0.3.4)
|
marcel (~> 1.0)
|
||||||
|
mini_mime (>= 1.1.0)
|
||||||
|
activesupport (7.0.4)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 2.0)
|
||||||
zeitwerk (~> 2.2, >= 2.2.2)
|
addressable (2.8.1)
|
||||||
addressable (2.7.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
bcrypt (3.1.18)
|
||||||
bcrypt (3.1.16)
|
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.5.0)
|
|
||||||
msgpack (~> 1.0)
|
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
byebug (11.1.3)
|
byebug (11.1.3)
|
||||||
capybara (3.33.0)
|
capybara (3.38.0)
|
||||||
addressable
|
addressable
|
||||||
|
matrix
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
rack (>= 1.6.0)
|
rack (>= 1.6.0)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (~> 1.5)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
concurrent-ruby (1.1.7)
|
chunky_png (1.4.0)
|
||||||
|
concurrent-ruby (1.1.10)
|
||||||
|
connection_pool (2.3.0)
|
||||||
|
crack (0.4.5)
|
||||||
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
database_cleaner (1.8.5)
|
cssbundling-rails (1.1.1)
|
||||||
devise (4.7.3)
|
railties (>= 6.0.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)
|
||||||
|
devise (4.8.1)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@@ -84,191 +104,242 @@ GEM
|
|||||||
devise_ldap_authenticatable (0.8.7)
|
devise_ldap_authenticatable (0.8.7)
|
||||||
devise (>= 3.4.1)
|
devise (>= 3.4.1)
|
||||||
net-ldap (>= 0.16.0)
|
net-ldap (>= 0.16.0)
|
||||||
diff-lcs (1.4.4)
|
diff-lcs (1.5.0)
|
||||||
dotenv (2.7.2)
|
dotenv (2.8.1)
|
||||||
dotenv-rails (2.7.2)
|
dotenv-rails (2.8.1)
|
||||||
dotenv (= 2.7.2)
|
dotenv (= 2.8.1)
|
||||||
railties (>= 3.2, < 6.1)
|
|
||||||
erubi (1.9.0)
|
|
||||||
factory_bot (6.1.0)
|
|
||||||
activesupport (>= 5.0.0)
|
|
||||||
factory_bot_rails (6.1.0)
|
|
||||||
factory_bot (~> 6.1.0)
|
|
||||||
railties (>= 5.0.0)
|
|
||||||
ffi (1.13.1)
|
|
||||||
globalid (0.4.2)
|
|
||||||
activesupport (>= 4.2.0)
|
|
||||||
i18n (1.8.5)
|
|
||||||
concurrent-ruby (~> 1.0)
|
|
||||||
jbuilder (2.10.1)
|
|
||||||
activesupport (>= 5.0.0)
|
|
||||||
launchy (2.4.3)
|
|
||||||
addressable (~> 2.3)
|
|
||||||
letter_opener (1.7.0)
|
|
||||||
launchy (~> 2.2)
|
|
||||||
letter_opener_web (1.3.4)
|
|
||||||
actionmailer (>= 3.2)
|
|
||||||
letter_opener (~> 1.0)
|
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
listen (3.2.1)
|
erubi (1.11.0)
|
||||||
|
et-orbi (1.2.7)
|
||||||
|
tzinfo
|
||||||
|
factory_bot (6.2.1)
|
||||||
|
activesupport (>= 5.0.0)
|
||||||
|
factory_bot_rails (6.2.0)
|
||||||
|
factory_bot (~> 6.2.0)
|
||||||
|
railties (>= 5.0.0)
|
||||||
|
faker (3.0.0)
|
||||||
|
i18n (>= 1.8.11, < 2)
|
||||||
|
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.0.0)
|
||||||
|
activesupport (>= 5.0)
|
||||||
|
hashdiff (1.0.1)
|
||||||
|
i18n (1.12.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
importmap-rails (1.1.5)
|
||||||
|
actionpack (>= 6.0.0)
|
||||||
|
railties (>= 6.0.0)
|
||||||
|
jbuilder (2.11.5)
|
||||||
|
actionview (>= 5.0.0)
|
||||||
|
activesupport (>= 5.0.0)
|
||||||
|
launchy (2.5.0)
|
||||||
|
addressable (~> 2.7)
|
||||||
|
letter_opener (1.8.1)
|
||||||
|
launchy (>= 2.2, < 3)
|
||||||
|
letter_opener_web (2.0.0)
|
||||||
|
actionmailer (>= 5.2)
|
||||||
|
letter_opener (~> 1.7)
|
||||||
|
railties (>= 5.2)
|
||||||
|
rexml
|
||||||
|
listen (3.7.1)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
loofah (2.7.0)
|
lockbox (1.1.0)
|
||||||
|
loofah (2.19.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
marcel (0.3.3)
|
marcel (1.0.2)
|
||||||
mimemagic (~> 0.3.2)
|
matrix (0.4.2)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mimemagic (0.3.5)
|
mini_mime (1.1.2)
|
||||||
mini_mime (1.0.2)
|
mini_portile2 (2.8.0)
|
||||||
mini_portile2 (2.4.0)
|
minitest (5.16.3)
|
||||||
minitest (5.14.2)
|
net-imap (0.3.1)
|
||||||
msgpack (1.3.3)
|
net-protocol
|
||||||
net-ldap (0.16.3)
|
net-ldap (0.17.1)
|
||||||
nio4r (2.5.4)
|
net-pop (0.1.2)
|
||||||
nokogiri (1.10.10)
|
net-protocol
|
||||||
mini_portile2 (~> 2.4.0)
|
net-protocol (0.1.3)
|
||||||
|
timeout
|
||||||
|
net-smtp (0.3.3)
|
||||||
|
net-protocol
|
||||||
|
nio4r (2.5.8)
|
||||||
|
nokogiri (1.13.9)
|
||||||
|
mini_portile2 (~> 2.8.0)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.13.9-x86_64-linux)
|
||||||
|
racc (~> 1.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
public_suffix (4.0.6)
|
public_suffix (5.0.0)
|
||||||
puma (4.3.6)
|
puma (4.3.12)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
rack (2.2.3)
|
raabro (1.4.0)
|
||||||
rack-proxy (0.6.5)
|
racc (1.6.0)
|
||||||
rack
|
rack (2.2.4)
|
||||||
rack-test (1.1.0)
|
rack-test (2.0.2)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.3)
|
||||||
rails (6.0.3.4)
|
rails (7.0.4)
|
||||||
actioncable (= 6.0.3.4)
|
actioncable (= 7.0.4)
|
||||||
actionmailbox (= 6.0.3.4)
|
actionmailbox (= 7.0.4)
|
||||||
actionmailer (= 6.0.3.4)
|
actionmailer (= 7.0.4)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 7.0.4)
|
||||||
actiontext (= 6.0.3.4)
|
actiontext (= 7.0.4)
|
||||||
actionview (= 6.0.3.4)
|
actionview (= 7.0.4)
|
||||||
activejob (= 6.0.3.4)
|
activejob (= 7.0.4)
|
||||||
activemodel (= 6.0.3.4)
|
activemodel (= 7.0.4)
|
||||||
activerecord (= 6.0.3.4)
|
activerecord (= 7.0.4)
|
||||||
activestorage (= 6.0.3.4)
|
activestorage (= 7.0.4)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 7.0.4)
|
||||||
bundler (>= 1.3.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.0.3.4)
|
railties (= 7.0.4)
|
||||||
sprockets-rails (>= 2.0.0)
|
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.3.0)
|
rails-html-sanitizer (1.4.3)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.3)
|
||||||
railties (6.0.3.4)
|
railties (7.0.4)
|
||||||
actionpack (= 6.0.3.4)
|
actionpack (= 7.0.4)
|
||||||
activesupport (= 6.0.3.4)
|
activesupport (= 7.0.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 12.2)
|
||||||
thor (>= 0.20.3, < 2.0)
|
thor (~> 1.0)
|
||||||
rake (13.0.1)
|
zeitwerk (~> 2.5)
|
||||||
rb-fsevent (0.10.4)
|
rake (13.0.6)
|
||||||
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
regexp_parser (1.8.2)
|
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)
|
responders (3.0.1)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
rspec-core (3.10.0)
|
rexml (3.2.5)
|
||||||
rspec-support (~> 3.10.0)
|
rqrcode (2.1.2)
|
||||||
rspec-expectations (3.10.0)
|
chunky_png (~> 1.0)
|
||||||
|
rqrcode_core (~> 1.0)
|
||||||
|
rqrcode_core (1.2.0)
|
||||||
|
rspec-core (3.12.0)
|
||||||
|
rspec-support (~> 3.12.0)
|
||||||
|
rspec-expectations (3.12.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.10.0)
|
rspec-support (~> 3.12.0)
|
||||||
rspec-mocks (3.10.0)
|
rspec-mocks (3.12.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.10.0)
|
rspec-support (~> 3.12.0)
|
||||||
rspec-rails (4.0.1)
|
rspec-rails (6.0.1)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 6.1)
|
||||||
railties (>= 4.2)
|
railties (>= 6.1)
|
||||||
rspec-core (~> 3.9)
|
rspec-core (~> 3.11)
|
||||||
rspec-expectations (~> 3.9)
|
rspec-expectations (~> 3.11)
|
||||||
rspec-mocks (~> 3.9)
|
rspec-mocks (~> 3.11)
|
||||||
rspec-support (~> 3.9)
|
rspec-support (~> 3.11)
|
||||||
rspec-support (3.10.0)
|
rspec-support (3.12.0)
|
||||||
sass-rails (6.0.0)
|
ruby2_keywords (0.0.5)
|
||||||
sassc-rails (~> 2.1, >= 2.1.1)
|
rufus-scheduler (3.8.2)
|
||||||
sassc (2.4.0)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
ffi (~> 1.9)
|
sidekiq (6.5.5)
|
||||||
sassc-rails (2.1.2)
|
connection_pool (>= 2.2.2)
|
||||||
railties (>= 4.0.0)
|
rack (~> 2.0)
|
||||||
sassc (>= 2.0)
|
redis (>= 4.5.0)
|
||||||
sprockets (> 3.0)
|
sidekiq-scheduler (4.0.3)
|
||||||
sprockets-rails
|
redis (>= 4.2.0)
|
||||||
tilt
|
rufus-scheduler (~> 3.2)
|
||||||
spring (2.1.1)
|
sidekiq (>= 4, < 7)
|
||||||
spring-watcher-listen (2.0.1)
|
tilt (>= 1.4.0)
|
||||||
listen (>= 2.7, < 4.0)
|
sprockets (4.1.1)
|
||||||
spring (>= 1.2, < 3.0)
|
|
||||||
sprockets (4.0.2)
|
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
sprockets-rails (3.2.2)
|
sprockets-rails (3.4.2)
|
||||||
actionpack (>= 4.0)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 5.2)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sqlite3 (1.4.2)
|
sqlite3 (1.5.4)
|
||||||
thor (1.0.1)
|
mini_portile2 (~> 2.8.0)
|
||||||
thread_safe (0.3.6)
|
sqlite3 (1.5.4-x86_64-linux)
|
||||||
tilt (2.0.10)
|
stimulus-rails (1.2.1)
|
||||||
turbolinks (5.2.1)
|
railties (>= 6.0.0)
|
||||||
turbolinks-source (~> 5.2)
|
thor (1.2.1)
|
||||||
turbolinks-source (5.2.0)
|
tilt (2.0.11)
|
||||||
tzinfo (1.2.7)
|
timeout (0.3.0)
|
||||||
thread_safe (~> 0.1)
|
turbo-rails (1.3.2)
|
||||||
|
actionpack (>= 6.0.0)
|
||||||
|
activejob (>= 6.0.0)
|
||||||
|
railties (>= 6.0.0)
|
||||||
|
tzinfo (2.0.5)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
view_component (2.78.0)
|
||||||
|
activesupport (>= 5.0.0, < 8.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
method_source (~> 1.0)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.1.0)
|
web-console (4.2.0)
|
||||||
actionview (>= 6.0.0)
|
actionview (>= 6.0.0)
|
||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
webpacker (4.3.0)
|
webmock (3.18.1)
|
||||||
activesupport (>= 4.2)
|
addressable (>= 2.8.0)
|
||||||
rack-proxy (>= 0.6.1)
|
crack (>= 0.3.2)
|
||||||
railties (>= 4.2)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
websocket-driver (0.7.3)
|
websocket-driver (0.7.5)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.4.1)
|
zeitwerk (2.6.6)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
bootsnap (>= 1.4.2)
|
byebug (~> 11.1)
|
||||||
byebug
|
|
||||||
capybara
|
capybara
|
||||||
|
cssbundling-rails
|
||||||
database_cleaner
|
database_cleaner
|
||||||
devise
|
devise
|
||||||
devise_ldap_authenticatable
|
devise_ldap_authenticatable
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
|
faker
|
||||||
|
faraday
|
||||||
|
importmap-rails
|
||||||
jbuilder (~> 2.7)
|
jbuilder (~> 2.7)
|
||||||
letter_opener
|
letter_opener
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
listen (~> 3.2)
|
listen (~> 3.2)
|
||||||
|
lockbox
|
||||||
net-ldap
|
net-ldap
|
||||||
pg (~> 1.2.3)
|
pg (~> 1.2.3)
|
||||||
puma (~> 4.1)
|
puma (~> 4.1)
|
||||||
rails (~> 6.0.3, >= 6.0.3.4)
|
rails (~> 7.0.2)
|
||||||
|
rqrcode (~> 2.0)
|
||||||
rspec-rails
|
rspec-rails
|
||||||
sass-rails (>= 6)
|
sidekiq (< 7)
|
||||||
spring
|
sidekiq-scheduler
|
||||||
spring-watcher-listen (~> 2.0.0)
|
sprockets-rails
|
||||||
sqlite3 (~> 1.4)
|
sqlite3 (~> 1.4)
|
||||||
turbolinks (~> 5)
|
stimulus-rails
|
||||||
|
turbo-rails
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
|
view_component
|
||||||
warden
|
warden
|
||||||
web-console (>= 3.3.0)
|
web-console (>= 3.3.0)
|
||||||
webpacker (~> 4.0)
|
webmock
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.0.2
|
2.3.7
|
||||||
|
|||||||
2
Procfile.dev
Normal file
2
Procfile.dev
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
web: bin/rails server -b 0.0.0.0 -p 3000
|
||||||
|
css: yarn build:css --watch
|
||||||
87
README.md
87
README.md
@@ -1,27 +1,33 @@
|
|||||||
|
[](https://drone.kosmos.org/kosmos/akkounts)
|
||||||
|
|
||||||
# Akkounts
|
# Akkounts
|
||||||
|
|
||||||
This app allows Kosmos/LDAP users to manage their accounts, including
|
This app allows Kosmos/LDAP users to manage their accounts, including
|
||||||
credentials, invites, donations, etc..
|
credentials, invites, donations, etc..
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* [x] Log in with existing LDAP account
|
|
||||||
* [x] Reset account password by providing both username and email address
|
|
||||||
* [x] Reset account password when logged in, via reset email
|
|
||||||
* [x] Log in with admin permissions
|
|
||||||
* [x] View LDAP users as admin
|
|
||||||
* [x] Sign up for a new account via invitation
|
|
||||||
* [ ] List my donations
|
|
||||||
* [ ] Invite new users from your account
|
|
||||||
* [ ] Sign up for a new account by donating upfront
|
|
||||||
* [ ] Sign up for a new account via proving contributions (via cryptographic signature)
|
|
||||||
* [ ] ...
|
|
||||||
|
|
||||||
_Planned features are not at all a complete or static list, of course.
|
|
||||||
Suggestions and pull requests welcome!_
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
The easiest way to get a working development setup is using Docker Compose like
|
||||||
|
so:
|
||||||
|
|
||||||
|
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
|
||||||
|
Docker Desktop)
|
||||||
|
2. Uncomment the `web` section in `docker-compose.yml`
|
||||||
|
3. Run `docker compose up` and wait until 389ds announces its successful start
|
||||||
|
in the log output
|
||||||
|
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
|
||||||
|
5. `docker compose run web rails ldap:setup`
|
||||||
|
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/ldap_users](http://localhost:3000/admin/ldap_users)
|
||||||
|
have the password "user is user".
|
||||||
|
|
||||||
### Rails app
|
### Rails app
|
||||||
|
|
||||||
Installing dependencies:
|
Installing dependencies:
|
||||||
@@ -34,24 +40,54 @@ Setting up local database (SQLite):
|
|||||||
bundle exec rails db:create
|
bundle exec rails db:create
|
||||||
bundle exec rails db:migrate
|
bundle exec rails db:migrate
|
||||||
|
|
||||||
Running the dev server:
|
Running the dev server and auto-building CSS files on change:
|
||||||
|
|
||||||
bundle exec rails server
|
bin/dev
|
||||||
|
|
||||||
|
Running the background workers (requires Redis):
|
||||||
|
|
||||||
|
bundle exec sidekiq -C config/sidekiq.yml
|
||||||
|
|
||||||
Running all specs:
|
Running all specs:
|
||||||
|
|
||||||
bundle exec rspec
|
bundle exec rspec
|
||||||
|
|
||||||
### LDAP server
|
### Docker (Compose)
|
||||||
|
|
||||||
TODO make it easy to run a local Kosmos LDAP server for development, without
|
There is a working Docker Compose config file, which allows you to spin up both
|
||||||
manual LDIF imports etc. (or provide a staging instance)
|
an app server for Rails as well as a local 389ds (LDAP) server.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
After creating the Docker container for the first time (or after deleting it),
|
||||||
|
you need to run the following command once, in order to create the dirsrv
|
||||||
|
back-end:
|
||||||
|
|
||||||
|
docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"
|
||||||
|
|
||||||
|
Now you can seed the back-end with data using this Rails task:
|
||||||
|
|
||||||
|
bundle exec rails ldap:setup
|
||||||
|
|
||||||
|
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 `tmp/389ds`. So if you want to start over
|
||||||
|
with a fresh installation, delete both that directory as well as the container.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
* [Ruby on Rails](https://guides.rubyonrails.org/)
|
* [Ruby on Rails](https://guides.rubyonrails.org/)
|
||||||
* [Sass](https://sass-lang.com/documentation)
|
* [Sass](https://sass-lang.com/documentation)
|
||||||
|
|
||||||
|
### Front-end
|
||||||
|
|
||||||
|
* [Tailwind CSS](https://tailwindcss.com/)
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
* [RSpec](https://rspec.info/documentation/)
|
* [RSpec](https://rspec.info/documentation/)
|
||||||
@@ -62,6 +98,13 @@ manual LDIF imports etc. (or provide a staging instance)
|
|||||||
* [devise_ldap_authenticatable](https://github.com/cschiewek/devise_ldap_authenticatable)
|
* [devise_ldap_authenticatable](https://github.com/cschiewek/devise_ldap_authenticatable)
|
||||||
* [net/ldap](https://www.rubydoc.info/gems/net-ldap/Net/LDAP)
|
* [net/ldap](https://www.rubydoc.info/gems/net-ldap/Net/LDAP)
|
||||||
|
|
||||||
|
### Asynchronous jobs/workers
|
||||||
|
|
||||||
|
* [Sidekiq](https://github.com/mperham/sidekiq/wiki/)
|
||||||
|
* [ActiveJob](https://github.com/mperham/sidekiq/wiki/Active-Job)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/)
|
[GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/)
|
||||||
|
|
||||||
|
[1]: https://docs.docker.com/compose/install/
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
//= link_tree ../images
|
//= link_tree ../images
|
||||||
//= link_directory ../stylesheets .css
|
//= link_tree ../../javascript .js
|
||||||
|
//= link_tree ../builds
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
$breakpoints-max: (
|
|
||||||
small: 600px,
|
|
||||||
medium: 960px,
|
|
||||||
large: 1280px
|
|
||||||
);
|
|
||||||
|
|
||||||
$breakpoints-min: (
|
|
||||||
small: 601px,
|
|
||||||
medium: 961px,
|
|
||||||
large: 1281px
|
|
||||||
);
|
|
||||||
|
|
||||||
@mixin media-max($screen-size) {
|
|
||||||
@if map-has-key($breakpoints-max, $screen-size) {
|
|
||||||
@media (max-width: map-get($breakpoints-max, $screen-size)) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
// Debugging
|
|
||||||
@warn "'#{$screen-size}' has not been declared as a breakpoint."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin media-min($screen-size) {
|
|
||||||
@if map-has-key($breakpoints-min, $screen-size) {
|
|
||||||
@media (min-width: map-get($breakpoints-min, $screen-size)) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
// Debugging
|
|
||||||
@warn "'#{$screen-size}' has not been declared as a breakpoint."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
html, body, h1, h2, h3, h4, h5, h6, p, pre, a, dl, dt, dd, ol, ul, li {
|
|
||||||
font-size: 100%;
|
|
||||||
vertical-align: baseline;
|
|
||||||
background: transparent;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: visible;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
||||||
* listed below.
|
|
||||||
*
|
|
||||||
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
|
|
||||||
* vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
||||||
*
|
|
||||||
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
||||||
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
||||||
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
||||||
* It is generally better to create a new file per style scope.
|
|
||||||
*
|
|
||||||
*= require_tree .
|
|
||||||
*= require_self
|
|
||||||
*/
|
|
||||||
10
app/assets/stylesheets/application.tailwind.css
Normal file
10
app/assets/stylesheets/application.tailwind.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@import "tailwindcss/base";
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
@import "tailwindcss/utilities";
|
||||||
|
|
||||||
|
@import "components/base";
|
||||||
|
@import "components/buttons";
|
||||||
|
@import "components/forms";
|
||||||
|
@import "components/links";
|
||||||
|
@import "components/notifications";
|
||||||
|
@import "components/tables";
|
||||||
46
app/assets/stylesheets/components/base.css
Normal file
46
app/assets/stylesheets/components/base.css
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply leading-none bg-cover bg-fixed;
|
||||||
|
background-image: linear-gradient(35deg, rgba(255,0,255,0.2) 0, rgba(13,79,153,0.8) 100%), url('/img/bg-1.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
body#admin {
|
||||||
|
background-image: linear-gradient(35deg, rgba(255,0,255,0.2) 0, rgba(153,12,14,0.9) 100%), url('/img/bg-1.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@apply text-3xl uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply text-2xl mb-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-xl mb-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main section {
|
||||||
|
@apply pt-8 sm:pt-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
main section:first-of-type {
|
||||||
|
@apply pt-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main p {
|
||||||
|
@apply mb-4 leading-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main ul {
|
||||||
|
@apply mb-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
main ul li {
|
||||||
|
@apply leading-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/assets/stylesheets/components/buttons.css
Normal file
39
app/assets/stylesheets/components/buttons.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
@layer components {
|
||||||
|
.btn {
|
||||||
|
@apply font-semibold rounded-md leading-none cursor-pointer text-center
|
||||||
|
transition-colors duration-75 focus:outline-none focus:ring-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-md {
|
||||||
|
@apply btn;
|
||||||
|
@apply py-3 px-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
@apply btn;
|
||||||
|
@apply py-1 px-2 text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
@apply px-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gray {
|
||||||
|
@apply bg-gray-100 hover:bg-gray-200
|
||||||
|
focus:ring-gray-300 focus:ring-opacity-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-blue {
|
||||||
|
@apply bg-blue-500 hover:bg-blue-600 text-white
|
||||||
|
focus:ring-blue-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text]:disabled {
|
||||||
|
@apply text-gray-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/assets/stylesheets/components/forms.css
Normal file
16
app/assets/stylesheets/components/forms.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@layer components {
|
||||||
|
input[type=text], input[type=email], input[type=password],
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field_with_errors {
|
||||||
|
@apply inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
@apply text-red-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
app/assets/stylesheets/components/links.css
Normal file
14
app/assets/stylesheets/components/links.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
@layer components {
|
||||||
|
.ks-text-link {
|
||||||
|
@apply text-blue-600;
|
||||||
|
&:hover { @apply underline; }
|
||||||
|
&:visited { @apply text-indigo-600; }
|
||||||
|
&:active { @apply text-red-600; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.devise-links {
|
||||||
|
a {
|
||||||
|
@apply ks-text-link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/assets/stylesheets/components/notifications.css
Normal file
39
app/assets/stylesheets/components/notifications.css
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
@layer components {
|
||||||
|
@keyframes notification-countdown {
|
||||||
|
from { width: 100%; }
|
||||||
|
to { width: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-enter {
|
||||||
|
@apply transform ease-out duration-300 transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-enter-from {
|
||||||
|
@apply translate-y-2 opacity-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-enter-to {
|
||||||
|
@apply translate-y-0 opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen sm {
|
||||||
|
.notification-enter-from {
|
||||||
|
@apply translate-y-0 translate-x-2;
|
||||||
|
}
|
||||||
|
.notification-enter-to {
|
||||||
|
@apply translate-x-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-leave {
|
||||||
|
@apply transition ease-in duration-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-leave-from {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-leave-to {
|
||||||
|
@apply opacity-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/assets/stylesheets/components/tables.css
Normal file
22
app/assets/stylesheets/components/tables.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@layer components {
|
||||||
|
table {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead tr {
|
||||||
|
@apply text-left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
@apply pb-3.5 text-sm font-normal uppercase text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th:not(:last-of-type),
|
||||||
|
table td:not(:last-of-type) {
|
||||||
|
@apply pr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
@apply py-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Raleway';
|
|
||||||
src: url('/fonts/raleway-light.woff') format('woff2');
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: "Open Sans", Helvetica, Arial, sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3 {
|
|
||||||
font-family: Raleway, inherit;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
form {
|
|
||||||
.field_with_errors {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-signup {
|
|
||||||
label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=text], input[type=email], input[type=password] {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.at-sign, span.domain {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-msg {
|
|
||||||
color: #bc0101;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accept-terms {
|
|
||||||
margin-top: 2rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
line-height: 1.5em;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
@import "mediaqueries";
|
|
||||||
|
|
||||||
$content-width: 800px;
|
|
||||||
$content-max-width: 100%;
|
|
||||||
|
|
||||||
body {
|
|
||||||
}
|
|
||||||
|
|
||||||
#wrapper {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
> header {
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 4rem 0;
|
|
||||||
text-align: center;
|
|
||||||
background: #0d4f99;
|
|
||||||
background: linear-gradient(35deg, #8955a0 0, #0d4f99 100%);
|
|
||||||
|
|
||||||
@include media-max(small) {
|
|
||||||
padding: 3rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
span.project-name {
|
|
||||||
display: none;
|
|
||||||
// font-size: .5em;
|
|
||||||
// text-transform: none;
|
|
||||||
// vertical-align: super;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.beta {
|
|
||||||
font-size: .5em;
|
|
||||||
font-style: italic;
|
|
||||||
text-transform: none;
|
|
||||||
vertical-align: super;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.bolt {
|
|
||||||
color: #ffd000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.current-user {
|
|
||||||
margin-top: 2rem;
|
|
||||||
color: rgba(255,255,255,0.6);
|
|
||||||
|
|
||||||
@include media-max(small) {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-weight: 400;
|
|
||||||
color: #fff;
|
|
||||||
// color: #ffd000;
|
|
||||||
// color: #ccff40;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: rgba(255,255,255,0.6);
|
|
||||||
transition: color 0.1s linear;
|
|
||||||
|
|
||||||
&:hover, &:active {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-msg {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem 0;
|
|
||||||
|
|
||||||
&.notice {
|
|
||||||
background: #efffc4;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.alert {
|
|
||||||
background: #fff4c2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
width: $content-width;
|
|
||||||
max-width: $content-max-width;
|
|
||||||
margin: 4rem auto;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
@include media-max(medium) {
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-max(small) {
|
|
||||||
margin: 3rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2, h3 {
|
|
||||||
margin-bottom: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
line-height: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
&.notice {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
|
|
||||||
li {
|
|
||||||
line-height: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
line-height: 1.5rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
border-bottom: 1px dotted #ccc;
|
|
||||||
padding-bottom: 4rem;
|
|
||||||
margin-bottom: 4rem;
|
|
||||||
|
|
||||||
@include media-max(small) {
|
|
||||||
padding-bottom: 3rem;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
|
|
||||||
&.services {
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
grid-row-gap: 1rem;
|
|
||||||
grid-column-gap: 2rem;
|
|
||||||
|
|
||||||
@include media-max(small) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
margin-top: 3rem;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-item {
|
|
||||||
p {
|
|
||||||
color: #888;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
app/components/header_compact_component.html.erb
Normal file
7
app/components/header_compact_component.html.erb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<header class="py-10">
|
||||||
|
<div class="max-w-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h1 class="text-3xl font-bold text-white text-center">
|
||||||
|
<%= @title %>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
7
app/components/header_compact_component.rb
Normal file
7
app/components/header_compact_component.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class HeaderCompactComponent < ViewComponent::Base
|
||||||
|
def initialize(title:)
|
||||||
|
@title = title
|
||||||
|
end
|
||||||
|
end
|
||||||
7
app/components/header_component.html.erb
Normal file
7
app/components/header_component.html.erb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<header class="py-10">
|
||||||
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h1 class="text-3xl font-bold text-white">
|
||||||
|
<%= @title %>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
7
app/components/header_component.rb
Normal file
7
app/components/header_component.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class HeaderComponent < ViewComponent::Base
|
||||||
|
def initialize(title:)
|
||||||
|
@title = title
|
||||||
|
end
|
||||||
|
end
|
||||||
5
app/components/main_compact_component.html.erb
Normal file
5
app/components/main_compact_component.html.erb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<main class="w-full max-w-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white rounded-lg shadow px-6 sm:px-12 py-8 sm:py-12">
|
||||||
|
<%= content %>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
5
app/components/main_compact_component.rb
Normal file
5
app/components/main_compact_component.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MainCompactComponent < ViewComponent::Base
|
||||||
|
|
||||||
|
end
|
||||||
5
app/components/main_simple_component.html.erb
Normal file
5
app/components/main_simple_component.html.erb
Normal file
@@ -0,0 +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 px-6 sm:px-12 py-8 sm:py-12">
|
||||||
|
<%= content %>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
5
app/components/main_simple_component.rb
Normal file
5
app/components/main_simple_component.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MainSimpleComponent < ViewComponent::Base
|
||||||
|
|
||||||
|
end
|
||||||
15
app/components/main_with_sidenav_component.html.erb
Normal file
15
app/components/main_with_sidenav_component.html.erb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
|
||||||
|
<div class="bg-white rounded-lg shadow">
|
||||||
|
<div class="md:min-h-[50vh] divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
|
||||||
|
<aside class="py-6 sm:py-8 lg:col-span-3">
|
||||||
|
<nav class="space-y-1">
|
||||||
|
<%= render partial: @sidenav_partial %>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<div class="lg:col-span-9 px-6 sm:px-12 py-8 sm:pt-10 sm:pb-12">
|
||||||
|
<%= content %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
7
app/components/main_with_sidenav_component.rb
Normal file
7
app/components/main_with_sidenav_component.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MainWithSidenavComponent < ViewComponent::Base
|
||||||
|
def initialize(sidenav_partial:)
|
||||||
|
@sidenav_partial = sidenav_partial
|
||||||
|
end
|
||||||
|
end
|
||||||
10
app/components/main_with_tabnav_component.html.erb
Normal file
10
app/components/main_with_tabnav_component.html.erb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
|
||||||
|
<div class="bg-white rounded-lg shadow">
|
||||||
|
<div class="px-6 sm:px-12 pt-2 sm:pt-4">
|
||||||
|
<%= render partial: @tabnav_partial %>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 sm:px-12 py-8 sm:py-12">
|
||||||
|
<%= content %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
7
app/components/main_with_tabnav_component.rb
Normal file
7
app/components/main_with_tabnav_component.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MainWithTabnavComponent < ViewComponent::Base
|
||||||
|
def initialize(tabnav_partial:)
|
||||||
|
@tabnav_partial = tabnav_partial
|
||||||
|
end
|
||||||
|
end
|
||||||
49
app/components/notification_component.html.erb
Normal file
49
app/components/notification_component.html.erb
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<div class="flash-msg <%= @type %> hidden max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto mt-4"
|
||||||
|
data-turbo="false"
|
||||||
|
data-notification-action-url="<%= @data.dig(:action, :url) %>"
|
||||||
|
data-notification-action-method="<%= @data.dig(:action, :method) %>"
|
||||||
|
data-notification-timeout="<%= @data[:timeout] %>"
|
||||||
|
data-controller="notification">
|
||||||
|
<div class="rounded-lg shadow-xs overflow-hidden">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="inline-block h-6 w-6 <%= @icon_color_class %>">
|
||||||
|
<%= render "icons/#{@icon_name}" %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 w-0 flex-1 pt-0.5">
|
||||||
|
<p class="text-sm leading-5 font-medium text-gray-900">
|
||||||
|
<%= @data[:title] %>
|
||||||
|
</p>
|
||||||
|
<% if @data[:body].present? %>
|
||||||
|
<p class="mt-1 text-sm leading-5 text-gray-500">
|
||||||
|
<%= @data[:body] %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<% if @data[:action].present? %>
|
||||||
|
<div class="mt-2" data-notification-target="buttons">
|
||||||
|
<a data-turbo-frame="_top" <% if @data.dig(:action, :method) == 'get' %> href="<%= @data.dig(:action, :url) %>" <% else %> href="#" data-action="notification#run" <% end %> class="text-sm leading-5 font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none focus:underline transition ease-in-out duration-150">
|
||||||
|
<%= @data.dig(:action, :name) %>
|
||||||
|
</a>
|
||||||
|
<button data-action="notification#close" class="ml-6 text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:underline transition ease-in-out duration-150">
|
||||||
|
<%= t('.dismiss') %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4 flex-shrink-0 flex">
|
||||||
|
<button class="inline-flex text-gray-400 focus:outline-none focus:text-gray-500 transition ease-in-out duration-150" data-action="notification#close">
|
||||||
|
<!-- Heroicon name: solid/x -->
|
||||||
|
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% if @data[:countdown] %>
|
||||||
|
<div class="bg-indigo-600 rounded-lg h-1 w-0" data-notification-target="countdown"></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
54
app/components/notification_component.rb
Normal file
54
app/components/notification_component.rb
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# @param type [String] Classic notification type `error`, `alert` and `info` + custom `success`
|
||||||
|
# @param data [String, Hash] `String` for backward compatibility,
|
||||||
|
# `Hash` for the new functionality `{title: '', body: '', timeout: 5, countdown: false, action: { url: '', method: '', name: ''}}`.
|
||||||
|
# The `title` attribute for `Hash` is mandatory.
|
||||||
|
class NotificationComponent < ViewComponent::Base
|
||||||
|
def initialize(type:, data:)
|
||||||
|
@type = type
|
||||||
|
@data = prepare_data(data)
|
||||||
|
@icon_name = icon_name
|
||||||
|
@icon_color_class = icon_color_class
|
||||||
|
|
||||||
|
@data[:timeout] ||= 5
|
||||||
|
@data[:action][:method] ||= "get" if @data[:action]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def prepare_data(data)
|
||||||
|
case data
|
||||||
|
when Hash
|
||||||
|
data
|
||||||
|
else
|
||||||
|
{ title: data }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def icon_name
|
||||||
|
case @type
|
||||||
|
when 'success'
|
||||||
|
'check-circle'
|
||||||
|
when 'error'
|
||||||
|
'alert-octagon'
|
||||||
|
when 'alert'
|
||||||
|
'alert-octagon'
|
||||||
|
else
|
||||||
|
'info'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def icon_color_class
|
||||||
|
case @type
|
||||||
|
when 'success'
|
||||||
|
'text-emerald-500'
|
||||||
|
when 'error'
|
||||||
|
'text-rose-600'
|
||||||
|
when 'alert'
|
||||||
|
'text-rose-600'
|
||||||
|
else
|
||||||
|
'text-gray-400'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
4
app/components/sidenav_link_component.html.erb
Normal file
4
app/components/sidenav_link_component.html.erb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<%= link_to @path, class: @link_class, title: (@disabled ? "Coming soon" : nil) do %>
|
||||||
|
<%= render partial: "icons/#{@icon}", locals: { custom_class: @icon_class } %>
|
||||||
|
<span class="truncate"><%= @name %></span>
|
||||||
|
<% end %>
|
||||||
33
app/components/sidenav_link_component.rb
Normal file
33
app/components/sidenav_link_component.rb
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SidenavLinkComponent < ViewComponent::Base
|
||||||
|
def initialize(name:, path:, icon:, active: false, disabled: false)
|
||||||
|
@name = name
|
||||||
|
@path = path
|
||||||
|
@icon = icon
|
||||||
|
@active = active
|
||||||
|
@disabled = disabled
|
||||||
|
@link_class = class_names_link(path)
|
||||||
|
@icon_class = class_names_icon(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def class_names_link(path)
|
||||||
|
if @active
|
||||||
|
"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
|
||||||
|
"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
|
||||||
|
"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
|
||||||
|
|
||||||
|
def class_names_icon(path)
|
||||||
|
if @active
|
||||||
|
"text-teal-500 group-hover:text-teal-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
|
||||||
|
elsif @disabled
|
||||||
|
"text-gray-300 group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
|
||||||
|
else
|
||||||
|
"text-gray-400 group-hover:text-gray-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
3
app/components/tabnav_link_component.html.erb
Normal file
3
app/components/tabnav_link_component.html.erb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<%= link_to @path, class: @link_class do %>
|
||||||
|
<%= @name %>
|
||||||
|
<% end %>
|
||||||
21
app/components/tabnav_link_component.rb
Normal file
21
app/components/tabnav_link_component.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TabnavLinkComponent < ViewComponent::Base
|
||||||
|
def initialize(name:, path:, active: false, disabled: false)
|
||||||
|
@name = name
|
||||||
|
@path = path
|
||||||
|
@active = active
|
||||||
|
@disabled = disabled
|
||||||
|
@link_class = class_names_link(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def class_names_link(path)
|
||||||
|
if @active
|
||||||
|
"border-indigo-500 text-indigo-600 w-1/2 py-4 px-1 text-center border-b-2"
|
||||||
|
elsif @disabled
|
||||||
|
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 w-1/2 py-4 px-1 text-center border-b-2"
|
||||||
|
else
|
||||||
|
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 w-1/2 py-4 px-1 text-center border-b-2"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
18
app/components/wallet_summary_component.html.erb
Normal file
18
app/components/wallet_summary_component.html.erb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<section class="w-full grid grid-cols-1 md:grid-cols-12 md:mb-0">
|
||||||
|
<div class="md:col-span-8">
|
||||||
|
<p>
|
||||||
|
Send and receive sats via the Bitcoin Lightning Network.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<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-xl"><%= number_with_delimiter @balance %> sats</span><br>
|
||||||
|
<span class="text-sm text-gray-500">Available balance</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="text-xl">n/a sats</span><br>
|
||||||
|
<span class="text-sm text-gray-500">Balance unavailable</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
8
app/components/wallet_summary_component.rb
Normal file
8
app/components/wallet_summary_component.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WalletSummaryComponent < ViewComponent::Base
|
||||||
|
def initialize(balance:)
|
||||||
|
@balance = balance
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
7
app/controllers/account_controller.rb
Normal file
7
app/controllers/account_controller.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
class AccountController < ApplicationController
|
||||||
|
before_action :require_user_signed_in
|
||||||
|
|
||||||
|
def index
|
||||||
|
@current_section = :account
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,5 +2,10 @@ class Admin::BaseController < ApplicationController
|
|||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :authorize_admin
|
before_action :authorize_admin
|
||||||
|
before_action :set_context
|
||||||
|
|
||||||
|
def set_context
|
||||||
|
@context = :admin
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class Admin::DashboardController < Admin::BaseController
|
class Admin::DashboardController < Admin::BaseController
|
||||||
def index
|
def index
|
||||||
|
@current_section = :dashboard
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
79
app/controllers/admin/donations_controller.rb
Normal file
79
app/controllers/admin/donations_controller.rb
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
class Admin::DonationsController < Admin::BaseController
|
||||||
|
before_action :set_donation, only: [:show, :edit, :update, :destroy]
|
||||||
|
before_action :set_current_section, only: [:index, :show, :new, :edit]
|
||||||
|
|
||||||
|
# GET /donations
|
||||||
|
# GET /donations.json
|
||||||
|
def index
|
||||||
|
@donations = Donation.all
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /donations/1
|
||||||
|
# GET /donations/1.json
|
||||||
|
def show
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /donations/new
|
||||||
|
def new
|
||||||
|
@donation = Donation.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /donations/1/edit
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /donations
|
||||||
|
# POST /donations.json
|
||||||
|
def create
|
||||||
|
@donation = Donation.new(donation_params)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# PATCH/PUT /donations/1
|
||||||
|
# PATCH/PUT /donations/1.json
|
||||||
|
def update
|
||||||
|
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
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to admin_donations_url, notice: 'Donation was successfully destroyed.' }
|
||||||
|
format.json { head :no_content }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# Use callbacks to share common setup or constraints between actions.
|
||||||
|
def set_donation
|
||||||
|
@donation = Donation.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Only allow a list of trusted parameters through.
|
||||||
|
def donation_params
|
||||||
|
params.require(:donation).permit(:user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :paid_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_current_section
|
||||||
|
@current_section = :donations
|
||||||
|
end
|
||||||
|
end
|
||||||
8
app/controllers/admin/invitations_controller.rb
Normal file
8
app/controllers/admin/invitations_controller.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
class Admin::InvitationsController < Admin::BaseController
|
||||||
|
def index
|
||||||
|
@current_section = :invitations
|
||||||
|
@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
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
class Admin::LdapUsersController < Admin::BaseController
|
class Admin::LdapUsersController < Admin::BaseController
|
||||||
|
before_action :set_current_section
|
||||||
|
|
||||||
def index
|
def index
|
||||||
attributes = %w{dn cn uid mail admin}
|
attributes = %w{dn cn uid mail admin}
|
||||||
filter = Net::LDAP::Filter.eq("uid", "*")
|
filter = Net::LDAP::Filter.eq("uid", "*")
|
||||||
if params[:ou]
|
|
||||||
treebase = "ou=#{params[:ou]},cn=users,dc=kosmos,dc=org"
|
@ou = params[:ou] || "kosmos.org"
|
||||||
else
|
treebase = "ou=#{@ou},cn=users,dc=kosmos,dc=org"
|
||||||
treebase = "ou=kosmos.org,cn=users,dc=kosmos,dc=org"
|
|
||||||
end
|
|
||||||
|
|
||||||
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
||||||
entries.sort_by! { |e| e.cn[0] }
|
entries.sort_by! { |e| e.cn[0] }
|
||||||
@@ -27,7 +27,7 @@ class Admin::LdapUsersController < Admin::BaseController
|
|||||||
def ldap_client
|
def ldap_client
|
||||||
ldap_client ||= Net::LDAP.new host: ldap_config['host'],
|
ldap_client ||= Net::LDAP.new host: ldap_config['host'],
|
||||||
port: ldap_config['port'],
|
port: ldap_config['port'],
|
||||||
encryption: ldap_config['ssl'],
|
# encryption: ldap_config['ssl'],
|
||||||
auth: {
|
auth: {
|
||||||
method: :simple,
|
method: :simple,
|
||||||
username: ldap_config['admin_user'],
|
username: ldap_config['admin_user'],
|
||||||
@@ -38,4 +38,8 @@ class Admin::LdapUsersController < Admin::BaseController
|
|||||||
def ldap_config
|
def ldap_config
|
||||||
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
|
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_current_section
|
||||||
|
@current_section = :ldap_users
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
5
app/controllers/api/base_controller.rb
Normal file
5
app/controllers/api/base_controller.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class Api::BaseController < ApplicationController
|
||||||
|
|
||||||
|
layout false
|
||||||
|
|
||||||
|
end
|
||||||
13
app/controllers/api/kredits_controller.rb
Normal file
13
app/controllers/api/kredits_controller.rb
Normal 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
|
||||||
10
app/controllers/contributions/donations_controller.rb
Normal file
10
app/controllers/contributions/donations_controller.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class Contributions::DonationsController < ApplicationController
|
||||||
|
before_action :require_user_signed_in
|
||||||
|
|
||||||
|
# GET /donations
|
||||||
|
# GET /donations.json
|
||||||
|
def index
|
||||||
|
@donations = current_user.donations.completed
|
||||||
|
@current_section = :contributions
|
||||||
|
end
|
||||||
|
end
|
||||||
8
app/controllers/contributions/projects_controller.rb
Normal file
8
app/controllers/contributions/projects_controller.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
class Contributions::ProjectsController < ApplicationController
|
||||||
|
before_action :require_user_signed_in
|
||||||
|
|
||||||
|
# GET /contributions
|
||||||
|
def index
|
||||||
|
@current_section = :contributions
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,5 +2,6 @@ class DashboardController < ApplicationController
|
|||||||
before_action :require_user_signed_in
|
before_action :require_user_signed_in
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@current_section = :dashboard
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ class InvitationsController < ApplicationController
|
|||||||
before_action :require_user_signed_in, except: ["show"]
|
before_action :require_user_signed_in, except: ["show"]
|
||||||
before_action :require_user_signed_out, only: ["show"]
|
before_action :require_user_signed_out, only: ["show"]
|
||||||
|
|
||||||
layout "signup", only: ["show"]
|
|
||||||
|
|
||||||
# GET /invitations
|
# GET /invitations
|
||||||
def index
|
def index
|
||||||
@invitations_unused = current_user.invitations.unused
|
@invitations_unused = current_user.invitations.unused
|
||||||
@invitations_used = current_user.invitations.used
|
@invitations_used = current_user.invitations.used
|
||||||
|
@current_section = :invitations
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /invitations/a-random-invitation-token
|
# GET /invitations/a-random-invitation-token
|
||||||
|
|||||||
75
app/controllers/lnurlpay_controller.rb
Normal file
75
app/controllers/lnurlpay_controller.rb
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
class LnurlpayController < ApplicationController
|
||||||
|
before_action :find_user_by_address
|
||||||
|
|
||||||
|
MIN_SATS = 10
|
||||||
|
MAX_SATS = 1_000_000
|
||||||
|
MAX_COMMENT_CHARS = 100
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: {
|
||||||
|
status: "OK",
|
||||||
|
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
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def invoice
|
||||||
|
amount = params[:amount].to_i / 1000 # msats
|
||||||
|
address = params[:address]
|
||||||
|
comment = params[:comment] || ""
|
||||||
|
|
||||||
|
if !valid_amount?(amount)
|
||||||
|
render json: { status: "ERROR", reason: "Invalid amount" }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
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 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 metadata(address)
|
||||||
|
"[[\"text/identifier\", \"#{address}\"], [\"text/plain\", \"Send sats, receive thanks.\"]]"
|
||||||
|
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
|
||||||
|
|
||||||
|
end
|
||||||
13
app/controllers/settings/account_controller.rb
Normal file
13
app/controllers/settings/account_controller.rb
Normal 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
|
||||||
11
app/controllers/settings/profile_controller.rb
Normal file
11
app/controllers/settings/profile_controller.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
class Settings::ProfileController < SettingsController
|
||||||
|
|
||||||
|
def index
|
||||||
|
@user = current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
class SettingsController < ApplicationController
|
class SettingsController < ApplicationController
|
||||||
before_action :require_user_signed_in
|
before_action :require_user_signed_in
|
||||||
|
before_action :set_current_section
|
||||||
|
|
||||||
def index
|
def index
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_password
|
private
|
||||||
current_user.send_reset_password_instructions
|
|
||||||
sign_out current_user
|
def set_current_section
|
||||||
msg = "We have sent you an email with a link to reset your password."
|
@current_section = :settings
|
||||||
redirect_to check_your_email_path, notice: msg
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ class SignupController < ApplicationController
|
|||||||
before_action :require_invitation
|
before_action :require_invitation
|
||||||
before_action :set_invitation
|
before_action :set_invitation
|
||||||
before_action :set_new_user, only: ["steps", "validate"]
|
before_action :set_new_user, only: ["steps", "validate"]
|
||||||
|
before_action :set_context
|
||||||
layout "signup"
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@invited_by_name = @invitation.user.address
|
@invited_by_name = @invitation.user.address
|
||||||
@@ -94,16 +93,19 @@ class SignupController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def complete_signup
|
def complete_signup
|
||||||
@user.save!
|
|
||||||
session[:new_user] = nil
|
session[:new_user] = nil
|
||||||
session[:validation_error] = nil
|
session[:validation_error] = nil
|
||||||
|
|
||||||
CreateAccount.call(
|
CreateAccount.call(
|
||||||
username: @user.cn,
|
username: @user.cn,
|
||||||
|
domain: "kosmos.org",
|
||||||
email: @user.email,
|
email: @user.email,
|
||||||
password: @user.password
|
password: @user.password,
|
||||||
|
invitation: @invitation
|
||||||
)
|
)
|
||||||
|
end
|
||||||
|
|
||||||
@invitation.update! invited_user_id: @user.id, used_at: DateTime.now
|
def set_context
|
||||||
|
@context = :signup
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
18
app/controllers/turbo_controller.rb
Normal file
18
app/controllers/turbo_controller.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
class TurboController < ApplicationController
|
||||||
|
class Responder < ActionController::Responder
|
||||||
|
def to_turbo_stream
|
||||||
|
controller.render(options.merge(formats: :html))
|
||||||
|
rescue ActionView::MissingTemplate => error
|
||||||
|
if get?
|
||||||
|
raise error
|
||||||
|
elsif has_errors? && default_action
|
||||||
|
render rendering_options.merge(formats: :html, status: :unprocessable_entity)
|
||||||
|
else
|
||||||
|
redirect_to navigation_location
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.responder = Responder
|
||||||
|
respond_to :html, :turbo_stream
|
||||||
|
end
|
||||||
18
app/controllers/users/devise_controller.rb
Normal file
18
app/controllers/users/devise_controller.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
class Users::DeviseController < ApplicationController
|
||||||
|
class Responder < ActionController::Responder
|
||||||
|
def to_turbo_stream
|
||||||
|
controller.render(options.merge(formats: :html))
|
||||||
|
rescue ActionView::MissingTemplate => error
|
||||||
|
if get?
|
||||||
|
raise error
|
||||||
|
elsif has_errors? && default_action
|
||||||
|
render rendering_options.merge(formats: :html, status: :unprocessable_entity)
|
||||||
|
else
|
||||||
|
redirect_to navigation_location
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.responder = Responder
|
||||||
|
respond_to :html, :turbo_stream
|
||||||
|
end
|
||||||
81
app/controllers/wallet_controller.rb
Normal file
81
app/controllers/wallet_controller.rb
Normal 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
|
||||||
@@ -1,2 +1,14 @@
|
|||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
|
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
|
||||||
|
"bg-gray-900/50 text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
|
||||||
|
else
|
||||||
|
"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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
2
app/helpers/donations_helper.rb
Normal file
2
app/helpers/donations_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module DonationsHelper
|
||||||
|
end
|
||||||
2
app/helpers/lnurlpay_helper.rb
Normal file
2
app/helpers/lnurlpay_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module LnurlpayHelper
|
||||||
|
end
|
||||||
2
app/helpers/wallet_helper.rb
Normal file
2
app/helpers/wallet_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module WalletHelper
|
||||||
|
end
|
||||||
3
app/javascript/application.js
Normal file
3
app/javascript/application.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
|
||||||
|
import "@hotwired/turbo-rails"
|
||||||
|
import "controllers"
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Action Cable provides the framework to deal with WebSockets in Rails.
|
|
||||||
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
|
|
||||||
|
|
||||||
import { createConsumer } from "@rails/actioncable"
|
|
||||||
|
|
||||||
export default createConsumer()
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// Load all the channels within this directory and all subdirectories.
|
|
||||||
// Channel files must be named *_channel.js.
|
|
||||||
|
|
||||||
const channels = require.context('.', true, /_channel\.js$/)
|
|
||||||
channels.keys().forEach(channels)
|
|
||||||
9
app/javascript/controllers/application.js
Normal file
9
app/javascript/controllers/application.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Application } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
const application = Application.start()
|
||||||
|
|
||||||
|
// Configure Stimulus development experience
|
||||||
|
application.debug = false
|
||||||
|
window.Stimulus = application
|
||||||
|
|
||||||
|
export { application }
|
||||||
16
app/javascript/controllers/clipboard_controller.js
Normal file
16
app/javascript/controllers/clipboard_controller.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["source", "trigger"]
|
||||||
|
|
||||||
|
copy (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
navigator.clipboard.writeText(this.sourceTarget.value);
|
||||||
|
this.triggerTarget.querySelector('.content-initial').classList.add('hidden');
|
||||||
|
this.triggerTarget.querySelector('.content-active').classList.remove('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.triggerTarget.querySelector('.content-initial').classList.remove('hidden');
|
||||||
|
this.triggerTarget.querySelector('.content-active').classList.add('hidden');
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/javascript/controllers/index.js
Normal file
11
app/javascript/controllers/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Import and register all your controllers from the importmap under controllers/*
|
||||||
|
|
||||||
|
import { application } from "controllers/application"
|
||||||
|
|
||||||
|
// Eager load all controllers defined in the import map under controllers/**/*_controller
|
||||||
|
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
||||||
|
eagerLoadControllersFrom("controllers", application)
|
||||||
|
|
||||||
|
// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
|
||||||
|
// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
|
||||||
|
// lazyLoadControllersFrom("controllers", application)
|
||||||
110
app/javascript/controllers/notification_controller.js
Normal file
110
app/javascript/controllers/notification_controller.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["buttons", "countdown"]
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const timeoutSeconds = parseInt(this.data.get("timeout"));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.element.classList.remove('hidden');
|
||||||
|
this.element.classList.add('notification-enter', 'notification-enter-from');
|
||||||
|
|
||||||
|
// Trigger transition
|
||||||
|
setTimeout(() => {
|
||||||
|
this.element.classList.add('notification-enter-to');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Trigger countdown
|
||||||
|
if (this.hasCountdownTarget) {
|
||||||
|
this.countdownTarget.style.animation = 'notification-countdown linear ' + timeoutSeconds + 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 500);
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
this.close();
|
||||||
|
}, timeoutSeconds * 1000 + 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
run(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.stop();
|
||||||
|
let _this = this;
|
||||||
|
this.buttonsTarget.innerHTML = '<span class="text-sm leading-5 font-medium text-grey-700">Processing...</span>';
|
||||||
|
|
||||||
|
// Call the action
|
||||||
|
fetch(this.data.get("action-url"), {
|
||||||
|
method: this.data.get("action-method").toUpperCase(),
|
||||||
|
dataType: 'script',
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-Token": this.csrfToken
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw Error(response.statusText);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Set new content
|
||||||
|
_this.buttonsTarget.innerHTML = '<span class="text-sm leading-5 font-medium text-green-700">' + data.message + '</span>';
|
||||||
|
|
||||||
|
// Remove hidden class and display the record
|
||||||
|
if (data.inline) {
|
||||||
|
document.getElementById(data.dom_id).classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close
|
||||||
|
setTimeout(() => {
|
||||||
|
if (data.inline) {
|
||||||
|
// Just close the notification
|
||||||
|
_this.close();
|
||||||
|
} else {
|
||||||
|
// Reload the page using Turbo
|
||||||
|
window.Turbo.visit(window.location.toString(), {action: 'replace'})
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
_this.buttonsTarget.innerHTML = '<span class="text-sm leading-5 font-medium text-red-700">Error!</span>';
|
||||||
|
setTimeout(() => {
|
||||||
|
_this.close();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
clearTimeout(this.timeoutId)
|
||||||
|
this.timeoutId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
continue() {
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
this.close();
|
||||||
|
}, parseInt(this.data.get("timeout")));
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.element.classList.remove('notification-enter', 'notification-enter-from', 'notification-enter-to');
|
||||||
|
this.element.classList.add('notification-leave', 'notification-leave-from')
|
||||||
|
|
||||||
|
// Trigger transition
|
||||||
|
setTimeout(() => {
|
||||||
|
this.element.classList.add('notification-leave-to');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Remove element after transition
|
||||||
|
setTimeout(() => {
|
||||||
|
this.element.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
get csrfToken() {
|
||||||
|
const element = document.head.querySelector('meta[name="csrf-token"]')
|
||||||
|
return element.getAttribute("content")
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/javascript/controllers/topbar_controller.js
Normal file
31
app/javascript/controllers/topbar_controller.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
function show (element) {
|
||||||
|
element.classList.add('block');
|
||||||
|
element.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide (element) {
|
||||||
|
element.classList.remove('block');
|
||||||
|
element.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ 'mobileMenu', 'iconMobileMenuOpen', 'iconMobileMenuClose' ];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.mobileMenuTarget.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMobileNav() {
|
||||||
|
if (this.mobileMenuTarget.classList.contains('hidden')) {
|
||||||
|
show(this.mobileMenuTarget);
|
||||||
|
show(this.iconMobileMenuCloseTarget);
|
||||||
|
hide(this.iconMobileMenuOpenTarget);
|
||||||
|
} else {
|
||||||
|
hide(this.mobileMenuTarget);
|
||||||
|
hide(this.iconMobileMenuCloseTarget);
|
||||||
|
show(this.iconMobileMenuOpenTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// This file is automatically compiled by Webpack, along with any other files
|
|
||||||
// present in this directory. You're encouraged to place your actual application logic in
|
|
||||||
// a relevant structure within app/javascript and only use these pack files to reference
|
|
||||||
// that code so it'll be compiled.
|
|
||||||
|
|
||||||
require("@rails/ujs").start()
|
|
||||||
require("turbolinks").start()
|
|
||||||
require("channels")
|
|
||||||
|
|
||||||
|
|
||||||
// Uncomment to copy all static images under ../images to the output folder and reference
|
|
||||||
// them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
|
|
||||||
// or the `imagePath` JavaScript helper below.
|
|
||||||
//
|
|
||||||
// const images = require.context('../images', true)
|
|
||||||
// const imagePath = (name) => images(name, true)
|
|
||||||
32
app/jobs/create_ldap_user_job.rb
Normal file
32
app/jobs/create_ldap_user_job.rb
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
class CreateLdapUserJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(username, domain, email, hashed_pw)
|
||||||
|
dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org"
|
||||||
|
attr = {
|
||||||
|
objectclass: ["top", "account", "person", "extensibleObject"],
|
||||||
|
cn: username,
|
||||||
|
sn: username,
|
||||||
|
uid: username,
|
||||||
|
mail: email,
|
||||||
|
userPassword: hashed_pw
|
||||||
|
}
|
||||||
|
|
||||||
|
ldap_client.add(dn: dn, attributes: attr)
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
13
app/jobs/create_lndhub_wallet_job.rb
Normal file
13
app/jobs/create_lndhub_wallet_job.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class CreateLndhubWalletJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(user)
|
||||||
|
return if user.ln_login.present? && user.ln_password.present?
|
||||||
|
|
||||||
|
lndhub = Lndhub.new
|
||||||
|
credentials = lndhub.create({ partnerid: user.ou, accounttype: "user" })
|
||||||
|
|
||||||
|
user.update! ln_login: credentials["login"],
|
||||||
|
ln_password: credentials["password"]
|
||||||
|
end
|
||||||
|
end
|
||||||
18
app/jobs/exchange_xmpp_contacts_job.rb
Normal file
18
app/jobs/exchange_xmpp_contacts_job.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
class ExchangeXmppContactsJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(inviter, username, domain)
|
||||||
|
ejabberd = EjabberdApiClient.new
|
||||||
|
|
||||||
|
ejabberd.add_rosteritem({
|
||||||
|
"localuser": username, "localhost": domain,
|
||||||
|
"user": inviter.cn, "host": inviter.ou,
|
||||||
|
"nick": inviter.cn, "group": "Friends", "subs": "both"
|
||||||
|
})
|
||||||
|
ejabberd.add_rosteritem({
|
||||||
|
"localuser": inviter.cn, "localhost": inviter.ou,
|
||||||
|
"user": username, "host": domain,
|
||||||
|
"nick": username, "group": "Friends", "subs": "both"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
13
app/models/donation.rb
Normal file
13
app/models/donation.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class Donation < ApplicationRecord
|
||||||
|
# Relations
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
# Validations
|
||||||
|
validates_presence_of :amount_sats
|
||||||
|
|
||||||
|
# Hooks
|
||||||
|
# TODO before_create :store_fiat_value
|
||||||
|
|
||||||
|
#Scopes
|
||||||
|
scope :completed, -> { where.not(paid_at: nil) }
|
||||||
|
end
|
||||||
@@ -3,12 +3,16 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
# Relations
|
# Relations
|
||||||
has_many :invitations, dependent: :destroy
|
has_many :invitations, dependent: :destroy
|
||||||
|
has_many :donations, dependent: :nullify
|
||||||
|
|
||||||
validates_uniqueness_of :cn
|
validates_uniqueness_of :cn
|
||||||
validates_length_of :cn, :minimum => 3
|
validates_length_of :cn, :minimum => 3
|
||||||
validates_uniqueness_of :email
|
validates_uniqueness_of :email
|
||||||
validates :email, email: true
|
validates :email, email: true
|
||||||
|
|
||||||
|
lockbox_encrypts :ln_login
|
||||||
|
lockbox_encrypts :ln_password
|
||||||
|
|
||||||
# Include default devise modules. Others available are:
|
# Include default devise modules. Others available are:
|
||||||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
||||||
devise :ldap_authenticatable,
|
devise :ldap_authenticatable,
|
||||||
@@ -52,4 +56,10 @@ class User < ApplicationRecord
|
|||||||
self.valid?
|
self.valid?
|
||||||
self.errors[attribute_name].blank?
|
self.errors[attribute_name].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ln_create_invoice(payload)
|
||||||
|
lndhub = Lndhub.new
|
||||||
|
lndhub.authenticate self
|
||||||
|
lndhub.addinvoice payload
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
32
app/services/btc_pay.rb
Normal file
32
app/services/btc_pay.rb
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#
|
||||||
|
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
|
||||||
|
#
|
||||||
|
class BtcPay
|
||||||
|
def initialize
|
||||||
|
@base_url = ENV["BTCPAY_API_URL"]
|
||||||
|
@store_id = Rails.application.credentials.btcpay[:store_id]
|
||||||
|
@auth_token = Rails.application.credentials.btcpay[:auth_token]
|
||||||
|
end
|
||||||
|
|
||||||
|
def onchain_wallet_balance
|
||||||
|
res = get "stores/#{@store_id}/payment-methods/onchain/BTC/wallet"
|
||||||
|
|
||||||
|
{
|
||||||
|
balance: res["balance"].to_f,
|
||||||
|
unconfirmed_balance: res["unconfirmedBalance"].to_f,
|
||||||
|
confirmed_balance: res["confirmedBalance"].to_f
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def get(endpoint)
|
||||||
|
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
|
||||||
|
"Content-Type" => "application/json",
|
||||||
|
"Accept" => "application/json",
|
||||||
|
"Authorization" => "token #{@auth_token}"
|
||||||
|
})
|
||||||
|
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,42 +1,57 @@
|
|||||||
class CreateAccount < ApplicationService
|
class CreateAccount < ApplicationService
|
||||||
def initialize(args)
|
def initialize(args)
|
||||||
@username = args[:username]
|
@username = args[:username]
|
||||||
@email = args[:email]
|
@domain = args[:ou] || "kosmos.org"
|
||||||
@password = args[:password]
|
@email = args[:email]
|
||||||
|
@password = args[:password]
|
||||||
|
@invitation = args[:invitation]
|
||||||
|
@confirmed = args[:confirmed]
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
user = create_user_in_database
|
||||||
add_ldap_document
|
add_ldap_document
|
||||||
|
create_lndhub_wallet(user)
|
||||||
|
|
||||||
|
if @invitation.present?
|
||||||
|
update_invitation(user.id)
|
||||||
|
exchange_xmpp_contacts
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def add_ldap_document
|
def create_user_in_database
|
||||||
dn = "cn=#{@username},ou=kosmos.org,cn=users,dc=kosmos,dc=org"
|
User.create!(
|
||||||
attr = {
|
|
||||||
objectclass: ["top", "account", "person", "extensibleObject"],
|
|
||||||
cn: @username,
|
cn: @username,
|
||||||
sn: @username,
|
ou: @domain,
|
||||||
uid: @username,
|
email: @email,
|
||||||
mail: @email,
|
password: @password,
|
||||||
userPassword: Devise.ldap_auth_password_builder.call(@password)
|
password_confirmation: @password,
|
||||||
}
|
confirmed_at: @confirmed ? DateTime.now : nil
|
||||||
|
)
|
||||||
ldap_client.add(dn: dn, attributes: attr)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def ldap_client
|
def update_invitation(user_id)
|
||||||
ldap_client ||= Net::LDAP.new host: ldap_config['host'],
|
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
|
||||||
port: ldap_config['port'],
|
|
||||||
encryption: ldap_config['ssl'],
|
|
||||||
auth: {
|
|
||||||
method: :simple,
|
|
||||||
username: ldap_config['admin_user'],
|
|
||||||
password: ldap_config['admin_password']
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def ldap_config
|
# TODO move to confirmation
|
||||||
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
|
# (and/or add email_confirmed to entry and use in login filter)
|
||||||
|
def add_ldap_document
|
||||||
|
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
|
||||||
|
CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw)
|
||||||
|
end
|
||||||
|
|
||||||
|
def exchange_xmpp_contacts
|
||||||
|
#TODO enable in development when we have easy setup of ejabberd etc.
|
||||||
|
return if Rails.env.development?
|
||||||
|
ExchangeXmppContactsJob.perform_later(@invitation.user, @username, @domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_lndhub_wallet(user)
|
||||||
|
#TODO enable in development when we have a local lndhub (mock?) API
|
||||||
|
return if Rails.env.development?
|
||||||
|
CreateLndhubWalletJob.perform_later(user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
20
app/services/ejabberd_api_client.rb
Normal file
20
app/services/ejabberd_api_client.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
class EjabberdApiClient
|
||||||
|
def initialize
|
||||||
|
@base_url = ENV["EJABBERD_API_URL"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def post(endpoint, payload)
|
||||||
|
res = Faraday.post("#{@base_url}/#{endpoint}", payload.to_json,
|
||||||
|
"Content-Type" => "application/json")
|
||||||
|
|
||||||
|
if res.status != 200
|
||||||
|
Rails.logger.error "[ejabberd] API request failed:"
|
||||||
|
Rails.logger.error res.body
|
||||||
|
#TODO add some kind of exception tracking/notifications
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_rosteritem(payload)
|
||||||
|
post "add_rosteritem", payload
|
||||||
|
end
|
||||||
|
end
|
||||||
135
app/services/ldap_service.rb
Normal file
135
app/services/ldap_service.rb
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
class LdapService < ApplicationService
|
||||||
|
def initialize
|
||||||
|
@suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_entry(dn, attrs, interactive=false)
|
||||||
|
puts "Adding entry: #{dn}" if interactive
|
||||||
|
res = ldap_client.add dn: dn, attributes: attrs
|
||||||
|
puts res.inspect if interactive && !res
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_attribute(dn, attr, value)
|
||||||
|
ldap_client.add_attribute dn, attr, value
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_entry(dn, interactive=false)
|
||||||
|
puts "Deleting entry: #{dn}" if interactive
|
||||||
|
res = ldap_client.delete dn: dn
|
||||||
|
puts res.inspect if interactive && !res
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_all_entries!
|
||||||
|
if Rails.env.production?
|
||||||
|
raise "Mass deletion of entries not allowed in production"
|
||||||
|
end
|
||||||
|
|
||||||
|
filter = Net::LDAP::Filter.eq("objectClass", "*")
|
||||||
|
entries = ldap_client.search(base: @suffix, filter: filter, attributes: %w{dn})
|
||||||
|
entries.sort_by!{ |e| e.dn.length }.reverse!
|
||||||
|
|
||||||
|
entries.each do |e|
|
||||||
|
delete_entry e.dn, true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_users(args={})
|
||||||
|
if args[:ou]
|
||||||
|
treebase = "ou=#{args[:ou]},cn=users,#{@suffix}"
|
||||||
|
else
|
||||||
|
treebase = ldap_config["base"]
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes = %w{dn cn uid mail admin}
|
||||||
|
filter = Net::LDAP::Filter.eq("uid", "*")
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_organizations
|
||||||
|
attributes = %w{dn ou description}
|
||||||
|
filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit")
|
||||||
|
# filter = Net::LDAP::Filter.eq("objectClass", "*")
|
||||||
|
treebase = "cn=users,#{@suffix}"
|
||||||
|
|
||||||
|
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
||||||
|
|
||||||
|
entries.sort_by! { |e| e.ou[0] }
|
||||||
|
|
||||||
|
entries = entries.collect do |e|
|
||||||
|
{
|
||||||
|
dn: e.dn,
|
||||||
|
ou: e.ou.first,
|
||||||
|
description: e.try(:description) ? e.description.first : nil,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_organization(ou, description, interactive=false)
|
||||||
|
dn = "ou=#{ou},cn=users,#{@suffix}"
|
||||||
|
|
||||||
|
aci = <<-EOS
|
||||||
|
(target="ldap:///cn=*,ou=#{ou},cn=users,#{@suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{@suffix}";)
|
||||||
|
EOS
|
||||||
|
|
||||||
|
attrs = {
|
||||||
|
objectClass: ["top", "organizationalUnit"],
|
||||||
|
description: description,
|
||||||
|
ou: ou,
|
||||||
|
aci: aci
|
||||||
|
}
|
||||||
|
|
||||||
|
add_entry dn, attrs, interactive
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_directory!
|
||||||
|
if Rails.env.production?
|
||||||
|
raise "Resetting the directory not allowed in production"
|
||||||
|
end
|
||||||
|
|
||||||
|
delete_all_entries!
|
||||||
|
|
||||||
|
user_read_aci = <<-EOS
|
||||||
|
(target="ldap:///#{@suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";)
|
||||||
|
EOS
|
||||||
|
|
||||||
|
add_entry @suffix, {
|
||||||
|
dc: "kosmos", objectClass: ["top", "domain"], aci: user_read_aci
|
||||||
|
}, true
|
||||||
|
|
||||||
|
add_entry "cn=users,#{@suffix}", {
|
||||||
|
cn: "users", objectClass: ["top", "organizationalRole"]
|
||||||
|
}, true
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ldap_client
|
||||||
|
ldap_client ||= Net::LDAP.new host: ldap_config['host'],
|
||||||
|
port: ldap_config['port'],
|
||||||
|
# TODO has to be :simple_tls if TLS is enabled
|
||||||
|
# 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
|
||||||
|
|
||||||
|
end
|
||||||
66
app/services/lndhub.rb
Normal file
66
app/services/lndhub.rb
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
class Lndhub
|
||||||
|
attr_accessor :auth_token
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@base_url = ENV["LNDHUB_API_URL"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def post(endpoint, payload)
|
||||||
|
headers = { "Content-Type" => "application/json" }
|
||||||
|
if auth_token
|
||||||
|
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||||
|
end
|
||||||
|
|
||||||
|
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
||||||
|
|
||||||
|
if res.status != 200
|
||||||
|
Rails.logger.error "[lndhub] API request failed:"
|
||||||
|
Rails.logger.error res.body
|
||||||
|
#TODO add some kind of exception tracking/notifications
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(endpoint, auth_token)
|
||||||
|
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
|
||||||
|
"Content-Type" => "application/json",
|
||||||
|
"Accept" => "application/json",
|
||||||
|
"Authorization" => "Bearer #{auth_token}"
|
||||||
|
})
|
||||||
|
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(payload)
|
||||||
|
post "create", payload
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate(user)
|
||||||
|
credentials = post "auth?type=auth", { login: user.ln_login, password: user.ln_password }
|
||||||
|
self.auth_token = credentials["access_token"]
|
||||||
|
self.auth_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def balance(user_token)
|
||||||
|
get "balance", user_token || auth_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def gettxs(user_token)
|
||||||
|
get "gettxs", user_token || auth_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def getuserinvoices(user_token)
|
||||||
|
get "getuserinvoices", user_token || auth_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def addinvoice(payload)
|
||||||
|
invoice = post "addinvoice", {
|
||||||
|
amt: payload[:amount],
|
||||||
|
memo: payload[:memo],
|
||||||
|
description_hash: payload[:description_hash]
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice["payment_request"]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<h2>Admin Panel</h2>
|
<%= render HeaderComponent.new(title: "Admin Panel") %>
|
||||||
<p>
|
|
||||||
Ohai there, admin human.
|
<%= render MainSimpleComponent.new do %>
|
||||||
</p>
|
<p class="text-center">
|
||||||
<p>
|
With great power comes great responsibility.
|
||||||
<%= link_to 'LDAP users', admin_ldap_users_path %>
|
</p>
|
||||||
</p>
|
<% end %>
|
||||||
|
|||||||
2
app/views/admin/donations/_donation.json.jbuilder
Normal file
2
app/views/admin/donations/_donation.json.jbuilder
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
json.extract! donation, :id, :user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :created_at, :updated_at
|
||||||
|
json.url donation_url(donation, format: :json)
|
||||||
58
app/views/admin/donations/_form.html.erb
Normal file
58
app/views/admin/donations/_form.html.erb
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<%= form_with(url: url, model: donation, local: true) do |form| %>
|
||||||
|
<% if donation.errors.any? %>
|
||||||
|
<div id="error_explanation">
|
||||||
|
<h3><%= pluralize(donation.errors.count, "error") %> prohibited this donation from being saved:</h3>
|
||||||
|
<ul>
|
||||||
|
<% donation.errors.full_messages.each do |message| %>
|
||||||
|
<li><%= message %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<p>
|
||||||
|
<%= form.label :user_id %>
|
||||||
|
<%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<p>
|
||||||
|
<%= form.label :amount_sats, "Amount BTC (sats)" %>
|
||||||
|
<%= form.number_field :amount_sats %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<p>
|
||||||
|
<%= form.label :amount_eur, "Amount EUR (cents)" %>
|
||||||
|
<%= form.number_field :amount_eur %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<p>
|
||||||
|
<%= form.label :amount_usd, "Amount USD (cents)"%>
|
||||||
|
<%= form.number_field :amount_usd %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<p>
|
||||||
|
<%= form.label :public_name %>
|
||||||
|
<%= form.text_field :public_name %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<p>
|
||||||
|
<%= form.label :paid_at %>
|
||||||
|
<%= form.text_field :paid_at %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-8">
|
||||||
|
<%= form.submit class: 'btn-md btn-blue' %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
12
app/views/admin/donations/edit.html.erb
Normal file
12
app/views/admin/donations/edit.html.erb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Donations") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<h2>Editing Donation</h2>
|
||||||
|
|
||||||
|
<%= render 'form', donation: @donation, url: admin_donation_path(@donation) %>
|
||||||
|
|
||||||
|
<p class="mt-8">
|
||||||
|
<%= link_to 'Show', admin_donation_path(@donation), class: 'ks-text-link' %> |
|
||||||
|
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
|
||||||
|
<p>
|
||||||
|
<% end %>
|
||||||
44
app/views/admin/donations/index.html.erb
Normal file
44
app/views/admin/donations/index.html.erb
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Donations") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<% if @donations.any? %>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th class="text-right">Amount BTC</th>
|
||||||
|
<th class="text-right">in EUR</th>
|
||||||
|
<th class="text-right">in USD</th>
|
||||||
|
<th class="pl-2">Public name</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th colspan="3"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<% @donations.each do |donation| %>
|
||||||
|
<tr>
|
||||||
|
<td><%= donation.user.address %></td>
|
||||||
|
<td class="text-right"><%= sats_to_btc donation.amount_sats %></td>
|
||||||
|
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
|
||||||
|
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
|
||||||
|
<td class="pl-2"><%= donation.public_name %></td>
|
||||||
|
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d") : "" %></td>
|
||||||
|
<td><%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %></td>
|
||||||
|
<td><%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %></td>
|
||||||
|
<td><%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
|
||||||
|
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% else %>
|
||||||
|
<p>
|
||||||
|
No donations yet.
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p class="mt-12">
|
||||||
|
<%= link_to 'Record an out-of-system donation', new_admin_donation_path, class: 'btn-md btn-gray' %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
1
app/views/admin/donations/index.json.jbuilder
Normal file
1
app/views/admin/donations/index.json.jbuilder
Normal file
@@ -0,0 +1 @@
|
|||||||
|
json.array! @donations, partial: "donations/donation", as: :donation
|
||||||
11
app/views/admin/donations/new.html.erb
Normal file
11
app/views/admin/donations/new.html.erb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Donations") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<h2>New Donation</h2>
|
||||||
|
|
||||||
|
<%= render 'form', donation: @donation, url: admin_donations_path %>
|
||||||
|
|
||||||
|
<p class="mt-8">
|
||||||
|
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
38
app/views/admin/donations/show.html.erb
Normal file
38
app/views/admin/donations/show.html.erb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Donations") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<p>
|
||||||
|
<strong>User:</strong>
|
||||||
|
<%= @donation.user.address %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Amount sats:</strong>
|
||||||
|
<%= @donation.amount_sats %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Amount eur:</strong>
|
||||||
|
<%= @donation.amount_eur %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Amount usd:</strong>
|
||||||
|
<%= @donation.amount_usd %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Public name:</strong>
|
||||||
|
<%= @donation.public_name %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Date:</strong>
|
||||||
|
<%= @donation.paid_at %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mt-8">
|
||||||
|
<%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'ks-text-link' %> |
|
||||||
|
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
1
app/views/admin/donations/show.json.jbuilder
Normal file
1
app/views/admin/donations/show.json.jbuilder
Normal file
@@ -0,0 +1 @@
|
|||||||
|
json.partial! "donations/donation", donation: @donation
|
||||||
37
app/views/admin/invitations/index.html.erb
Normal file
37
app/views/admin/invitations/index.html.erb
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Invitations") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
There are currently <strong><%= @invitations_unused_count %>
|
||||||
|
unused invitations</strong> available to existing users.
|
||||||
|
<strong><%= @users_with_referrals_count %> users</strong> have successfully
|
||||||
|
invited new users.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<% if @invitations_used.any? %>
|
||||||
|
<section>
|
||||||
|
<h3>Accepted (<%= @invitations_used.length %>)</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Token</th>
|
||||||
|
<th>Accepted</th>
|
||||||
|
<th>Inviter</th>
|
||||||
|
<th>Invited user</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @invitations_used.each do |invitation| %>
|
||||||
|
<tr>
|
||||||
|
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
|
||||||
|
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
||||||
|
<td><%= invitation.user.address %></td>
|
||||||
|
<td><%= User.find(invitation.invited_user_id).address %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@@ -1,27 +1,34 @@
|
|||||||
<h2>LDAP users</h2>
|
<%= render HeaderComponent.new(title: "LDAP Users: #{@ou}") %>
|
||||||
|
|
||||||
<ul>
|
<%= render MainSimpleComponent.new do %>
|
||||||
<li><%= link_to 'kosmos.org', admin_ldap_users_path %></li>
|
<h3 class="hidden">Domains</h3>
|
||||||
<li><%= link_to '5apps.com', admin_ldap_users_path(ou: '5apps.com') %></li>
|
<ul class="mb-10">
|
||||||
</ul>
|
<li class="inline-block">
|
||||||
|
<%= link_to 'kosmos.org', admin_ldap_users_path, class: "ks-text-link" %>
|
||||||
|
</li>
|
||||||
|
<li class="inline-block ml-6">
|
||||||
|
<%= link_to '5apps.com', admin_ldap_users_path(ou: '5apps.com'), class: "ks-text-link" %>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>UID</th>
|
<th>UID</th>
|
||||||
<th>E-Mail</th>
|
<th>E-Mail</th>
|
||||||
<th>Admin</th>
|
<th>Admin</th>
|
||||||
<!-- <th>Password</th> -->
|
<!-- <th>Password</th> -->
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% @entries.each do |entry| %>
|
<% @entries.each do |entry| %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= entry[:uid] %></td>
|
<td><%= entry[:uid] %></td>
|
||||||
<td><%= entry[:mail] %></td>
|
<td><%= entry[:mail] %></td>
|
||||||
<td><%= entry[:admin] %></td>
|
<td><%= entry[:admin] %></td>
|
||||||
<!-- <td><%= entry[:password] %></td> -->
|
<!-- <td><%= entry[:password] %></td> -->
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<% end %>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user