167 Commits

Author SHA1 Message Date
aca13a25c3 WIP Process payments for expired invoices 2025-01-02 08:42:33 -05:00
7df56479a4 Fix 500 when pubkey is nil 2025-01-02 08:30:58 -05:00
8aa3ca9e23 Merge pull request 'Let users upload their OpenPGP public key, and serve WKD response' (#205) from feature/191-gpg_keys_wkd into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #205
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-10-14 14:08:31 +00:00
3ad1d03785 Merge pull request 'Encrypt all system emails for users with PGP key' (#207) from feature/encrypted_system_emails into feature/191-gpg_keys_wkd
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
Reviewed-on: #207
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-10-14 13:39:01 +00:00
e258a8bd27 Merge pull request 'Use ASCII format for nostrKey LDAP schema' (#206) from chore/nostr_key_ldap_schema into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #206
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-10-10 14:18:31 +00:00
339462f320 Refactor mailer options usage
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 5s
2024-10-08 14:06:10 +02:00
c4c2d16342 Encrypt outgoing emails when possible 2024-10-08 14:05:50 +02:00
3ee76e26ab Re-import user's pubkey on access
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Sometimes, the pubkey might not be imported in the local keychain
(anymore), but at this point in the code it had been successfully
imported at least once before. So we just (re-)import every time for it
to never fail.
2024-10-08 11:34:18 +02:00
729e4fd566 Add WKD policy endpoint
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-09-26 23:11:21 +02:00
8ad6adbaeb Use ASCII format for nostrKey LDAP schema
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Failing after 10m11s
No need for UTF-8
2024-09-25 18:35:48 +02:00
534e5a9d3c Gracefully handle wrong capitalization of username
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-09-25 00:20:30 +02:00
1b72c97f42 Remove obsolete code 2024-09-25 00:17:30 +02:00
bfd8ca16a9 Merge branch 'master' into feature/191-gpg_keys_wkd 2024-09-25 00:16:39 +02:00
64de4deddd Fix serviceEnabled indicator on admin page
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-24 21:38:01 +02:00
9f6fa6deba Remove example link
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Until we have a live example on kosmos.org
2024-09-23 20:36:05 +02:00
37b106e73c Whitespace
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-09-23 19:22:52 +02:00
c3f1f97e1a Add display name and PGP key to admin user page
Link the key to the ASCII Armor WKD endpoint, if it contains the user's
account address
2024-09-23 19:21:59 +02:00
4a677178e8 Add Web Key Directory endpoint
Serve public keys in binary and armored text, if they contain a user's
account address.
2024-09-23 19:20:10 +02:00
3042a02a17 Allow users to update their OpenPGP pubkey
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-23 18:13:39 +02:00
118fddb497 Document URLs for settings controller actions
No need to read the route sources all the time
2024-09-23 16:07:02 +02:00
ba683a7b95 Move some Rails app services to UserManager namespace
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-23 16:03:02 +02:00
90a8a70c15 Add OpenPGP key to LDAP directory and User model
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-23 15:20:00 +02:00
8f7994d82e 0.10.0
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2024-09-18 15:49:07 +02:00
a7d0e71ab6 Fix spec
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-18 14:46:46 +02:00
27d9f73c61 Set host for RS auth url
Some checks failed
continuous-integration/drone/push Build is failing
With X-Forwarded-Host set on the proxied request, Rails uses that host
for URLs. But we need it to be the accounts domain.
2024-09-14 17:17:09 +02:00
ed3de8b16f Allow CORS for all LNURL endpoints
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-14 16:46:14 +02:00
d7b4c67953 Fix config when set to empty string
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-14 16:40:22 +02:00
7489d4a32f Merge pull request 'Add config for separate primary domain Nostr pubkey' (#204) from feature/nostr_pubkey_primary_domain into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #204
Reviewed-by: Greg <greg@noreply.kosmos.org>
2024-09-13 12:33:11 +00:00
ac77e5b7c1 Allow ENV var for new setting
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 5s
2024-09-11 16:31:04 +02:00
e544c28105 Config for separate primary domain Nostr pubkey
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Allow to configure a separate key for the NIP-05 address of the primary
domain vs the accounts domain.
2024-09-11 16:28:12 +02:00
4909dac5c2 Fix typo
All checks were successful
continuous-integration/drone/push Build is passing
The return value of `strip!` is `nil`
2024-09-11 16:26:48 +02:00
3cf4348695 Merge pull request 'Make default user services configurable by admins' (#203) from feature/default_service_settings into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #203
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-09-11 11:21:38 +00:00
af3da0a26c Set CORS headers for all .well-known responses
All checks were successful
continuous-integration/drone/push Build is passing
So we don't have to consider it for reverse proxies etc.
2024-09-10 16:06:11 +02:00
2d32320c7d Style check boxes
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 5s
2024-09-05 11:24:38 +02:00
fc2bec6246 Make default user services configurable by admin
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-09-05 11:11:32 +02:00
5addd25186 Add service details config, use for known services 2024-09-05 11:10:54 +02:00
215d178e69 Remove empty spec files 2024-09-05 11:10:10 +02:00
5474bf66e7 Turn default services into a configurable setting
With the default value being all enabled services
2024-09-04 13:06:32 +02:00
ef2a37e2bf Sort user services in LDAP entry
Makes it predictable for programmatic comparisons (e.g. tests)
2024-09-04 13:05:36 +02:00
0e3180602c Rename "xmpp" user service back to "ejabberd"
If we ever add support for others, we can combine them as "xmpp" in
helper methods
2024-09-04 13:03:45 +02:00
15e2f9b962 Remove "in development" note
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-28 14:55:34 +02:00
4ae10c9b53 Refactor settings model
All checks were successful
continuous-integration/drone/push Build is passing
Move the various sections to their own concerns, so they're easier to
find and maintain
2024-08-28 14:39:08 +02:00
45137e0cfe Merge pull request 'Fix Ruby issue on Apple silicon (without compiling a patched Ruby)' (#201) from chore/update_docker_image into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #201
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-08-28 08:12:31 +00:00
717fe93104 Fix spec
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-22 14:07:54 +02:00
fdac789ccb Add compatibility section to RS service page
Some checks failed
continuous-integration/drone/push Build is failing
2024-08-19 15:13:19 +02:00
9355dab6b6 Enable RS service for all new users for now
Some checks failed
continuous-integration/drone/push Build is failing
2024-08-19 14:48:24 +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
e08ea64f47 Update Docker base image
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 5s
Fixes the bug with Ruby on Apple silicon
2024-08-12 10:34:02 +02:00
8cc2c9554f Revert "Fix Ruby in Docker container on Apple silicon"
This reverts commit bbf3fb91a0.
2024-08-12 10:15:18 +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
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
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
230 changed files with 5753 additions and 940 deletions

View File

@@ -29,8 +29,10 @@
#
# Service Integrations
# (sorted alphabetically by service name)
#
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
# BTCPAY_API_URL='http://localhost:23001/api/v1'
# BTCPAY_STORE_ID=''
# BTCPAY_AUTH_TOKEN=''
@@ -57,8 +59,13 @@
# 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
AKKOUNTS_DOMAIN=accounts.kosmos.org
REDIS_URL='redis://localhost:6379/0'
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
BTCPAY_STORE_ID='123456'
@@ -10,10 +12,15 @@ DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
EJABBERD_API_URL='http://xmpp.example.com/api'
MASTODON_PUBLIC_URL='http://example.social'
LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/1'

View File

@@ -1,18 +1,11 @@
# syntax=docker/dockerfile:1
FROM debian:bullseye-slim as base
FROM ruby:3.3.4
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# TODO Remove when upstream Ruby works properly on Apple silicon
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 update -qq && apt-get install -y --no-install-recommends curl \
ldap-utils tini libvips
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 apt-get update && apt-get install -y nodejs

View File

@@ -44,6 +44,8 @@ gem 'pagy', '~> 6.0', '>= 6.0.2'
gem 'flipper'
gem 'flipper-active_record'
gem 'flipper-ui'
gem 'gpgme', '~> 2.0.24'
gem 'zbase32', '~> 0.1.1'
# HTTP requests
gem 'faraday'
@@ -61,8 +63,8 @@ gem "sentry-rails"
# Services
gem 'discourse_api'
gem "lnurl"
gem 'manifique'
gem 'nostr'
gem 'manifique', '~> 1.1.0'
gem 'nostr', '~> 0.6.0'
group :development, :test do
# Use sqlite3 as the database for Active Record

View File

@@ -155,7 +155,7 @@ GEM
ruby2_keywords
e2mmap (0.1.0)
ecdsa (1.2.0)
ecdsa_ext (0.5.0)
ecdsa_ext (0.5.1)
ecdsa (~> 1.2.0)
erubi (1.12.0)
et-orbi (1.2.7)
@@ -197,6 +197,8 @@ GEM
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
gpgme (2.0.24)
mini_portile2 (~> 2.7)
hashdiff (1.1.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
@@ -245,7 +247,7 @@ GEM
net-imap
net-pop
net-smtp
manifique (1.0.1)
manifique (1.1.0)
faraday (~> 2.9.0)
faraday-follow_redirects (= 0.3.0)
nokogiri (~> 1.16.0)
@@ -278,9 +280,9 @@ GEM
racc (~> 1.4)
nokogiri (1.16.0-x86_64-linux)
racc (~> 1.4)
nostr (0.5.0)
nostr (0.6.0)
bech32 (~> 1.4)
bip-schnorr (~> 0.6)
bip-schnorr (~> 0.7)
ecdsa (~> 1.2)
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
@@ -483,6 +485,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.34)
zbase32 (0.1.1)
zeitwerk (2.6.12)
PLATFORMS
@@ -507,6 +510,7 @@ DEPENDENCIES
flipper
flipper-active_record
flipper-ui
gpgme (~> 2.0.24)
image_processing (~> 1.12.2)
importmap-rails
jbuilder (~> 2.7)
@@ -515,9 +519,9 @@ DEPENDENCIES
listen (~> 3.2)
lnurl
lockbox
manifique
manifique (~> 1.1.0)
net-ldap
nostr
nostr (~> 0.6.0)
pagy (~> 6.0, >= 6.0.2)
pg (~> 1.5)
puma (~> 4.1)
@@ -540,6 +544,7 @@ DEPENDENCIES
warden
web-console (~> 4.2)
webmock
zbase32 (~> 0.1.1)
BUNDLED WITH
2.5.5

View File

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

View File

@@ -1,5 +1,5 @@
@layer components {
.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

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

View File

@@ -2,7 +2,7 @@
module FormElements
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
@positioning = :vertical
@title = title
@@ -10,6 +10,7 @@ module FormElements
@key = key.to_sym
@type = type
@resettable = is_resettable?(@key)
@placeholder = placeholder
end
def is_resettable?(key)

View File

@@ -6,7 +6,7 @@
<div class="flex flex-col">
<label class="font-bold mb-1"><%= @title %></label>
<% if @description.present? %>
<p class="text-gray-500"><%= @descripton %></p>
<p class="text-gray-500"><%= @description %></p>
<% end %>
</div>
<div class="relative ml-4 inline-flex flex-shrink-0">

View File

@@ -12,7 +12,7 @@ module FormElements
@enabled = enabled
@input_enabled = input_enabled
@title = title
@descripton = description
@description = description
@button_text = @enabled ? "Switch off" : "Switch on"
end
end

View File

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

View File

@@ -12,7 +12,7 @@
<!-- Modal 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">
<!-- Modal Card -->
<div class="m-1 bg-white rounded shadow">

View File

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

View File

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

View File

@@ -9,4 +9,12 @@ class Admin::Settings::RegistrationsController < Admin::SettingsController
success: "Settings saved"
}
end
private
def setting_params
params.require(:setting).permit([
:reserved_usernames, default_services: []
])
end
end

View File

@@ -9,11 +9,12 @@ class Admin::SettingsController < Admin::BaseController
changed_keys = []
setting_params.keys.each do |key|
next if setting_params[key].nil? ||
(Setting.send(key).to_s == setting_params[key].strip)
next if clean_param(key).nil? ||
(Setting.send(key).to_s == clean_param(key))
changed_keys.push(key)
setting = Setting.new(var: key)
setting.value = setting_params[key].strip
setting.value = clean_param(key)
unless setting.valid?
@errors.merge!(setting.errors)
end
@@ -24,7 +25,7 @@ class Admin::SettingsController < Admin::BaseController
end
changed_keys.each do |key|
Setting.send("#{key}=", setting_params[key].strip)
Setting.send("#{key}=", clean_param(key))
end
end
@@ -37,4 +38,12 @@ class Admin::SettingsController < Admin::BaseController
def setting_params
params.require(:setting).permit(Setting.editable_keys.map(&:to_sym))
end
def clean_param(key)
if Setting.get_field(key)[:type] == :string
setting_params[key].strip
else
setting_params[key]
end
end
end

View File

@@ -30,7 +30,7 @@ class Admin::UsersController < Admin::BaseController
amount = params[:amount].to_i
notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user])
CreateInvitations.call(user: @user, amount: amount, notify: notify_user)
UserManager::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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,11 +5,10 @@ class Services::RemotestorageController < Services::BaseController
# Dashboard
def show
# unless current_user.services_enabled.include?(:remotestorage)
# unless current_user.service_enabled?(:remotestorage)
# redirect_to service_remotestorage_info_path
# end
@rs_auths = current_user.remote_storage_authorizations
# TODO sort by app name
# @rs_apps_connected = current_user.remote_storage_authorizations.any?
end
private

View File

@@ -3,13 +3,18 @@ class Services::RsAuthsController < Services::BaseController
before_action :require_feature_enabled
before_action :require_service_available
# 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
@auth.destroy!
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'
}
end

View File

@@ -12,15 +12,21 @@ class SettingsController < ApplicationController
end
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)
end
end
# PUT /settings/:section
def update
@user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name]
@user.avatar_new = user_params[:avatar]
@user.avatar_new = user_params[:avatar]
@user.pgp_pubkey = user_params[:pgp_pubkey]
if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
@@ -31,6 +37,10 @@ class SettingsController < ApplicationController
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
end
if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key])
UserManager::UpdatePgpKey.call(user: @user)
end
redirect_to setting_path(@settings_section), flash: {
success: 'Settings saved.'
}
@@ -40,6 +50,7 @@ class SettingsController < ApplicationController
end
end
# POST /settings/update_email
def update_email
if @user.valid_ldap_authentication?(security_params[:current_password])
if @user.update email: email_params[:email]
@@ -57,6 +68,7 @@ class SettingsController < ApplicationController
end
end
# POST /settings/reset_email_password
def reset_email_password
@user.current_password = security_params[:current_password]
@@ -79,6 +91,7 @@ class SettingsController < ApplicationController
end
end
# POST /settings/reset_password
def reset_password
current_user.send_reset_password_instructions
sign_out current_user
@@ -86,41 +99,41 @@ class SettingsController < ApplicationController
redirect_to check_your_email_path, notice: msg
end
# POST /settings/set_nostr_pubkey
def set_nostr_pubkey
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
is_valid_id = NostrManager::ValidateId.call(event: signed_event)
is_valid_sig = NostrManager::VerifySignature.call(event: signed_event)
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
signed_event = Nostr::Event.new(**nostr_event_from_params)
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"
http_status :unprocessable_entity and return
end
pubkey_taken = User.all_except(current_user).where(
ou: current_user.ou, nostr_pubkey: signed_event[:pubkey]
).any?
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
if pubkey_taken
if user_with_pubkey.present? && (user_with_pubkey != current_user)
flash[:alert] = "Public key already in use for a different account"
http_status :unprocessable_entity and return
end
current_user.update! nostr_pubkey: signed_event[:pubkey]
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event.pubkey)
session[:shared_secret] = nil
flash[:success] = "Public key verification successful"
http_status :ok
rescue
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end
# DELETE /settings/nostr_pubkey
def remove_nostr_pubkey
current_user.update! nostr_pubkey: nil
# 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'
}
end
@@ -134,8 +147,8 @@ class SettingsController < ApplicationController
def set_settings_section
@settings_section = params[:section]
allowed_sections = [
:profile, :account, :xmpp, :email, :lightning, :remotestorage,
:experiments
:profile, :account, :xmpp, :email,
:lightning, :remotestorage, :nostr
]
unless allowed_sections.include?(@settings_section.to_sym)
@@ -148,11 +161,10 @@ class SettingsController < ApplicationController
end
def user_params
params.require(:user).permit(:display_name, :avatar, preferences: [
:lightning_notify_sats_received,
:remotestorage_notify_auth_created,
:xmpp_exchange_contacts_with_invitees
])
params.require(:user).permit(
:display_name, :avatar, :pgp_pubkey,
preferences: UserPreferences.pref_keys
)
end
def email_params
@@ -163,12 +175,6 @@ class SettingsController < ApplicationController
params.require(:user).permit(:current_password)
end
def nostr_event_params
params.permit(signed_event: [
: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

View File

@@ -96,7 +96,7 @@ class SignupController < ApplicationController
session[:new_user] = nil
session[:validation_error] = nil
CreateAccount.call(account: {
UserManager::CreateAccount.call(account: {
username: @user.cn,
domain: Setting.primary_domain,
email: @user.email,

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

@@ -0,0 +1,35 @@
class WebKeyDirectoryController < WellKnownController
before_action :allow_cross_origin_requests
# /.well-known/openpgpkey/hu/:hashed_username(.txt)
def show
@user = User.find_by(cn: params[:l].downcase)
if @user.nil? ||
@user.pgp_pubkey.blank? ||
!@user.pgp_pubkey_contains_user_address?
http_status :not_found and return
end
if params[:hashed_username] != @user.wkd_hash
http_status :unprocessable_entity and return
end
respond_to do |format|
format.text do
response.headers['Content-Type'] = 'text/plain'
render plain: @user.pgp_pubkey
end
format.any do
key = @user.gnupg_key.export
send_data key, filename: "#{@user.wkd_hash}.pem",
type: "application/octet-stream"
end
end
end
def policy
head :ok
end
end

View File

@@ -1,20 +1,19 @@
class WebfingerController < ApplicationController
class WebfingerController < WellKnownController
before_action :allow_cross_origin_requests, only: [:show]
layout false
def show
resource = params[:resource]
if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1)
@username, @org = @useraddress.split("@")
@username, @domain = @useraddress.split("@")
unless Rails.env.development?
# 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
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
end
@@ -28,22 +27,60 @@ class WebfingerController < ApplicationController
private
def webfinger
links = [];
jrd = {
subject: "acct:#{@user.address}",
aliases: [],
links: []
}
# TODO check if storage service is enabled for user, not just globally
links << remotestorage_link if Setting.remotestorage_enabled
if Setting.mastodon_enabled && @user.service_enabled?(:mastodon)
# 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
def remotestorage_link
auth_url = new_rs_oauth_url(@username)
auth_url = new_rs_oauth_url(@username, host: Setting.accounts_domain)
storage_url = "#{Setting.rs_storage_url}/#{@username}"
{
"rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage",
"href" => storage_url,
"properties" => {
rel: "http://tools.ietf.org/id/draft-dejong-remotestorage",
href: storage_url,
properties: {
"http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13",
"http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url,
"http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter
@@ -52,10 +89,4 @@ class WebfingerController < ApplicationController
}
}
end
def allow_cross_origin_requests
return unless Rails.env.development?
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
module ServicesHelper
def service_human_name(key, category = :external)
SERVICES[category][key][:name] || key.to_s
end
def service_display_name(key, category = :external)
SERVICES[category][key][:display_name] ||
service_human_name(key, category)
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -3,7 +3,12 @@ import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="settings--nostr-pubkey"
export default class extends Controller {
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
static values = { userAddress: String, pubkeyHex: String, sharedSecret: String }
static values = {
userAddress: String,
pubkeyHex: String,
site: String,
sharedSecret: String
}
connect () {
if (window.nostr) {
@@ -19,11 +24,15 @@ export default class extends Controller {
this.setPubkeyTarget.disabled = true
try {
// Auth based on NIP-42
const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: `Connect my public key to ${this.userAddressValue} (confirmation ${this.sharedSecretValue})`
kind: 22242,
tags: [
["site", this.siteValue],
["challenge", this.sharedSecretValue]
],
content: ""
})
const res = await fetch("/settings/set_nostr_pubkey", {

View File

@@ -0,0 +1,37 @@
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 "Processing"
re_enqueue_job(donation)
when "Settled"
mark_as_paid(donation)
when "Expired"
if invoice["additionalStatus"] &&
invoice["additionalStatus"] == "PaidLate"
mark_as_paid(donation)
end
end
end
def re_enqueue_job(donation)
self.class.set(wait: 20.seconds).perform_later(donation)
end
def mark_as_paid(donation)
donation.paid_at = DateTime.now
donation.payment_status = "settled"
donation.save!
NotificationMailer.with(user: donation.user)
.bitcoin_donation_confirmed
.deliver_later
end
end

View File

@@ -1,7 +1,7 @@
class CreateLdapUserJob < ApplicationJob
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"
attr = {
objectclass: ["top", "account", "person", "extensibleObject"],
@@ -12,6 +12,10 @@ class CreateLdapUserJob < ApplicationJob
userPassword: hashed_pw
}
if confirmed
attr[:serviceEnabled] = Setting.default_services
end
ldap_client.add(dn: dn, attributes: attr)
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
def perform(inviter, invitee)
return unless inviter.services_enabled.include?("xmpp") &&
invitee.services_enabled.include?("xmpp") &&
return unless inviter.service_enabled?(:ejabberd) &&
invitee.service_enabled?(:ejabberd) &&
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new

View File

@@ -1,3 +1,90 @@
class ApplicationMailer < ActionMailer::Base
default Rails.application.config.action_mailer.default_options
layout 'mailer'
private
def send_mail
@template ||= "#{self.class.name.underscore}/#{caller[0][/`([^']*)'/, 1]}"
headers['Message-ID'] = message_id
if @user.pgp_pubkey.present?
mail(to: @user.email, subject: "...", content_type: pgp_content_type) do |format|
format.text { render plain: pgp_content }
end
else
mail(to: @user.email, subject: @subject) do |format|
format.text { render @template }
end
end
end
def from_address
self.class.default[:from]
end
def from_domain
Mail::Address.new(from_address).domain
end
def message_id
@message_id ||= "#{SecureRandom.uuid}@#{from_domain}"
end
def boundary
@boundary ||= SecureRandom.hex(8)
end
def pgp_content_type
"multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"------------#{boundary}\""
end
def pgp_nested_content
message_content = render_to_string(template: @template)
message_content_base64 = Base64.encode64(message_content)
nested_boundary = SecureRandom.hex(8)
<<~NESTED_CONTENT
Content-Type: multipart/mixed; boundary="------------#{nested_boundary}"; protected-headers="v1"
Subject: #{@subject}
From: <#{from_address}>
To: #{@user.display_name || @user.cn} <#{@user.email}>
Message-ID: <#{message_id}>
--------------#{nested_boundary}
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: base64
#{message_content_base64}
--------------#{nested_boundary}--
NESTED_CONTENT
end
def pgp_content
encrypted_content = UserManager::PgpEncrypt.call(user: @user, text: pgp_nested_content)
encrypted_base64 = Base64.encode64(encrypted_content.to_s)
<<~EMAIL_CONTENT
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
--------------#{boundary}
Content-Type: application/pgp-encrypted
Content-Description: PGP/MIME version identification
Version: 1
--------------#{boundary}
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc"
-----BEGIN PGP MESSAGE-----
#{encrypted_base64}
-----END PGP MESSAGE-----
--------------#{boundary}--
EMAIL_CONTENT
end
end

View File

@@ -18,6 +18,6 @@ class CustomMailer < ApplicationMailer
@user = params[:user]
@subject = params[:subject]
@body = params[:body]
mail(to: @user.email, subject: @subject)
send_mail
end
end

View File

@@ -3,7 +3,7 @@ class NotificationMailer < ApplicationMailer
@user = params[:user]
@amount_sats = params[:amount_sats]
@subject = "Sats received"
mail to: @user.email, subject: @subject
send_mail
end
def remotestorage_auth_created
@@ -15,12 +15,19 @@ class NotificationMailer < ApplicationMailer
"#{access} #{directory}"
end
@subject = "New app connected to your storage"
mail to: @user.email, subject: @subject
send_mail
end
def new_invitations_available
@user = params[:user]
@subject = "New invitations added to your account"
mail to: @user.email, subject: @subject
send_mail
end
def bitcoin_donation_confirmed
@user = params[:user]
@donation = params[:donation]
@subject = "Donation confirmed"
send_mail
end
end

View File

@@ -0,0 +1,24 @@
module Settings
module BtcpaySettings
extend ActiveSupport::Concern
included do
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_public_url, type: :string,
default: ENV["BTCPAY_PUBLIC_URL"].presence
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
end
end
end

View File

@@ -0,0 +1,16 @@
module Settings
module DiscourseSettings
extend ActiveSupport::Concern
included do
field :discourse_public_url, type: :string,
default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean,
default: ENV["DISCOURSE_PUBLIC_URL"].present?
field :discourse_connect_secret, type: :string,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
end
end
end

View File

@@ -0,0 +1,13 @@
module Settings
module DroneCiSettings
extend ActiveSupport::Concern
included do
field :droneci_public_url, type: :string,
default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean,
default: ENV["DRONECI_PUBLIC_URL"].present?
end
end
end

View File

@@ -0,0 +1,19 @@
module Settings
module EjabberdSettings
extend ActiveSupport::Concern
included do
field :ejabberd_enabled, type: :boolean,
default: ENV["EJABBERD_API_URL"].present?
field :ejabberd_api_url, type: :string,
default: ENV["EJABBERD_API_URL"].presence
field :ejabberd_admin_url, type: :string,
default: ENV["EJABBERD_ADMIN_URL"].presence
field :ejabberd_buddy_roster, type: :string,
default: "Buddies"
end
end
end

View File

@@ -0,0 +1,28 @@
module Settings
module EmailSettings
extend ActiveSupport::Concern
included do
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
end
end
end

View File

@@ -0,0 +1,34 @@
module Settings
module GeneralSettings
extend ActiveSupport::Concern
included do
field :primary_domain, type: :string,
default: ENV["PRIMARY_DOMAIN"].presence
field :accounts_domain, type: :string,
default: ENV["AKKOUNTS_DOMAIN"].presence
#
# Internal services
#
field :redis_url, type: :string,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
field :s3_enabled, type: :boolean,
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
field :sentry_enabled, type: :boolean, readonly: true,
default: ENV["SENTRY_DSN"].present?
#
# Registrations
#
field :reserved_usernames, type: :array, default: %w[
account accounts donations mail webmaster support
]
end
end
end

View File

@@ -0,0 +1,13 @@
module Settings
module GiteaSettings
extend ActiveSupport::Concern
included do
field :gitea_public_url, type: :string,
default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean,
default: ENV["GITEA_PUBLIC_URL"].present?
end
end
end

View File

@@ -0,0 +1,25 @@
module Settings
module LightningNetworkSettings
extend ActiveSupport::Concern
included do
field :lndhub_api_url, type: :string,
default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean,
default: ENV["LNDHUB_API_URL"].present?
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean,
default: ENV["LNDHUB_ADMIN_UI"] || false
field :lndhub_public_key, type: :string,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present? }
end
end
end

View File

@@ -0,0 +1,16 @@
module Settings
module MastodonSettings
extend ActiveSupport::Concern
included do
field :mastodon_public_url, type: :string,
default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean,
default: ENV["MASTODON_PUBLIC_URL"].present?
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
end
end
end

View File

@@ -0,0 +1,13 @@
module Settings
module MediaWikiSettings
extend ActiveSupport::Concern
included do
field :mediawiki_public_url, type: :string,
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean,
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
end
end
end

View File

@@ -0,0 +1,25 @@
module Settings
module NostrSettings
extend ActiveSupport::Concern
included do
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_public_key_primary_domain, type: :string,
default: ENV["NOSTR_PUBLIC_KEY_PRIMARY_DOMAIN"].presence
field :nostr_relay_url, type: :string,
default: ENV["NOSTR_RELAY_URL"].presence
field :nostr_zaps_relay_limit, type: :integer,
default: 12
end
end
end

View File

@@ -0,0 +1,9 @@
module Settings
module OpenCollectiveSettings
extend ActiveSupport::Concern
included do
field :opencollective_enabled, type: :boolean, default: true
end
end
end

View File

@@ -0,0 +1,16 @@
module Settings
module RemoteStorageSettings
extend ActiveSupport::Concern
included do
field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present?
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence
field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
end
end
end

View File

@@ -0,0 +1,11 @@
module Settings
module XmppSettings
extend ActiveSupport::Concern
included do
field :xmpp_default_rooms, type: :array, default: []
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
field :xmpp_notifications_from_address, type: :string, default: primary_domain
end
end
end

View File

@@ -4,12 +4,25 @@ class Donation < ApplicationRecord
# Validations
validates_presence_of :user
validates_presence_of :amount_sats
validates_presence_of :paid_at
# Hooks
# TODO before_create :store_fiat_value
validates_presence_of :donation_method,
inclusion: { in: %w[ custom btcpay lndhub ] }
validates_presence_of :payment_status, allow_nil: true,
inclusion: { in: %w[ processing settled ] }
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
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

View File

@@ -2,199 +2,30 @@
class Setting < RailsSettings::Base
cache_prefix { "v1" }
field :primary_domain, type: :string,
default: ENV["PRIMARY_DOMAIN"].presence
Dir[Rails.root.join('app', 'models', 'concerns', 'settings', '*.rb')].each do |file|
require file
end
field :accounts_domain, type: :string,
default: ENV["AKKOUNTS_DOMAIN"].presence
include Settings::GeneralSettings
include Settings::BtcpaySettings
include Settings::DiscourseSettings
include Settings::DroneCiSettings
include Settings::EjabberdSettings
include Settings::EmailSettings
include Settings::GiteaSettings
include Settings::LightningNetworkSettings
include Settings::MastodonSettings
include Settings::MediaWikiSettings
include Settings::NostrSettings
include Settings::OpenCollectiveSettings
include Settings::RemoteStorageSettings
include Settings::XmppSettings
#
# Internal services
#
def self.available_services
known_services = SERVICES[:external].keys
known_services.select {|s| Setting.send "#{s}_enabled?" }
end
field :redis_url, type: :string,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
field :s3_enabled, type: :boolean,
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
#
# Registrations
#
field :reserved_usernames, type: :array, default: %w[
account accounts donations mail webmaster support
]
#
# XMPP
#
field :xmpp_default_rooms, type: :array, default: []
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
field :xmpp_notifications_from_address, type: :string, default: primary_domain
#
# Sentry
#
field :sentry_enabled, type: :boolean, readonly: true,
default: ENV["SENTRY_DSN"].present?
#
# BTCPay Server
#
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
#
# Discourse
#
field :discourse_public_url, type: :string,
default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean,
default: ENV["DISCOURSE_PUBLIC_URL"].present?
field :discourse_connect_secret, type: :string,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
#
# Drone CI
#
field :droneci_public_url, type: :string,
default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean,
default: ENV["DRONECI_PUBLIC_URL"].present?
#
# ejabberd
#
field :ejabberd_enabled, type: :boolean,
default: ENV["EJABBERD_API_URL"].present?
field :ejabberd_api_url, type: :string,
default: ENV["EJABBERD_API_URL"].presence
field :ejabberd_admin_url, type: :string,
default: ENV["EJABBERD_ADMIN_URL"].presence
field :ejabberd_buddy_roster, type: :string,
default: "Buddies"
#
# Gitea
#
field :gitea_public_url, type: :string,
default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean,
default: ENV["GITEA_PUBLIC_URL"].present?
#
# Lightning Network
#
field :lndhub_api_url, type: :string,
default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean,
default: ENV["LNDHUB_API_URL"].present?
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean,
default: ENV["LNDHUB_ADMIN_UI"] || false
field :lndhub_public_key, type: :string,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present? }
#
# Mastodon
#
field :mastodon_public_url, type: :string,
default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean,
default: ENV["MASTODON_PUBLIC_URL"].present?
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
#
# MediaWiki
#
field :mediawiki_public_url, type: :string,
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean,
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
#
# Nostr
#
field :nostr_enabled, type: :boolean, default: true
#
# RemoteStorage
#
field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present?
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence
field :rs_redis_url, type: :string,
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
field :default_services, type: :array,
default: self.available_services
end

View File

@@ -3,9 +3,10 @@ require 'nostr'
class User < ApplicationRecord
include EmailValidatable
attr_accessor :display_name
attr_accessor :avatar_new
attr_accessor :current_password
attr_accessor :avatar_new
attr_accessor :display_name
attr_accessor :pgp_pubkey
serialize :preferences, coder: UserPreferences
@@ -17,16 +18,15 @@ class User < ApplicationRecord
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user
has_many :invitees, through: :invitations
has_many :donations, dependent: :nullify
has_many :remote_storage_authorizations
has_many :zaps
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
primary_key: "ln_account", foreign_key: "login"
has_many :accounts, through: :lndhub_user
has_many :remote_storage_authorizations
#
# Validations
#
@@ -50,10 +50,10 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) }
validates_uniqueness_of :nostr_pubkey, allow_blank: true
validate :acceptable_avatar
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
#
# Scopes
#
@@ -95,9 +95,7 @@ class User < ApplicationRecord
LdapManager::UpdateEmail.call(dn: self.dn, address: self.email)
else
# E-Mail from signup confirmed (i.e. account activation)
# TODO Make configurable, only activate globally enabled services
enable_service %w[ discourse gitea mediawiki xmpp ]
enable_default_services
# TODO enable in development when we have easy setup of ejabberd etc.
return if Rails.env.development? || !Setting.ejabberd_enabled?
@@ -135,7 +133,7 @@ class User < ApplicationRecord
def mastodon_address
return nil unless Setting.mastodon_enabled?
"#{self.cn}@#{Setting.mastodon_address_domain}"
"#{self.cn.gsub("-", "_")}@#{Setting.mastodon_address_domain}"
end
def valid_attribute?(attribute_name)
@@ -143,10 +141,8 @@ class User < ApplicationRecord
self.errors[attribute_name].blank?
end
def ln_create_invoice(payload)
lndhub = Lndhub.new
lndhub.authenticate self
lndhub.addinvoice payload
def enable_default_services
enable_service Setting.default_services
end
def dn
@@ -163,30 +159,8 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name]
end
def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end
def services_enabled
ldap_entry[:service] || []
end
def enable_service(service)
current_services = services_enabled
new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq
ldap.replace_attribute(dn, :service, services)
end
def disable_service(service)
current_services = services_enabled
disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :service, services)
end
def disable_all_services
ldap.delete_attribute(dn,:service)
def nostr_pubkey
@nostr_pubkey ||= ldap_entry[:nostr_key]
end
def nostr_pubkey_bech32
@@ -194,6 +168,54 @@ class User < ApplicationRecord
Nostr::PublicKey.new(nostr_pubkey).to_bech32
end
def pgp_pubkey
@pgp_pubkey ||= ldap_entry[:pgp_key]
end
def gnupg_key
return nil unless pgp_pubkey.present?
GPGME::Key.import(pgp_pubkey)
GPGME::Key.get(pgp_fpr)
end
def pgp_pubkey_contains_user_address?
gnupg_key.uids.map(&:email).include?(address)
end
def wkd_hash
ZBase32.encode(Digest::SHA1.digest(cn))
end
def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end
def services_enabled
ldap_entry[:services_enabled] || []
end
def service_enabled?(name)
services_enabled.map(&:to_sym).include?(name.to_sym)
end
def enable_service(service)
current_services = services_enabled
new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq.sort
ldap.replace_attribute(dn, :serviceEnabled, services)
end
def disable_service(service)
current_services = services_enabled
disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq.sort
ldap.replace_attribute(dn, :serviceEnabled, services)
end
def disable_all_services
ldap.delete_attribute(dn,:service)
end
private
def ldap
@@ -213,4 +235,10 @@ class User < ApplicationRecord
errors.add(:avatar, "must be a JPEG or PNG file")
end
end
def acceptable_pgp_key_format
unless GPGME::Key.valid?(pgp_pubkey)
errors.add(:pgp_pubkey, 'is not a valid armored PGP public key block')
end
end
end

View File

@@ -26,4 +26,8 @@ class UserPreferences
end
hash.stringify_keys!.to_h
end
def self.pref_keys
DEFAULT_PREFS.keys.map(&:to_sym)
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

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

View File

@@ -1,7 +1,7 @@
module BtcpayManager
class FetchOnchainWalletBalance < BtcpayManagerService
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

View File

@@ -2,23 +2,35 @@
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
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
def get(endpoint)
res = Faraday.get("#{base_url}/#{endpoint}", {}, {
def base_url
@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",
"Accept" => "application/json",
"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)
end
end

View File

@@ -1,50 +0,0 @@
class CreateAccount < ApplicationService
def initialize(account:)
@username = account[:username]
@domain = account[:ou] || Setting.primary_domain
@email = account[:email]
@password = account[:password]
@invitation = account[:invitation]
@confirmed = account[:confirmed]
end
def call
user = create_user_in_database
add_ldap_document
create_lndhub_account(user) if Setting.lndhub_enabled
if @invitation.present?
update_invitation(user.id)
end
end
private
def create_user_in_database
User.create!(
cn: @username,
ou: @domain,
email: @email,
password: @password,
password_confirmation: @password,
confirmed_at: @confirmed ? DateTime.now : nil
)
end
def update_invitation(user_id)
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
end
# TODO move to confirmation
# (and/or add email_confirmed to entry and use in login filter)
def add_ldap_document
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw)
end
def create_lndhub_account(user)
#TODO enable in development when we have a local lndhub (mock?) API
return if Rails.env.development?
CreateLndhubAccountJob.perform_later(user)
end
end

View File

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

@@ -9,7 +9,7 @@ module LdapManager
attributes = %w{ jpegPhoto }
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
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

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
module UserManager
class CreateAccount < UserManagerService
def initialize(account:)
@username = account[:username]
@domain = account[:ou] || Setting.primary_domain
@email = account[:email]
@password = account[:password]
@invitation = account[:invitation]
@confirmed = account[:confirmed]
end
def call
user = create_user_in_database
add_ldap_document
create_lndhub_account(user) if Setting.lndhub_enabled
if @invitation.present?
update_invitation(user.id)
end
end
private
def create_user_in_database
User.create!(
cn: @username,
ou: @domain,
email: @email,
password: @password,
password_confirmation: @password,
confirmed_at: @confirmed ? DateTime.now : nil
)
end
def update_invitation(user_id)
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
end
def add_ldap_document
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
CreateLdapUserJob.perform_later(
username: @username,
domain: @domain,
email: @email,
hashed_pw: hashed_pw,
confirmed: @confirmed
)
end
def create_lndhub_account(user)
#TODO enable in development when we have a local lndhub (mock?) API
return if Rails.env.development?
CreateLndhubAccountJob.perform_later(user)
end
end
end

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