162 Commits

Author SHA1 Message Date
a97bbf61a8 Fix postgresql query for deleting auth expiry job
All checks were successful
continuous-integration/drone/push Build is passing
Solid Queue uses a text column, instead of a jsonb, so we need to cast
it as jsonb on the fly.
2025-05-05 17:37:58 +04:00
5a523fd220 Merge pull request 'Refactor database configs' (#220) from chore/db_configs into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #220
2025-05-05 12:54:22 +00:00
889c9ae824 Refactor database configs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
* Move postgres credentials to ENV vars
* Allow postgres in development
* Allow SQlite in production
* Refactor optional lndhub db config

Co-authored-by: Greg Karékinian <greg@karekinian.com>
2025-05-05 15:25:25 +04:00
e686cf42e8 Merge pull request 'Switch from Sidekiq to Solid Queue' (#219) from dev/sidekiq_to_solidqueue into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #219
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-05 11:24:56 +00:00
906468d156 Allow to immediately expire auth via job
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
When running the job before its schedule
2025-05-05 12:46:46 +04:00
ee5c6d86d0 Port RS auth job removal to Solid Queue
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-05 11:07:30 +04:00
d1eea85b04 Add Redis gem explicitly, remove sidekiq require
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-04 18:14:49 +04:00
ecd814641a Remove Sidekiq initializer
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-04 17:44:37 +04:00
b1dd5800b2 Update lockfile
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-04 17:42:31 +04:00
0cad4cdcfe WIP Switch from Sidekiq to Solid Queue
Some checks failed
continuous-integration/drone/push Build is running
continuous-integration/drone/pr Build is failing
2025-05-04 17:40:33 +04:00
b61906059c Merge pull request 'Upgrade Rails to 8.0' (#216) from chore/upgrade_rails into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #216
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-04-30 08:36:16 +00:00
aef779a59c Switch from Sprockets to Propshaft
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 1s
2025-04-29 17:11:21 +04:00
1ddecab2c3 Upgrade Rails to 8.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-04-28 17:49:54 +04:00
74b4bc3875 Upgrade Rails to 7.2
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-28 00:17:25 +04:00
646c95ecc2 Fix local/development RS auth URL
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-27 16:09:32 +04:00
fb054ae455 Add task for generating ctags
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-26 12:37:10 +04:00
536052e9bf Merge pull request 'Upgrade strfry/deno, port strfry policies to @nostrify/policies' (#214) from chore/upgrade_strfry_deno into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #214
2025-04-18 10:51:35 +00:00
b29a0abb0b Document strfry integration
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
2025-04-16 17:34:10 +04:00
29ff486683 Port strfry policies to @nostrify/policies
Use packages from JSR and adapt code for new policy APIs
2025-04-15 19:01:22 +04:00
e53b9dd186 Upgrade strfry docker image
Contains latest strfry (1.0.4) and deno (2.2.10)
2025-04-15 19:00:52 +04:00
a2921297fe Fix seeds
The CreateAccount service has moved to a namespace
2025-04-11 16:14:44 +04: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
213 changed files with 5668 additions and 1457 deletions

View File

@@ -1,6 +1,15 @@
# PRIMARY_DOMAIN=kosmos.org
# AKKOUNTS_DOMAIN=accounts.example.com
# The default backend is SQLite
# DB_ADAPTER=postgresql
# PG_HOST=localhost
# PG_PORT=5432
# PG_DATABASE=akkounts
# PG_DATABASE_QUEUE=akkounts_queue
# PG_USERNAME=akkounts
# PG_PASSWORD=
# SMTP_SERVER=smtp.example.com
# SMTP_PORT=587
# SMTP_LOGIN=accounts
@@ -29,6 +38,7 @@
#
# Service Integrations
# (sorted alphabetically by service name)
#
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
@@ -62,5 +72,9 @@
# 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,4 +1,5 @@
PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.kosmos.org
REDIS_URL='redis://localhost:6379/0'
@@ -11,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'

4
.gitignore vendored
View File

@@ -37,6 +37,7 @@
/yarn-error.log
yarn-debug.log*
.yarn-integrity
bun.lock
# Ignore local dotenv config file
.env
@@ -47,3 +48,6 @@ dump.rdb
/app/assets/builds/*
!/app/assets/builds/.keep
# Ignore generated ctags
*.tags

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

23
Gemfile
View File

@@ -2,13 +2,13 @@ source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 7.1'
gem 'rails', '~> 8.0'
# Use Puma as the app server
gem 'puma', '~> 4.1'
gem 'puma', '~> 6.6'
# View components
gem "view_component"
# Separate dependency since Rails 7.0
gem 'sprockets-rails'
# Asset bundler
gem 'propshaft'
# Allows custom JS build tasks to integrate with the asset pipeline
gem 'cssbundling-rails'
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
@@ -19,8 +19,6 @@ gem "turbo-rails"
gem "stimulus-rails"
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1'
@@ -44,6 +42,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'
@@ -51,8 +51,8 @@ gem 'down'
gem 'aws-sdk-s3', require: false
# Background/scheduled jobs
gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler'
gem 'solid_queue'
gem "mission_control-jobs"
# Monitoring
gem "sentry-ruby"
@@ -61,12 +61,13 @@ gem "sentry-rails"
# Services
gem 'discourse_api'
gem "lnurl"
gem 'manifique'
gem 'nostr'
gem 'manifique', '~> 1.1.0'
gem 'nostr', '~> 0.6.0'
gem "redis", "~> 5.4"
group :development, :test do
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.7.2'
gem 'sqlite3', '>= 2.1'
gem 'rspec-rails'
gem 'rails-controller-testing'
end

View File

@@ -1,110 +1,109 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.1.3)
actionpack (= 7.1.3)
activesupport (= 7.1.3)
actioncable (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.1.3)
actionpack (= 7.1.3)
activejob (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.3)
actionpack (= 7.1.3)
actionview (= 7.1.3)
activejob (= 7.1.3)
activesupport (= 7.1.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
actionmailbox (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
actionmailer (8.0.2)
actionpack (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.1.3)
actionview (= 7.1.3)
activesupport (= 7.1.3)
actionpack (8.0.2)
actionview (= 8.0.2)
activesupport (= 8.0.2)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actiontext (7.1.3)
actionpack (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
useragent (~> 0.16)
actiontext (8.0.2)
actionpack (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.1.3)
activesupport (= 7.1.3)
actionview (8.0.2)
activesupport (= 8.0.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.1.3)
activesupport (= 7.1.3)
activejob (8.0.2)
activesupport (= 8.0.2)
globalid (>= 0.3.6)
activemodel (7.1.3)
activesupport (= 7.1.3)
activerecord (7.1.3)
activemodel (= 7.1.3)
activesupport (= 7.1.3)
activemodel (8.0.2)
activesupport (= 8.0.2)
activerecord (8.0.2)
activemodel (= 8.0.2)
activesupport (= 8.0.2)
timeout (>= 0.4.0)
activestorage (7.1.3)
actionpack (= 7.1.3)
activejob (= 7.1.3)
activerecord (= 7.1.3)
activesupport (= 7.1.3)
activestorage (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activesupport (= 8.0.2)
marcel (~> 1.0)
activesupport (7.1.3)
activesupport (8.0.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.886.0)
aws-sdk-core (3.191.0)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.3)
aws-eventstream (1.3.2)
aws-partitions (1.1092.0)
aws-sdk-core (3.222.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.77.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.143.0)
aws-sdk-core (~> 3, >= 3.191.0)
logger
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.183.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
backport (1.2.0)
base64 (0.2.0)
bcrypt (3.1.20)
bech32 (1.4.2)
bech32 (1.5.0)
thor (>= 1.1.0)
benchmark (0.3.0)
bigdecimal (3.1.6)
benchmark (0.4.0)
bigdecimal (3.1.9)
bindex (0.8.1)
bip-schnorr (0.7.0)
ecdsa_ext (~> 0.5.0)
builder (3.2.4)
builder (3.3.0)
capybara (3.40.0)
addressable
matrix
@@ -114,23 +113,25 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
crack (0.4.6)
concurrent-ruby (1.3.4)
connection_pool (2.5.2)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
cssbundling-rails (1.4.0)
cssbundling-rails (1.4.3)
railties (>= 6.0.0)
database_cleaner (2.0.2)
database_cleaner (2.1.0)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.1.0)
database_cleaner-active_record (2.2.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.4)
devise (4.9.3)
date (3.4.1)
devise (4.9.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -139,105 +140,113 @@ GEM
devise_ldap_authenticatable (0.8.7)
devise (>= 3.4.1)
net-ldap (>= 0.16.0)
diff-lcs (1.5.1)
diff-lcs (1.6.1)
discourse_api (2.0.1)
faraday (~> 2.7)
faraday-follow_redirects
faraday-multipart
rack (>= 1.6)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
down (5.4.1)
dotenv (3.1.8)
dotenv-rails (3.1.8)
dotenv (= 3.1.8)
railties (>= 6.1)
down (5.4.2)
addressable (~> 2.8)
drb (2.2.0)
ruby2_keywords
e2mmap (0.1.0)
drb (2.2.1)
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)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
event_emitter (0.2.6)
eventmachine (1.2.7)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
factory_bot (6.5.1)
activesupport (>= 6.1.0)
factory_bot_rails (6.4.4)
factory_bot (~> 6.5)
railties (>= 5.0.0)
faker (3.2.3)
faker (3.5.1)
i18n (>= 1.8.11, < 2)
faraday (2.9.0)
faraday (2.9.2)
faraday-net_http (>= 2.0, < 3.2)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.1.0)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (3.1.1)
net-http
faye-websocket (0.11.3)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.16.3)
flipper (1.2.2)
ffi (1.17.2)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
flipper (1.3.4)
concurrent-ruby (< 2)
flipper-active_record (1.2.2)
activerecord (>= 4.2, < 8)
flipper (~> 1.2.2)
flipper-ui (1.2.2)
flipper-active_record (1.3.4)
activerecord (>= 4.2, < 9)
flipper (~> 1.3.4)
flipper-ui (1.3.4)
erubi (>= 1.0.0, < 2.0.0)
flipper (~> 1.2.2)
flipper (~> 1.3.4)
rack (>= 1.4, < 4)
rack-protection (>= 1.5.3, <= 4.0.0)
sanitize (< 7)
fugit (1.9.0)
et-orbi (~> 1, >= 1.2.7)
rack-protection (>= 1.5.3, < 5.0.0)
rack-session (>= 1.0.2, < 3.0.0)
sanitize (< 8)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
hashdiff (1.1.0)
i18n (1.14.1)
gpgme (2.0.24)
mini_portile2 (~> 2.7)
hashdiff (1.1.2)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (2.0.1)
importmap-rails (2.1.0)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.7.2)
irb (1.11.1)
rdoc
io-console (0.8.0)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jaro_winkler (1.5.6)
jbuilder (2.11.5)
jaro_winkler (1.6.0)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jmespath (1.6.2)
json (2.7.1)
kramdown (2.4.0)
rexml
json (2.11.3)
kramdown (2.5.1)
rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.3)
launchy (2.5.2)
language_server-protocol (3.17.0.4)
launchy (3.1.1)
addressable (~> 2.8)
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
letter_opener_web (2.0.0)
actionmailer (>= 5.2)
letter_opener (~> 1.7)
railties (>= 5.2)
childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (3.0.0)
actionmailer (>= 6.1)
letter_opener (~> 1.9)
railties (>= 6.1)
rexml
listen (3.8.0)
lint_roller (1.1.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
lnurl (1.1.0)
lnurl (1.1.1)
bech32 (~> 1.1)
lockbox (1.3.2)
loofah (2.22.0)
lockbox (2.0.1)
logger (1.7.0)
loofah (2.24.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -245,22 +254,31 @@ 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)
marcel (1.0.2)
marcel (1.0.4)
matrix (0.4.2)
method_source (1.0.0)
mini_magick (4.12.0)
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.21.2)
multipart-post (2.3.0)
mutex_m (0.2.0)
net-http (0.4.1)
mini_portile2 (2.8.8)
minitest (5.25.5)
mission_control-jobs (1.0.2)
actioncable (>= 7.1)
actionpack (>= 7.1)
activejob (>= 7.1)
activerecord (>= 7.1)
importmap-rails (>= 1.2.1)
irb (~> 1.13)
railties (>= 7.1)
stimulus-rails
turbo-rails
multipart-post (2.4.1)
net-http (0.6.0)
uri
net-imap (0.4.9.1)
net-imap (0.5.7)
date
net-protocol
net-ldap (0.19.0)
@@ -268,62 +286,74 @@ GEM
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.4.0.1)
net-smtp (0.5.1)
net-protocol
nio4r (2.7.0)
nokogiri (1.16.0)
nio4r (2.7.4)
nokogiri (1.16.8)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.0-arm64-darwin)
nokogiri (1.16.8-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.0-x86_64-linux)
nokogiri (1.16.8-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)
json (~> 2.6)
observer (0.1.2)
orm_adapter (0.5.0)
pagy (6.4.3)
parallel (1.24.0)
parser (3.3.0.5)
ostruct (0.6.1)
pagy (6.5.0)
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1)
racc
pg (1.5.4)
psych (5.1.2)
pg (1.5.9)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
prism (1.4.0)
propshaft (1.1.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
railties (>= 7.0.0)
psych (5.2.3)
date
stringio
public_suffix (5.0.4)
puma (4.3.12)
public_suffix (6.0.1)
puma (6.6.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.7.3)
rack (2.2.8)
racc (1.8.1)
rack (2.2.13)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-session (1.0.2)
rack (< 3)
rack-test (2.1.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (1.0.0)
rackup (1.0.1)
rack (< 3)
webrick
rails (7.1.3)
actioncable (= 7.1.3)
actionmailbox (= 7.1.3)
actionmailer (= 7.1.3)
actionpack (= 7.1.3)
actiontext (= 7.1.3)
actionview (= 7.1.3)
activejob (= 7.1.3)
activemodel (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
rails (8.0.2)
actioncable (= 8.0.2)
actionmailbox (= 8.0.2)
actionmailer (= 8.0.2)
actionpack (= 8.0.2)
actiontext (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activemodel (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
bundler (>= 1.15.0)
railties (= 7.1.3)
railties (= 8.0.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -332,138 +362,140 @@ GEM
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (~> 1.14)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-settings-cached (2.8.3)
activerecord (>= 5.0.0)
railties (>= 5.0.0)
railties (7.1.3)
actionpack (= 7.1.3)
activesupport (= 7.1.3)
irb
railties (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.1.0)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
rb-inotify (0.11.1)
ffi (~> 1.0)
rbs (2.8.4)
rdoc (6.6.2)
rbs (3.9.2)
logger
rdoc (6.13.1)
psych (>= 4.0.0)
redis (4.8.1)
regexp_parser (2.9.0)
reline (0.4.2)
redis (5.4.0)
redis-client (>= 0.22.0)
redis-client (0.24.0)
connection_pool
regexp_parser (2.10.0)
reline (0.6.1)
io-console (~> 0.5)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
reverse_markdown (2.1.1)
reverse_markdown (3.0.0)
nokogiri
rexml (3.2.6)
rexml (3.4.1)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
rspec-core (3.13.3)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.6)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.1.1)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.1)
rubocop (1.60.2)
rspec-support (~> 3.13.0)
rspec-rails (7.1.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.2)
rubocop (1.75.3)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.30.0, < 2.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.44.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
ruby-progressbar (1.13.0)
ruby-vips (2.2.0)
ruby-vips (2.2.3)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
rufus-scheduler (3.9.1)
fugit (~> 1.1, >= 1.1.6)
sanitize (6.1.0)
logger
sanitize (7.0.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
sentry-rails (5.16.1)
nokogiri (>= 1.16.8)
securerandom (0.4.1)
sentry-rails (5.23.0)
railties (>= 5.0)
sentry-ruby (~> 5.16.1)
sentry-ruby (5.16.1)
sentry-ruby (~> 5.23.0)
sentry-ruby (5.23.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
sidekiq-scheduler (5.0.3)
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0)
solargraph (0.50.0)
solargraph (0.54.2)
backport (~> 1.2)
benchmark
benchmark (~> 0.4)
bundler (~> 2.0)
diff-lcs (~> 1.4)
e2mmap
jaro_winkler (~> 1.5)
jaro_winkler (~> 1.6)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1)
logger (~> 1.6)
observer (~> 0.1)
ostruct (~> 0.6)
parser (~> 3.0)
rbs (~> 2.0)
reverse_markdown (~> 2.0)
rbs (~> 3.3)
reverse_markdown (~> 3.0)
rubocop (~> 1.38)
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
sprockets (4.2.1)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.7.2)
yard-solargraph (~> 0.1)
solid_queue (1.1.5)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11.0)
railties (>= 7.1)
thor (~> 1.3.1)
sqlite3 (2.6.0)
mini_portile2 (~> 2.8.0)
sqlite3 (1.7.2-arm64-darwin)
sqlite3 (1.7.2-x86_64-linux)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
stringio (3.1.0)
thor (1.3.0)
tilt (2.3.0)
timeout (0.4.1)
turbo-rails (1.5.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
sqlite3 (2.6.0-arm64-darwin)
sqlite3 (2.6.0-x86_64-linux-gnu)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.7)
thor (1.3.2)
tilt (2.6.0)
timeout (0.4.3)
turbo-rails (2.0.13)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
uri (0.13.0)
view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
useragent (0.16.11)
view_component (3.22.0)
activesupport (>= 5.2.0, < 8.1)
concurrent-ruby (= 1.3.4)
method_source (~> 1.0)
warden (1.2.9)
rack (>= 2.0.9)
@@ -472,18 +504,22 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.19.1)
webmock (3.25.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.1)
websocket-driver (0.7.6)
webrick (1.9.1)
websocket-driver (0.7.7)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.34)
zeitwerk (2.6.12)
yard (0.9.37)
yard-solargraph (0.1.0)
yard (~> 0.9)
zbase32 (0.1.1)
zeitwerk (2.7.2)
PLATFORMS
arm64-darwin-22
@@ -507,6 +543,7 @@ DEPENDENCIES
flipper
flipper-active_record
flipper-ui
gpgme (~> 2.0.24)
image_processing (~> 1.12.2)
importmap-rails
jbuilder (~> 2.7)
@@ -515,24 +552,25 @@ DEPENDENCIES
listen (~> 3.2)
lnurl
lockbox
manifique
manifique (~> 1.1.0)
mission_control-jobs
net-ldap
nostr
nostr (~> 0.6.0)
pagy (~> 6.0, >= 6.0.2)
pg (~> 1.5)
puma (~> 4.1)
rails (~> 7.1)
propshaft
puma (~> 6.6)
rails (~> 8.0)
rails-controller-testing
rails-settings-cached (~> 2.8.3)
redis (~> 5.4)
rqrcode (~> 2.0)
rspec-rails
sentry-rails
sentry-ruby
sidekiq (< 7)
sidekiq-scheduler
solargraph
sprockets-rails
sqlite3 (~> 1.7.2)
solid_queue
sqlite3 (>= 2.1)
stimulus-rails
turbo-rails
tzinfo-data
@@ -540,6 +578,7 @@ DEPENDENCIES
warden
web-console (~> 4.2)
webmock
zbase32 (~> 0.1.1)
BUNDLED WITH
2.5.5

View File

@@ -57,7 +57,7 @@ Running the test suite:
Running the test suite with Docker Compose requires overriding the Rails
environment:
docker-compose run -e "RAILS_ENV=test" web rspec
docker-compose exec -e "RAILS_ENV=test" web rspec
### Docker Compose

View File

@@ -1,4 +0,0 @@
//= link_tree ../images
//= link_tree ../../javascript .js
//= link_tree ../builds
//= link_tree ../../../vendor/javascript .js

View File

@@ -42,6 +42,11 @@
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

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

@@ -63,4 +63,9 @@ class ApplicationController < ActionController::Base
@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,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

@@ -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 == "nostr"
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,26 +99,29 @@ 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
signed_event = Nostr::Event.new(**nostr_event_from_params)
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]})"
is_valid_sig = signed_event.verify_signature
is_valid_auth = NostrManager::VerifyAuth.call(
event: signed_event,
challenge: session[:shared_secret]
)
unless is_valid_id && is_valid_sig && is_correct_content
unless is_valid_sig && is_valid_auth
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event[:pubkey])
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
if user_with_pubkey.present? && (user_with_pubkey != current_user)
flash[:alert] = "Public key already in use for a different account"
http_status :unprocessable_entity and return
end
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event[:pubkey])
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event.pubkey)
session[:shared_secret] = nil
flash[:success] = "Public key verification successful"
@@ -145,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
@@ -160,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, :content, :sig, tags: []
])
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.rs_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,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

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

@@ -3,8 +3,6 @@ class RemoteStorageExpireAuthorizationJob < ApplicationJob
def perform(rs_auth_id)
rs_auth = RemoteStorageAuthorization.find rs_auth_id
return unless rs_auth.expire_at.nil? || rs_auth.expire_at <= DateTime.now
rs_auth.destroy!
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,19 +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"
mail to: @user.email, subject: @subject
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,19 @@
module Settings
module RemoteStorageSettings
extend ActiveSupport::Concern
included do
field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present?
field :rs_accounts_domain, type: :string,
default: ENV["RS_AKKOUNTS_DOMAIN"] || ENV["AKKOUNTS_DOMAIN"]
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

@@ -2,7 +2,7 @@ class RemoteStorageAuthorization < ApplicationRecord
belongs_to :user
belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true
serialize :permissions unless Rails.env.production?
serialize :permissions, coder: YAML unless Rails.env.production?
validates_presence_of :permissions
validates_presence_of :client_id
@@ -69,11 +69,19 @@ class RemoteStorageAuthorization < ApplicationRecord
end
def remove_token_expiry_job
queue = Sidekiq::Queue.new(RemoteStorageExpireAuthorizationJob.queue_name)
queue.each do |job|
next unless job.display_class == "RemoteStorageExpireAuthorizationJob"
job.delete if job.display_args == [id]
end
job_class = RemoteStorageExpireAuthorizationJob
job_args = [id]
query = SolidQueue::Job.where(class_name: job_class.to_s)
case ActiveRecord::Base.connection.adapter_name.downcase
when /sqlite/
query.where("json_extract(arguments, '$.arguments') = ?", job_args.to_json)
when /postgres/
query.where("CAST(arguments AS jsonb)->>'arguments' = ?", job_args.to_json)
else
raise "Unsupported database adapter"
end.destroy_all
end
def find_or_create_web_app

View File

@@ -2,208 +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_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
#
# 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: false
#
# OpenCollective
#
field :opencollective_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
#
@@ -52,6 +52,8 @@ class User < ApplicationRecord
validate :acceptable_avatar
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
#
# Scopes
#
@@ -93,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?
@@ -133,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)
@@ -141,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
@@ -170,6 +168,24 @@ 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
@@ -178,17 +194,21 @@ class User < ApplicationRecord
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
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
services = (current_services - disabled_services).uniq.sort
ldap.replace_attribute(dn, :serviceEnabled, services)
end
@@ -215,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

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

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

@@ -58,7 +58,7 @@ class LdapService < ApplicationService
attributes = %w[
dn cn uid mail displayName admin serviceEnabled
mailRoutingAddress mailpassword nostrKey
mailRoutingAddress mailpassword nostrKey pgpKey
]
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
@@ -73,7 +73,8 @@ class LdapService < ApplicationService
services_enabled: e.try(:serviceEnabled),
email_maildrop: e.try(:mailRoutingAddress),
email_password: e.try(:mailpassword),
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil,
pgp_key: e.try(:pgpKey) ? e.pgpKey.first : nil
}
end
end
@@ -101,7 +102,7 @@ class LdapService < ApplicationService
dn = "ou=#{ou},cn=users,#{ldap_suffix}"
aci = <<-EOS
(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_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,#{ldap_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 = {

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

View File

@@ -0,0 +1,19 @@
module UserManager
class CreateInvitations < UserManagerService
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
end

View File

@@ -0,0 +1,19 @@
require 'gpgme'
module UserManager
class PgpEncrypt < UserManagerService
def initialize(user:, text:)
@user = user
@text = text
end
def call
crypto = GPGME::Crypto.new
crypto.encrypt(
@text,
recipients: @user.gnupg_key,
always_trust: true
)
end
end
end

View File

@@ -0,0 +1,24 @@
module UserManager
class UpdatePgpKey < UserManagerService
def initialize(user:)
@user = user
end
def call
if @user.pgp_pubkey.blank?
@user.update! pgp_fpr: nil
else
result = GPGME::Key.import(@user.pgp_pubkey)
if result.imports.present?
@user.update! pgp_fpr: result.imports.first.fpr
else
# TODO notify Sentry, user
raise "Failed to import OpenPGP pubkey"
end
end
LdapManager::UpdatePgpKey.call(dn: @user.dn, pubkey: @user.pgp_pubkey)
end
end
end

View File

@@ -0,0 +1,2 @@
class UserManagerService < ApplicationService
end

View File

@@ -38,8 +38,8 @@
<tr>
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
<td><%= link_to invitation.user.address, admin_user_path(invitation.user.address), class: "ks-text-link" %></td>
<td><%= link_to invitation.invitee.address, admin_user_path(invitation.invitee.address), class: "ks-text-link" %></td>
<td><%= link_to invitation.user.cn, admin_user_path(invitation.user.cn), class: "ks-text-link" %></td>
<td><%= link_to invitation.invitee.cn, admin_user_path(invitation.invitee.cn), class: "ks-text-link" %></td>
</tr>
<% end %>
</tbody>

View File

@@ -36,7 +36,7 @@
</td>
<td>
<% if user = @users.find{ |u| u[2] == account.login } %>
<%= link_to "#{user[0]}@#{user[1]}", admin_user_path("#{user[0]}@#{user[1]}"), class: "ks-text-link" %>
<%= link_to user[0], admin_user_path(user[0]), class: "ks-text-link" %>
<% end %>
</td>
<td><%= number_with_delimiter account.balance.to_i.to_s %></td>

View File

@@ -9,18 +9,36 @@
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
<% end %>
<label class="block">
<p class="font-bold mb-1">Reserved usernames</p>
<p class="text-gray-500">
These usernames cannot be registered as accounts:
</p>
<%= f.text_area :reserved_usernames,
value: Setting.reserved_usernames.join("\n"),
class: "h-44 mb-2" %>
<p class="text-sm text-gray-500">
One username per line
</p>
</label>
<ul role="list">
<%= render FormElements::FieldsetComponent.new(
title: "Reserved usernames",
description: "These usernames cannot be registered as accounts."
) do %>
<%= f.text_area :reserved_usernames,
value: Setting.reserved_usernames.join("\n"),
class: "h-44 w-60" %>
<p class="text-sm text-gray-500">
One username per line
</p>
<% end %>
<li>
<p class="font-bold mb-1">Default services</p>
<p class="text-gray-500">
These services are enabled for new users by default after signup.
</p>
<div class="flex flex-wrap gap-x-6 gap-y-2">
<% Setting.available_services.each do |option| %>
<div class="md:inline-block">
<%= f.check_box :default_services,
{ multiple: true, checked: Setting.default_services.include?(option),
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600 mr-0.5" },
option, nil %>
<%= f.label "default_services_#{option.parameterize}", service_human_name(option) %>
</div>
<% end %>
</div>
</li>
</ul>
</section>
<section>

View File

@@ -7,4 +7,37 @@
title: "Enable Nostr integration (experimental)",
description: "Allow adding nostr pubkeys and resolve user addresses via NIP-05"
) %>
<% if Setting.nostr_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_private_key,
type: :password,
title: "Private key",
description: "The private key of the accounts service, used when publishing events (e.g. zap receipts)"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_public_key,
title: "Public key",
description: "The corresponding public key of the accounts service"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_public_key_primary_domain,
title: "Public key for primary domain (NIP-05)",
description: "(optional) A different pubkey to announce for the _@#{Setting.primary_domain} Nostr address"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_relay_url,
title: "Relay URL",
description: "Websockets URL of a relay associated with #{Setting.primary_domain}"
) %>
</ul>
</section>
<section>
<h3>Zaps</h3>
<ul role="list">
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_zaps_relay_limit,
title: "Relay limit",
description: "The maximum number of relays to publish zap receipts to"
) %>
</ul>
<% end %>

View File

@@ -1,5 +1,4 @@
<h3>RemoteStorage</h3>
<p class="text-red-600 mb-8">Feature currently in development.</p>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,

View File

@@ -36,7 +36,7 @@
<th>Invited by</th>
<td>
<% if @user.inviter %>
<%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %>
<%= link_to @user.inviter.cn, admin_user_path(@user.inviter.cn), class: 'ks-text-link' %>
<% else %>&mdash;<% end %>
</td>
</tr>
@@ -78,7 +78,7 @@
<% if @user.invitees.length > 0 %>
<ul class="mb-0">
<% @user.invitees.order(cn: :asc).each do |invitee| %>
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %></li>
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.cn, admin_user_path(invitee.cn), class: 'ks-text-link' %></li>
<% end %>
</ul>
<% else %>&mdash;<% end %>
@@ -89,13 +89,47 @@
</section>
<section class="sm:flex-1 sm:pt-0">
<% if @avatar.present? %>
<h3>LDAP<h3>
<p>
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
</p>
<% end %>
<!-- <h3>Actions</h3> -->
<h3>LDAP</h3>
<table class="divided">
<tbody>
<tr>
<th>Avatar</th>
<td>
<% if @avatar.present? %>
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
<% else %>
&mdash;
<% end %>
</td>
</tr>
<tr>
<th>Display name</th>
<td><%= @user.display_name || "—" %></td>
</tr>
<tr>
<th class="align-top">PGP key</th>
<td class="align-top leading-5">
<% if @user.pgp_pubkey.present? %>
<span class="font-mono" title="<%= @user.pgp_fpr %>">
<% if @user.pgp_pubkey_contains_user_address? %>
<%= link_to wkd_key_url(hashed_username: @user.wkd_hash, l: @user.cn, format: :txt),
class: "ks-text-link", target: "_blank" do %>
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
<% end %>
<% else %>
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
<% end %>
</span><br />
<% @user.gnupg_key.uids.each do |uid| %>
<%= uid.uid %><br />
<% end %>
<% else %>
&mdash;
<% end %>
</td>
</tr>
</tbody>
</table>
</section>
</div>
@@ -124,6 +158,19 @@
</td>
</tr>
<% end %>
<% if Setting.email_enabled %>
<tr>
<td>E-Mail</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: Flipper.enabled?(:email, current_user),
input_enabled: false
) %>
</td>
<td class="text-right">
</td>
</tr>
<% end %>
<% if Setting.gitea_enabled %>
<tr>
<td>Gitea</td>
@@ -171,7 +218,7 @@
<td>XMPP (ejabberd)</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("xmpp"),
enabled: @services_enabled.include?("ejabberd"),
input_enabled: false
) %>
</td>
@@ -182,6 +229,33 @@
</td>
</tr>
<% end %>
<% if Setting.nostr_enabled %>
<tr>
<td>Nostr</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @user.nostr_pubkey.present?,
input_enabled: false
) %>
</td>
<td class="text-right">
<%= link_to "Open profile", "https://njump.me/#{@user.nostr_pubkey_bech32}", class: "btn-sm btn-gray" %>
</td>
</tr>
<% end %>
<% if Setting.remotestorage_enabled %>
<tr>
<td>remoteStorage</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: Flipper.enabled?(:remotestorage, current_user) && @services_enabled.include?("remotestorage"),
input_enabled: false
) %>
</td>
<td class="text-right">
</td>
</tr>
<% end %>
</tbody>
</table>
</section>

View File

@@ -16,8 +16,8 @@
<p>
There's something to do for everyone, especially non-programmers! For
example, we need more help with graphics, UI/UX design, and
content/copywriting. We also need moderators for social media. And beta
testers for our software. The list doesn't end there.
content/copywriting. Also, testing any of our software and reporting
issues you encounter along the way is very valuable.
</p>
<p>
A good way to get started is to join one of our
@@ -43,7 +43,7 @@
</p>
<p>
We have run two 6-month trials so far, with the next trial period
starting sometime in Q1 2024. Watch your email for notifications about it!
starting sometime soon. Watch your email for notifications about it!
</p>
</section>
<% end %>

View File

@@ -5,7 +5,7 @@
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<% if Setting.ejabberd_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-cover bg-[center_top_-50px] bg-no-repeat
bg-[length:86%] bg-[center_top_-40px] bg-no-repeat
bg-[url(/img/logos/icon_xmpp.svg)]">
<%= link_to services_chat_path,
class: "block h-full px-6 py-6 rounded-md" do %>
@@ -18,7 +18,7 @@
<% end %>
<% if Setting.mastodon_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:80%] bg-[right_top_-30px] bg-no-repeat
bg-[length:88%] bg-[center_top_-40px] bg-no-repeat
bg-[url(/img/logos/icon_mastodon.svg)]">
<%= link_to services_mastodon_path, class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Mastodon</h3>
@@ -30,7 +30,9 @@
<% end %>
<% if Setting.email_enabled? &&
Flipper.enabled?(:email, current_user) %>
<div class="border border-gray-300 rounded-md hover:border-gray-400">
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:90%] bg-[center_top_-160px] bg-no-repeat
bg-[url(/img/logos/icon_mail.svg)]">
<%= link_to services_email_path, class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">E-Mail</h3>
<p class="text-gray-600">
@@ -39,15 +41,16 @@
<% end %>
</div>
<% end %>
<% if Setting.discourse_enabled? %>
<% if Setting.remotestorage_enabled? &&
Flipper.enabled?(:remotestorage, current_user) %>
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:95%] bg-center bg-no-repeat
bg-[url(/img/logos/icon_discourse.svg)]">
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
bg-[length:80%] bg-[center_top_-156px] bg-no-repeat
bg-[url(/img/logos/icon_remotestorage.svg)]">
<%= link_to services_storage_path,
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Discourse</h3>
<h3 class="mb-3.5">Storage</h3>
<p class="text-gray-600">
Kosmos community forums and user support/help site
Sync your data between apps and devices
</p>
<% end %>
</div>
@@ -65,21 +68,22 @@
<% end %>
</div>
<% end %>
<% if Setting.remotestorage_enabled? &&
Flipper.enabled?(:remotestorage, current_user) %>
<div class="border border-gray-300 rounded-md hover:border-gray-400">
<%= link_to services_storage_path,
<% if Setting.discourse_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:80%] bg-center bg-no-repeat
bg-[url(/img/logos/icon_discourse.svg)]">
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Storage</h3>
<h3 class="mb-3.5">Discourse</h3>
<p class="text-gray-600">
Sync your data between apps and devices
Community forums and support/help site
</p>
<% end %>
</div>
<% end %>
<% if Setting.gitea_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-cover bg-center bg-no-repeat
bg-[length:92%] bg-center bg-no-repeat
bg-[url(/img/logos/icon_gitea.png)]">
<%= link_to Setting.gitea_public_url,
class: "block h-full px-6 py-6 rounded-md" do %>
@@ -92,7 +96,7 @@
<% end %>
<% if Setting.droneci_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-cover bg-[center_top_-70px] bg-no-repeat
bg-[length:86%] bg-[center_top_-60px] bg-no-repeat
bg-[url(/img/logos/icon_droneci.svg)]">
<%= link_to Setting.droneci_public_url,
class: "block h-full px-6 py-6 rounded-md" do %>

View File

@@ -55,4 +55,27 @@
<%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %>
</p>
<% end %>
<div data-controller="nostr-login"
data-nostr-login-target="loginForm"
data-nostr-login-site-value="<%= Setting.accounts_domain %>"
data-nostr-login-shared-secret-value="<%= session[:shared_secret] %>"
class="hidden">
<div class="relative my-6">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
<div class="w-full border-t border-gray-200"></div>
</div>
<div class="relative flex justify-center">
<span class="bg-white px-2 text-sm text-gray-500 italic">or</span>
</div>
</div>
<p>
<button disabled tabindex="5"
class="w-full btn-md btn-gray text-purple-600"
data-nostr-login-target="loginButton"
data-action="nostr-login#login">
Log in with Nostr
</button>
</p>
</div>
<% end %>

View File

@@ -100,6 +100,14 @@
["Website", "https://www.thunderbird.net"]
]
) %>
<%= render AppInfoComponent.new(
name: "Geary",
description: "Built around conversations, for the GNOME desktop",
icon_path: "/img/logos/icon_geary.png",
links: [
["Website", "https://wiki.gnome.org/Apps/Geary"]
]
) %>
</div>
<div id="apps-windows" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel">

View File

@@ -98,7 +98,17 @@
description: "The official Web app",
icon_path: "/img/logos/icon_mastodon-2.svg",
links: [
["Launch", "https://kosmos.social"]
["Launch", "https://kosmos.social"],
["GitHub", "https://github.com/mastodon/mastodon"]
]
) %>
<%= render AppInfoComponent.new(
name: "Phanpy",
description: " A slick, feature-rich Web app for mobile and desktop",
icon_path: "/img/logos/icon_phanpy.svg",
links: [
["Launch", "https://phanpy.social"],
["GitHub", "https://github.com/cheeaun/phanpy"]
]
) %>
<%= render AppInfoComponent.new(
@@ -150,6 +160,15 @@
["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"]
]
) %>
<%= render AppInfoComponent.new(
name: "Phanpy",
description: " A slick, feature-rich Web app for mobile and desktop",
icon_path: "/img/logos/icon_phanpy.svg",
links: [
["Launch", "https://phanpy.social"],
["GitHub", "https://github.com/cheeaun/phanpy"]
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
@@ -180,6 +199,15 @@
["App Store", "https://apps.apple.com/app/mammoth-for-mastodon/id1667573899"]
]
) %>
<%= render AppInfoComponent.new(
name: "Phanpy",
description: " A slick, feature-rich Web app for mobile and desktop",
icon_path: "/img/logos/icon_phanpy.svg",
links: [
["Launch", "https://phanpy.social"],
["GitHub", "https://github.com/cheeaun/phanpy"]
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(

View File

@@ -2,15 +2,163 @@
<%= render MainSimpleComponent.new do %>
<section>
<h3 class="mb-10">Connected Apps</h3>
<% if @rs_auths.any? %>
<div class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-10 gap-x-12">
<% @rs_auths.each do |auth| %>
<%= render RsAuthComponent.new(auth: auth) %>
<% end %>
<p class="mb-6">
Store and synchronize your app data across different devices.
</p>
</section>
<%= render partial: "shared/tabnav_remotestorage" %>
<section>
<h3>Your Storage Address</h3>
<p class="mb-6">
In order to connect an app to your storage account, give it your address:
</p>
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
<img src="/img/logos/icon_remotestorage.svg"
class="inline-block h-6 w-6 mr-1 self-center">
<input type="text" id="user_address" class="grow"
value=<%= current_user.address %> disabled="disabled"
data-clipboard-target="source" />
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard">
<span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
<span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
</button>
</p>
</section>
<section>
<h3>Compatible Apps</h3>
<p>
Your Storage account is based on a new open standard called
<a href="https://remotestorage.io" target="_blank">
<img src="/img/logos/icon_remotestorage.svg" class="h-4 w-4 inline">
<strong>remoteStorage</strong>
</a>, which is not yet widely supported. Look
for the remoteStorage icon, or check the Sync settings in apps.
</p>
<p>
If you want your favorite apps to support syncing data with your own
Storage account, let the developers know! All relevant information is
available on the <a href="https://remotestorage.io"
target="_blank" class="ks-text-link">remoteStorage website</a>.
</p>
</section>
<section>
<h3>Recommended Apps</h3>
<div data-controller="tabs"
data-tabs-active-tab-class="-mb-px border-gray-200 border-l border-t border-r rounded-t text-indigo-600 hover:text-indigo-600"
data-tabs-inactive-tab-class="text-gray-500 hover:text-gray-700"
class="mb-12">
<select data-action="tabs#change" data-tabs-target="select"
class="block w-full mb-8 sm:hidden">
<option>Productivity</option>
<option>Bookmarks</option>
<option>Reading</option>
<option>File sharing</option>
<option>Learning</option>
</select>
<ul class="hidden sm:flex list-reset mb-8 border-gray-200 border-b">
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Productivity
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Bookmarks
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Reading
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
File sharing
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Learning
</a>
</li>
</ul>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Hyperdraft",
description: "Create text notes and (optionally) turn them into a website",
icon_path: "/img/app_icons/hyperdraft.png",
links: [
["Website", "https://hyperdraft.rosano.ca"],
]
) %>
<%= render AppInfoComponent.new(
name: "Notes Together",
description: "A powerful note-taking app, with support for attaching images and other files",
icon_path: "/img/app_icons/notes-together.png",
links: [
["Web App", "https://notestogether.hominidsoftware.com"],
]
) %>
<%= render AppInfoComponent.new(
name: "Papiers",
description: "A simple note-taking app",
icon_path: "/img/app_icons/papiers.png",
links: [
["Web App", "https://papiers.gitlab.io"],
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Webmarks",
description: "Archive your bookmarks in your remote storage",
icon_path: "/img/app_icons/webmarks.png",
links: [
["Web App", "https://webmarks.5apps.com"],
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Pétrolette",
description: "A news aggregator that syncs with your remote storage",
icon_path: "/img/app_icons/petrolette.png",
links: [
["Web App", "https://petrolette.space"],
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Sharesome",
description: "Quickly and easily share files from your remote storage",
icon_path: "/img/app_icons/sharesome.png",
links: [
["Web App", "https://sharesome.5apps.com"],
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Kommit",
description: "Create flashcards and learn them with spaced-repetition",
icon_path: "/img/app_icons/kommit.png",
links: [
["Website", "https://kommit.rosano.ca"],
]
) %>
</div>
</div>
<% else %>
<p>No apps connected yet.</p>
<% end %>
</section>
<% end %>

View File

@@ -0,0 +1,33 @@
<%= render HeaderComponent.new(title: "Storage") %>
<%= render MainSimpleComponent.new do %>
<section>
<p class="mb-6">
Store and synchronize your app data across different devices.
</p>
</section>
<%= render partial: "shared/tabnav_remotestorage" %>
<section>
<% if @rs_auths.any? %>
<div class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-10 gap-x-12 mt-4">
<% @rs_auths.each do |auth| %>
<%= render RsAuthComponent.new(auth: auth) %>
<% end %>
</div>
<% else %>
<div class="text-center">
<p class="mt-4 mb-12 inline-flex align-center items-center">
<%= image_tag("/img/illustrations/undraw_friends_r511.svg", class: 'h-48') %>
</p>
<h3>
No apps connected
</h3>
<p class="text-gray-500">
When connected, your apps will show up here.
</p>
</div>
<% end %>
</section>
<% end %>

View File

@@ -1,6 +1,6 @@
<%= tag.section data: {
controller: "settings--account--email",
"settings--account--email-validation-failed-value": @validation_errors.present?
"settings--account--email-validation-failed-value": @validation_errors&.[](:email)&.present?
} do %>
<h3>E-Mail</h3>
<%= form_for(@user, url: update_email_settings_path, method: "post") do |f| %>
@@ -23,7 +23,7 @@
</span>
</button>
</p>
<% if @validation_errors.present? && @validation_errors[:email].present? %>
<% if @validation_errors&.[](:email)&.present? %>
<p class="error-msg"><%= @validation_errors[:email].first %></p>
<% end %>
<div class="initial-hidden">
@@ -41,10 +41,33 @@
<% end %>
<section>
<h3>Password</h3>
<p class="mb-8">Use the following button to request an email with a password reset link:</p>
<p class="mb-6">Use the following button to request an email with a password reset link:</p>
<%= form_with(url: reset_password_settings_path, method: :post) do %>
<p>
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
</p>
<% end %>
</section>
<%= form_for(@user, url: setting_path(:account), html: { :method => :put }) do |f| %>
<section class="!pt-8 sm:!pt-12">
<h3>OpenPGP</h3>
<ul role="list">
<%= render FormElements::FieldsetComponent.new(
title: "Public key",
description: "Your OpenPGP public key in ASCII Armor format"
) do %>
<%= f.text_area :pgp_pubkey,
value: @user.pgp_pubkey,
class: "h-24 w-full" %>
<% if @validation_errors&.[](:pgp_pubkey)&.present? %>
<p class="error-msg">This <%= @validation_errors[:pgp_pubkey].first %></p>
<% end %>
<% end %>
</ul>
</section>
<section>
<p class="pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p>
</section>
<% end %>

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