69 Commits

Author SHA1 Message Date
8e64a7cf78 Merge pull request 'Fix invoice amount for lnurlp payments' (#40) from bugfix/lnurlp_amounts into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #40
2021-11-23 21:29:33 +00:00
8b5bd66598 Fix invoice amount for lnurlp payments
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
Incoming amount is msats, but we create invoices with sats.
2021-11-23 15:28:16 -06:00
ac8552362c Merge pull request 'Adjust tailwind purge config' (#39) from bugfix/35-tailwind into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #39
2021-11-23 21:27:22 +00:00
99c86c42c5 Adjust tailwind purge config
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-23 15:15:10 -06:00
d0267cb760 Update README
Some checks failed
continuous-integration/drone/push Build is failing
2021-11-22 17:05:46 -06:00
25ddab9241 Merge pull request 'Add LndHub wallets' (#33) from feature/lndhub into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #33
2021-11-22 23:04:19 +00:00
bf76ac55ee Do not allow comments for lnurlp senders
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-22 17:03:18 -06:00
40e5c3609e Remove obsolete files
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-22 16:29:23 -06:00
1078c034ad Remove obsolete comment
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-22 16:26:05 -06:00
bfa38ad7b2 Adjust spec for new development config
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-22 16:24:42 -06:00
4f20cd0d0a Add Rake task for generating wallets
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-22 16:22:53 -06:00
e2ee33a1da Configure LndHub for production
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-22 16:19:16 -06:00
8662a4c8c1 Don't overwrite existing lndhub wallet credentials
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2021-11-22 15:51:30 -06:00
dbc811b840 Add LndHub service, lnurl-pay endpoints
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
Enables the lnurl-pay payment workflow
2021-11-22 15:41:05 -06:00
884070a3cb Show available balance on wallet page
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-21 16:47:55 -06:00
3c350155de Formatting
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-11-21 15:34:24 -06:00
21c6ebc137 Fix small issue with turbolinks
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
The JS was run again, and failed to assign variables using `const` then.
2021-11-21 13:27:55 -06:00
0a1052fcb7 Add wallet page
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
With setup code to connect Blue Wallet to Kosmos account wallets
2021-11-20 16:13:43 -06:00
f94227f9f3 Create LndHub accounts 2021-11-19 20:10:36 -06:00
088961dfec Merge pull request 'Switch to shared Kosmos font(s)' (#30) from feature/webfonts_open-sans into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #30
2021-02-25 17:11:39 +00:00
31cf353d3a Load remote fonts before other stylesheets
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
They won't apply when loaded afterwards.
2021-02-25 18:04:27 +01:00
4eb40abc9c Import webfont from shared URL
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-02-19 15:45:22 +01:00
682c78c7c3 Move headline styles to tailwind base
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-19 15:11:53 +01:00
f9726ad9be Use Open Sans as default font for everything 2021-02-19 15:02:07 +01:00
89188f5081 Don't purge CSS in base and component layers
All checks were successful
continuous-integration/drone/push Build is passing
The production build was purging input[type=text] styles, because it
couldn't find it in the Rails templates. Change config, so it only
purges utility classes.
2021-02-10 16:37:34 +01:00
6a6ff84ff2 Merge pull request 'Add Tailwind CSS, migrate most of the styles' (#27) from feature/tailwind-css into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #27
2021-02-10 14:29:06 +00:00
b6949acc96 Style forms, migrate more styles to Tailwind
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-02-09 20:32:10 +01:00
814633034f WIP Add Tailwind CSS
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-09 02:05:31 +01:00
260dedb6cf Merge pull request 'Set up async workers/jobs via Sidekiq' (#26) from feature/sidekiq into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #26
2021-02-03 18:12:48 +00:00
656c887811 Add missing hook to spec
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-02-03 19:11:43 +01:00
7e9af716ac Make them colors pop
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-03 13:37:44 +01:00
58cc6811f9 Move XMPP contacts exchange to background job
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-02-03 13:16:47 +01:00
8ad85636d9 Create LDAP users asynchronously
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-02 21:16:24 +01:00
35e2c8cd30 Add Sidekiq, configure admin access to Web UI
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-02 11:44:17 +01:00
4526c941b8 Merge pull request 'Add invitations page to admin panel' (#24) from feature/admin_invitations into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #24
2021-02-01 22:53:31 +00:00
4f5ebd5330 Merge pull request 'Add cosmic background to header' (#25) from ui/kosmic_sky into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #25
2021-02-01 22:53:15 +00:00
d7e4c6f3ae Add cosmic background to header
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Going back to space, where we belong.
2021-02-01 23:10:54 +01:00
14caefe2d1 Replace yellow menu highlight with blue color 2021-02-01 22:49:42 +01:00
0110f27ada Add invitation stats
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Show some stats about unused invitations and active inviters
2021-02-01 22:35:30 +01:00
dc7cf107c2 New admin page for invitations 2021-02-01 21:53:18 +01:00
4fbfaadb44 Merge pull request 'Various UI improvements' (#23) from ux/various_design_changes into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #23
2021-02-01 18:33:06 +00:00
a01cb9ae21 Adjust site header in admin, signup layouts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-02-01 18:58:34 +01:00
698e4381c2 Improve table styles
All checks were successful
continuous-integration/drone/push Build is passing
* Nicer table headers
* Hide invitation IDs on small screens
2021-02-01 18:53:48 +01:00
8997349186 Move password change to new Security tab
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-01 18:39:51 +01:00
92bfc33bf0 Remove bottom border from last section on page 2021-02-01 18:24:01 +01:00
c6eb21faad Change site name to "Account", add comet icon
All checks were successful
continuous-integration/drone/push Build is passing
... and remove the "beta" tag.
2021-02-01 18:17:26 +01:00
2d9bc90b16 Merge pull request 'Use new .local domain for Postgres in production' (#22) from chore/postgres_hostname into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #22
2021-01-23 14:04:45 +00:00
a0c579e319 Use new .local domain for Postgres in production
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-01-23 15:03:16 +01:00
f289ee9365 Switch menu items
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-29 11:09:04 +01:00
46a7345ce9 Merge pull request 'Add main navigation bar' (#20) from feature/main_nav into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #20
2020-12-29 10:04:42 +00:00
e12d02a988 Fix spec
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Content changed
2020-12-29 11:02:24 +01:00
5e8618f25a Merge pull request 'Add admin layout with admin navigation' (#21) from feature/admin_layout into feature/main_nav
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
Reviewed-on: #21
2020-12-29 09:58:57 +00:00
2bdf08a523 Add admin layout with admin navigation
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
And remove the hacky link list from the dashboard.
2020-12-28 09:32:04 +01:00
9ddd36c414 Add missing section markup
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2020-12-27 14:21:16 +01:00
9372ea7343 Add small-screen layout for main navigation 2020-12-27 14:14:53 +01:00
c62ce00184 Add main navigation bar
Make donations and invitations accessible to everyone
2020-12-27 14:03:40 +01:00
4d8cd740ba Argh
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-22 17:15:46 +01:00
9858572a2f Remove useless bundler version requirement
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-22 17:03:14 +01:00
51edf55ae9 Use zerotier for connecting to postgres
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-22 12:24:18 +01:00
75485ce8e9 Merge pull request 'Update postgres master host' (#19) from chore/update_postgres_host into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #19
2020-12-22 10:42:51 +00:00
fcbfcc4007 Update postgres master host
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2020-12-22 11:41:40 +01:00
cdcb7b3aef Update README
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-21 15:49:58 +01:00
bcf5172956 Merge pull request 'Add basic donation records' (#18) from feature/donation_records into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #18
2020-12-21 14:46:50 +00:00
26c6c5a3b2 Nullify donation owners when related record destroyed
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2020-12-21 13:59:46 +01:00
4a65573934 Format numbers on admin donations page
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
And fix the wrong unit display in the user donations list.
2020-12-19 14:59:16 +01:00
5e2d5c3b28 Add paid_at date to donations
All checks were successful
continuous-integration/drone/push Build is passing
2020-12-19 13:28:47 +01:00
2f70bae523 Format and style user donations 2020-12-19 13:16:04 +01:00
40f3e8327a Basic donation records
Adds donation model/table and basic manual management in the admin
panel, as well as basic listing of users' own donations.
2020-12-17 21:56:51 +01:00
f3d6e29e4e Remove time from used invitations list
Date is enough.
2020-12-17 17:02:30 +01:00
101 changed files with 1962 additions and 532 deletions

View File

@@ -1 +1,3 @@
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'

View File

@@ -1 +1,3 @@
EJABBERD_API_URL='https://xmpp.kosmos.org:5443/api'
LNDHUB_API_URL='http://10.1.1.163:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'

5
.gitignore vendored
View File

@@ -39,3 +39,8 @@ yarn-debug.log*
# Ignore local dotenv config file
.env
# Ignore redis dumps from sidekiq
dump.rdb
/config/credentials/development.key

13
Gemfile
View File

@@ -21,15 +21,28 @@ gem 'jbuilder', '~> 2.7'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.2', require: false
# Configuration
gem 'dotenv-rails'
# Security
gem 'lockbox'
# Authentication
gem 'warden'
gem 'devise'
gem 'devise_ldap_authenticatable'
gem 'net-ldap'
# Utilities
gem "rqrcode", "~> 2.0"
# HTTP requests
gem 'faraday'
# Background/scheduled jobs
gem 'sidekiq'
gem 'sidekiq-scheduler'
group :development, :test do
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4'

View File

@@ -1,83 +1,91 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (6.0.3.4)
actionpack (= 6.0.3.4)
actioncable (6.0.4.1)
actionpack (= 6.0.4.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.3.4)
actionpack (= 6.0.3.4)
activejob (= 6.0.3.4)
activerecord (= 6.0.3.4)
activestorage (= 6.0.3.4)
activesupport (= 6.0.3.4)
actionmailbox (6.0.4.1)
actionpack (= 6.0.4.1)
activejob (= 6.0.4.1)
activerecord (= 6.0.4.1)
activestorage (= 6.0.4.1)
activesupport (= 6.0.4.1)
mail (>= 2.7.1)
actionmailer (6.0.3.4)
actionpack (= 6.0.3.4)
actionview (= 6.0.3.4)
activejob (= 6.0.3.4)
actionmailer (6.0.4.1)
actionpack (= 6.0.4.1)
actionview (= 6.0.4.1)
activejob (= 6.0.4.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.3.4)
actionview (= 6.0.3.4)
activesupport (= 6.0.3.4)
actionpack (6.0.4.1)
actionview (= 6.0.4.1)
activesupport (= 6.0.4.1)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.3.4)
actionpack (= 6.0.3.4)
activerecord (= 6.0.3.4)
activestorage (= 6.0.3.4)
activesupport (= 6.0.3.4)
actiontext (6.0.4.1)
actionpack (= 6.0.4.1)
activerecord (= 6.0.4.1)
activestorage (= 6.0.4.1)
activesupport (= 6.0.4.1)
nokogiri (>= 1.8.5)
actionview (6.0.3.4)
activesupport (= 6.0.3.4)
actionview (6.0.4.1)
activesupport (= 6.0.4.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.3.4)
activesupport (= 6.0.3.4)
activejob (6.0.4.1)
activesupport (= 6.0.4.1)
globalid (>= 0.3.6)
activemodel (6.0.3.4)
activesupport (= 6.0.3.4)
activerecord (6.0.3.4)
activemodel (= 6.0.3.4)
activesupport (= 6.0.3.4)
activestorage (6.0.3.4)
actionpack (= 6.0.3.4)
activejob (= 6.0.3.4)
activerecord (= 6.0.3.4)
marcel (~> 0.3.1)
activesupport (6.0.3.4)
activemodel (6.0.4.1)
activesupport (= 6.0.4.1)
activerecord (6.0.4.1)
activemodel (= 6.0.4.1)
activesupport (= 6.0.4.1)
activestorage (6.0.4.1)
actionpack (= 6.0.4.1)
activejob (= 6.0.4.1)
activerecord (= 6.0.4.1)
marcel (~> 1.0.0)
activesupport (6.0.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.7.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
bcrypt (3.1.16)
bindex (0.8.1)
bootsnap (1.5.0)
bootsnap (1.9.1)
msgpack (~> 1.0)
builder (3.2.4)
byebug (11.1.3)
capybara (3.33.0)
capybara (3.36.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (~> 1.5)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
concurrent-ruby (1.1.7)
crack (0.4.3)
safe_yaml (~> 1.0.0)
chunky_png (1.4.0)
concurrent-ruby (1.1.9)
connection_pool (2.2.5)
crack (0.4.5)
rexml
crass (1.0.6)
database_cleaner (1.8.5)
devise (4.7.3)
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.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -87,117 +95,149 @@ GEM
devise (>= 3.4.1)
net-ldap (>= 0.16.0)
diff-lcs (1.4.4)
dotenv (2.7.2)
dotenv-rails (2.7.2)
dotenv (= 2.7.2)
railties (>= 3.2, < 6.1)
erubi (1.9.0)
factory_bot (6.1.0)
dotenv (2.7.6)
dotenv-rails (2.7.6)
dotenv (= 2.7.6)
railties (>= 3.2)
e2mmap (0.1.0)
erubi (1.10.0)
et-orbi (1.2.6)
tzinfo
factory_bot (6.2.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.1.0)
factory_bot (~> 6.1.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faraday (0.17.0)
faraday (1.8.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
multipart-post (>= 1.2, < 3)
ffi (1.13.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
hashdiff (0.4.0)
i18n (1.8.5)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
ffi (1.15.4)
fugit (1.5.2)
et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.4)
globalid (0.5.2)
activesupport (>= 5.0)
hashdiff (1.0.1)
i18n (1.8.11)
concurrent-ruby (~> 1.0)
jbuilder (2.10.1)
jbuilder (2.11.3)
activesupport (>= 5.0.0)
launchy (2.4.3)
addressable (~> 2.3)
launchy (2.5.0)
addressable (~> 2.7)
letter_opener (1.7.0)
launchy (~> 2.2)
letter_opener_web (1.3.4)
letter_opener_web (1.4.1)
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
listen (3.2.1)
listen (3.7.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.7.0)
lockbox (0.6.6)
loofah (2.12.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
marcel (1.0.2)
matrix (0.4.2)
method_source (1.0.0)
mimemagic (0.3.5)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.2)
msgpack (1.3.3)
mini_mime (1.1.2)
minitest (5.14.4)
msgpack (1.4.2)
multipart-post (2.1.1)
net-ldap (0.16.3)
nio4r (2.5.4)
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
net-ldap (0.17.0)
nio4r (2.5.8)
nokogiri (1.12.5-x86_64-linux)
racc (~> 1.4)
orm_adapter (0.5.0)
pg (1.2.3)
public_suffix (4.0.6)
puma (4.3.6)
puma (4.3.10)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.6.0)
rack (2.2.3)
rack-proxy (0.6.5)
rack-proxy (0.7.0)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.3.4)
actioncable (= 6.0.3.4)
actionmailbox (= 6.0.3.4)
actionmailer (= 6.0.3.4)
actionpack (= 6.0.3.4)
actiontext (= 6.0.3.4)
actionview (= 6.0.3.4)
activejob (= 6.0.3.4)
activemodel (= 6.0.3.4)
activerecord (= 6.0.3.4)
activestorage (= 6.0.3.4)
activesupport (= 6.0.3.4)
rails (6.0.4.1)
actioncable (= 6.0.4.1)
actionmailbox (= 6.0.4.1)
actionmailer (= 6.0.4.1)
actionpack (= 6.0.4.1)
actiontext (= 6.0.4.1)
actionview (= 6.0.4.1)
activejob (= 6.0.4.1)
activemodel (= 6.0.4.1)
activerecord (= 6.0.4.1)
activestorage (= 6.0.4.1)
activesupport (= 6.0.4.1)
bundler (>= 1.3.0)
railties (= 6.0.3.4)
railties (= 6.0.4.1)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
rails-html-sanitizer (1.4.2)
loofah (~> 2.3)
railties (6.0.3.4)
actionpack (= 6.0.3.4)
activesupport (= 6.0.3.4)
railties (6.0.4.1)
actionpack (= 6.0.4.1)
activesupport (= 6.0.4.1)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rake (13.0.1)
rb-fsevent (0.10.4)
rake (13.0.6)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
regexp_parser (1.8.2)
redis (4.5.1)
regexp_parser (2.1.1)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rspec-core (3.10.0)
rexml (3.2.5)
rqrcode (2.1.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec-core (3.10.1)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.0)
rspec-expectations (3.10.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-mocks (3.10.0)
rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-rails (4.0.1)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.9)
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.10.0)
safe_yaml (1.0.5)
rspec-rails (5.0.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
railties (>= 5.2)
rspec-core (~> 3.10)
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.10.3)
ruby2_keywords (0.0.5)
rufus-scheduler (3.8.0)
fugit (~> 1.1, >= 1.1.6)
sass-rails (6.0.0)
sassc-rails (~> 2.1, >= 2.1.1)
sassc (2.4.0)
@@ -208,6 +248,17 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
sidekiq (6.3.1)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)
sidekiq-scheduler (3.1.0)
e2mmap
redis (>= 3, < 5)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
thwait
tilt (>= 1.4.0)
spring (2.1.1)
spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0)
@@ -215,43 +266,46 @@ GEM
sprockets (4.0.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.2)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets-rails (3.4.0)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.4.2)
thor (1.0.1)
thor (1.1.0)
thread_safe (0.3.6)
thwait (0.2.0)
e2mmap
tilt (2.0.10)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
tzinfo (1.2.7)
tzinfo (1.2.9)
thread_safe (~> 0.1)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.1.0)
web-console (4.2.0)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.6.0)
addressable (>= 2.3.6)
webmock (3.14.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpacker (4.3.0)
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
websocket-driver (0.7.3)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.4.1)
zeitwerk (2.5.1)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
bootsnap (>= 1.4.2)
@@ -267,12 +321,16 @@ DEPENDENCIES
letter_opener
letter_opener_web
listen (~> 3.2)
lockbox
net-ldap
pg (~> 1.2.3)
puma (~> 4.1)
rails (~> 6.0.3, >= 6.0.3.4)
rqrcode (~> 2.0)
rspec-rails
sass-rails (>= 6)
sidekiq
sidekiq-scheduler
spring
spring-watcher-listen (~> 2.0.0)
sqlite3 (~> 1.4)
@@ -284,4 +342,4 @@ DEPENDENCIES
webpacker (~> 4.0)
BUNDLED WITH
2.0.2
2.2.2

View File

@@ -11,8 +11,9 @@ credentials, invites, donations, etc..
* [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
* [x] List my donations
* [x] Invite new users from your account
* [x] See Lightning wallet balance and setup code for Blue Wallet
* [ ] Sign up for a new account by donating upfront
* [ ] Sign up for a new account via proving contributions (via cryptographic signature)
* [ ] ...
@@ -38,6 +39,10 @@ Running the dev server:
bundle exec rails server
Running the background workers (requires Redis):
bundle exec sidekiq -C config/sidekiq.yml
Running all specs:
bundle exec rspec
@@ -62,6 +67,11 @@ manual LDIF imports etc. (or provide a staging instance)
* [devise_ldap_authenticatable](https://github.com/cschiewek/devise_ldap_authenticatable)
* [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
[GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/)

View File

@@ -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;
}

View File

@@ -0,0 +1,13 @@
$content-width: 800px;
$content-max-width: 100%;
$text-color-body: #222;
$text-color-discreet: #888;
$background-color-notice: #efffc4;
$background-color-alert: #fff4c2;
$color-blue: #0d4f99;
$color-purple: #8955a0;
$color-red-bright: #c00;
$color-red-dark: #990c0e;

View File

@@ -0,0 +1,25 @@
@import "variables";
body#admin-panel {
#wrapper {
> header {
background: $color-red-bright;
background: linear-gradient(35deg, rgba(255,0,255,0.2) 0, rgba(153,12,14,0.9) 100%),
url('/img/bg-1.jpg');
}
}
#main-nav {
ul {
grid-template-columns: repeat(4, 1fr);
li {
a {
&.active {
border-bottom: 2px solid $color-red-bright;
}
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
ul.donations {
list-style: none;
li {
margin-bottom: 2rem;
display: grid;
grid-row-gap: 0.5rem;
grid-column-gap: 2rem;
grid-template-columns: 1fr 1fr;
grid-template-areas:
"date amount-btc"
"public-name amounts-fiat";
h3 {
grid-area: "date";
margin-bottom: 0;
}
p {
margin-bottom: 0;
&.amount-btc {
grid-area: amount-btc;
text-align: right;
font-family: monospace;
font-size: 1.25rem;
}
&.amounts-fiat {
grid-area: amounts-fiat;
text-align: right;
font-family: monospace;
font-size: 0.85rem;
color: #888;
}
&.public-name {
grid-area: public-name;
}
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -1,11 +1,6 @@
@import "variables";
@import "mediaqueries";
$content-width: 800px;
$content-max-width: 100%;
body {
}
#wrapper {
width: 100%;
text-align: center;
@@ -14,50 +9,39 @@ body {
margin: 0 auto;
padding: 4rem 0;
text-align: center;
background: #0d4f99;
background: linear-gradient(35deg, #8955a0 0, #0d4f99 100%);
background: linear-gradient(35deg, rgba(255,0,255,0.2) 0, rgba(13,79,153,0.8) 100%),
url('/img/bg-1.jpg');
background-size: cover;
@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.icon {
svg {
display: inline-block;
height: 1.875rem;
vertical-align: top;
width: auto;
}
span.bolt {
color: #ffd000;
margin-right: 0.5rem;
}
}
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 {
@@ -77,18 +61,18 @@ body {
padding: 2rem 0;
&.notice {
background: #efffc4;
background: $background-color-notice;
}
&.alert {
background: #fff4c2;
background: $background-color-alert;
}
}
main {
width: $content-width;
max-width: $content-max-width;
margin: 4rem auto;
margin: 4rem auto 6rem auto;
text-align: left;
@include media-max(medium) {
@@ -99,18 +83,6 @@ main {
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;
@@ -128,19 +100,32 @@ main {
}
}
th, td {
line-height: 1.5rem;
padding-right: 1rem;
section {
margin-bottom: 3rem;
}
section {
border-bottom: 1px dotted #ccc;
padding-bottom: 4rem;
margin-bottom: 4rem;
table {
width: 100%;
@include media-max(small) {
padding-bottom: 3rem;
margin-bottom: 3rem;
th, td {
&.hide-small {
@include media-max(small) {
display: none;
}
}
}
th {
color: $text-color-discreet;
font-weight: normal;
text-transform: uppercase;
font-size: 0.85rem;
padding-bottom: 0.825rem;
}
td {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
}
}
@@ -156,18 +141,5 @@ main {
@include media-max(small) {
grid-template-columns: 1fr;
}
margin-top: 3rem;
h3 {
margin-bottom: 1rem;
}
.grid-item {
p {
color: #888;
font-size: 0.85rem;
}
}
}
}

View File

@@ -0,0 +1,55 @@
@import "variables";
@import "mediaqueries";
#main-nav {
width: 100%;
text-align: center;
background-color: #efefef;
.wrapper {
width: $content-width;
max-width: $content-max-width;
margin: 0 auto;
}
ul {
@include media-max(large) {
display: grid;
grid-template-columns: repeat(1fr);
grid-template-columns: 1fr 1fr 1fr 1fr;
}
li {
@include media-min(large) {
display: inline;
}
@include media-max(large) {
display: block;
}
a {
display: inline-block;
padding: 1.5rem 2rem;
text-decoration: none;
color: $text-color-discreet;
@include media-max(large) {
display: block;
text-align: center;
padding-left: 0;
padding-right: 0;
}
@include media-max(small) {
font-size: 0.85rem;
}
&.active {
color: $text-color-body;
border-bottom: 2px solid #4ea2df;
}
}
}
}
}

View File

@@ -3,4 +3,6 @@ class Admin::BaseController < ApplicationController
before_action :authenticate_user!
before_action :authorize_admin
layout "admin"
end

View File

@@ -1,4 +1,5 @@
class Admin::DashboardController < Admin::BaseController
def index
@current_section = :dashboard
end
end

View 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

View 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

View File

@@ -1,4 +1,6 @@
class Admin::LdapUsersController < Admin::BaseController
before_action :set_current_section
def index
attributes = %w{dn cn uid mail admin}
filter = Net::LDAP::Filter.eq("uid", "*")
@@ -38,4 +40,8 @@ class Admin::LdapUsersController < Admin::BaseController
def ldap_config
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
end
def set_current_section
@current_section = :ldap_users
end
end

View File

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

View File

@@ -0,0 +1,10 @@
class 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

View File

@@ -8,6 +8,7 @@ class InvitationsController < ApplicationController
def index
@invitations_unused = current_user.invitations.unused
@invitations_used = current_user.invitations.used
@current_section = :invitations
end
# GET /invitations/a-random-invitation-token

View File

@@ -0,0 +1,57 @@
class LnurlpayController < ApplicationController
before_action :find_user_by_address
def index
render json: {
status: "OK",
callback: "https://accounts.kosmos.org/lnurlpay/#{@user.address}/invoice",
tag: "payRequest",
maxSendable: 1000000,
minSendable: 1000,
metadata: metadata(@user.address),
commentAllowed: 0
}
end
def invoice
amount = params[:amount].to_i # msats
address = params[:address]
validate_amount(amount)
payment_request = @user.ln_create_invoice({
amount: amount / 1000, # we create invoices in sats
description_hash: Digest::SHA2.hexdigest(metadata(address))
})
render json: {
status: "OK",
successAction: {
tag: "message",
message: "Payment received. Thanks!"
},
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\", \"Sats for #{address}\"]]"
end
def validate_amount(amount)
if amount > 1000000 || amount < 1000
render json: { status: "ERROR", reason: "Invalid amount" }
return
end
end
end

View File

@@ -0,0 +1,7 @@
class SecurityController < ApplicationController
before_action :require_user_signed_in
def index
@current_section = :security
end
end

View File

@@ -0,0 +1,44 @@
require "rqrcode"
class WalletController < ApplicationController
before_action :require_user_signed_in
before_action :authenticate_with_lndhub
def index
@current_section = :wallet
@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
)
@balance = fetch_balance rescue nil
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 fetch_balance
lndhub = Lndhub.new
data = lndhub.balance @ln_auth_token
data["BTC"]["AvailableBalance"]
end
end

View File

@@ -1,2 +1,5 @@
module ApplicationHelper
def sats_to_btc(sats)
sats.to_f / 100000000
end
end

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ require("@rails/ujs").start()
require("turbolinks").start()
require("channels")
import "stylesheets/application"
// 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.

View File

@@ -0,0 +1,8 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "base";
@import "buttons";
@import "forms";
@import "links";

View File

@@ -0,0 +1,21 @@
@layer base {
body {
line-height: 1;
}
h1, h2, h3 {
@apply font-light;
}
h1 {
@apply text-3xl uppercase;
}
h2 {
@apply text-2xl mb-8;
}
h3 {
@apply text-xl mb-6;
}
}

View File

@@ -0,0 +1,31 @@
@layer components {
.btn {
@apply font-semibold rounded-md leading-none cursor-pointer
transition-colors duration-75 focus:outline-none focus:ring-4;
}
.btn-md {
@apply btn;
@apply py-2.5 px-5 shadow-md;
}
.btn-sm {
@apply btn;
@apply py-1 px-2 text-sm shadow-sm;
}
.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;
}
}

View File

@@ -0,0 +1,17 @@
@layer components {
form {
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-gray-500 focus:ring-0;
}
.field_with_errors {
@apply inline-block;
}
.error-msg {
@apply text-red-700;
}
}
}

View 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;
}
}
}

View File

@@ -0,0 +1,26 @@
const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {
purge: {
layers: ['base', 'components', 'utilities'],
content: [
"./app/**/*.html.erb",
"./app/helpers/**/*.rb",
"./app/javascript/**/*.js"
]
},
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
fontFamily: {
sans: ['Open Sans', 'sans-serif']
}
},
},
variants: {
extend: {},
},
plugins: [
require('@tailwindcss/forms')
],
}

View 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

View File

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

View File

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

13
app/models/donation.rb Normal file
View 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

View File

@@ -3,12 +3,16 @@ class User < ApplicationRecord
# Relations
has_many :invitations, dependent: :destroy
has_many :donations, dependent: :nullify
validates_uniqueness_of :cn
validates_length_of :cn, :minimum => 3
validates_uniqueness_of :email
validates :email, email: true
encrypts :ln_login
encrypts :ln_password
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :ldap_authenticatable,
@@ -52,4 +56,10 @@ class User < ApplicationRecord
self.valid?
self.errors[attribute_name].blank?
end
def ln_create_invoice(payload)
lndhub = Lndhub.new
lndhub.authenticate self
lndhub.addinvoice payload
end
end

View File

@@ -10,6 +10,7 @@ class CreateAccount < ApplicationService
def call
user = create_user_in_database
add_ldap_document
create_lndhub_wallet
if @invitation.present?
update_invitation(user.id)
@@ -33,51 +34,19 @@ class CreateAccount < ApplicationService
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
end
# TODO move to confirmation
def add_ldap_document
dn = "cn=#{@username},ou=kosmos.org,cn=users,dc=kosmos,dc=org"
attr = {
objectclass: ["top", "account", "person", "extensibleObject"],
cn: @username,
sn: @username,
uid: @username,
mail: @email,
userPassword: Devise.ldap_auth_password_builder.call(@password)
}
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]
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
ejabberd = EjabberdApiClient.new
inviter = @invitation.user
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"
})
def create_lndhub_wallet
CreateLndhubWalletJob.perform_later(user)
end
end

57
app/services/lndhub.rb Normal file
View File

@@ -0,0 +1,57 @@
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 addinvoice(payload)
invoice = post "addinvoice", {
amt: payload[:amount],
description_hash: payload[:description_hash]
}
invoice["payment_request"]
end
end

View File

@@ -1,7 +1,3 @@
<h2>Admin Panel</h2>
<p>
Ohai there, admin human.
</p>
<p>
<%= link_to 'LDAP users', admin_ldap_users_path %>
<p class="text-center">
With great power comes great responsibility.
</p>

View 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)

View 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 %>

View File

@@ -0,0 +1,8 @@
<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>

View File

@@ -0,0 +1,41 @@
<h2>Donations</h2>
<% if @donations.any? %>
<table>
<thead>
<tr>
<th>User</th>
<th>Amount BTC</th>
<th>in EUR</th>
<th>in USD</th>
<th>Public name</th>
<th>Date</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @donations.each do |donation| %>
<tr>
<td><%= donation.user.cn %></td>
<td><%= sats_to_btc donation.amount_sats %> BTC</td>
<td><%= number_to_currency donation.amount_eur / 100, unit: "" %></td>
<td><%= number_to_currency donation.amount_usd / 100, unit: "" %></td>
<td><%= 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', method: :delete, data: { 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>

View File

@@ -0,0 +1 @@
json.array! @donations, partial: "donations/donation", as: :donation

View File

@@ -0,0 +1,7 @@
<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>

View File

@@ -0,0 +1,36 @@
<p id="notice"><%= notice %></p>
<p>
<strong>User:</strong>
<%= @donation.user_id %>
</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>

View File

@@ -0,0 +1 @@
json.partial! "donations/donation", donation: @donation

View File

@@ -0,0 +1,32 @@
<section>
<h2>Invitations</h2>
<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>Invited user</th>
</tr>
</thead>
<tbody>
<% @invitations_used.each do |invitation| %>
<tr>
<td class="overflow-ellipsis"><%= invitation.token %></td>
<td><%= invitation.used_at.strftime("%Y-%m-%d") %></td>
<td><%= User.find(invitation.invited_user_id).address %></td>
</tr>
<% end %>
</tbody>
</table>
</section>
<% end %>

View File

@@ -4,45 +4,46 @@
Your Kosmos account and password currently give you access to these
services:
</p>
<div class="grid services">
<div class="grid-item chat">
<h3><%= link_to "Chat", "https://wiki.kosmos.org/Services:Chat" %></h3>
<p>
<div class="grid services mt-12">
<div>
<h3 class="mb-3.5">
<%= link_to "Chat", "https://wiki.kosmos.org/Services:Chat", class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
Chat rooms and instant messaging (XMPP/Jabber)
</p>
</div>
<div class="grid-item wiki">
<h3><%= link_to "Wiki", "https://wiki.kosmos.org" %></h3>
<p>
<div>
<h3 class="mb-3.5">
<%= link_to "Wiki", "https://wiki.kosmos.org", class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
Kosmos documentation and knowledge base
</p>
</div>
<div class="grid-item discourse">
<h3><%= link_to "Discourse", "https://community.kosmos.org" %></h3>
<p>
<div>
<h3 class="mb-3.5">
<%= link_to "Discourse", "https://community.kosmos.org", class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
Kosmos community forums and user support/help site
</p>
</div>
<div class="grid-item gitea">
<h3><%= link_to "Gitea", "https://gitea.kosmos.org" %></h3>
<p>
<div>
<h3 class="mb-3.5">
<%= link_to "Gitea", "https://gitea.kosmos.org", class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
Code hosting and collaboration for software projects
</p>
</div>
<div class="grid-item gitea">
<h3><%= link_to "Drone CI", "https://drone.kosmos.org" %></h3>
<p>
Continuous integration for software projects, tied to our Gitea
<div>
<h3 class="mb-3.5">
<%= link_to "Drone CI", "https://drone.kosmos.org", class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
Continuous integration for software projects on Gitea
</p>
</div>
</div>
</section>
<section>
<h3>Password change</h3>
<p>
<%= form_with(url: settings_reset_password_path, method: :post) do %>
<%= submit_tag("Send me a password reset link") %>
<% end %>
</p>
</section>

View File

@@ -2,19 +2,13 @@
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<p>
<%= f.label :email, 'Email address' %><br />
<%= f.email_field :email, required: true, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
</p>
</div>
<div class="actions">
<p>
<%= f.submit "Resend confirmation instructions" %>
</p>
</div>
<p>
<%= f.label :email, 'Email address', class: 'block mb-1' %>
<%= f.email_field :email, required: true, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
</p>
<p class="mt-8">
<%= f.submit "Resend confirmation instructions", class: 'btn-md btn-blue' %>
</p>
<% end %>
<%= render "devise/shared/links" %>

View File

@@ -4,22 +4,24 @@
<%= render "devise/shared/error_messages", resource: resource %>
<%= f.hidden_field :reset_password_token %>
<div class="field">
<%= f.label :password, "New password" %><br />
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em><br />
<% end %>
<p class="mb-1">
<%= f.label :password, "New password" %>
</p>
<p>
<%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
</div>
<div class="field">
<%= f.label :password_confirmation, "Confirm new password" %><br />
<% if @minimum_password_length %>
<br><em class="text-sm text-gray-500">(<%= @minimum_password_length %> characters minimum)</em>
<% end %>
</p>
<p class="mb-1">
<%= f.label :password_confirmation, "Confirm new password" %>
</p>
<p>
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>
<div class="actions">
<%= f.submit "Change my password" %>
</div>
</p>
<p class="mt-8">
<%= f.submit "Change my password", class: 'btn-md btn-blue' %>
</p>
<% end %>
<%= render "devise/shared/links" %>

View File

@@ -2,26 +2,17 @@
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<p>
<%= f.label :cn, 'User' %><br />
<%= f.text_field :cn, autofocus: true, autocomplete: "username", required: true %> @ kosmos.org
</p>
</div>
<div class="field">
<p>
<%= f.label :email, 'Email address' %><br />
<%= f.email_field :email, autocomplete: "email", required: true %>
</p>
</div>
<div class="actions">
<p>
<%= f.submit "Send me reset password instructions" %>
</p>
</div>
<p>
<%= f.label :cn, 'User', class: 'block' %>
<%= f.text_field :cn, autofocus: true, autocomplete: "username", required: true %> @ kosmos.org
</p>
<p>
<%= f.label :email, 'Email address', class: 'block' %>
<%= f.email_field :email, autocomplete: "email", required: true %>
</p>
<p class="mt-8">
<%= f.submit "Send me reset password instructions", class: 'btn-md btn-blue' %>
</p>
<% end %>
<%= render "devise/shared/links" %>

View File

@@ -31,9 +31,9 @@
<%= f.password_field :current_password, autocomplete: "current-password" %>
</div>
<div class="actions">
<%= f.submit "Update" %>
</div>
<p class="mt-8">
<%= f.submit "Update", class: 'btn-md btn-blue' %>
</p>
<% end %>
<h3>Cancel my account</h3>

View File

@@ -2,24 +2,17 @@
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<p>
<%= f.label :cn, 'User' %><br />
<%= f.text_field :cn, autofocus: true, autocomplete: "username" %> @ kosmos.org
</p>
</div>
<div class="field">
<p>
<%= f.label :password %><br />
<%= f.password_field :password, autocomplete: "current-password" %>
</p>
</div>
<div class="actions">
<p>
<%= f.submit "Log in" %>
</p>
</div>
<p>
<%= f.label :cn, 'User', class: 'block' %>
<%= f.text_field :cn, autofocus: true, autocomplete: "username" %> @ kosmos.org
</p>
<p>
<%= f.label :password, class: 'block' %>
<%= f.password_field :password, autocomplete: "current-password" %>
</p>
<p class="mt-8">
<%= f.submit "Log in", class: 'btn-md btn-blue' %>
</p>
<% end %>
<%= render "devise/shared/links" %>

View File

@@ -1,24 +1,24 @@
<div class="devise-links">
<div class="devise-links mt-8 text-sm">
<%- if controller_name != 'sessions' %>
<p>
<p class="mb-1.5">
<%= link_to "Log in", new_session_path(resource_name) %><br />
</p>
<% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<p>
<p class="mb-1.5">
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
</p>
<% end %>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<p>
<p class="mb-1.5">
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
</p>
<% end %>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<p>
<p class="mb-1.5">
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
</p>
<% end %>

View File

@@ -2,15 +2,13 @@
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<p>
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>
<div class="actions">
<%= f.submit "Resend unlock instructions" %>
</div>
</p>
<p class="mt-8">
<%= f.submit "Resend unlock instructions", class: 'btn-md btn-blue' %>
</p>
<% end %>
<%= render "devise/shared/links" %>

View File

@@ -0,0 +1,38 @@
<section>
<h2>Donations</h2>
<p>
Your financial contributions to the development and
upkeep of Kosmos software and services.
</p>
</section>
<section>
<% if @donations.any? %>
<ul class="donations">
<% @donations.each do |donation| %>
<li>
<h3>
<%= donation.paid_at.strftime("%B %d, %Y") %>
</h3>
<p class="amount-btc">
<%= sats_to_btc donation.amount_sats %> BTC
</p>
<p class="amounts-fiat">
(~ <%= number_to_currency donation.amount_eur / 100, unit: "" %> EUR)
</p>
<p class="public-name">
<% if donation.public_name.present? %>
Public name: <%= donation.public_name %>
<% else %>
Anonymous
<% end %>
</p>
</li>
<% end %>
</ul>
<% else %>
<p>
No donations to show.
</p>
<% end %>
</section>

View File

@@ -0,0 +1 @@
json.array! @donations, partial: "donations/donation", as: :donation

View File

@@ -24,23 +24,25 @@
</section>
<% if @invitations_used.any? %>
<h3>Accepted Invitations</h3>
<table>
<thead>
<tr>
<th>URL</th>
<th>Used at</th>
<th>Invited user</th>
</tr>
</thead>
<tbody>
<% @invitations_used.each do |invitation| %>
<section>
<h3>Accepted Invitations</h3>
<table>
<thead>
<tr>
<td><%= invitation_url(invitation.token) %></td>
<td><%= invitation.used_at %></td>
<td><%= User.find(invitation.invited_user_id).address %></td>
<th class="hide-small">ID</th>
<th>Accepted</th>
<th>Invited user</th>
</tr>
<% end %>
</tbody>
</table>
</thead>
<tbody>
<% @invitations_used.each do |invitation| %>
<tr>
<td class="hide-small"><%= invitation.token %></td>
<td><%= invitation.used_at.strftime("%Y-%m-%d") %></td>
<td><%= User.find(invitation.invited_user_id).address %></td>
</tr>
<% end %>
</tbody>
</table>
</section>
<% end %>

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<title>Admin Panel | Kosmos Accounts</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://assets.kosmos.org/fonts/open-sans/open-sans.css" rel="stylesheet">
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body id="admin-panel">
<div id="wrapper">
<header>
<h1>
<span class ="icon"><%= render partial: "shared/icons/comet" %></span>
<span class ="project-name">Kosmos</span>
<span class ="site-name">Accounts</span>
</h1>
<%= render partial: 'shared/header_account' %>
</header>
<% if user_signed_in? && current_user.confirmed? %>
<%= render partial: 'shared/admin_nav' %>
<% end %>
<% flash.each do |type, msg| %>
<div class="flash-msg <%= type %>">
<p><%= msg %></p>
</div>
<% end %>
<main>
<%= yield %>
</main>
</div>
</body>
</html>

View File

@@ -4,29 +4,27 @@
<title>Kosmos Accounts</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://assets.kosmos.org/fonts/open-sans/open-sans.css" rel="stylesheet">
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<div id="wrapper">
<header>
<h1>
<span class ="icon"><%= render partial: "shared/icons/comet" %></span>
<span class ="project-name">Kosmos</span>
<span class ="site-name">Akkounts</span>
<span class="beta"><span class="bolt">⚡</span> beta</span>
<span class ="site-name">Account</span>
</h1>
<% if user_signed_in? %>
<p class="current-user">
Signed in as <strong><%= current_user.cn %>@kosmos.org</strong>.
<%= link_to "Log out", destroy_user_session_path, method: :delete %>
</p>
<% end %>
<%= render partial: 'shared/header_account' %>
</header>
<% if user_signed_in? && current_user.confirmed? %>
<%= render partial: 'shared/main_nav' %>
<% end %>
<% flash.each do |type, msg| %>
<div class="flash-msg <%= type %>">
<p><%= msg %></p>

View File

@@ -4,20 +4,19 @@
<title>Sign up | Kosmos Accounts</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://assets.kosmos.org/fonts/open-sans/open-sans.css" rel="stylesheet">
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body class="layout-signup">
<div id="wrapper">
<header>
<h1>
<span class ="icon"><%= render partial: "shared/icons/comet" %></span>
<span class ="project-name">Kosmos</span>
<span class ="site-name">Sign Up</span>
<!-- <span class="beta"><span class="bolt">⚡</span> beta</span> -->
</h1>
<% if user_signed_in? %>
<p class="current-user">

View File

@@ -0,0 +1,12 @@
<section>
<h2>Security</h2>
</section>
<section>
<h3>Password change</h3>
<p>
<%= form_with(url: settings_reset_password_path, method: :post) do %>
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray') %>
<% end %>
</p>
</section>

View File

@@ -0,0 +1,22 @@
<nav id="main-nav">
<div class="wrapper">
<ul class="pages">
<li>
<%= link_to "Dashboard", admin_root_path,
class: @current_section == :dashboard ? "active" : nil %>
</li>
<li>
<%= link_to "Invitations", admin_invitations_path,
class: @current_section == :invitations ? "active" : nil %>
</li>
<li>
<%= link_to "Donations", admin_donations_path,
class: @current_section == :donations ? "active" : nil %>
</li>
<li>
<%= link_to "LDAP Users", admin_ldap_users_path,
class: @current_section == :ldap_users ? "active" : nil %>
</li>
</ul>
</div>
</nav>

View File

@@ -0,0 +1,6 @@
<% if user_signed_in? %>
<p class="current-user mt-8">
Signed in as <strong class="text-white font-normal"><%= current_user.cn %>@kosmos.org</strong>.
<%= link_to "Log out", destroy_user_session_path, method: :delete, class: 'underline' %>
</p>
<% end %>

View File

@@ -0,0 +1,26 @@
<nav id="main-nav">
<div class="wrapper">
<ul class="pages">
<li>
<%= link_to "Services", root_path,
class: @current_section == :dashboard ? "active" : nil %>
</li>
<li>
<%= link_to "Invitations", invitations_path,
class: @current_section == :invitations ? "active" : nil %>
</li>
<li>
<%= link_to "Donations", donations_path,
class: @current_section == :contributions ? "active" : nil %>
</li>
<li>
<%= link_to "Wallet", wallet_path,
class: @current_section == :wallet ? "active" : nil %>
</li>
<li>
<%= link_to "Security", security_path,
class: @current_section == :security ? "active" : nil %>
</li>
</ul>
</div>
</nav>

View File

@@ -0,0 +1 @@
<svg id="icon-comet" width="65.364" height="55.773" enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 65.364 55.773" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g id="layer1" transform="translate(28.868 20.259)" fill="#fff"><path id="path2" d="m22.81-9.2546-0.0137-0.0072c-0.0445-0.0196-0.0895-0.04052-0.13335-0.06078l-23.822-10.937s2.0034 9.219 2.914 11.778c0 0-27.292-8.1582-30.623-8.9354 1.0916 4.2618 20.006 40.848 20.006 40.848 3.8225 7.7608 12.677 12.083 21.912 12.083 12.949 0 23.446-10.497 23.446-23.446 6.6e-4 -9.4655-5.609-17.62-13.685-21.323z" fill="#fff" stroke-width=".65365"/></g></svg>

After

Width:  |  Height:  |  Size: 627 B

View File

@@ -7,6 +7,6 @@
This invitation can only be used once, and sign-up is currently only possible
by invitation. Seems like you have good friends!
</p>
<p>
<%= link_to "Get started", signup_steps_path(1), class: "next-step" %>
<p class="mt-12">
<%= link_to "Get started", signup_steps_path(1), class: "btn btn-md btn-blue" %>
</p>

View File

@@ -2,60 +2,53 @@
<% when 1 %>
<h2>Choose a username</h2>
<%= form_for @user, :url => signup_validate_url do |f| %>
<div class="field">
<p>
<%= f.label :cn, 'Username' %><br />
<%= f.text_field :cn, autofocus: true, autocomplete: "username" %>
<span class="at-sign">@</span>
<span class="domain">kosmos.org</span>
</p>
<% if @validation_error.present? %>
<p class="error-msg">Username <%= @validation_error %></p>
<% end %>
</div>
<div class="actions">
<p><%= f.submit "Continue" %></p>
</div>
<p>
<%= f.label :cn, 'Username', class: 'hidden' %>
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
class: 'text-xl' %>
<span class="text-xl ml-1">@</span>
<span class="text-xl">kosmos.org</span>
</p>
<% if @validation_error.present? %>
<p class="error-msg">Username <%= @validation_error %></p>
<% end %>
<p class="mt-12">
<%= f.submit "Continue", class: 'btn btn-md btn-blue' %>
</p>
<% end %>
<% when 2 %>
<h2>What's your email?</h2>
<%= form_for @user, :url => signup_validate_url do |f| %>
<div class="field">
<p>
<%= f.label :email, 'Email address' %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</p>
<% if @validation_error.present? %>
<p class="error-msg">Email <%= @validation_error %></p>
<% end %>
</div>
<div class="actions">
<p><%= f.submit "Continue" %></p>
</div>
<p>
<%= f.label :email, 'Email address', class: 'hidden' %>
<%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'text-xl' %>
</p>
<% if @validation_error.present? %>
<p class="error-msg">Email <%= @validation_error %></p>
<% end %>
<p class="mt-12">
<%= f.submit "Continue", class: 'btn btn-md btn-blue' %>
</p>
<% end %>
<% when 3 %>
<h2>Choose a password</h2>
<%= form_for @user, :url => signup_validate_url do |f| %>
<div class="field">
<p>
<%= f.label :password, 'Password' %><br />
<%= f.password_field :password, autofocus: true %>
</p>
<% if @validation_error.present? %>
<p class="error-msg">Password <%= @validation_error %></p>
<% end %>
</div>
<p class="accept-terms">
<small>
By clicking the button below, you accept our future Terms of Service
and Privacy Policy. Don't worry, they will be excellent!
</small>
<p>
<%= f.label :password, 'Password', class: 'hidden' %>
<%= f.password_field :password, autofocus: true, class: 'text-xl' %>
</p>
<% if @validation_error.present? %>
<p class="error-msg">Password <%= @validation_error %></p>
<% end %>
<p class="mt-8 text-sm text-gray-500">
By clicking the button below, you accept our future Terms of Service
and Privacy Policy. Don't worry, they will be excellent!
</p>
<p class="mt-8">
<%= f.submit "Create account", class: 'btn-md btn-blue' %>
</p>
<div class="actions">
<p><%= f.submit "Create account" %></p>
</div>
<% end %>
<% end %>

View File

@@ -0,0 +1,54 @@
<section class="w-full grid grid-cols-1 md:grid-cols-2">
<div>
<h2>Wallet</h2>
<p>
Send and receive BTC via the Lightning Network.
</p>
</div>
<p class="font-mono mt-4 md:text-right md:mt-0">
<% if @balance %>
<span class="text-xl"><%= @balance %> sats</span><br>
<span class="text-sm text-gray-500">Available balance</span>
<% end %>
</p>
</section>
<section>
<h3>Blue Wallet</h3>
<p>
You can connect
<%= link_to "Blue Wallet", "https://bluewallet.io",
class: "ks-text-link", target: "_blank" %>
(Android or iOS) to your Kosmos lightning wallet. In order to do so,
scan the setup QR code from the Import Wallet screen in the app.
</p>
<p>
<a id="show-setup-code" class="ks-text-link cursor-pointer">Show setup code</a>
<a id="hide-setup-code" class="ks-text-link cursor-pointer" style="display: none;">Hide setup code</a>
</p>
<p id="setup-code" style="display: none;">
<%= raw @svg %>
</p>
</section>
<script type="text/javascript">
let linkShow = document.querySelector('#show-setup-code');
let linkHide = document.querySelector('#hide-setup-code');
let setupCode = document.querySelector('#setup-code');
linkShow.addEventListener('click', function(ev) {
ev.preventDefault();
setupCode.style.display = 'block';
linkShow.style.display = 'none';
linkHide.style.display = 'block';
window.scrollTo(0, document.body.scrollHeight);
});
linkHide.addEventListener('click', function(ev) {
ev.preventDefault();
const el = document.querySelector('#setup-code');
setupCode.style.display = 'none';
linkShow.style.display = 'block';
linkHide.style.display = 'none';
});
</script>

View File

@@ -39,5 +39,8 @@ module Akkounts
g.fixture_replacement :factory_bot, suffix_factory: 'factory', dir: 'spec/factories'
g.stylesheets false
end
config.active_job.queue_adapter = :sidekiq
config.action_mailer.deliver_later_queue_name = nil # use "default" queue
end
end

View File

@@ -1 +1 @@
LWyKwPZq9Kd97rn/7+q3MEkh7kITScDMHD3JvVuaV3A4YIHJHU+460k+PaEGlsH1xkbuClGiAb57rk1XLyDnmVGtbSueYOtinkw6kar8ZfKWZob061LwGjpMVRQkS49TjCUZlqCFrXeKxlH03mXWBnqAj9RUIPrm7eibb3c7qmJFglR1380RSVsfZnp8A3QwGm4Wh9OWtpUa6P2lne0jQsOuSe8ur3DUF0LplzS4CbkMxAUDOom+pXB13AlxOH9NQE7F4dsYHugHkh1tG3r3ER3xAUD/9Kn6UZZP7BnwUs3zqhoZdULRpRgA5dK7ueTIAnO/jtJDF4562VS8ECo7AnNoVxNe8/mBMFIOUfqg+db/72N2pIk3r4lK7Uzm/4jJ5/99ItnQjHQPcApiwZXIr3OyDLUvq5+d0UVmAMXdwcAjvctVQXFx5imG149Y0ISHKWVm1ca37aAspxWPU+CIj8/HW0yEpjp3vhwDUbjCaZeAPm8UQC14MxZwSK3N+EUSQXdltiweFynabDB7zGGQsjMM8LwMtyo9bTBzJA78Cl96MDyd20i1zSF9ntLuKulwGm3oZowpbNuvo2anY6r9yBlDJBOEISbtXv2tLX4SqcM=--bRcsE4K/29XzyZat--+G3iQCLBqgSwLaQ+7+4YvA==
PRI1laGy6XiLpsII0ixFOTCpzzDbrRpIQ7GJ2flvA+UEEiPVQQ4e/pt6ZjumaZ/geTBHCRZklLtS1u6HjucVKHGSR4K/lE1q7u6nfi/wAvwGQdCTJeKf8PetdlsiVZg2OW4Wi2YF7qtb1ipGS/uRlOeLP8JOwp5BNVS1J6tvNX4T5gA7rrB4Cvtfwp1dwLWODHiXbeZK7SN2j7V3NPGZl0LbNg2jquC4DzQOBhMOlwu1IAgOAcqKbHh9tDabpVOcOD5pNSaVaoBO01P8ObbX8x0S0Ug7BPkM5nnyJTQdNH7LHKvtNmrUEk7+fd+VZR6WqEp6SFBoAA7XxvOLy9dIOqvXbAYnrNZX7iI4IpgZWRNIQ7G1qSBjpyPLKC7+nSPmvd4IcgCHVUBdYp0+yYDxLzwuuhPVtyAY4JAIdZY1mhYxx7BzGNiMc3p8AlMS6hPeO9lVQZykViBGnTjR+iah9OQYAihX9FYywtBkNvMXLiYqsVZmv6em9Uk/ivKSIsaFwNwrelCI5H4Q/+/hKFJ1JaMjbYheXCJgcGgfhiC+fIHig/8y8rXd9lWGa1T+PrpA8akcq/K8I6gbxYxpLWcy5Vnz/SyaMurLPVfCq4cD/JfpLSfy0HophaiycZ7D6dr43rnHsPfrt6kveDOgWUK9d7CqHxsWdXbCpJeBoh8/1usIsGsmyhfwHq0dEpRx78bR2EJeM4LOAuQgf69/5rFJydIbXq76l1BftTls1Pgh--OI8c/RQGQ30wT1Ff--M/ltnmdl+FRdlNliTb79lg==

View File

@@ -0,0 +1 @@
Ga2G0A==--RCOGf20OFDHTm7qw--PLI8AZQQylfrplQVYg603Q==

View File

@@ -49,4 +49,6 @@ Rails.application.configure do
protocol: "https",
from: "accounts@kosmos.org"
}
config.active_job.queue_adapter = :test
end

View File

@@ -1,3 +1,5 @@
require 'sidekiq/web'
Rails.application.routes.draw do
devise_for :users
@@ -11,11 +13,26 @@ Rails.application.routes.draw do
get 'settings', to: 'settings#index'
post 'settings_reset_password', to: 'settings#reset_password'
get 'security', to: 'security#index'
resources :invitations, only: ['index', 'show', 'create', 'destroy']
resources :donations
get 'wallet', to: 'wallet#index'
get 'lnurlpay/:address', to: 'lnurlpay#index', constraints: { address: /[^\/]+/}
get 'lnurlpay/:address/invoice', to: 'lnurlpay#invoice', constraints: { address: /[^\/]+/}
namespace :admin do
root to: 'dashboard#index'
get 'invitations', to: 'invitations#index'
get 'ldap_users', to: 'ldap_users#index'
resources :donations
end
authenticate :user, ->(user) { user.is_admin? } do
mount Sidekiq::Web => '/sidekiq'
end
# Letter Opener (open "sent" emails in dev and staging)

3
config/sidekiq.yml Normal file
View File

@@ -0,0 +1,3 @@
:concurrency: 2
:queues:
- default

View File

@@ -8,6 +8,7 @@ class CreateInvitations < ActiveRecord::Migration[6.0]
t.timestamps
end
add_index :invitations, :user_id
add_index :invitations, :invited_user_id
end

View File

@@ -0,0 +1,15 @@
class CreateDonations < ActiveRecord::Migration[6.0]
def change
create_table :donations do |t|
t.integer :user_id
t.integer :amount_sats
t.integer :amount_eur
t.integer :amount_usd
t.string :public_name
t.timestamps
end
add_index :donations, :user_id
end
end

View File

@@ -0,0 +1,5 @@
class AddPaidAtToDonations < ActiveRecord::Migration[6.0]
def change
add_column :donations, :paid_at, :datetime
end
end

View File

@@ -0,0 +1,6 @@
class AddLndhubCredentialsToUser < ActiveRecord::Migration[6.0]
def change
add_column :users, :ln_login_ciphertext, :text
add_column :users, :ln_password_ciphertext, :text
end
end

View File

@@ -10,7 +10,19 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_11_30_132533) do
ActiveRecord::Schema.define(version: 2021_11_20_010540) do
create_table "donations", force: :cascade do |t|
t.integer "user_id"
t.integer "amount_sats"
t.integer "amount_eur"
t.integer "amount_usd"
t.string "public_name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.datetime "paid_at"
t.index ["user_id"], name: "index_donations_on_user_id"
end
create_table "invitations", force: :cascade do |t|
t.string "token"
@@ -33,6 +45,8 @@ ActiveRecord::Schema.define(version: 2020_11_30_132533) do
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.text "ln_login_ciphertext"
t.text "ln_password_ciphertext"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end

8
lib/tasks/lndhub.rake Normal file
View File

@@ -0,0 +1,8 @@
namespace :lndhub do
desc "Generate wallets for all users"
task :generate_wallets => :environment do |t, args|
User.all.each do |user|
CreateLndhubWalletJob.perform_later(user)
end
end
end

View File

@@ -5,6 +5,10 @@
"@rails/actioncable": "^6.0.0",
"@rails/ujs": "^6.0.0",
"@rails/webpacker": "4.3.0",
"@tailwindcss/forms": "^0.2.1",
"autoprefixer": "^9",
"postcss": "^7",
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
"turbolinks": "^5.2.0"
},
"version": "0.1.0",

View File

@@ -1,5 +1,6 @@
module.exports = {
plugins: [
require("tailwindcss")("./app/javascript/stylesheets/tailwind.config.js"),
require('postcss-import'),
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({

BIN
public/img/bg-1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

View File

@@ -0,0 +1,9 @@
FactoryBot.define do
factory :donation do
user_id { 1 }
amount_sats { 100000 }
amount_eur { 10 }
amount_usd { 13 }
public_name { nil }
end
end

View File

@@ -10,6 +10,6 @@ RSpec.describe 'Admin dashboard', type: :feature do
scenario 'View dashboard' do
visit admin_root_path
expect(page).to have_content('Admin Panel')
expect(page).to have_content('great power')
end
end

View File

@@ -0,0 +1,9 @@
require 'rails_helper'
describe ApplicationHelper do
describe "sats_to_btc" do
it "converts satoshis to BTC" do
expect(helper.sats_to_btc(120000000)).to eq(1.2)
end
end
end

View File

@@ -0,0 +1,15 @@
require 'rails_helper'
# Specs in this file have access to a helper object that includes
# the DonationsHelper. For example:
#
# describe DonationsHelper do
# describe "string concat" do
# it "concats two strings with spaces" do
# expect(helper.concat_strings("this","that")).to eq("this that")
# end
# end
# end
RSpec.describe DonationsHelper, type: :helper do
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@@ -0,0 +1,34 @@
require 'rails_helper'
RSpec.describe CreateLdapUserJob, type: :job do
let(:ldap_client_mock) { instance_double(Net::LDAP) }
subject(:job) {
described_class.any_instance.stub(:ldap_client).and_return(ldap_client_mock)
described_class.perform_later(
'halfinney', 'kosmos.org', 'halfinney@example.com',
'remember-remember-the-5th-of-november'
)
}
it "creates a new document with the correct attributes" do
ldap_client_mock.should_receive(:add).with(
dn: "cn=halfinney,ou=kosmos.org,cn=users,dc=kosmos,dc=org",
attributes: {
objectclass: ["top", "account", "person", "extensibleObject"],
cn: "halfinney",
sn: "halfinney",
uid: "halfinney",
mail: "halfinney@example.com",
userPassword: "remember-remember-the-5th-of-november"
}
)
perform_enqueued_jobs { job }
end
after do
clear_enqueued_jobs
clear_performed_jobs
end
end

View File

@@ -0,0 +1,48 @@
require 'rails_helper'
require 'webmock/rspec'
RSpec.describe CreateLndhubWalletJob, type: :job do
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
subject(:job) { described_class.perform_later(user) }
before do
stub_request(:post, "http://localhost:3023/create")
.to_return(status: 200, headers: {},
body: { login: "abc123", password: "def456" }.to_json)
end
it "creates a new LndHub account" do
perform_enqueued_jobs { job }
expect(WebMock).to have_requested(:post, "http://localhost:3023/create")
.with { |req| req.body == '{"partnerid":"kosmos.org","accounttype":"user"}' }
user.reload
expect(user.ln_login).to eq("abc123")
expect(user.ln_password).to eq("def456")
end
context "with existing credentials stored" do
before do
user.ln_login = "foo"
user.ln_password = "bar"
user.save!
end
it "does not create a new LndHub account" do
perform_enqueued_jobs { job }
expect(WebMock).to_not have_requested(:post, "http://localhost:3023/create")
user.reload
expect(user.ln_login).to eq("foo")
expect(user.ln_password).to eq("bar")
end
end
after do
clear_enqueued_jobs
clear_performed_jobs
end
end

View File

@@ -0,0 +1,29 @@
require 'rails_helper'
require 'webmock/rspec'
RSpec.describe ExchangeXmppContactsJob, type: :job do
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
subject(:job) {
described_class.perform_later(user, 'isaacnewton', 'kosmos.org')
}
before do
stub_request(:post, "http://xmpp.example.com/api/add_rosteritem")
.to_return(status: 200, body: "", headers: {})
end
it "posts add_rosteritem commands to the ejabberd API" do
perform_enqueued_jobs { job }
expect(WebMock).to have_requested(:post, "http://xmpp.example.com/api/add_rosteritem")
.with { |req| req.body == '{"localuser":"isaacnewton","localhost":"kosmos.org","user":"willherschel","host":"kosmos.org","nick":"willherschel","group":"Friends","subs":"both"}' }
expect(WebMock).to have_requested(:post, "http://xmpp.example.com/api/add_rosteritem")
.with { |req| req.body == '{"localuser":"willherschel","localhost":"kosmos.org","user":"isaacnewton","host":"kosmos.org","nick":"isaacnewton","group":"Friends","subs":"both"}' }
end
after do
clear_enqueued_jobs
clear_performed_jobs
end
end

View File

@@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe Donation, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@@ -69,5 +69,6 @@ RSpec.configure do |config|
config.include Devise::Test::ControllerHelpers, :type => :controller
config.include Warden::Test::Helpers
config.include FactoryBot::Syntax::Methods
config.include ActiveJob::TestHelper, type: :job
config.extend ControllerMacros, :type => :controller
end

View File

@@ -1,14 +1,6 @@
require 'rails_helper'
require 'webmock/rspec'
require 'json'
RSpec.describe CreateAccount, type: :model do
let(:ldap_client_mock) { instance_double(Net::LDAP) }
before do
allow(service).to receive(:ldap_client).and_return(ldap_client_mock)
end
describe "#create_user_in_database" do
let(:service) { CreateAccount.new(
username: 'isaacnewton',
@@ -48,30 +40,34 @@ RSpec.describe CreateAccount, type: :model do
end
describe "#add_ldap_document" do
include ActiveJob::TestHelper
let(:service) { CreateAccount.new(
username: 'halfinney',
email: 'halfinney@example.com',
password: 'remember-remember-the-5th-of-november'
)}
it "creates a new document with the correct attributes" do
expect(ldap_client_mock).to receive(:add).with(
dn: "cn=halfinney,ou=kosmos.org,cn=users,dc=kosmos,dc=org",
attributes: {
objectclass: ["top", "account", "person", "extensibleObject"],
cn: "halfinney",
sn: "halfinney",
uid: "halfinney",
mail: "halfinney@example.com",
userPassword: /^{SSHA512}.{171}=/
}
)
it "enqueues a job to create the LDAP user document" do
service.send(:add_ldap_document)
expect(enqueued_jobs.size).to eq(1)
args = enqueued_jobs.first['arguments']
expect(args[0]).to eq('halfinney')
expect(args[1]).to eq('kosmos.org')
expect(args[2]).to eq('halfinney@example.com')
expect(args[3]).to match(/^{SSHA512}.{171}=/)
end
after do
clear_enqueued_jobs
end
end
describe "#exchange_xmpp_contacts" do
include ActiveJob::TestHelper
let(:inviter) { create :user, cn: "willherschel", ou: "kosmos.org" }
let(:invitation) { create :invitation, user: inviter }
let(:service) { CreateAccount.new(
@@ -81,18 +77,19 @@ RSpec.describe CreateAccount, type: :model do
invitation: invitation
)}
before do
stub_request(:post, "http://xmpp.example.com/api/add_rosteritem")
.to_return(status: 200, body: "", headers: {})
end
it "posts add_rosteritem commands to the ejabberd API" do
it "enqueues a job to exchange XMPP contacts between inviter and invitee" do
service.send(:exchange_xmpp_contacts)
expect(WebMock).to have_requested(:post, "http://xmpp.example.com/api/add_rosteritem")
.with { |req| req.body == '{"localuser":"isaacnewton","localhost":"kosmos.org","user":"willherschel","host":"kosmos.org","nick":"willherschel","group":"Friends","subs":"both"}' }
expect(WebMock).to have_requested(:post, "http://xmpp.example.com/api/add_rosteritem")
.with { |req| req.body == '{"localuser":"willherschel","localhost":"kosmos.org","user":"isaacnewton","host":"kosmos.org","nick":"isaacnewton","group":"Friends","subs":"both"}' }
expect(enqueued_jobs.size).to eq(1)
args = enqueued_jobs.first['arguments']
expect(args[0]['_aj_globalid']).to match('gid://akkounts/User')
expect(args[1]).to eq('isaacnewton')
expect(args[2]).to eq('kosmos.org')
end
after do
clear_enqueued_jobs
end
end
end

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