205 Commits

Author SHA1 Message Date
14c5dd22d6 Add strfry doc draft
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-19 14:37:54 +02:00
f3676949d2 Fix redirect
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 14:49:19 +02:00
79952b73c5 Fix link descriptions
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 14:45:31 +02:00
17c419403e Merge pull request 'Finish MVP of remoteStorage service pages/UI' (#202) from feature/rs_service_page into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #202
Reviewed-by: Greg <greg@noreply.kosmos.org>
2024-08-17 12:33:48 +00:00
6d06312a5c Update manifique gem
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 6s
Fixes a bug with some manifest files
2024-08-14 18:07:27 +02:00
acb399b0b7 Add app recommendation for Notes Together
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-08-14 16:32:06 +02:00
bf20b6467e Re-order services on dashboard
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-08-14 13:37:22 +02:00
b91d90d75c Fix some specs, improve config
Allow empty string to unset nostr relay URL config
2024-08-14 13:37:15 +02:00
3284bbf6ca Add recommended apps for RS 2024-08-14 13:35:49 +02:00
171b84ee81 Add tabnav, dedicated auths view to RS service page
Includes a nicer view and illustration for when no auths exist yet
2024-08-14 13:35:02 +02:00
54b01dd282 Drive-by content update 2024-08-12 11:14:12 +02:00
32dff9c67f Merge pull request 'Add dashboard icons for remoteStorage and email' (#200) from chore/dashboard_service_icons into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #200
2024-08-12 07:03:22 +00:00
126b8b20e0 Improve dashboard icon opacity, layout
All checks were successful
continuous-integration/drone Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 5s
2024-08-10 12:44:49 +02:00
5abf69f356 Add email service icon to dashboard 2024-08-10 12:44:25 +02:00
210a69bd9b Add Geary app recommendation to email page 2024-08-09 14:19:49 +02:00
bbed3cd367 Add RS logo to service grid, resize others 2024-08-09 12:37:18 +02:00
7943da0f17 Add note 2024-08-09 12:34:10 +02:00
620167eedf Merge pull request 'Admin pages: fix more user links, add missing services to user page' (#199) from feature/admin_pages into master
Reviewed-on: #199
2024-08-09 10:33:29 +00:00
e077debfc2 Use npub for njump link
All checks were successful
continuous-integration/drone/push Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-06-23 17:30:03 +02:00
531b2c3002 Fix more links 2024-06-23 17:29:48 +02:00
6d2bc729b8 Add new services to admin user page
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-23 17:26:33 +02:00
2630ec2af4 Fix admin user links
All checks were successful
continuous-integration/drone/push Build is passing
refs #166
2024-06-23 17:24:48 +02:00
daed5c1eea Merge pull request 'Allow non-members to publish zap receipts for members' (#197) from feature/strfry_zap_receipts into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #197
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2024-06-22 17:52:03 +00:00
2e9429bb32 Merge pull request 'Add support for integrated Nostr relay service' (#198) from feature/own_relay into feature/strfry_zap_receipts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 6s
Reviewed-on: #198
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2024-06-22 17:51:40 +00:00
37c15c7a62 Check in deno lockfile
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 6s
2024-06-20 15:51:40 +02:00
01ecea74ff Add pubkey whitelist to strfry policy
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
And allow the local akkounts instance to publish on the local relay
2024-06-20 15:28:17 +02:00
f401a03590 Fix exception for NIP-05 JSON of "_" with relay configured
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-20 14:50:02 +02:00
fff6dea100 Add support for placeholder attribute to component
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 13:54:59 +02:00
48ab96dda9 Support "_" placeholder username for domain's own NIP-05
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-19 20:57:22 +02:00
7ac3130c18 Consistent formatting
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-19 20:31:31 +02:00
cbfa148051 Publish zap receipts to own relay in addition to requested ones
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-19 20:26:24 +02:00
87d900b627 Add own relay to NIP-05 relay list if configured 2024-06-19 20:06:07 +02:00
926dc06294 Add global setting for own nostr relay 2024-06-19 19:57:09 +02:00
00b73b06d7 Remove obsolete variable
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-19 15:56:45 +02:00
0daac33915 Allow non-members to publish zap receipts for members
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-19 15:43:56 +02:00
0e472bc311 Improve strfry extras usage 2024-06-19 15:43:24 +02:00
40b34d0935 Merge pull request 'Add strfry policies and members-only LDAP policy' (#196) from feature/strfry_policies into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #196
2024-06-11 20:10:34 +00:00
61cb8f4941 Add script for syncing notes from remote relays
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-06-11 22:06:51 +02:00
433ac4dc8e Use new strfry Docker image 2024-06-11 22:06:12 +02:00
62fe0d8fac Add nostrKey to default org service ACI 2024-06-11 22:05:07 +02:00
2a675fd135 Hand LDAP config to policy from main policy file
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Deployments will differ in production. The policy itself just needs the
configs, but should not care where credentials are fetched from.
2024-06-09 23:15:56 +02:00
c2c3ebc2e1 Add strfry policies and members-only LDAP policy
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
This will look up nostr pubkeys in the LDAP directory to allow or deny
publishing notes to the relay.
2024-06-09 22:49:44 +02:00
5a5c316c14 Fix time format in migration
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-09 13:29:39 +02:00
f0d5457ec1 Merge pull request 'Zap model improvements' (#195) from chore/zap_model_improvements into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #195
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2024-06-09 11:16:34 +00:00
5588e3b3e8 Add settled_at to zaps, scope by settlement status
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-06-07 15:11:06 +02:00
8949d76d26 Fix zap receipt not being stored correctly
All checks were successful
continuous-integration/drone/push Build is passing
fixes #194
2024-06-07 13:40:49 +02:00
8bc9bbdc33 Merge pull request 'Add new Lightning notification settings' (#193) from feature/ln_notification_settings into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #193
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2024-06-04 10:39:07 +00:00
d6d09b57b8 Merge pull request 'Add support for Lightning Zaps' (#190) from feature/170-nostr_zaps into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #190
2024-06-03 16:44:48 +00:00
1685d6ecf8 Respect new Lightning notification settings
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-06-01 17:51:20 +02:00
5348a229a6 WIP Add new lightning notification settings 2024-05-29 15:12:07 +01:00
bad3b7a2be Use dynamic list for allowed user preference params 2024-05-23 00:23:42 +02:00
b541e95bb7 Change default for lightning notifications 2024-05-23 00:22:38 +02:00
3f43fe8101 Fix missing description for FieldsetToggleComponent 2024-05-23 00:01:25 +02:00
231dfc8404 Log correct publish status
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-05-21 18:28:46 +02:00
eeb9b0a331 Improve NostrManager::PublishEvent
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
* Use URI hostname as relay name
* Log relay name/URL for every websocket event
* Fix variable assignment for nostr event
* Fix Sidekiq job finishing too early, by creating a new thread waiting
  for it to be closed from a callback
2024-05-21 18:08:14 +02:00
08e783d185 Remove default nil values
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-19 17:07:27 +02:00
fa5dc8ca46 Fix argument name
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-19 16:54:51 +02:00
bc34e9c5e0 Allow CORS requests for lnurlp invoice
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-19 16:48:09 +02:00
f388bd0237 Merge branch 'master' into feature/170-nostr_zaps
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-10 12:01:27 +00:00
48041630ca Limit number of relays to publish zap receipts to
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-10 13:57:25 +02:00
2d1ff29eca Improve nostr settings, fix allowsNostr property name
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-10 13:19:09 +02:00
46fa42e387 Merge pull request 'Refactor Nostr auth, add login via Nostr (web extension)' (#188) from feature/nostr_login into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #188
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2024-05-10 11:01:00 +00:00
c6c5d80fb4 WIP Persist zaps, create and send zap receipts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-09 14:31:37 +02:00
c0f4e7925e Use zap comment for description/memo
Some checks failed
continuous-integration/drone/push Build is failing
But use the hashed zap request event for the description hash.
2024-05-04 17:07:23 +02:00
49d24990b4 Add zap model, user relation 2024-05-04 17:05:34 +02:00
619bd954b7 WIP 2024-04-21 10:51:41 +02:00
e27c64b5f1 WIP Check for zaps, send zap receipt on incoming zap tx 2024-04-21 10:35:30 +02:00
b36baf26eb Refactor WebhooksController 2024-04-21 10:02:17 +02:00
adedaa5f7b Add task for easily creating test invoices 2024-04-21 10:01:54 +02:00
596ed7fccc Use lndhub.go v2 endpoint for invoice creation 2024-04-21 10:01:18 +02:00
5685e1b7bc Move lndhub invoice creation to service 2024-04-16 20:19:15 +02:00
c3b82fc2a9 WIP Verify and respond to zap requests
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-16 19:13:10 +02:00
77e2fe5792 Add helper method for parsing nostr event tags 2024-04-16 19:10:48 +02:00
bc43082839 Add admin settings for nostr keys 2024-04-16 19:07:52 +02:00
b09225543b Add Nostr relay service to Docker Compose config 2024-04-15 14:03:37 +02:00
f2507409a3 Announce nostr pubkey on lnurlp endpoint 2024-04-15 14:03:37 +02:00
46b4723999 Add global settings for account service's Nostr keys 2024-04-15 14:03:37 +02:00
3f90a011c4 Document URLs 2024-04-15 14:03:37 +02:00
3ba333e802 Indentation 2024-04-15 14:03:37 +02:00
d9dff3e872 Merge branch 'master' into feature/nostr_login
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-04-15 12:03:12 +00:00
6ddeacb779 Merge pull request 'Add Mastodon aliases and links to Webfinger when enabled' (#189) from feature/mastodon_webfinger into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #189
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-04-14 10:18:15 +00:00
78aff3d796 Fix spec
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
The test env has Mastodon enabled now
2024-04-04 17:22:57 +03:00
8f600f44bd Add Mastodon aliases and links to Webfinger when enabled
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
Also requires "remotestorage" service to be enabled via attribute
2024-04-04 17:17:57 +03:00
819ecf6ad8 Add #service_enabled? method to user model 2024-04-04 13:28:09 +03:00
945eaba5e1 Add login via nostr (web extension)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-01 19:04:48 +03:00
22d362e1a0 Refactor Nostr settings/connect
* Use NIP-42 auth event instead of short text note
* Verify event ID and signature using the nostr gem instead of custom code
2024-04-01 18:27:08 +03:00
d4e67a830c Update nostr gem 2024-04-01 18:27:08 +03:00
670b2da1ef Ad-hoc content update
All checks were successful
continuous-integration/drone/push Build is passing
Before #186 is implemented
2024-03-29 10:33:28 +04:00
ed5c5b3081 Add remotestorage queue to Sidekiq config
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-29 09:47:30 +04:00
4ee6bfddfa Merge pull request 'Improvements/adjustments for Mastodon integration' (#185) from chore/mastodon into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #185
2024-03-29 05:24:10 +00:00
8b60890061 Add Phanpy to recommended Mastodon apps
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
It's too good not to.
2024-03-29 09:21:17 +04:00
0367450c4b Replace hyphen with underscore in Mastodon address
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Unfortunately, Mastodon only allows underscores for usernames, and
reversely, akkounts only allows hyphens and no underscores.
2024-03-29 09:08:15 +04:00
e6f5623c7f Enable Mastodon service by default (for now) 2024-03-29 09:06:41 +04:00
367f566ccb Merge pull request 'Add global setting for default services, enable for preconfirmed accounts' (#184) from feature/preconfirmed_accounts into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #184
2024-03-28 13:23:22 +00:00
80e69df75c Add global setting for default services, enable for preconfirmed accounts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
Co-authored-by: Greg Karékinian <greg@karekinian.com>
2024-03-28 17:21:20 +04:00
02af69b055 Add missing env var to example config
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-28 10:56:42 +04:00
5d459e7e7d Fix LDAP attribute name
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-19 18:18:06 +01:00
51a3cb60ec Merge pull request 'Add custom LDAP attributes to schema' (#181) from feature/custom_ldap_attributes into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #181
Reviewed-by: greg <greg@noreply.kosmos.org>
2024-03-19 14:46:44 +00:00
43c57c128f Merge pull request 'Move nostr pubkeys to LDAP attribute' (#183) from feature/173-nostr_ldap into feature/custom_ldap_attributes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
Reviewed-on: #183
Reviewed-by: greg <greg@noreply.kosmos.org>
2024-03-19 14:43:02 +00:00
5a3adba603 Move nostr pubkeys to LDAP attribute
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
closes #173
2024-03-17 11:04:11 +01:00
3715cb518b User Settings: Rename Experiments to Nostr
All checks were successful
continuous-integration/drone/push Build is passing
And use a nostr icon
2024-03-16 16:03:15 +01:00
2c9ecc1fef Add nostr icons 2024-03-16 16:03:00 +01:00
095747e89b Fix broken admin links
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-13 18:19:25 +01:00
2130369604 Update db schema
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-13 18:15:42 +01:00
c996351930 Fix PostgreSQL query issue 2024-03-13 18:13:17 +01:00
8b897168cc Merge pull request 'Let users donate sats via BTCPay Server' (#176) from feature/donations_btcpay into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #176
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-03-13 16:31:54 +00:00
4217ba52e0 Switch service LDAP attribute to serviceEnabled
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Improve internal naming on the way
2024-03-13 16:41:49 +01:00
de20931d30 Add tasks for modifying schema, first custom attributes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
refs #172, #173
2024-03-13 14:30:03 +01:00
8de0a2e26e Improve seed output 2024-03-13 14:28:31 +01:00
06521d1c34 LDAP: add delete_all_users method, use in seeds 2024-03-13 14:27:39 +01:00
38b3d68fd5 LDAP: Rename client method, add modify method 2024-03-13 14:26:44 +01:00
eac8fa6edb 0.9.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-07 14:48:27 +01:00
43f918a074 Update liquor-cabinet image, fix LC/redis networking issue on Linux
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-06 22:07:35 +01:00
e322867d79 Merge pull request 'Fix login redirect for existing RS auth' (#180) from bugfix/178-rs_login_redirect into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #180
2024-03-06 21:06:27 +00:00
4d6fa318b7 Fix login redirect for existing RS auth
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
fixes #178
2024-03-06 22:00:15 +01:00
7f2df3b025 Fix donation record for amounts given in sats
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2024-03-06 11:22:53 +01:00
da22a9d448 Add spec for reported regression
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-03-06 11:20:43 +01:00
e3b96d5cff Merge branch 'master' into feature/donations_btcpay
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-03 12:50:16 +01:00
4e8878a4b5 Merge pull request 'Allow running specs in Docker container, update README' (#177) from dev/docker_rspec into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #177
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-03-03 11:47:53 +00:00
e65b890880 Update db schema
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2024-03-02 17:31:44 +01:00
f57edd4d3b Update README to account for Docker Compose everywhere
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-02 16:57:07 +01:00
1afd56fb80 Allow running specs in Docker (Web) container 2024-03-02 16:56:07 +01:00
71669a4b96 Merge pull request 'Refactor admin settings routes' (#156) from feature/content_settings into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #156
2024-03-02 14:30:21 +00:00
c312e30c17 Fix link in admin settings/services sidenav
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2024-03-02 15:26:12 +01:00
51f4556ede Refactor admin settings routes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
This is much cleaner, and semantically more correct.
2024-03-02 14:22:08 +00:00
c36cf5eee6 Merge branch 'master' into feature/donations_btcpay
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-02 15:07:40 +01:00
54220019bb Send email confirmation when BTC payment is confirmed
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2024-03-02 14:31:48 +01:00
079ee8833c Implement bitcoin donations via BTCPay 2024-03-02 14:31:48 +01:00
26d613bdca Allow other controllers to access lndhub user balance 2024-03-02 14:31:48 +01:00
69b3afb8f7 DRY up btcpay and lndhub services
Removing initialize methods from the main/manager class also allows for
different iniitalizers in specific task services
2024-03-02 14:31:48 +01:00
fee951c05c Move past donations to partial 2024-03-02 14:31:45 +01:00
4fa4ae6b54 Merge pull request 'Comment out settings in .env.example' (#175) from task/env-example into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #175
Reviewed-by: Râu Cao <raucao@kosmos.org>
2024-03-02 13:30:18 +00:00
869ff4691b Comment out settings in .env.example
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2024-03-02 12:43:59 +01:00
822a2dc018 Fix specs
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-01 17:15:02 +01:00
5b7fc3707b Hide avatar settings behind feature flag
Some checks failed
continuous-integration/drone/push Build is failing
In favor of #157
2024-03-01 11:13:49 +01:00
0e2dc54dc6 Merge pull request 'Upgrade Rails to 7.1, update dependencies, require Ruby 3.x' (#160) from chore/update_dependencies into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #160
Reviewed-by: slvrbckt <slvrbckt@noreply.kosmos.org>
2024-02-27 18:56:59 +00:00
87f09c94d0 Merge pull request 'Fix/improve local ActiveStorage backend usage and handling of WebApp icons' (#162) from bugfix/local_web_app_icons into chore/update_dependencies
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
Reviewed-on: #162
Reviewed-by: greg <greg@noreply.kosmos.org>
2024-02-27 16:07:55 +00:00
b33b8104a8 Fix typo
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2024-02-27 14:33:37 +01:00
4a4a222973 Merge branch 'chore/update_dependencies' into bugfix/local_web_app_icons
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-23 18:25:23 +00:00
8c524abcf5 Merge pull request 'Fix Docker volume permissions on some host platforms' (#171) from bugfix/macos_docker_volumes into chore/update_dependencies
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewed-on: #171
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-02-23 18:24:10 +00:00
a852ab75ae Fix Docker volume permissions on some host platforms
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
Use named volumes instead of bind mounts.
2024-02-23 16:43:56 +01:00
de1f234c15 Merge branch 'chore/update_dependencies' into bugfix/local_web_app_icons
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-22 15:13:18 +01:00
4581900427 Merge pull request 'Fix Ruby in Docker container on Apple silicon' (#168) from chore/fix_docker_ruby_on_apple_silicon into chore/update_dependencies
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewed-on: #168
Reviewed-by: slvrbckt <slvrbckt@noreply.kosmos.org>
2024-02-22 14:12:05 +00:00
56d91083e5 Fix seeds for new keyword argument
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2024-02-22 13:24:41 +01:00
ba7c3795f8 Add pkg-config
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-22 11:29:56 +01:00
bbf3fb91a0 Fix Ruby in Docker container on Apple silicon
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-22 10:47:21 +01:00
1754df73cb Merge pull request 'Allow admins to add and remove invitations per account' (#167) from feature/164-invites into chore/update_dependencies
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewed-on: #167
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-02-17 10:17:47 +00:00
9a1f9abf84 Formatting
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2024-02-10 12:53:26 +01:00
2753388e1e Add specs for admin user management 2024-02-10 12:53:11 +01:00
f3159d30f1 Allow admins to add and remove invitations per account
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-10 11:21:45 +01:00
ca238be6f4 Add option for hiding close button in modal windows 2024-02-10 10:24:09 +01:00
8747ce4eb0 Remove multi-domain support on admin user pages
All checks were successful
continuous-integration/drone/push Build is passing
refs #166
2024-02-10 08:55:15 +01:00
fcda3b9c8c WIP Make dropdowns more configurable, add invitations menu to admin page 2024-02-09 18:57:07 +01:00
67689dcce3 Add service for creating invites
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-09 17:59:07 +01:00
22ffcd54db Patch away a deprecation warning caused by Devise
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-09 17:58:28 +01:00
bd1b177993 Rescue all icon download/upload errors, send to Sentry
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-08 13:36:17 +01:00
3f110995a4 Add timestamp to icon filenames
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
There can be race condition when a background job is supposed to delete
an icon while there is a new one being attached. Also, this encodes the
date/time when the icon has been added, for inspection and convenience.
2024-02-08 13:03:32 +01:00
a7410058fa Save WebApp before fetching icons 2024-02-08 13:02:08 +01:00
411587456b Destroy dependent RS auths when destroying a WebApp 2024-02-08 13:01:19 +01:00
84e915ece9 Allow custom path for ActiveStorage local/disk backend 2024-02-08 13:01:07 +01:00
70ac3b0a70 Fix RS dashboard for auths without Web App
RS auths without a valid domain name will not fetch any metadata and
therefore not create a WebApp record. This fixes icons being looked up
anyway, resulting in exceptions
2024-02-08 12:51:53 +01:00
a7cbd8ce36 Allow disabling S3 explicitly, disable in Docker Compose
For example when there is a .env.development for running the app on a
host machine directly, but as a developer you also want to run it with
Docker Compose from time to time.
2024-02-08 12:50:34 +01:00
c9052b35f6 Database update for Flipper
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-08 12:29:11 +01:00
3b96130491 Upgrade web-console, fix it for Docker
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Was failing silently in Docker, because the warnings were turned off.
2024-02-08 12:26:28 +01:00
176b1a10c6 Remove obsolete closing tag 2024-02-08 12:10:14 +01:00
1c54e4c0b5 New CI image Dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-03 11:36:06 +02:00
7796a22491 Switch to newly published manifique gem
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-02 17:55:20 +02:00
7e6e917ae1 Use new CI image with Ruby 3.3.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-02 17:28:33 +02:00
28cfe4b1e7 Fix deprecation warning 2024-02-02 16:58:04 +02:00
179a82d2dd Use keyword arguments for ApplicationService calls
Not all services are using keywords, which breaks those calls in Ruby 3
2024-02-02 15:50:25 +02:00
420442c1c0 Update Ruby for Dockerfile/Compose 2024-02-02 14:34:09 +02:00
68c5758ecc Update dependencies, upgrade to Rails 7.1, require Ruby 3.x 2024-02-02 14:25:47 +02:00
c5dd3c30a6 Use full URL for S3 alias host
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-02 14:01:47 +02:00
422d5c7cd2 Fix address missing in lightning address receive notifications
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-01 16:22:20 +02:00
5a23d523a8 Add fallback icons for apps on RS app dashboard
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-29 18:33:06 +02:00
f8da034e66 Fail gracefully when remote icon is 404
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-29 14:54:18 +02:00
b0b56fcf92 Fix lnurlp route
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-29 11:18:51 +02:00
0cf000c1b8 Merge pull request 'Only support primary domain for Lightning Address' (#158) from chore/well-known_routes into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #158
2024-01-29 09:03:37 +00:00
fa9a924b0a Merge pull request 'Fix RS auth array usage in production' (#159) from bugfix/postgresql_arrays into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #159
2024-01-29 08:58:02 +00:00
50f91cc7d7 Fix RS auth array usage in production
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
Serialization into YAML breaks the native PostgreSQL array usage.

Needs to be adjusted later to not use the environment, but database
adapter (issue #149).
2024-01-29 10:52:52 +02:00
a628a03f84 Only support primary domain for Lightning Address
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
Part of the process of removing support for serving multiple domains
from a single akkounts instance.

Also puts the Lightning Address discovery routes under the .well-known
path. Combined, these changes simplify reverse-proxying to the
.well-known endpoints.
2024-01-26 16:08:21 +02:00
eaf41e0835 Adjust spec for c32fc51
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-26 16:02:47 +02:00
243cf9c08d Don't add CORS headers for Webfinger in production
Some checks failed
continuous-integration/drone/push Build is failing
The reverse proxy should handle it.
2024-01-26 11:01:45 +03:00
c32fc51aab Do not enable email service by default
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-26 09:38:38 +03:00
aa9178d569 Sort service ENV vars alphabetically, add missing lndhub var
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-26 08:36:58 +03:00
281938dd64 Only set API CORS headers in development
All checks were successful
continuous-integration/drone/push Build is passing
In production, this is the reverse proxy's responsibility
2024-01-22 15:35:13 +03:00
fafc5d8f6f Improve copy
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-22 12:10:17 +03:00
1238359b5f Remove superfluous header text
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-22 12:04:55 +03:00
84220beb1c Merge pull request 'Add email service and settings' (#154) from feature/email_service into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #154
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-01-22 09:01:18 +00:00
1e9ec9bb76 Fix wrong prefix for email QR code
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2024-01-22 11:52:45 +03:00
21e51a7c40 Merge pull request 'Update nostr gem, switch to Ruby for bech32 encoding' (#155) from chore/bech32_handling into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #155
2024-01-21 09:31:51 +00:00
e3c30f7b16 Remove obsolete function
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2024-01-15 13:00:48 +03:00
b4f0c60ea0 Update nostr gem, switch to Ruby for bech32 encoding
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-01-15 12:54:58 +03:00
1a5a2177b4 Update spec
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-15 12:38:27 +03:00
7e8443c598 Change Lightning balance property
Some checks failed
continuous-integration/drone/push Build is failing
... so that clients can use the same property with all balances
2024-01-15 11:39:24 +03:00
7b71f2cf76 Revert "Fix fixture file"
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
This reverts commit c7b137e5eb.
2024-01-10 18:35:04 +03:00
c7b137e5eb Fix fixture file
Some checks failed
continuous-integration/drone Build is failing
2024-01-10 18:30:19 +03:00
958d18d61a Add email service and settings 2024-01-10 18:30:05 +03:00
3aa0c49507 Set CORS headers for BTCPay API endpoints 2024-01-02 09:49:09 +03:00
Râu Cao
4e566a0607 Merge pull request 'Fetch/store Web App metadata and icons, finish RS integration' (#153) from feature/142-webapp_database into master
Reviewed-on: #153
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-01-01 13:18:47 +00:00
Râu Cao
aab6793b86 Improve permission list in RS emails
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-11-20 18:32:52 +01:00
Râu Cao
cfd0935bdc Notify user about new RS authorizations 2023-11-20 18:24:34 +01:00
Râu Cao
c2dae105ff Add settings page for Storage, add notification prefs 2023-11-20 18:22:06 +01:00
Râu Cao
e301ac8e2e Fix title
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-01 22:47:59 +01:00
Râu Cao
03a1d9f277 Allow existing user records with reserved usernames to be saved
Some checks are pending
continuous-integration/drone/push Build is running
2023-11-01 22:26:53 +01:00
236 changed files with 5878 additions and 1058 deletions

View File

@@ -17,7 +17,7 @@ steps:
branch: branch:
- master - master
- name: rspec - name: rspec
image: gitea.kosmos.org/kosmos/akkounts-ci:0.1.0 image: gitea.kosmos.org/kosmos/akkounts-ci:0.9.1
environment: environment:
RAILS_ENV: test RAILS_ENV: test
REDIS_URL: redis://redis:6379/0 REDIS_URL: redis://redis:6379/0
@@ -28,6 +28,8 @@ steps:
- bundle config set cache_path 'vendor/cache' - bundle config set cache_path 'vendor/cache'
- bundle config set with 'development test' - bundle config set with 'development test'
- bundle install --jobs=3 --retry=3 - bundle install --jobs=3 --retry=3
- bundle exec rails db:create
- bundle exec rails db:migrate
- yarn install - yarn install
- rake css:build - rake css:build
- bundle exec rspec - bundle exec rspec

View File

@@ -1,56 +1,71 @@
PRIMARY_DOMAIN=kosmos.org # PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.example.com # AKKOUNTS_DOMAIN=accounts.example.com
SMTP_SERVER=smtp.example.com # SMTP_SERVER=smtp.example.com
SMTP_PORT=587 # SMTP_PORT=587
SMTP_LOGIN=accounts # SMTP_LOGIN=accounts
SMTP_PASSWORD=123abc # SMTP_PASSWORD=123abc
SMTP_FROM_ADDRESS=accounts@example.com # SMTP_FROM_ADDRESS=accounts@example.com
SMTP_DOMAIN=example.com # SMTP_DOMAIN=example.com
SMTP_AUTH_METHOD=plain # SMTP_AUTH_METHOD=plain
SMTP_ENABLE_STARTTLS=auto # SMTP_ENABLE_STARTTLS=auto
# S3_ENABLED=true # S3_ENABLED=true
# S3_ENDPOINT=https://s3.kosmos.org # S3_ENDPOINT=https://s3.kosmos.org
# S3_REGION=garage # S3_REGION=garage
# S3_BUCKET=akkounts-production # S3_BUCKET=akkounts-production
# S3_ALIAS_HOST=accounts.s3.kosmos.org # S3_ALIAS_HOST=https://accounts.web.s3.kosmos.org
# S3_ACCESS_KEY=123456abcdefg # S3_ACCESS_KEY=123456abcdefg
# S3_SECRET_KEY=123456789123456789123456789 # S3_SECRET_KEY=123456789123456789123456789
LDAP_HOST=localhost # LDAP_HOST=localhost
LDAP_PORT=389 # LDAP_PORT=389
LDAP_ADMIN_PASSWORD=passthebutter # LDAP_ADMIN_PASSWORD=passthebutter
LDAP_SUFFIX='dc=kosmos,dc=org' # LDAP_SUFFIX='dc=kosmos,dc=org'
REDIS_URL='redis://localhost:6379/1' # REDIS_URL='redis://localhost:6379/1'
WEBHOOKS_ALLOWED_IPS='10.1.1.163' # WEBHOOKS_ALLOWED_IPS='10.1.1.163'
DISCOURSE_PUBLIC_URL='https://community.kosmos.org' #
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw' # Service Integrations
# (sorted alphabetically by service name)
#
DRONECI_PUBLIC_URL='https://drone.kosmos.org' # BTCPAY_PUBLIC_URL='https://btcpay.example.com'
# BTCPAY_API_URL='http://localhost:23001/api/v1'
# BTCPAY_STORE_ID=''
# BTCPAY_AUTH_TOKEN=''
GITEA_PUBLIC_URL='https://gitea.kosmos.org' # DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
MASTODON_PUBLIC_URL='https://kosmos.social' # DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/2'
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin' # DRONECI_PUBLIC_URL='https://drone.kosmos.org'
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
BTCPAY_API_URL='http://localhost:23001/api/v1' # EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
BTCPAY_STORE_ID='' # EJABBERD_API_URL='https://xmpp.kosmos.org/api'
BTCPAY_AUTH_TOKEN=''
LNDHUB_API_URL='http://localhost:3023' # GITEA_PUBLIC_URL='https://gitea.kosmos.org'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946' # LNDHUB_API_URL='http://localhost:3023'
LNDHUB_ADMIN_UI=true # LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PG_HOST=localhost # LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
LNDHUB_PG_PORT=5432 # LNDHUB_ADMIN_UI=true
LNDHUB_PG_DATABASE=lndhub # LNDHUB_ADMIN_TOKEN=123456789
LNDHUB_PG_USERNAME=lndhub # LNDHUB_PG_HOST=localhost
LNDHUB_PG_PASSWORD='' # LNDHUB_PG_PORT=5432
# LNDHUB_PG_DATABASE=lndhub
# LNDHUB_PG_USERNAME=lndhub
# LNDHUB_PG_PASSWORD=''
# MASTODON_PUBLIC_URL='https://kosmos.social'
# MASTODON_ADDRESS_DOMAIN='https://kosmos.org'
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
# NOSTR_PRIVATE_KEY='123456abcdef...'
# NOSTR_PUBLIC_KEY='123456abcdef...'
# NOSTR_RELAY_URL='wss://nostr.kosmos.org'
# RS_STORAGE_URL='https://storage.kosmos.org'
# RS_REDIS_URL='redis://localhost:6379/2'

View File

@@ -1,7 +1,9 @@
PRIMARY_DOMAIN=kosmos.org PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.kosmos.org
REDIS_URL='redis://localhost:6379/0' REDIS_URL='redis://localhost:6379/0'
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
BTCPAY_API_URL='http://btcpay.example.com/api/v1' BTCPAY_API_URL='http://btcpay.example.com/api/v1'
BTCPAY_STORE_ID='123456' BTCPAY_STORE_ID='123456'
@@ -10,10 +12,15 @@ DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
EJABBERD_API_URL='http://xmpp.example.com/api' EJABBERD_API_URL='http://xmpp.example.com/api'
MASTODON_PUBLIC_URL='http://example.social'
LNDHUB_API_URL='http://localhost:3026' LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946' LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
RS_STORAGE_URL='https://storage.kosmos.org' RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/1' RS_REDIS_URL='redis://localhost:6379/1'

View File

@@ -1 +1 @@
2.7.2 3.3.0

View File

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

17
Gemfile
View File

@@ -2,7 +2,7 @@ source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" } git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 7.0.2' gem 'rails', '~> 7.1'
# Use Puma as the app server # Use Puma as the app server
gem 'puma', '~> 4.1' gem 'puma', '~> 4.1'
# View components # View components
@@ -22,7 +22,7 @@ gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production # Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0' # gem 'redis', '~> 4.0'
# Use Active Model has_secure_password # Use Active Model has_secure_password
# gem 'bcrypt', '~> 3.1.7' gem 'bcrypt', '~> 3.1'
# Configuration # Configuration
gem 'dotenv-rails' gem 'dotenv-rails'
@@ -61,20 +61,19 @@ gem "sentry-rails"
# Services # Services
gem 'discourse_api' gem 'discourse_api'
gem "lnurl" gem "lnurl"
gem 'manifique', git: 'https://gitea.kosmos.org/5apps/manifique.git', branch: 'master' gem 'manifique', '~> 1.1.0'
gem 'nostr', git: 'https://gitea.kosmos.org/kosmos/nostr-gem.git', branch: 'feature/ruby_2.7_compat' gem 'nostr', '~> 0.6.0'
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.7.2'
gem 'rspec-rails' gem 'rspec-rails'
gem 'rails-controller-testing' gem 'rails-controller-testing'
gem "byebug", "~> 11.1"
end end
group :development do group :development do
# Access an interactive console on exception pages or by calling 'console' anywhere in the code. # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
gem 'web-console', '>= 3.3.0' gem 'web-console', '~> 4.2'
gem 'listen', '~> 3.2' gem 'listen', '~> 3.2'
gem 'letter_opener' gem 'letter_opener'
gem 'letter_opener_web' gem 'letter_opener_web'
@@ -90,8 +89,8 @@ group :test do
end end
group :production do group :production do
# Use postgresql as the database for Active Record gem 'pg', '~> 1.5'
gem 'pg', '~> 1.2.3'
end end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

View File

@@ -1,141 +1,127 @@
GIT
remote: https://gitea.kosmos.org/5apps/manifique.git
revision: 8d79113438ee7c3e4288f840a135622519cffd5c
branch: master
specs:
manifique (0.1.0)
faraday (~> 2.7.11)
faraday-follow_redirects (= 0.3.0)
nokogiri (~> 1.15.4)
GIT
remote: https://gitea.kosmos.org/kosmos/nostr-gem.git
revision: 596529d9eb50d13b3f385245636698fccf37b442
branch: feature/ruby_2.7_compat
specs:
nostr (0.4.0)
bech32 (~> 1.3)
bip-schnorr (~> 0.4)
ecdsa (~> 1.2)
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
json (~> 2.6)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.0.8) actioncable (7.1.3)
actionpack (= 7.0.8) actionpack (= 7.1.3)
activesupport (= 7.0.8) activesupport (= 7.1.3)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (7.0.8) zeitwerk (~> 2.6)
actionpack (= 7.0.8) actionmailbox (7.1.3)
activejob (= 7.0.8) actionpack (= 7.1.3)
activerecord (= 7.0.8) activejob (= 7.1.3)
activestorage (= 7.0.8) activerecord (= 7.1.3)
activesupport (= 7.0.8) activestorage (= 7.1.3)
activesupport (= 7.1.3)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.0.8) actionmailer (7.1.3)
actionpack (= 7.0.8) actionpack (= 7.1.3)
actionview (= 7.0.8) actionview (= 7.1.3)
activejob (= 7.0.8) activejob (= 7.1.3)
activesupport (= 7.0.8) activesupport (= 7.1.3)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.2)
actionpack (7.0.8) actionpack (7.1.3)
actionview (= 7.0.8) actionview (= 7.1.3)
activesupport (= 7.0.8) activesupport (= 7.1.3)
rack (~> 2.0, >= 2.2.4) nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.6)
actiontext (7.0.8) actiontext (7.1.3)
actionpack (= 7.0.8) actionpack (= 7.1.3)
activerecord (= 7.0.8) activerecord (= 7.1.3)
activestorage (= 7.0.8) activestorage (= 7.1.3)
activesupport (= 7.0.8) activesupport (= 7.1.3)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.0.8) actionview (7.1.3)
activesupport (= 7.0.8) activesupport (= 7.1.3)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.11)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.6)
activejob (7.0.8) activejob (7.1.3)
activesupport (= 7.0.8) activesupport (= 7.1.3)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.0.8) activemodel (7.1.3)
activesupport (= 7.0.8) activesupport (= 7.1.3)
activerecord (7.0.8) activerecord (7.1.3)
activemodel (= 7.0.8) activemodel (= 7.1.3)
activesupport (= 7.0.8) activesupport (= 7.1.3)
activestorage (7.0.8) timeout (>= 0.4.0)
actionpack (= 7.0.8) activestorage (7.1.3)
activejob (= 7.0.8) actionpack (= 7.1.3)
activerecord (= 7.0.8) activejob (= 7.1.3)
activesupport (= 7.0.8) activerecord (= 7.1.3)
activesupport (= 7.1.3)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) activesupport (7.1.3)
activesupport (7.0.8) base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.5) addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.2.0) aws-eventstream (1.3.0)
aws-partitions (1.839.0) aws-partitions (1.886.0)
aws-sdk-core (3.185.1) aws-sdk-core (3.191.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.72.0) aws-sdk-kms (1.77.0)
aws-sdk-core (~> 3, >= 3.184.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.136.0) aws-sdk-s3 (1.143.0)
aws-sdk-core (~> 3, >= 3.181.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6) aws-sigv4 (~> 1.8)
aws-sigv4 (1.6.0) aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
backport (1.2.0) backport (1.2.0)
base64 (0.1.1) base64 (0.2.0)
bcrypt (3.1.19) bcrypt (3.1.20)
bech32 (1.4.2) bech32 (1.4.2)
thor (>= 1.1.0) thor (>= 1.1.0)
benchmark (0.2.1) benchmark (0.3.0)
bigdecimal (3.1.6)
bindex (0.8.1) bindex (0.8.1)
bip-schnorr (0.6.0) bip-schnorr (0.7.0)
ecdsa_ext (~> 0.5.0) ecdsa_ext (~> 0.5.0)
brow (0.4.1)
builder (3.2.4) builder (3.2.4)
byebug (11.1.3) capybara (3.40.0)
capybara (3.39.2)
addressable addressable
matrix matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.11)
rack (>= 1.6.0) rack (>= 1.6.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
chunky_png (1.4.0) chunky_png (1.4.0)
concurrent-ruby (1.2.2) concurrent-ruby (1.2.3)
connection_pool (2.4.1) connection_pool (2.4.1)
crack (0.4.5) crack (0.4.6)
bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
cssbundling-rails (1.3.3) cssbundling-rails (1.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
database_cleaner (2.0.2) database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (>= 2, < 3)
@@ -143,7 +129,7 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.3.3) date (3.3.4)
devise (4.9.3) devise (4.9.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
@@ -153,7 +139,7 @@ GEM
devise_ldap_authenticatable (0.8.7) devise_ldap_authenticatable (0.8.7)
devise (>= 3.4.1) devise (>= 3.4.1)
net-ldap (>= 0.16.0) net-ldap (>= 0.16.0)
diff-lcs (1.5.0) diff-lcs (1.5.1)
discourse_api (2.0.1) discourse_api (2.0.1)
faraday (~> 2.7) faraday (~> 2.7)
faraday-follow_redirects faraday-follow_redirects
@@ -165,67 +151,72 @@ GEM
railties (>= 3.2) railties (>= 3.2)
down (5.4.1) down (5.4.1)
addressable (~> 2.8) addressable (~> 2.8)
drb (2.2.0)
ruby2_keywords
e2mmap (0.1.0) e2mmap (0.1.0)
ecdsa (1.2.0) ecdsa (1.2.0)
ecdsa_ext (0.5.0) ecdsa_ext (0.5.1)
ecdsa (~> 1.2.0) ecdsa (~> 1.2.0)
erubi (1.12.0) erubi (1.12.0)
et-orbi (1.2.7) et-orbi (1.2.7)
tzinfo tzinfo
event_emitter (0.2.6) event_emitter (0.2.6)
eventmachine (1.2.7) eventmachine (1.2.7)
factory_bot (6.2.1) factory_bot (6.4.6)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.2.0) factory_bot_rails (6.4.3)
factory_bot (~> 6.2.0) factory_bot (~> 6.4)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.2.1) faker (3.2.3)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.7.11) faraday (2.9.0)
base64 faraday-net_http (>= 2.0, < 3.2)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-follow_redirects (0.3.0) faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3) faraday (>= 1, < 3)
faraday-multipart (1.0.4) faraday-multipart (1.0.4)
multipart-post (~> 2) multipart-post (~> 2)
faraday-net_http (3.0.2) faraday-net_http (3.1.0)
net-http
faye-websocket (0.11.3) faye-websocket (0.11.3)
eventmachine (>= 0.12.0) eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1) websocket-driver (>= 0.5.1)
ffi (1.16.3) ffi (1.16.3)
flipper (1.0.0) flipper (1.2.2)
brow (~> 0.4.1)
concurrent-ruby (< 2) concurrent-ruby (< 2)
flipper-active_record (1.0.0) flipper-active_record (1.2.2)
activerecord (>= 4.2, < 8) activerecord (>= 4.2, < 8)
flipper (~> 1.0.0) flipper (~> 1.2.2)
flipper-ui (1.0.0) flipper-ui (1.2.2)
erubi (>= 1.0.0, < 2.0.0) erubi (>= 1.0.0, < 2.0.0)
flipper (~> 1.0.0) flipper (~> 1.2.2)
rack (>= 1.4, < 4) rack (>= 1.4, < 4)
rack-protection (>= 1.5.3, <= 4.0.0) rack-protection (>= 1.5.3, <= 4.0.0)
sanitize (< 7) sanitize (< 7)
fugit (1.8.1) fugit (1.9.0)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
hashdiff (1.0.1) hashdiff (1.1.0)
i18n (1.14.1) i18n (1.14.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
importmap-rails (1.2.1) importmap-rails (2.0.1)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
io-console (0.7.2)
irb (1.11.1)
rdoc
reline (>= 0.4.2)
jaro_winkler (1.5.6) jaro_winkler (1.5.6)
jbuilder (2.11.5) jbuilder (2.11.5)
actionview (>= 5.0.0) actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
jmespath (1.6.2) jmespath (1.6.2)
json (2.6.3) json (2.7.1)
kramdown (2.4.0) kramdown (2.4.0)
rexml rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
@@ -245,8 +236,8 @@ GEM
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
lnurl (1.1.0) lnurl (1.1.0)
bech32 (~> 1.1) bech32 (~> 1.1)
lockbox (1.3.0) lockbox (1.3.2)
loofah (2.21.4) loofah (2.22.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@@ -254,59 +245,85 @@ GEM
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
manifique (1.1.0)
faraday (~> 2.9.0)
faraday-follow_redirects (= 0.3.0)
nokogiri (~> 1.16.0)
marcel (1.0.2) marcel (1.0.2)
matrix (0.4.2) matrix (0.4.2)
method_source (1.0.0) method_source (1.0.0)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (5.20.0) mini_portile2 (2.8.5)
minitest (5.21.2)
multipart-post (2.3.0) multipart-post (2.3.0)
net-imap (0.3.7) mutex_m (0.2.0)
net-http (0.4.1)
uri
net-imap (0.4.9.1)
date date
net-protocol net-protocol
net-ldap (0.18.0) net-ldap (0.19.0)
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.1) net-protocol (0.2.2)
timeout timeout
net-smtp (0.4.0) net-smtp (0.4.0.1)
net-protocol net-protocol
nio4r (2.5.9) nio4r (2.7.0)
nokogiri (1.15.4-arm64-darwin) nokogiri (1.16.0)
mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.15.4-x86_64-linux) nokogiri (1.16.0-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.0-x86_64-linux)
racc (~> 1.4)
nostr (0.6.0)
bech32 (~> 1.4)
bip-schnorr (~> 0.7)
ecdsa (~> 1.2)
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
json (~> 2.6)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pagy (6.1.0) pagy (6.4.3)
parallel (1.23.0) parallel (1.24.0)
parser (3.2.2.4) parser (3.3.0.5)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.2.3) pg (1.5.4)
public_suffix (5.0.3) psych (5.1.2)
stringio
public_suffix (5.0.4)
puma (4.3.12) puma (4.3.12)
nio4r (~> 2.0) nio4r (~> 2.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.7.1) racc (1.7.3)
rack (2.2.8) rack (2.2.8)
rack-protection (3.1.0) rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4) rack (~> 2.2, >= 2.2.4)
rack-session (1.0.2)
rack (< 3)
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rails (7.0.8) rackup (1.0.0)
actioncable (= 7.0.8) rack (< 3)
actionmailbox (= 7.0.8) webrick
actionmailer (= 7.0.8) rails (7.1.3)
actionpack (= 7.0.8) actioncable (= 7.1.3)
actiontext (= 7.0.8) actionmailbox (= 7.1.3)
actionview (= 7.0.8) actionmailer (= 7.1.3)
activejob (= 7.0.8) actionpack (= 7.1.3)
activemodel (= 7.0.8) actiontext (= 7.1.3)
activerecord (= 7.0.8) actionview (= 7.1.3)
activestorage (= 7.0.8) activejob (= 7.1.3)
activesupport (= 7.0.8) activemodel (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.0.8) railties (= 7.1.3)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1)
@@ -321,21 +338,26 @@ GEM
rails-settings-cached (2.8.3) rails-settings-cached (2.8.3)
activerecord (>= 5.0.0) activerecord (>= 5.0.0)
railties (>= 5.0.0) railties (>= 5.0.0)
railties (7.0.8) railties (7.1.3)
actionpack (= 7.0.8) actionpack (= 7.1.3)
activesupport (= 7.0.8) activesupport (= 7.1.3)
method_source irb
rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.5) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.0.6) rake (13.1.0)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
rbs (2.8.4) rbs (2.8.4)
rdoc (6.6.2)
psych (>= 4.0.0)
redis (4.8.1) redis (4.8.1)
regexp_parser (2.8.2) regexp_parser (2.9.0)
reline (0.4.2)
io-console (~> 0.5)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
@@ -354,7 +376,7 @@ GEM
rspec-mocks (3.12.6) rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-rails (6.0.3) rspec-rails (6.1.1)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
@@ -363,19 +385,18 @@ GEM
rspec-mocks (~> 3.12) rspec-mocks (~> 3.12)
rspec-support (~> 3.12) rspec-support (~> 3.12)
rspec-support (3.12.1) rspec-support (3.12.1)
rubocop (1.57.1) rubocop (1.60.2)
base64 (~> 0.1.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.2.4) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.1, < 2.0) rubocop-ast (>= 1.30.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0) rubocop-ast (1.30.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.0) ruby-vips (2.2.0)
@@ -386,10 +407,10 @@ GEM
sanitize (6.1.0) sanitize (6.1.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
sentry-rails (5.12.0) sentry-rails (5.16.1)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.12.0) sentry-ruby (~> 5.16.1)
sentry-ruby (5.12.0) sentry-ruby (5.16.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (6.5.12) sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3) connection_pool (>= 2.2.5, < 3)
@@ -399,7 +420,7 @@ GEM
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8) sidekiq (>= 6, < 8)
tilt (>= 1.4.0) tilt (>= 1.4.0)
solargraph (0.49.0) solargraph (0.50.0)
backport (~> 1.2) backport (~> 1.2)
benchmark benchmark
bundler (~> 2.0) bundler (~> 2.0)
@@ -422,13 +443,16 @@ GEM
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.6.7-arm64-darwin) sqlite3 (1.7.2)
sqlite3 (1.6.7-x86_64-linux) mini_portile2 (~> 2.8.0)
stimulus-rails (1.3.0) sqlite3 (1.7.2-arm64-darwin)
sqlite3 (1.7.2-x86_64-linux)
stimulus-rails (1.3.3)
railties (>= 6.0.0) railties (>= 6.0.0)
stringio (3.1.0)
thor (1.3.0) thor (1.3.0)
tilt (2.3.0) tilt (2.3.0)
timeout (0.4.0) timeout (0.4.1)
turbo-rails (1.5.0) turbo-rails (1.5.0)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0) activejob (>= 6.0.0)
@@ -436,7 +460,8 @@ GEM
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0) unicode-display_width (2.5.0)
view_component (3.6.0) uri (0.13.0)
view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0) activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
method_source (~> 1.0) method_source (~> 1.0)
@@ -451,6 +476,7 @@ GEM
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.1)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
@@ -466,7 +492,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
aws-sdk-s3 aws-sdk-s3
byebug (~> 11.1) bcrypt (~> 3.1)
capybara capybara
cssbundling-rails cssbundling-rails
database_cleaner database_cleaner
@@ -489,13 +515,13 @@ DEPENDENCIES
listen (~> 3.2) listen (~> 3.2)
lnurl lnurl
lockbox lockbox
manifique! manifique (~> 1.1.0)
net-ldap net-ldap
nostr! nostr (~> 0.6.0)
pagy (~> 6.0, >= 6.0.2) pagy (~> 6.0, >= 6.0.2)
pg (~> 1.2.3) pg (~> 1.5)
puma (~> 4.1) puma (~> 4.1)
rails (~> 7.0.2) rails (~> 7.1)
rails-controller-testing rails-controller-testing
rails-settings-cached (~> 2.8.3) rails-settings-cached (~> 2.8.3)
rqrcode (~> 2.0) rqrcode (~> 2.0)
@@ -506,14 +532,14 @@ DEPENDENCIES
sidekiq-scheduler sidekiq-scheduler
solargraph solargraph
sprockets-rails sprockets-rails
sqlite3 (~> 1.4) sqlite3 (~> 1.7.2)
stimulus-rails stimulus-rails
turbo-rails turbo-rails
tzinfo-data tzinfo-data
view_component view_component
warden warden
web-console (>= 3.3.0) web-console (~> 4.2)
webmock webmock
BUNDLED WITH BUNDLED WITH
2.3.7 2.5.5

View File

@@ -14,8 +14,10 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in 1. Make sure [Docker Compose is installed][1] and Docker is running (included in
Docker Desktop) Docker Desktop)
3. Run `docker compose up` and wait until 389ds announces its successful start 3. Run `docker compose up --build` and wait until all services have started
in the log output (389ds might take an extra minute to be ready). This will take a while when
running for the first time, so you might want to do something else in the
meantime.
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"` 4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
5. `docker compose run web rails ldap:setup` 5. `docker compose run web rails ldap:setup`
6. `docker compose run web rails db:setup` 6. `docker compose run web rails db:setup`
@@ -28,38 +30,44 @@ have the password "user is user".
### Rails app ### Rails app
_Note: when using Docker Compose, prefix the following commands with `docker-compose
run web`._
Installing dependencies: Installing dependencies:
bundle install bundle install
yarn install yarn install
Setting up local database (SQLite): Migrating the local database (after schema changes):
bundle exec rails db:create
bundle exec rails db:migrate bundle exec rails db:migrate
Running the dev server and auto-building CSS files on change: Running the dev server, and auto-building CSS files on change _(automatic with Docker Compose)_:
bin/dev bin/dev
Running the background workers (requires Redis): Running the background workers (requires Redis) _(automatic with Docker Compose)_:
bundle exec sidekiq -C config/sidekiq.yml bundle exec sidekiq -C config/sidekiq.yml
Running all specs: Running the test suite:
bundle exec rspec bundle exec rspec
### Docker (Compose) Running the test suite with Docker Compose requires overriding the Rails
environment:
There is a working Docker Compose config file, which define a number of services including docker-compose run -e "RAILS_ENV=test" web rspec
an app server for Rails as well as a local 389ds (LDAP) server.
For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`, ### Docker Compose
listening on port 389 on your machine.
You can pick and choose your services adding them by name (listed in `docker-compose.yml`) at Services/containers are configured in `docker-compose.yml`.
the end of the docker compose command. eg. `docker compose up ldap redis`
You can run services selectively, for example if you want to run the Rails app
and test suite on the host machine. Just add the service names of the
containers you want to run to the `up` command, like so:
docker-compose up ldap redis
#### LDAP server #### LDAP server
@@ -76,13 +84,15 @@ Now you can seed the back-end with data using this Rails task:
The setup task will first delete any existing entries in the directory tree The setup task will first delete any existing entries in the directory tree
("dc=kosmos,dc=org"), and then create our development entries. ("dc=kosmos,dc=org"), and then create our development entries.
Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over Note that all 389ds data is stored in the `389ds-data` volume. So if you want
with a fresh installation, delete both that directory as well as the container. to start over with a fresh installation, delete both that volume as well as the
container.
#### Minio / RS #### Minio / remoteStorage
If you want to run remoteStorage accounts locally, you will have to create the If you want to run remoteStorage accounts locally, you will have to create the
respective bucket first: respective bucket first. With the `minio` container running (run by default
when using Docker Compose), follow these steps:
* `docker compose up web redis minio liquor-cabinet` * `docker compose up web redis minio liquor-cabinet`
* Head to http://localhost:9001 and log in with user `minioadmin`, password * Head to http://localhost:9001 and log in with user `minioadmin`, password

View File

@@ -32,11 +32,21 @@
focus:ring-blue-400 focus:ring-opacity-75; focus:ring-blue-400 focus:ring-opacity-75;
} }
.btn-emerald {
@apply bg-emerald-500 hover:bg-emerald-600 text-white
focus:ring-emerald-400 focus:ring-opacity-75;
}
.btn-red { .btn-red {
@apply bg-red-600 hover:bg-red-700 text-white @apply bg-red-600 hover:bg-red-700 text-white
focus:ring-red-500 focus:ring-opacity-75; focus:ring-red-500 focus:ring-opacity-75;
} }
.btn-outline-purple {
@apply border-2 border-purple-500 hover:bg-purple-100
focus:ring-purple-400 focus:ring-opacity-75;
}
.btn:disabled { .btn:disabled {
@apply bg-gray-100 hover:bg-gray-200 text-gray-400 @apply bg-gray-100 hover:bg-gray-200 text-gray-400
focus:ring-gray-300 focus:ring-opacity-75; focus:ring-gray-300 focus:ring-opacity-75;

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,21 @@
<div class="relative inline-block"> <div class="relative inline-block">
<div role="button" tabindex="0" data-dropdown-target="button" <div role="button" tabindex="0" data-dropdown-target="button"
class="inline-block select-none"> class="inline-block select-none">
<% if @size == :large %>
<span class="appearance-none flex items-center inline-block"> <span class="appearance-none flex items-center inline-block">
<span class="p-2 bg-gray-50 hover:bg-gray-100 rounded-full"> <span class="p-2 bg-gray-50 hover:bg-gray-100 rounded-full">
<%= render partial: "icons/kebab-menu", locals: { <%= render partial: "icons/#{@icon_name}",
custom_class: "inline text-gray-500 h-6 w-6" locals: { custom_class: "inline text-gray-500 h-6 w-6" } %>
} %>
</span> </span>
</span> </span>
<% elsif @size == :small %>
<span class="appearance-none flex items-center inline-block">
<span class="text-gray-500 hover:text-blue-600">
<%= render partial: "icons/#{@icon_name}",
locals: { custom_class: "inline h-4 w-4" } %>
</span>
</span>
<% end %>
</div> </div>
<div data-dropdown-target="menu" <div data-dropdown-target="menu"
data-transition-enter="transition ease-out duration-200" data-transition-enter="transition ease-out duration-200"

View File

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

View File

@@ -6,6 +6,7 @@
) do %> ) do %>
<%= method("#{@type}_field").call :setting, @key, <%= method("#{@type}_field").call :setting, @key,
value: Setting.public_send(@key), value: Setting.public_send(@key),
placeholder: @placeholder,
data: { data: {
:'default-value' => Setting.get_field(@key)[:default] :'default-value' => Setting.get_field(@key)[:default]
}, },

View File

@@ -2,7 +2,7 @@
module FormElements module FormElements
class FieldsetResettableSettingComponent < ViewComponent::Base class FieldsetResettableSettingComponent < ViewComponent::Base
def initialize(tag: "li", key:, type: :text, title:, description: nil) def initialize(tag: "li", key:, type: :text, title:, description: nil, placeholder: nil)
@tag = tag @tag = tag
@positioning = :vertical @positioning = :vertical
@title = title @title = title
@@ -10,6 +10,7 @@ module FormElements
@key = key.to_sym @key = key.to_sym
@type = type @type = type
@resettable = is_resettable?(@key) @resettable = is_resettable?(@key)
@placeholder = placeholder
end end
def is_resettable?(key) def is_resettable?(key)

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8"> <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] bg-white rounded-lg shadow">
<div class="px-6 sm:px-12 pt-2 sm:pt-4"> <div class="px-6 sm:px-12 pt-2 sm:pt-4">
<%= render partial: @tabnav_partial %> <%= render partial: @tabnav_partial %>
</div> </div>

View File

@@ -12,15 +12,17 @@
<!-- Modal Container --> <!-- Modal Container -->
<div data-modal-target="container" <div data-modal-target="container"
class="max-h-screen w-auto max-w-lg relative class="relative m-4 max-h-screen w-auto max-w-full
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center"> hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
<!-- Modal Card --> <!-- Modal Card -->
<div class="m-1 bg-white rounded shadow"> <div class="m-1 bg-white rounded shadow">
<div class="p-8"> <div class="p-8">
<%= content %> <%= content %>
<% if @show_close_button %>
<div class="flex justify-end items-center flex-wrap mt-6"> <div class="flex justify-end items-center flex-wrap mt-6">
<button class="btn-md btn-blue" data-action="click->modal#close:prevent">Close</button> <button class="btn-md btn-blue" data-action="click->modal#close:prevent">Close</button>
</div> </div>
<% end %>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -34,6 +34,8 @@ class NotificationComponent < ViewComponent::Base
'alert-octagon' 'alert-octagon'
when 'alert' when 'alert'
'alert-octagon' 'alert-octagon'
when 'warning'
'alert-octagon'
else else
'info' 'info'
end end

View File

@@ -1,10 +1,10 @@
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="h-16 w-16 flex-none"> <div class="h-16 w-16 flex-none">
<%= image_tag s3_image_url(@web_app.icon), class: "h-full w-full" %> <%= render AppCatalog::WebAppIconComponent.new(web_app: @web_app) %>
</div> </div>
<div class="flex-grow"> <div class="flex-grow">
<h4 class="mb-1 text-lg font-bold"> <h4 class="mb-1 text-lg font-bold">
<%= @web_app.name %> <%= @web_app&.name || @auth.app_name %>
</h4> </h4>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
<%= @auth.client_id %> <%= @auth.client_id %>

View File

@@ -3,18 +3,16 @@ class Admin::DonationsController < Admin::BaseController
before_action :set_current_section, only: [:index, :show, :new, :edit] before_action :set_current_section, only: [:index, :show, :new, :edit]
# GET /donations # GET /donations
# GET /donations.json
def index def index
@pagy, @donations = pagy(Donation.all.order('created_at desc')) @pagy, @donations = pagy(Donation.completed.order('paid_at desc'))
@stats = { @stats = {
overall_sats: @donations.all.sum("amount_sats"), overall_sats: @donations.sum("amount_sats"),
donor_count: Donation.distinct.count(:user_id) donor_count: Donation.completed.count(:user_id)
} }
end end
# GET /donations/1 # GET /donations/1
# GET /donations/1.json
def show def show
end end
@@ -28,55 +26,42 @@ class Admin::DonationsController < Admin::BaseController
end end
# POST /donations # POST /donations
# POST /donations.json
def create def create
@donation = Donation.new(donation_params) @donation = Donation.new(donation_params)
respond_to do |format| if @donation.paid_at == nil
@donation.errors.add(:paid_at, message: "is required")
render :new, status: :unprocessable_entity and return
end
if @donation.save if @donation.save
format.html do
redirect_to admin_donation_url(@donation), flash: { redirect_to admin_donation_url(@donation), flash: {
success: 'Donation was successfully created.' success: 'Donation was successfully created.'
} }
end
format.json { render :show, status: :created, location: @donation }
else else
format.html { render :new, status: :unprocessable_entity } render :new, status: :unprocessable_entity
format.json { render json: @donation.errors, status: :unprocessable_entity }
end
end end
end end
# PATCH/PUT /donations/1 # PUT /donations/1
# PATCH/PUT /donations/1.json
def update def update
respond_to do |format|
if @donation.update(donation_params) if @donation.update(donation_params)
format.html do
redirect_to admin_donation_url(@donation), flash: { redirect_to admin_donation_url(@donation), flash: {
success: 'Donation was successfully updated.' success: 'Donation was successfully updated.'
} }
end
format.json { render :show, status: :ok, location: @donation }
else else
format.html { render :edit, status: :unprocessable_entity } render :edit, status: :unprocessable_entity
format.json { render json: @donation.errors, status: :unprocessable_entity }
end
end end
end end
# DELETE /donations/1 # DELETE /donations/1
# DELETE /donations/1.json
def destroy def destroy
@donation.destroy @donation.destroy
respond_to do |format|
format.html do redirect_to admin_donations_url, flash: { redirect_to admin_donations_url, flash: {
success: 'Donation was successfully destroyed.' success: 'Donation was successfully destroyed.'
} }
end end
format.json { head :no_content }
end
end
private private
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
@@ -86,7 +71,10 @@ class Admin::DonationsController < Admin::BaseController
# Only allow a list of trusted parameters through. # Only allow a list of trusted parameters through.
def donation_params def donation_params
params.require(:donation).permit(:user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :paid_at) params.require(:donation).permit(
:user_id, :donation_method,
:amount_sats, :fiat_amount, :fiat_currency,
:public_name, :paid_at)
end end
def set_current_section def set_current_section

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
class Api::BtcpayController < Api::BaseController class Api::BtcpayController < Api::BaseController
before_action :require_feature_enabled before_action :require_feature_enabled
before_action :set_cors_access_control_headers
def onchain_btc_balance def onchain_btc_balance
balance = BtcpayManager::FetchOnchainWalletBalance.call balance = BtcpayManager::FetchOnchainWalletBalance.call
@@ -26,4 +27,11 @@ class Api::BtcpayController < Api::BaseController
http_status :not_found and return http_status :not_found and return
end end
end end
def set_cors_access_control_headers
return unless Rails.env.development?
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Headers'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end end

View File

@@ -41,4 +41,31 @@ class ApplicationController < ActionController::Base
def after_sign_in_path_for(user) def after_sign_in_path_for(user)
session[:user_return_to] || root_path session[:user_return_to] || root_path
end end
def lndhub_authenticate(options={})
if session[:ln_auth_token].present? && !options[:force_reauth]
@ln_auth_token = session[:ln_auth_token]
else
lndhub = Lndhub.new
auth_token = lndhub.authenticate(current_user)
session[:ln_auth_token] = auth_token
@ln_auth_token = auth_token
end
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
def lndhub_fetch_balance
@balance = LndhubManager::FetchUserBalance.call(auth_token: @ln_auth_token)
rescue AuthError
lndhub_authenticate(force_reauth: true)
raise if @fetch_balance_retried
@fetch_balance_retried = true
lndhub_fetch_balance
end
def nostr_event_from_params
params.permit!
params[:signed_event].to_h.symbolize_keys
end
end end

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ class Services::ChatController < Services::BaseController
before_action :require_service_available before_action :require_service_available
def show def show
@service_enabled = current_user.services_enabled.include?(:xmpp) @service_enabled = current_user.service_enabled?(:xmpp)
end end
private private

View File

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

View File

@@ -2,10 +2,11 @@ require "rqrcode"
require "lnurl" require "lnurl"
class Services::LightningController < ApplicationController class Services::LightningController < ApplicationController
before_action :authenticate_user!
before_action :authenticate_with_lndhub
before_action :set_current_section before_action :set_current_section
before_action :fetch_balance before_action :require_service_available
before_action :authenticate_user!
before_action :lndhub_authenticate
before_action :lndhub_fetch_balance
def index def index
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}" @wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
@@ -55,32 +56,12 @@ class Services::LightningController < ApplicationController
private private
def authenticate_with_lndhub(options={})
if session[:ln_auth_token].present? && !options[:force_reauth]
@ln_auth_token = session[:ln_auth_token]
else
lndhub = Lndhub.new
auth_token = lndhub.authenticate(current_user)
session[:ln_auth_token] = auth_token
@ln_auth_token = auth_token
end
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
def set_current_section def set_current_section
@current_section = :services @current_section = :services
end end
def fetch_balance def require_service_available
lndhub = Lndhub.new http_status :not_found unless Setting.lndhub_enabled?
data = lndhub.balance @ln_auth_token
@balance = data["BTC"]["AvailableBalance"] rescue nil
rescue AuthError
authenticate_with_lndhub(force_reauth: true)
raise if @fetch_balance_retried
@fetch_balance_retried = true
fetch_balance
end end
def fetch_transactions def fetch_transactions

View File

@@ -3,7 +3,7 @@ class Services::MastodonController < Services::BaseController
before_action :require_service_available before_action :require_service_available
def show def show
@service_enabled = current_user.services_enabled.include?(:mastodon) @service_enabled = current_user.service_enabled?(:mastodon)
end end
private private

View File

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

View File

@@ -3,13 +3,18 @@ class Services::RsAuthsController < Services::BaseController
before_action :require_feature_enabled before_action :require_feature_enabled
before_action :require_service_available before_action :require_service_available
# before_action :require_service_enabled # before_action :require_service_enabled
before_action :find_rs_auth before_action :find_rs_auth, only: [:destroy, :launch_app]
def index
@rs_auths = current_user.remote_storage_authorizations
# TODO sort by app name?
end
def destroy def destroy
@auth.destroy! @auth.destroy!
respond_to do |format| respond_to do |format|
format.html do redirect_to services_storage_url, flash: { format.html do redirect_to apps_services_storage_url, flash: {
success: 'App authorization revoked' success: 'App authorization revoked'
} }
end end

View File

@@ -1,17 +1,22 @@
require 'securerandom' require "securerandom"
require "bcrypt"
class SettingsController < ApplicationController class SettingsController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_main_nav_section before_action :set_main_nav_section
before_action :set_settings_section, only: [:show, :update, :update_email] before_action :set_settings_section, only: [:show, :update, :update_email, :reset_email_password]
before_action :set_user, only: [:show, :update, :update_email] before_action :set_user, only: [:show, :update, :update_email, :reset_email_password]
def index def index
redirect_to setting_path(:profile) redirect_to setting_path(:profile)
end end
def show def show
if @settings_section == "experiments" case @settings_section
when "lightning"
@notifications_enabled = @user.preferences[:lightning_notify_sats_received] != "disabled" ||
@user.preferences[:lightning_notify_zap_received] != "disabled"
when "nostr"
session[:shared_secret] ||= SecureRandom.base64(12) session[:shared_secret] ||= SecureRandom.base64(12)
end end
end end
@@ -23,11 +28,11 @@ class SettingsController < ApplicationController
if @user.save if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name]) if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
LdapManager::UpdateDisplayName.call(@user.dn, @user.display_name) LdapManager::UpdateDisplayName.call(dn: @user.dn, display_name: @user.display_name)
end end
if @user.avatar_new.present? if @user.avatar_new.present?
LdapManager::UpdateAvatar.call(@user.dn, @user.avatar_new) LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
end end
redirect_to setting_path(@settings_section), flash: { redirect_to setting_path(@settings_section), flash: {
@@ -40,7 +45,7 @@ class SettingsController < ApplicationController
end end
def update_email def update_email
if @user.valid_ldap_authentication?(email_params[:current_password]) if @user.valid_ldap_authentication?(security_params[:current_password])
if @user.update email: email_params[:email] if @user.update email: email_params[:email]
redirect_to setting_path(:account), flash: { redirect_to setting_path(:account), flash: {
notice: 'Please confirm your new address using the confirmation link we just sent you.' notice: 'Please confirm your new address using the confirmation link we just sent you.'
@@ -56,6 +61,28 @@ class SettingsController < ApplicationController
end end
end end
def reset_email_password
@user.current_password = security_params[:current_password]
if @user.valid_ldap_authentication?(@user.current_password)
@user.current_password = nil
session[:new_email_password] = generate_email_password
hashed_password = hash_email_password(session[:new_email_password])
LdapManager::UpdateEmailPassword.call(dn: @user.dn, password_hash: hashed_password)
if @user.ldap_entry[:email_maildrop] != @user.address
LdapManager::UpdateEmailMaildrop.call(dn: @user.dn, address: @user.address)
end
redirect_to new_password_services_email_path
else
@validation_errors = {
current_password: [ "Wrong password. Try again!" ]
}
render :show, status: :forbidden
end
end
def reset_password def reset_password
current_user.send_reset_password_instructions current_user.send_reset_password_instructions
sign_out current_user sign_out current_user
@@ -64,40 +91,39 @@ class SettingsController < ApplicationController
end end
def set_nostr_pubkey def set_nostr_pubkey
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys signed_event = Nostr::Event.new(**nostr_event_from_params)
is_valid_id = NostrManager::ValidateId.call(signed_event)
is_valid_sig = NostrManager::VerifySignature.call(signed_event)
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
unless is_valid_id && is_valid_sig && is_correct_content is_valid_sig = signed_event.verify_signature
is_valid_auth = NostrManager::VerifyAuth.call(
event: signed_event,
challenge: session[:shared_secret]
)
unless is_valid_sig && is_valid_auth
flash[:alert] = "Public key could not be verified" flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return http_status :unprocessable_entity and return
end end
pubkey_taken = User.all_except(current_user).where( user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
ou: current_user.ou, nostr_pubkey: signed_event[:pubkey]
).any?
if pubkey_taken if user_with_pubkey.present? && (user_with_pubkey != current_user)
flash[:alert] = "Public key already in use for a different account" flash[:alert] = "Public key already in use for a different account"
http_status :unprocessable_entity and return http_status :unprocessable_entity and return
end end
current_user.update! nostr_pubkey: signed_event[:pubkey] LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event.pubkey)
session[:shared_secret] = nil session[:shared_secret] = nil
flash[:success] = "Public key verification successful" flash[:success] = "Public key verification successful"
http_status :ok http_status :ok
rescue
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end end
# DELETE /settings/nostr_pubkey # DELETE /settings/nostr_pubkey
def remove_nostr_pubkey def remove_nostr_pubkey
current_user.update! nostr_pubkey: nil # TODO require current pubkey or password to delete
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: nil)
redirect_to setting_path(:experiments), flash: { redirect_to setting_path(:nostr), flash: {
success: 'Public key removed from account' success: 'Public key removed from account'
} }
end end
@@ -110,7 +136,10 @@ class SettingsController < ApplicationController
def set_settings_section def set_settings_section
@settings_section = params[:section] @settings_section = params[:section]
allowed_sections = [:profile, :account, :lightning, :xmpp, :experiments] allowed_sections = [
:profile, :account, :xmpp, :email,
:lightning, :remotestorage, :nostr
]
unless allowed_sections.include?(@settings_section.to_sym) unless allowed_sections.include?(@settings_section.to_sym)
redirect_to setting_path(:profile) redirect_to setting_path(:profile)
@@ -122,19 +151,26 @@ class SettingsController < ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:display_name, :avatar, preferences: [ params.require(:user).permit(
:lightning_notify_sats_received, :display_name, :avatar, preferences: UserPreferences.pref_keys
:xmpp_exchange_contacts_with_invitees )
])
end end
def email_params def email_params
params.require(:user).permit(:email, :current_password) params.require(:user).permit(:email)
end end
def nostr_event_params def security_params
params.permit(signed_event: [ params.require(:user).permit(:current_password)
:id, :pubkey, :created_at, :kind, :tags, :content, :sig end
])
def generate_email_password
characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join
end
def hash_email_password(password)
salt = BCrypt::Engine.generate_salt
BCrypt::Engine.hash_secret(password, salt)
end end
end end

View File

@@ -96,13 +96,13 @@ class SignupController < ApplicationController
session[:new_user] = nil session[:new_user] = nil
session[:validation_error] = nil session[:validation_error] = nil
CreateAccount.call( CreateAccount.call(account: {
username: @user.cn, username: @user.cn,
domain: Setting.primary_domain, domain: Setting.primary_domain,
email: @user.email, email: @user.email,
password: @user.password, password: @user.password,
invitation: @invitation invitation: @invitation
) })
end end
def set_context def set_context

View File

@@ -0,0 +1,62 @@
# frozen_string_literal: true
class Users::SessionsController < Devise::SessionsController
# before_action :configure_sign_in_params, only: [:create]
# GET /resource/sign_in
def new
session[:shared_secret] = SecureRandom.base64(12)
super
end
# POST /resource/sign_in
# def create
# super
# end
# DELETE /resource/sign_out
# def destroy
# super
# end
# POST /users/nostr_login
def nostr_login
signed_event = Nostr::Event.new(**nostr_event_from_params)
is_valid_sig = signed_event.verify_signature
is_valid_auth = NostrManager::VerifyAuth.call(
event: signed_event,
challenge: session[:shared_secret]
)
session[:shared_secret] = nil
unless is_valid_sig && is_valid_auth
flash[:alert] = "Login verification failed"
http_status :unauthorized and return
end
user = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
if user.present?
set_flash_message!(:notice, :signed_in)
sign_in("user", user)
render json: { redirect_url: after_sign_in_path_for(user) }, status: :ok
else
flash[:alert] = "Failed to find your account. Nostr login may be disabled."
http_status :unauthorized
end
end
protected
def set_flash_message(key, kind, options = {})
# Hide flash message after redirecting from a signin route while logged in
super unless key == :alert && kind == "already_authenticated"
end
# If you have extra params to permit, append them to the sanitizer.
# def configure_sign_in_params
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
# end
end

View File

@@ -7,14 +7,15 @@ class WebfingerController < ApplicationController
resource = params[:resource] resource = params[:resource]
if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1) if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1)
@username, @org = @useraddress.split("@") @username, @domain = @useraddress.split("@")
unless Rails.env.development? unless Rails.env.development?
# Allow different domains (e.g. localhost:3000) in development only # Allow different domains (e.g. localhost:3000) in development only
head 404 and return unless @org == Setting.primary_domain head 404 and return unless @domain == Setting.primary_domain
end end
unless User.where(cn: @username.downcase, ou: Setting.primary_domain).any? unless @user = User.where(ou: Setting.primary_domain)
.find_by(cn: @username.downcase)
head 404 and return head 404 and return
end end
@@ -28,12 +29,50 @@ class WebfingerController < ApplicationController
private private
def webfinger def webfinger
links = []; jrd = {
subject: "acct:#{@user.address}",
aliases: [],
links: []
}
# TODO check if storage service is enabled for user, not just globally if Setting.mastodon_enabled && @user.service_enabled?(:mastodon)
links << remotestorage_link if Setting.remotestorage_enabled # https://docs.joinmastodon.org/spec/webfinger/
jrd[:aliases] += mastodon_aliases
jrd[:links] += mastodon_links
end
{ "links" => links } if Setting.remotestorage_enabled && @user.service_enabled?(:remotestorage)
# https://datatracker.ietf.org/doc/draft-dejong-remotestorage/
jrd[:links] << remotestorage_link
end
jrd
end
def mastodon_aliases
[
"#{Setting.mastodon_public_url}/@#{@user.cn}",
"#{Setting.mastodon_public_url}/users/#{@user.cn}"
]
end
def mastodon_links
[
{
rel: "http://webfinger.net/rel/profile-page",
type: "text/html",
href: "#{Setting.mastodon_public_url}/@#{@user.cn}"
},
{
rel: "self",
type: "application/activity+json",
href: "#{Setting.mastodon_public_url}/users/#{@user.cn}"
},
{
rel: "http://ostatus.org/schema/1.0/subscribe",
template: "#{Setting.mastodon_public_url}/authorize_interaction?uri={uri}"
}
]
end end
def remotestorage_link def remotestorage_link
@@ -41,9 +80,9 @@ class WebfingerController < ApplicationController
storage_url = "#{Setting.rs_storage_url}/#{@username}" storage_url = "#{Setting.rs_storage_url}/#{@username}"
{ {
"rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage", rel: "http://tools.ietf.org/id/draft-dejong-remotestorage",
"href" => storage_url, href: storage_url,
"properties" => { properties: {
"http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13", "http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13",
"http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url, "http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url,
"http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter "http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter
@@ -54,7 +93,8 @@ class WebfingerController < ApplicationController
end end
def allow_cross_origin_requests def allow_cross_origin_requests
headers['Access-Control-Allow-Origin'] = '*' return unless Rails.env.development?
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS' headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end end
end end

View File

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

View File

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

View File

@@ -1,10 +1,6 @@
module ApplicationHelper module ApplicationHelper
include Pagy::Frontend include Pagy::Frontend
def sats_to_btc(sats)
sats.to_f / 100000000
end
def main_nav_class(current_section, link_to_section) def main_nav_class(current_section, link_to_section)
if 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" "bg-gray-900/50 text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"

View File

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

View File

@@ -0,0 +1,53 @@
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="nostr-login"
export default class extends Controller {
static targets = [ "loginForm", "loginButton" ]
static values = { site: String, sharedSecret: String }
connect() {
if (window.nostr) {
this.loginButtonTarget.disabled = false
this.loginFormTarget.classList.remove("hidden")
}
}
async login () {
this.loginButtonTarget.disabled = true
try {
// Auth based on NIP-42
const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000),
kind: 22242,
tags: [
["site", this.siteValue],
["challenge", this.sharedSecretValue]
],
content: ""
})
const res = await fetch("/users/nostr_login", {
method: "POST", credentials: "include", headers: {
"Accept": "application/json", 'Content-Type': 'application/json',
"X-CSRF-Token": this.csrfToken
}, body: JSON.stringify({ signed_event: signedEvent })
})
if (res.status === 200) {
res.json().then(r => { window.location.href = r.redirect_url })
} else {
window.location.reload()
}
} catch (error) {
console.warn('Unable to authenticate:', error.message)
} finally {
this.loginButtonTarget.disabled = false
}
}
get csrfToken () {
const element = document.head.querySelector('meta[name="csrf-token"]')
return element.getAttribute("content")
}
}

View File

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

View File

@@ -1,24 +1,16 @@
import { Controller } from "@hotwired/stimulus" import { Controller } from "@hotwired/stimulus"
import { bech32 } from "bech32"
function hexToBytes (hex) {
let bytes = []
for (let c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16))
}
return bytes
}
// Connects to data-controller="settings--nostr-pubkey" // Connects to data-controller="settings--nostr-pubkey"
export default class extends Controller { export default class extends Controller {
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ] static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
static values = { userAddress: String, pubkeyHex: String, sharedSecret: String } static values = {
userAddress: String,
connect () { pubkeyHex: String,
if (this.hasPubkeyHexValue && this.pubkeyHexValue.length > 0) { site: String,
this.pubkeyBech32InputTarget.value = this.pubkeyBech32 sharedSecret: String
} }
connect () {
if (window.nostr) { if (window.nostr) {
if (this.hasSetPubkeyTarget) { if (this.hasSetPubkeyTarget) {
this.setPubkeyTarget.disabled = false this.setPubkeyTarget.disabled = false
@@ -32,11 +24,15 @@ export default class extends Controller {
this.setPubkeyTarget.disabled = true this.setPubkeyTarget.disabled = true
try { try {
// Auth based on NIP-42
const signedEvent = await window.nostr.signEvent({ const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
kind: 1, kind: 22242,
tags: [], tags: [
content: `Connect my public key to ${this.userAddressValue} (confirmation ${this.sharedSecretValue})` ["site", this.siteValue],
["challenge", this.sharedSecretValue]
],
content: ""
}) })
const res = await fetch("/settings/set_nostr_pubkey", { const res = await fetch("/settings/set_nostr_pubkey", {
@@ -53,11 +49,6 @@ export default class extends Controller {
} }
} }
get pubkeyBech32 () {
const words = bech32.toWords(hexToBytes(this.pubkeyHexValue))
return bech32.encode('npub', words)
}
get csrfToken () { get csrfToken () {
const element = document.head.querySelector('meta[name="csrf-token"]') const element = document.head.querySelector('meta[name="csrf-token"]')
return element.getAttribute("content") return element.getAttribute("content")

View File

@@ -0,0 +1,28 @@
class BtcpayCheckDonationJob < ApplicationJob
queue_as :default
def perform(donation)
return if donation.completed?
invoice = BtcpayManager::FetchInvoice.call(
invoice_id: donation.btcpay_invoice_id
)
case invoice["status"]
when "Settled"
donation.paid_at = DateTime.now
donation.payment_status = "settled"
donation.save!
NotificationMailer.with(user: donation.user)
.bitcoin_donation_confirmed
.deliver_later
when "Processing"
re_enqueue_job(donation)
end
end
def re_enqueue_job(donation)
self.class.set(wait: 20.seconds).perform_later(donation)
end
end

View File

@@ -1,7 +1,7 @@
class CreateLdapUserJob < ApplicationJob class CreateLdapUserJob < ApplicationJob
queue_as :default queue_as :default
def perform(username, domain, email, hashed_pw) def perform(username:, domain:, email:, hashed_pw:, confirmed: false)
dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org" dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org"
attr = { attr = {
objectclass: ["top", "account", "person", "extensibleObject"], objectclass: ["top", "account", "person", "extensibleObject"],
@@ -12,6 +12,10 @@ class CreateLdapUserJob < ApplicationJob
userPassword: hashed_pw userPassword: hashed_pw
} }
if confirmed
attr[:serviceEnabled] = Setting.default_services
end
ldap_client.add(dn: dn, attributes: attr) ldap_client.add(dn: dn, attributes: attr)
end end

View File

@@ -0,0 +1,7 @@
class NostrPublishEventJob < ApplicationJob
queue_as :nostr
def perform(event:, relay_url:)
NostrManager::PublishEvent.call(event: event, relay_url: relay_url)
end
end

View File

@@ -2,8 +2,8 @@ class XmppExchangeContactsJob < ApplicationJob
queue_as :default queue_as :default
def perform(inviter, invitee) def perform(inviter, invitee)
return unless inviter.services_enabled.include?("xmpp") && return unless inviter.service_enabled?(:xmpp) &&
invitee.services_enabled.include?("xmpp") && invitee.service_enabled?(:xmpp) &&
inviter.preferences[:xmpp_exchange_contacts_with_invitees] inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new ejabberd = EjabberdApiClient.new

View File

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

View File

@@ -1,7 +1,7 @@
class AppCatalog::WebApp < ApplicationRecord class AppCatalog::WebApp < ApplicationRecord
store :metadata, coder: JSON store :metadata, coder: JSON
has_many :remote_storage_authorizations has_many :remote_storage_authorizations, dependent: :destroy
has_one_attached :icon has_one_attached :icon
has_one_attached :apple_touch_icon has_one_attached :apple_touch_icon
@@ -11,6 +11,6 @@ class AppCatalog::WebApp < ApplicationRecord
if: Proc.new { |a| a.url.present? } if: Proc.new { |a| a.url.present? }
def update_metadata def update_metadata
AppCatalogManager::UpdateMetadata.call(self) AppCatalogManager::UpdateMetadata.call(app: self)
end end
end end

View File

@@ -4,12 +4,25 @@ class Donation < ApplicationRecord
# Validations # Validations
validates_presence_of :user validates_presence_of :user
validates_presence_of :amount_sats validates_presence_of :donation_method,
validates_presence_of :paid_at inclusion: { in: %w[ custom btcpay lndhub ] }
validates_presence_of :payment_status, allow_nil: true,
# Hooks inclusion: { in: %w[ processing settled ] }
# TODO before_create :store_fiat_value validates_presence_of :paid_at, allow_nil: true
validates_presence_of :amount_sats, allow_nil: true
validates_presence_of :fiat_amount, allow_nil: true
validates_presence_of :fiat_currency, allow_nil: true,
inclusion: { in: %w[ EUR USD ] }
#Scopes #Scopes
scope :completed, -> { where.not(paid_at: nil) } scope :processing, -> { where(payment_status: "processing") }
scope :completed, -> { where(payment_status: "settled") }
def processing?
payment_status == "processing"
end
def completed?
payment_status == "settled"
end
end end

View File

@@ -2,7 +2,7 @@ class RemoteStorageAuthorization < ApplicationRecord
belongs_to :user belongs_to :user
belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true
serialize :permissions serialize :permissions unless Rails.env.production?
validates_presence_of :permissions validates_presence_of :permissions
validates_presence_of :client_id validates_presence_of :client_id
@@ -18,7 +18,7 @@ class RemoteStorageAuthorization < ApplicationRecord
before_create :store_token_in_redis before_create :store_token_in_redis
before_create :find_or_create_web_app before_create :find_or_create_web_app
after_create :schedule_token_expiry after_create :schedule_token_expiry
# after_create :notify_user after_create :notify_user
before_destroy :delete_token_from_redis before_destroy :delete_token_from_redis
after_destroy :remove_token_expiry_job after_destroy :remove_token_expiry_job
@@ -93,4 +93,22 @@ class RemoteStorageAuthorization < ApplicationRecord
rescue URI::InvalidURIError rescue URI::InvalidURIError
false false
end end
def notify_user
notify = user.preferences[:remotestorage_notify_auth_created]
case notify
when "xmpp"
router = Router.new
payload = {
type: "normal", to: user.address,
from: Setting.xmpp_notifications_from_address,
body: "You have just granted '#{self.client_id}' access to your Kosmos Storage. Visit your Storage dashboard to check on your connected apps and revoke permissions anytime: #{router.services_storage_url}"
}
XmppSendMessageJob.perform_later(payload)
when "email"
NotificationMailer.with(user: user, auth: self)
.remotestorage_auth_created.deliver_later
end
end
end end

View File

@@ -15,6 +15,9 @@ class Setting < RailsSettings::Base
field :redis_url, type: :string, field :redis_url, type: :string,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0" default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
field :s3_enabled, type: :boolean,
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
# #
# Registrations # Registrations
# #
@@ -48,6 +51,9 @@ class Setting < RailsSettings::Base
field :btcpay_enabled, type: :boolean, field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present? default: ENV["BTCPAY_API_URL"].present?
field :btcpay_public_url, type: :string,
default: ENV["BTCPAY_PUBLIC_URL"].presence
field :btcpay_store_id, type: :string, field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence default: ENV["BTCPAY_STORE_ID"].presence
@@ -154,7 +160,26 @@ class Setting < RailsSettings::Base
# Nostr # Nostr
# #
field :nostr_enabled, type: :boolean, default: true field :nostr_enabled, type: :boolean,
default: ENV["NOSTR_PRIVATE_KEY"].present?
field :nostr_private_key, type: :string,
default: ENV["NOSTR_PRIVATE_KEY"].presence
field :nostr_public_key, type: :string,
default: ENV["NOSTR_PUBLIC_KEY"].presence
field :nostr_relay_url, type: :string,
default: ENV["NOSTR_RELAY_URL"].presence
field :nostr_zaps_relay_limit, type: :integer,
default: 12
#
# OpenCollective
#
field :opencollective_enabled, type: :boolean, default: true
# #
# RemoteStorage # RemoteStorage
@@ -168,4 +193,35 @@ class Setting < RailsSettings::Base
field :rs_redis_url, type: :string, field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1" default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
#
# E-Mail Service
#
field :email_enabled, type: :boolean,
default: ENV["EMAIL_SMTP_HOST"].present?
# field :email_smtp_host, type: :string,
# default: ENV["EMAIL_SMTP_HOST"].presence
#
# field :email_smtp_port, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || 587
#
# field :email_smtp_enable_starttls, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || true
#
# field :email_auth_method, type: :string,
# default: ENV["EMAIL_AUTH_METHOD"].presence || "plain"
#
# field :email_imap_host, type: :string,
# default: ENV["EMAIL_IMAP_HOST"].presence
#
# field :email_imap_port, type: :string,
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
def self.default_services
# TODO Make configurable from respective service settings page
%w[ discourse gitea mastodon mediawiki xmpp ]
end
end end

View File

@@ -1,10 +1,13 @@
require 'nostr'
class User < ApplicationRecord class User < ApplicationRecord
include EmailValidatable include EmailValidatable
attr_accessor :display_name attr_accessor :display_name
attr_accessor :avatar_new attr_accessor :avatar_new
attr_accessor :current_password
serialize :preferences, UserPreferences serialize :preferences, coder: UserPreferences
# #
# Relations # Relations
@@ -14,16 +17,15 @@ class User < ApplicationRecord
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id' has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user has_one :inviter, through: :invitation, source: :user
has_many :invitees, through: :invitations has_many :invitees, through: :invitations
has_many :donations, dependent: :nullify has_many :donations, dependent: :nullify
has_many :remote_storage_authorizations
has_many :zaps
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user", has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
primary_key: "ln_account", foreign_key: "login" primary_key: "ln_account", foreign_key: "login"
has_many :accounts, through: :lndhub_user has_many :accounts, through: :lndhub_user
has_many :remote_storage_authorizations
# #
# Validations # Validations
# #
@@ -38,7 +40,8 @@ class User < ApplicationRecord
message: "is invalid. Usernames need to start with a letter." message: "is invalid. Usernames need to start with a letter."
# FIXME This needs a server restart to apply values # FIXME This needs a server restart to apply values
validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i, validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i,
message: "has already been taken" message: "has already been taken",
unless: Proc.new{ |u| u.persisted? }
validates_uniqueness_of :email validates_uniqueness_of :email
validates :email, email: true validates :email, email: true
@@ -46,8 +49,6 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true, validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) } if: -> { defined?(@display_name) }
validates_uniqueness_of :nostr_pubkey, allow_blank: true
validate :acceptable_avatar validate :acceptable_avatar
# #
@@ -88,13 +89,12 @@ class User < ApplicationRecord
def devise_after_confirmation def devise_after_confirmation
if ldap_entry[:mail] != self.email if ldap_entry[:mail] != self.email
# E-Mail update confirmed # E-Mail update confirmed
LdapManager::UpdateEmail.call(self.dn, self.email) LdapManager::UpdateEmail.call(dn: self.dn, address: self.email)
else else
# TODO Make configurable
# E-Mail from signup confirmed (i.e. account activation) # E-Mail from signup confirmed (i.e. account activation)
enable_service %w[ discourse gitea mediawiki xmpp ] enable_default_services
#TODO enable in development when we have easy setup of ejabberd etc. # TODO enable in development when we have easy setup of ejabberd etc.
return if Rails.env.development? || !Setting.ejabberd_enabled? return if Rails.env.development? || !Setting.ejabberd_enabled?
XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present? XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present?
@@ -130,7 +130,7 @@ class User < ApplicationRecord
def mastodon_address def mastodon_address
return nil unless Setting.mastodon_enabled? return nil unless Setting.mastodon_enabled?
"#{self.cn}@#{Setting.mastodon_address_domain}" "#{self.cn.gsub("-", "_")}@#{Setting.mastodon_address_domain}"
end end
def valid_attribute?(attribute_name) def valid_attribute?(attribute_name)
@@ -138,10 +138,8 @@ class User < ApplicationRecord
self.errors[attribute_name].blank? self.errors[attribute_name].blank?
end end
def ln_create_invoice(payload) def enable_default_services
lndhub = Lndhub.new enable_service Setting.default_services
lndhub.authenticate self
lndhub.addinvoice payload
end end
def dn def dn
@@ -158,26 +156,39 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name] @display_name ||= ldap_entry[:display_name]
end end
def nostr_pubkey
@nostr_pubkey ||= ldap_entry[:nostr_key]
end
def nostr_pubkey_bech32
return nil unless nostr_pubkey.present?
Nostr::PublicKey.new(nostr_pubkey).to_bech32
end
def avatar def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn, ou: ou) @avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end end
def services_enabled def services_enabled
ldap_entry[:service] || [] ldap_entry[:services_enabled] || []
end
def service_enabled?(name)
services_enabled.map(&:to_sym).include?(name.to_sym)
end end
def enable_service(service) def enable_service(service)
current_services = services_enabled current_services = services_enabled
new_services = Array(service).map(&:to_s) new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq services = (current_services + new_services).uniq
ldap.replace_attribute(dn, :service, services) ldap.replace_attribute(dn, :serviceEnabled, services)
end end
def disable_service(service) def disable_service(service)
current_services = services_enabled current_services = services_enabled
disabled_services = Array(service).map(&:to_s) disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :service, services) ldap.replace_attribute(dn, :serviceEnabled, services)
end end
def disable_all_services def disable_all_services

View File

@@ -26,4 +26,8 @@ class UserPreferences
end end
hash.stringify_keys!.to_h hash.stringify_keys!.to_h
end end
def self.pref_keys
DEFAULT_PREFS.keys.map(&:to_sym)
end
end end

20
app/models/zap.rb Normal file
View File

@@ -0,0 +1,20 @@
class Zap < ApplicationRecord
belongs_to :user
scope :settled, -> { where.not(settled_at: nil) }
scope :unpaid, -> { where(settled_at: nil) }
def request_event
nostr_event_from_hash(request)
end
def receipt_event
nostr_event_from_hash(receipt)
end
private
def nostr_event_from_hash(hash)
Nostr::Event.new(**hash.symbolize_keys)
end
end

View File

@@ -3,7 +3,7 @@ require "down"
module AppCatalogManager module AppCatalogManager
class UpdateMetadata < AppCatalogManagerService class UpdateMetadata < AppCatalogManagerService
def initialize(app) def initialize(app:)
@app = app @app = app
end end
@@ -18,6 +18,10 @@ module AppCatalogManager
@app.metadata[prop] = metadata.send(prop) if prop @app.metadata[prop] = metadata.send(prop) if prop
end end
@app.save!
# TODO move icon downloads to separate, async job
if icon = metadata.select_icon(sizes: "256x256") || if icon = metadata.select_icon(sizes: "256x256") ||
icon = metadata.select_icon(sizes: "192x192") icon = metadata.select_icon(sizes: "192x192")
attach_remote_image(:icon, icon) attach_remote_image(:icon, icon)
@@ -27,8 +31,6 @@ module AppCatalogManager
if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon") if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon")
attach_remote_image(:apple_touch_icon, apple_touch_icon) attach_remote_image(:apple_touch_icon, apple_touch_icon)
end end
@app.save!
rescue Manifique::Error => e rescue Manifique::Error => e
msg = "Fetching web app manifest failed for #{e.url}: #{e.type}" msg = "Fetching web app manifest failed for #{e.url}: #{e.type}"
Rails.logger.warn(msg) Rails.logger.warn(msg)
@@ -42,11 +44,20 @@ module AppCatalogManager
else else
download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}" download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}"
end end
filename = "#{attachment_name}.png" filename = "#{attachment_name}-#{Time.now.to_i}.png"
key = "web_apps/#{@app.id}/icons/#{attachment_name}.png" key = "web_apps/#{@app.id}/icons/#{filename}"
begin
tempfile = Down.download(download_url) tempfile = Down.download(download_url)
@app.send(attachment_name).attach(key: key, io: tempfile, filename: filename) @app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
rescue Down::NotFound
msg = "Download of \"#{attachment_name}\" failed: NotFound error for #{download_url}"
Rails.logger.warn(msg)
Sentry.capture_message(msg)
rescue => e
Rails.logger.warn "Saving attachment \"#{attachment_name}\" failed: \"#{e.message}\""
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
end end
end end
end end

View File

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

View File

@@ -0,0 +1,21 @@
module BtcpayManager
class CreateInvoice < BtcpayManagerService
def initialize(amount:, currency:, redirect_url:)
@amount = amount
@currency = currency
@redirect_url = redirect_url
end
def call
post "/invoices", {
amount: @amount.to_s,
currency: @currency,
checkout: {
redirectURL: @redirect_url,
redirectAutomatically: true,
requiresRefundEmail: false
}
}
end
end
end

View File

@@ -0,0 +1,14 @@
module BtcpayManager
class FetchExchangeRate < BtcpayManagerService
def initialize(fiat_currency:)
@fiat_currency = fiat_currency
end
def call
pair_str = "BTC_#{@fiat_currency}"
res = get "rates", { currencyPair: pair_str }
pair = res.find{|p| p["currencyPair"] == pair_str }
rate = pair["rate"].to_f
end
end
end

View File

@@ -0,0 +1,14 @@
module BtcpayManager
class FetchInvoice < BtcpayManagerService
def initialize(invoice_id:)
@invoice_id = invoice_id
end
def call
invoice = get "/invoices/#{@invoice_id}"
payment_methods = get "/invoices/#{@invoice_id}/payment-methods"
invoice["paymentMethods"] = payment_methods
invoice
end
end
end

View File

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

View File

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

View File

@@ -2,23 +2,35 @@
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/ # API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
# #
class BtcpayManagerService < ApplicationService class BtcpayManagerService < ApplicationService
attr_reader :base_url, :store_id, :auth_token
def initialize
@base_url = Setting.btcpay_api_url
@store_id = Setting.btcpay_store_id
@auth_token = Setting.btcpay_auth_token
end
private private
def get(endpoint) def base_url
res = Faraday.get("#{base_url}/#{endpoint}", {}, { @base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
end
def auth_token
@auth_token ||= Setting.btcpay_auth_token
end
def headers
{
"Content-Type" => "application/json", "Content-Type" => "application/json",
"Accept" => "application/json", "Accept" => "application/json",
"Authorization" => "token #{auth_token}" "Authorization" => "token #{auth_token}"
}) }
end
def endpoint_url(path)
"#{base_url}/#{path.gsub(/^\//, '')}"
end
def get(path, params = {})
res = Faraday.get endpoint_url(path), params, headers
JSON.parse(res.body)
end
def post(path, payload)
res = Faraday.post endpoint_url(path), payload.to_json, headers
JSON.parse(res.body) JSON.parse(res.body)
end end
end end

View File

@@ -1,11 +1,11 @@
class CreateAccount < ApplicationService class CreateAccount < ApplicationService
def initialize(args) def initialize(account:)
@username = args[:username] @username = account[:username]
@domain = args[:ou] || Setting.primary_domain @domain = account[:ou] || Setting.primary_domain
@email = args[:email] @email = account[:email]
@password = args[:password] @password = account[:password]
@invitation = args[:invitation] @invitation = account[:invitation]
@confirmed = args[:confirmed] @confirmed = account[:confirmed]
end end
def call def call
@@ -35,11 +35,15 @@ class CreateAccount < ApplicationService
@invitation.update! invited_user_id: user_id, used_at: DateTime.now @invitation.update! invited_user_id: user_id, used_at: DateTime.now
end end
# TODO move to confirmation
# (and/or add email_confirmed to entry and use in login filter)
def add_ldap_document def add_ldap_document
hashed_pw = Devise.ldap_auth_password_builder.call(@password) hashed_pw = Devise.ldap_auth_password_builder.call(@password)
CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw) CreateLdapUserJob.perform_later(
username: @username,
domain: @domain,
email: @email,
hashed_pw: hashed_pw,
confirmed: @confirmed
)
end end
def create_lndhub_account(user) def create_lndhub_account(user)

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
module LdapManager
class FetchUserByNostrKey < LdapManagerService
def initialize(pubkey:)
@ou = Setting.primary_domain
@pubkey = pubkey
end
def call
treebase = "ou=#{@ou},cn=users,#{ldap_suffix}"
attributes = %w{ cn }
filter = Net::LDAP::Filter.eq("nostrKey", @pubkey)
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
User.find_by cn: entry.cn, ou: @ou unless entry.nil?
end
end
end

View File

@@ -2,7 +2,7 @@ require "image_processing/vips"
module LdapManager module LdapManager
class UpdateAvatar < LdapManagerService class UpdateAvatar < LdapManagerService
def initialize(dn, file) def initialize(dn:, file:)
@dn = dn @dn = dn
@img_data = process(file) @img_data = process(file)
end end

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
module LdapManager
class UpdateNostrKey < LdapManagerService
def initialize(dn:, pubkey:)
@dn = dn
@pubkey = pubkey
end
def call
if @pubkey.present?
replace_attribute @dn, :nostrKey, @pubkey
else
delete_attribute @dn, :nostrKey
end
end
end
end

View File

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

View File

@@ -1,41 +1,47 @@
class LdapService < ApplicationService class LdapService < ApplicationService
def initialize def modify(dn, operations=[])
@suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org" client.modify dn: dn, operations: operations
client.get_operation_result.code
end end
def add_attribute(dn, attr, values) def add_attribute(dn, attr, values)
ldap_client.add_attribute dn, attr, values client.add_attribute dn, attr, values
client.get_operation_result.code
end end
def replace_attribute(dn, attr, values) def replace_attribute(dn, attr, values)
ldap_client.replace_attribute dn, attr, values client.replace_attribute dn, attr, values
client.get_operation_result.code
end end
def delete_attribute(dn, attr) def delete_attribute(dn, attr)
ldap_client.delete_attribute dn, attr client.delete_attribute dn, attr
client.get_operation_result.code
end end
def add_entry(dn, attrs, interactive=false) def add_entry(dn, attrs, interactive=false)
puts "Adding entry: #{dn}" if interactive puts "Add entry: #{dn}" if interactive
res = ldap_client.add dn: dn, attributes: attrs client.add dn: dn, attributes: attrs
puts res.inspect if interactive && !res client.get_operation_result.code
res
end end
def delete_entry(dn, interactive=false) def delete_entry(dn, interactive=false)
puts "Deleting entry: #{dn}" if interactive puts "Delete entry: #{dn}" if interactive
res = ldap_client.delete dn: dn client.delete dn: dn
puts res.inspect if interactive && !res client.get_operation_result.code
res
end end
def delete_all_entries! def delete_all_users!
delete_all_entries!(objectclass: "person")
end
def delete_all_entries!(objectclass: "*")
if Rails.env.production? if Rails.env.production?
raise "Mass deletion of entries not allowed in production" raise "Mass deletion of entries not allowed in production"
end end
filter = Net::LDAP::Filter.eq("objectClass", "*") filter = Net::LDAP::Filter.eq("objectClass", objectclass)
entries = ldap_client.search(base: @suffix, filter: filter, attributes: %w{dn}) entries = client.search(base: ldap_suffix, filter: filter, attributes: %w{dn})
entries.sort_by!{ |e| e.dn.length }.reverse! entries.sort_by!{ |e| e.dn.length }.reverse!
entries.each do |e| entries.each do |e|
@@ -45,15 +51,18 @@ class LdapService < ApplicationService
def fetch_users(args={}) def fetch_users(args={})
if args[:ou] if args[:ou]
treebase = "ou=#{args[:ou]},cn=users,#{@suffix}" treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}"
else else
treebase = ldap_config["base"] treebase = ldap_config["base"]
end end
attributes = %w{dn cn uid mail displayName admin service} attributes = %w[
dn cn uid mail displayName admin serviceEnabled
mailRoutingAddress mailpassword nostrKey
]
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes) entries = client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] } entries.sort_by! { |e| e.cn[0] }
entries = entries.collect do |e| entries = entries.collect do |e|
{ {
@@ -61,7 +70,10 @@ class LdapService < ApplicationService
mail: e.try(:mail) ? e.mail.first : nil, mail: e.try(:mail) ? e.mail.first : nil,
display_name: e.try(:displayName) ? e.displayName.first : nil, display_name: e.try(:displayName) ? e.displayName.first : nil,
admin: e.try(:admin) ? 'admin' : nil, admin: e.try(:admin) ? 'admin' : nil,
service: e.try(:service) services_enabled: e.try(:serviceEnabled),
email_maildrop: e.try(:mailRoutingAddress),
email_password: e.try(:mailpassword),
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil
} }
end end
end end
@@ -70,9 +82,9 @@ class LdapService < ApplicationService
attributes = %w{dn ou description} attributes = %w{dn ou description}
filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit") filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit")
# filter = Net::LDAP::Filter.eq("objectClass", "*") # filter = Net::LDAP::Filter.eq("objectClass", "*")
treebase = "cn=users,#{@suffix}" treebase = "cn=users,#{ldap_suffix}"
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes) entries = client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.ou[0] } entries.sort_by! { |e| e.ou[0] }
@@ -86,10 +98,10 @@ class LdapService < ApplicationService
end end
def add_organization(ou, description, interactive=false) def add_organization(ou, description, interactive=false)
dn = "ou=#{ou},cn=users,#{@suffix}" dn = "ou=#{ou},cn=users,#{ldap_suffix}"
aci = <<-EOS 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}";) (target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
EOS EOS
attrs = { attrs = {
@@ -110,22 +122,22 @@ class LdapService < ApplicationService
delete_all_entries! delete_all_entries!
user_read_aci = <<-EOS user_read_aci = <<-EOS
(target="ldap:///#{@suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";) (target="ldap:///#{ldap_suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";)
EOS EOS
add_entry @suffix, { add_entry ldap_suffix, {
dc: "kosmos", objectClass: ["top", "domain"], aci: user_read_aci dc: "kosmos", objectClass: ["top", "domain"], aci: user_read_aci
}, true }, true
add_entry "cn=users,#{@suffix}", { add_entry "cn=users,#{ldap_suffix}", {
cn: "users", objectClass: ["top", "organizationalRole"] cn: "users", objectClass: ["top", "organizationalRole"]
}, true }, true
end end
private private
def ldap_client def client
ldap_client ||= Net::LDAP.new host: ldap_config['host'], client ||= Net::LDAP.new host: ldap_config['host'],
port: ldap_config['port'], port: ldap_config['port'],
# TODO has to be :simple_tls if TLS is enabled # TODO has to be :simple_tls if TLS is enabled
# encryption: ldap_config['ssl'], # encryption: ldap_config['ssl'],
@@ -139,4 +151,8 @@ class LdapService < ApplicationService
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 ldap_suffix
@ldap_suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end
end end

View File

@@ -1,24 +1,20 @@
class Lndhub class Lndhub < ApplicationService
attr_accessor :auth_token attr_accessor :auth_token
def initialize def post(path, payload)
@base_url = ENV["LNDHUB_API_URL"]
end
def post(endpoint, payload)
headers = { "Content-Type" => "application/json" } headers = { "Content-Type" => "application/json" }
if auth_token if auth_token
headers.merge!({ "Authorization" => "Bearer #{auth_token}" }) headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
end end
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers res = Faraday.post endpoint_url(path), payload.to_json, headers
log_error(res) if res.status != 200 log_error(res) if res.status != 200
JSON.parse(res.body) JSON.parse(res.body)
end end
def get(endpoint, auth_token) def get(path, auth_token)
res = Faraday.get("#{@base_url}/#{endpoint}", {}, { res = Faraday.get(endpoint_url(path), {}, {
"Content-Type" => "application/json", "Content-Type" => "application/json",
"Accept" => "application/json", "Accept" => "application/json",
"Authorization" => "Bearer #{auth_token}" "Authorization" => "Bearer #{auth_token}"
@@ -42,7 +38,7 @@ class Lndhub
self.auth_token self.auth_token
end end
def balance(user_token=nil) def fetch_balance(user_token=nil)
get "balance", user_token || auth_token get "balance", user_token || auth_token
end end
@@ -72,4 +68,14 @@ class Lndhub
Sentry.capture_message("Lndhub API request failed: #{res.body}") Sentry.capture_message("Lndhub API request failed: #{res.body}")
end end
end end
private
def base_url
@base_url ||= Setting.lndhub_api_url
end
def endpoint_url(path)
"#{base_url}/#{path.gsub(/^\//, '')}"
end
end end

View File

@@ -0,0 +1,13 @@
module LndhubManager
class CreateUserInvoice < LndhubV2
def initialize(user:, payload:)
@user = user
@payload = payload
end
def call
authenticate @user
create_invoice @payload
end
end
end

View File

@@ -0,0 +1,12 @@
module LndhubManager
class FetchUserBalance < Lndhub
def initialize(auth_token:)
@auth_token = auth_token
end
def call
data = fetch_balance(auth_token)
data["BTC"]["AvailableBalance"] rescue nil
end
end
end

View File

@@ -1,13 +1,13 @@
class LndhubV2 < Lndhub class LndhubV2 < Lndhub
def post(endpoint, payload, options={}) def post(path, payload, options={})
headers = { "Content-Type" => "application/json" } headers = { "Content-Type" => "application/json" }
if auth_token if auth_token
headers.merge!({ "Authorization" => "Bearer #{auth_token}" }) headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
elsif options[:admin_token] elsif options[:admin_token]
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" }) headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
end end
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers res = Faraday.post endpoint_url(path), payload.to_json, headers
log_error(res) if res.status != 200 log_error(res) if res.status != 200
JSON.parse(res.body) JSON.parse(res.body)

View File

@@ -0,0 +1,25 @@
module NostrManager
class CreateZapReceipt < NostrManagerService
def initialize(zap:, paid_at:, preimage:)
@zap, @paid_at, @preimage = zap, paid_at, preimage
end
def call
request_tags = parse_tags(@zap.request_event.tags)
site_user.create_event(
kind: 9735,
created_at: @paid_at,
content: "",
tags: [
["p", request_tags[:p].first],
["e", request_tags[:e]&.first],
["a", request_tags[:a]&.first],
["bolt11", @zap.payment_request],
["preimage", @preimage],
["description", @zap.request_event.to_json]
].reject { |t| t[1].nil? }
)
end
end
end

View File

@@ -0,0 +1,50 @@
module NostrManager
class PublishEvent < NostrManagerService
def initialize(event:, relay_url:)
relay_name = URI.parse(relay_url).host
@relay = Nostr::Relay.new(url: relay_url, name: relay_name)
if event.is_a?(Nostr::Event)
@event = event
else
@event = Nostr::Event.new(**event.symbolize_keys)
end
@client = Nostr::Client.new
end
def call
client, relay, event = @client, @relay, @event
log_prefix = "[nostr][#{relay.name}]"
thread = Thread.new do
client.on :connect do
puts "#{log_prefix} Publishing #{event.id}..."
client.publish event
end
client.on :error do |e|
puts "#{log_prefix} Error: #{e}"
puts "#{log_prefix} Closing thread..."
thread.exit
end
client.on :message do |m|
puts "#{log_prefix} Message: #{m}"
msg = JSON.parse(m) rescue []
if msg[0] == "OK" && msg[1] == event.id && msg[2]
puts "#{log_prefix} Event published. Closing thread..."
else
puts "#{log_prefix} Unexpected message from relay. Closing thread..."
end
thread.exit
end
puts "#{log_prefix} Connecting to #{relay.url}..."
client.connect relay
end
thread.join
end
end
end

View File

@@ -0,0 +1,24 @@
module NostrManager
class PublishZapReceipt < NostrManagerService
def initialize(zap:, delayed: true)
@zap, @delayed = zap, delayed
end
def call
tags = parse_tags(@zap.request_event.tags)
relays = tags[:relays].take(Setting.nostr_zaps_relay_limit)
if Setting.nostr_relay_url.present?
relays << Setting.nostr_relay_url
end
relays.uniq.each do |relay_url|
if @delayed
NostrPublishEventJob.perform_later(event: @zap.receipt, relay_url: relay_url)
else
NostrManager::PublishEvent.call(event: @zap.receipt_event, relay_url: relay_url)
end
end
end
end
end

View File

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

View File

@@ -0,0 +1,18 @@
module NostrManager
class VerifyAuth < NostrManagerService
def initialize(event:, challenge:)
@event = event
@challenge_expected = challenge
@site_expected = Setting.accounts_domain
end
def call
tags = parse_tags(@event.tags)
site_given = tags[:site].first
challenge_given = tags[:challenge].first
site_given == @site_expected &&
challenge_given == @challenge_expected
end
end
end

View File

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

View File

@@ -0,0 +1,51 @@
module NostrManager
class VerifyZapRequest < NostrManagerService
def initialize(amount:, event:, lnurl: nil)
@amount, @event, @lnurl = amount, event, lnurl
end
# https://github.com/nostr-protocol/nips/blob/27fef638e2460139cc9078427a0aec0ce4470517/57.md#appendix-d-lnurl-server-zap-request-validation
def call
tags = parse_tags(@event.tags)
@event.verify_signature &&
@event.kind == 9734 &&
tags.present? &&
valid_p_tag?(tags[:p]) &&
valid_e_tag?(tags[:e]) &&
valid_a_tag?(tags[:a]) &&
valid_amount_tag?(tags[:amount]) &&
valid_lnurl_tag?(tags[:lnurl])
end
def valid_p_tag?(tag)
return false unless tag.present? && tag.length == 1
key = Nostr::PublicKey.new(tag.first) rescue nil
key.present?
end
def valid_e_tag?(tag)
return true unless tag.present?
# TODO validate format of event ID properly
tag.length == 1 && tag.first.is_a?(String)
end
def valid_a_tag?(tag)
return true unless tag.present?
# TODO validate format of event coordinate properly
tag.length == 1 && tag.first.is_a?(String)
end
def valid_amount_tag?(tag)
return true unless tag.present?
amount = tag.first
amount.is_a?(String) && amount.to_i == @amount
end
def valid_lnurl_tag?(tag)
return true unless tag.present?
# TODO validate lnurl matching recipient's lnurlp
tag.first.is_a?(String)
end
end
end

View File

@@ -1,4 +1,22 @@
require "nostr" require "nostr"
class NostrManagerService < ApplicationService class NostrManagerService < ApplicationService
def parse_tags(tags)
out = {}
tags.each do |tag|
out[tag[0].to_sym] = tag[1, tag.length]
end
out
end
def site_keypair
Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.new(Setting.nostr_private_key),
public_key: Nostr::PublicKey.new(Setting.nostr_public_key)
)
end
def site_user
Nostr::User.new(keypair: site_keypair)
end
end end

7
app/services/router.rb Normal file
View File

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

View File

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

@@ -14,14 +14,24 @@
<%= form.label :user_id %> <%= form.label :user_id %>
<%= form.collection_select :user_id, User.where(ou: Setting.primary_domain).order(:cn), :id, :cn, {} %> <%= form.collection_select :user_id, User.where(ou: Setting.primary_domain).order(:cn), :id, :cn, {} %>
<%= form.label :donation_method, "Donation method" %>
<%= form.select :donation_method, options_for_select([
["Custom (manual)", "custom"],
["BTCPay", "btcpay"],
["LndHub account", "lndhub"],
["OpenCollective", "opencollective"]
], selected: (donation.donation_method || "custom")) %>
<%= form.label :amount_sats, "Amount BTC (sats)" %> <%= form.label :amount_sats, "Amount BTC (sats)" %>
<%= form.number_field :amount_sats %> <%= form.number_field :amount_sats %>
<%= form.label :amount_eur, "Amount EUR (cents)" %> <%= form.label :fiat_amount, "Fiat Amount (cents)" %>
<%= form.number_field :amount_eur %> <%= form.number_field :fiat_amount %>
<%= form.label :amount_usd, "Amount USD (cents)"%> <%= form.label :fiat_currency, "Fiat Currency" %>
<%= form.number_field :amount_usd %> <%= form.select :fiat_currency, options_for_select([
["EUR", "EUR"], ["USD", "USD"], ["sats", "sats"]
], selected: donation.fiat_currency) %>
<%= form.label :public_name %> <%= form.label :public_name %>
<%= form.text_field :public_name %> <%= form.text_field :public_name %>

View File

@@ -25,9 +25,8 @@
<thead> <thead>
<tr> <tr>
<th>User</th> <th>User</th>
<th class="text-right">Amount BTC</th> <th class="text-right">Sats</th>
<th class="text-right">in EUR</th> <th class="text-right">Fiat Amount</th>
<th class="text-right">in USD</th>
<th class="pl-2">Public name</th> <th class="pl-2">Public name</th>
<th>Date</th> <th>Date</th>
<th></th> <th></th>
@@ -36,10 +35,9 @@
<tbody> <tbody>
<% @donations.each do |donation| %> <% @donations.each do |donation| %>
<tr> <tr>
<td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td> <td><%= link_to donation.user.cn, admin_user_path(donation.user.cn), class: 'ks-text-link' %></td>
<td class="text-right"><%= sats_to_btc donation.amount_sats %></td> <td class="text-right"><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></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.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% 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 class="pl-2"><%= donation.public_name %></td>
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td> <td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td>
<td class="text-right"> <td class="text-right">

View File

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

View File

@@ -6,19 +6,19 @@
<tbody> <tbody>
<tr> <tr>
<th>User</th> <th>User</th>
<td><%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td> <td><%= link_to @donation.user.cn, admin_user_path(@donation.user.cn), class: 'ks-text-link' %></td>
</tr>
<tr>
<th>Donation Method</th>
<td><%= @donation.donation_method %></td>
</tr> </tr>
<tr> <tr>
<th>Amount sats</th> <th>Amount sats</th>
<td><%= @donation.amount_sats %></td> <td><%= @donation.amount_sats %></td>
</tr> </tr>
<tr> <tr>
<th>Amount EUR</th> <th>Fiat amount</th>
<td><%= @donation.amount_eur %></td> <td><% if @donation.fiat_amount.present? %><%= number_to_currency @donation.fiat_amount.to_f / 100, unit: "" %> <%= @donation.fiat_currency %><% end %></td>
</tr>
<tr>
<th>Amount USD</th>
<td><%= @donation.amount_usd %></td>
</tr> </tr>
<tr> <tr>
<th>Public name</th> <th>Public name</th>
@@ -26,7 +26,7 @@
</tr> </tr>
<tr> <tr>
<th>Date</th> <th>Date</th>
<td><%= @donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td> <td><%= @donation.paid_at&.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

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

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