152 Commits

Author SHA1 Message Date
Râu Cao 462dd24da3 WIP contribution nav
continuous-integration/drone/push Build is passing
2023-06-12 14:32:59 +02:00
Râu Cao 8eb5f093a4 Don't show flash message when opening the root URL while signed out 2023-06-08 08:04:23 +03:00
raucao de45d070aa Merge pull request 'Report Lndhub API errors to Sentry' (#133) from refactor/lndhub_integration into master
continuous-integration/drone/push Build is passing
Reviewed-on: #133
2023-06-06 15:44:36 +00:00
raucao c0b1112e49 Merge pull request 'Hide unsuccessful outgoing lndhub txs in list' (#132) from bugfix/lndhub_tx_list into master
continuous-integration/drone/push Build is passing
Reviewed-on: #132
2023-06-06 15:43:38 +00:00
Râu Cao 2f90393eb6 Lndhub v2 service inherits from v1, only adds v2-specific code
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-06-05 13:53:24 +03:00
Râu Cao 8b87072485 Raise custom auth error, re-raise on failed re-auth 2023-06-05 13:52:41 +03:00
Râu Cao 82019f47be Report lndhub errors to Sentry 2023-06-05 13:51:59 +03:00
Râu Cao 259e72167b Hide unsuccessful outgoing lndhub txs in list
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
2023-06-05 13:06:49 +03:00
Râu Cao 7000908891 Auto-login Discourse link
continuous-integration/drone/push Build is passing
2023-06-04 15:15:09 +03:00
Râu Cao df0c13b400 Fix potential nil access
continuous-integration/drone/push Build is passing
2023-05-31 14:43:00 +02:00
Râu Cao 387a2fa2e6 0.6.0
continuous-integration/drone/push Build is passing
2023-05-31 14:12:26 +02:00
raucao 68eba80fd7 Merge pull request 'Integrate Discourse Connect (SSO)' (#131) from feature/126_discourse_sso into master
continuous-integration/drone/push Build is passing
Reviewed-on: #131
2023-05-31 10:02:43 +00:00
Râu Cao 7e05530ab7 Add specs for Discourse Connect
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-05-31 12:00:33 +02:00
Râu Cao 745a319b3d Minor refactoring 2023-05-31 12:00:31 +02:00
Râu Cao f829bb3379 Use devise method for requiring login 2023-05-31 12:00:02 +02:00
Râu Cao 19bafe081f Integrate Discourse Connect (SSO) 2023-05-31 12:00:02 +02:00
greg d130f2f68b Merge pull request 'Allow users to set/update their display name in LDAP' (#128) from feature/123-display_names into master
continuous-integration/drone/push Build is passing
Reviewed-on: #128
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-05-31 09:13:50 +00:00
Râu Cao e284996c1c Remove obsolete route
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
2023-05-28 15:28:51 +02:00
Râu Cao 51489a83ab Use feature block for email update specs
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-05-28 15:25:53 +02:00
Râu Cao 05426e4ced Add specs for display name update 2023-05-28 15:25:42 +02:00
Râu Cao 445cdfa024 Only validate display name when updated
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Otherwise we needlessly fetch the validated one from LDAP every time a
model is saved.
2023-05-27 20:11:01 +02:00
Râu Cao f74227fedb Allow users to set/update their display name in LDAP
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-05-27 19:59:49 +02:00
Râu Cao 32d1992632 Set user instance var for settings routes where needed 2023-05-27 19:58:59 +02:00
greg 48be35f1b1 Merge pull request 'Allow updating one's email address on the account settings page' (#127) from feature/103-update_email into master
continuous-integration/drone/push Build is passing
Reviewed-on: #127
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-05-26 18:07:07 +00:00
greg 87720ef285 Merge pull request 'Add feature flags' (#125) from feature/124-feature_flags into master
continuous-integration/drone/push Build is passing
Reviewed-on: #125
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-05-26 17:56:50 +00:00
Râu Cao 193a4c2edd Remove obsolete function argument
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
2023-05-25 19:31:16 +02:00
Râu Cao 134c81460a Allow email address updates on account settings page
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-05-25 16:58:53 +02:00
Râu Cao b1a693e7cf Send different Devise mail for re-confirmations 2023-05-25 16:58:45 +02:00
Râu Cao 75bd879f84 Rename settings menu item for Lightning 2023-05-25 16:57:14 +02:00
Râu Cao 33a9e1eaa9 Use username instead of email in Devise mails 2023-05-25 16:56:40 +02:00
Râu Cao 7b321577db Update LDAP mail attribute when re-confirming email 2023-05-25 16:55:27 +02:00
Râu Cao 61f12c2741 Improve form fields with errors for model updates 2023-05-25 16:53:16 +02:00
Râu Cao c58358c66e Add feature flags, RS dashboard dummy
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
closes #124
refs #94
2023-05-23 19:18:11 +02:00
Râu Cao 287adbd365 Add flipper gem and database migration/tables 2023-05-23 14:09:35 +02:00
Râu Cao 9048052318 Fix URL in email template
continuous-integration/drone/push Build is passing
2023-05-16 13:22:44 +02:00
raucao cddc1e86f6 Merge pull request 'Show fees of Lightning transactions' (#122) from feature/lightning_fees into master
continuous-integration/drone/push Build is passing
Reviewed-on: #122
Reviewed-by: hueso <hueso@noreply.kosmos.org>
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2023-05-10 12:27:24 +00:00
Râu Cao ce7387a409 Remove obsolete routes
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
2023-05-03 21:54:33 +02:00
Râu Cao f1ae5667de Shape tx details UI a bit
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-05-03 12:51:22 +02:00
Râu Cao 67a9fc02d7 Rename Wallet to Lightning Network, move to Services
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-05-01 16:13:41 +02:00
Râu Cao 34849b28b0 WIP show fees of Lightning transactions 2023-05-01 15:15:23 +02:00
raucao 8ce5f9708f Merge pull request 'Add configurable default chatroom bookmarks for new users' (#116) from feature/default_chatrooms into master
continuous-integration/drone/push Build is passing
Reviewed-on: #116
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-04-19 13:07:00 +00:00
Râu Cao cb2197893c Merge branch 'master' into feature/default_chatrooms
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
2023-04-18 17:00:48 +02:00
raucao 7a50bd23d6 Merge pull request 'Add user preferences and configurable notifications' (#113) from feature/user_preferences into master
continuous-integration/drone/push Build is passing
Reviewed-on: #113
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-04-11 21:04:46 +00:00
raucao 64c8c3cb06 Merge pull request 'WebFinger endpoint' (#118) from feature/webfinger into master
continuous-integration/drone/push Build is passing
Reviewed-on: #118
Reviewed-by: raucao <raucao@noreply.kosmos.org>
2023-04-11 09:44:39 +00:00
Râu Cao a2100b23a9 Formatting, wording
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Update release notes draft
2023-04-11 11:41:30 +02:00
raucao 27195f693a Merge pull request 'Fix failing spec expectation when using Ruby 3.x' (#119) from fix/ruby-3-failed-expectation into master
continuous-integration/drone/push Build is passing
Reviewed-on: #119
Reviewed-by: raucao <raucao@noreply.kosmos.org>
2023-04-11 09:32:46 +00:00
galfert 9e74c89a80 Fix failing spec expectation when using Ruby 3.x
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Update release notes draft
2023-04-10 23:03:59 +02:00
galfert 0774c88918 WebFinger endpoint
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-04-10 21:23:21 +02:00
raucao ef2d2b6422 Merge pull request 'Add remoteStorage settings' (#117) from feature/rs-settings into master
continuous-integration/drone/push Build is passing
Reviewed-on: #117
Reviewed-by: raucao <raucao@noreply.kosmos.org>
2023-04-09 09:45:19 +00:00
galfert a47e4fc16b Add RS storage URL to test env
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Update release notes draft
2023-04-09 10:12:12 +02:00
galfert 9b89101afc Basic RemoteStorage settings
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-04-08 21:49:16 +02:00
Râu Cao ad90fcd539 Add specs for xmpp default bookmarks, refactor xmpp job usage
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-04-08 16:37:21 +02:00
Râu Cao 705bd63b42 Add configurable default room bookmarks for new users
continuous-integration/drone/push Build is passing
2023-04-07 23:03:43 +02:00
Râu Cao 83e418cdee Update README
continuous-integration/drone/push Build is passing
2023-04-07 20:11:45 +02:00
Râu Cao 7a193d6647 Add comment
continuous-integration/drone/push Build is passing
2023-04-06 16:25:01 +02:00
Râu Cao bb82b6b462 Update README 2023-04-06 16:24:46 +02:00
Râu Cao 4e2e13108c Refactor user preferences, add defaults from file
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Update release notes draft
* Turn prefs into a flat hash structure, since nesting is not worth the
trouble
* Add a custom serializer class for prefs
* Add a config file for defaults and merge set prefs with unset ones
* Use booleans for "true" and "false", and integers where appropriate
2023-04-05 17:02:35 +02:00
Râu Cao ca7475dca2 Add notification mailer, make wallet notifications configurable
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-04-04 13:39:32 +02:00
Râu Cao 43a43e1a2c Use setting instead of ENV var
continuous-integration/drone/push Build is passing
2023-04-04 12:46:09 +02:00
Râu Cao 595bb03c5a Do not exchange XMPP contacts when turned off by inviter
continuous-integration/drone/push Build is running
2023-04-04 12:45:13 +02:00
Râu Cao 62cd0eb7d1 Re-rename "ejabberd" service to "xmpp"
Shouldn't matter which implementation is integrated if someone adds
another one
2023-04-04 12:29:39 +02:00
Râu Cao f19baaf22a Add new user settings pages for Chat and Wallet 2023-04-04 12:28:53 +02:00
Râu Cao 23821f9e65 Add preferences to user model 2023-04-04 12:27:49 +02:00
Râu Cao a33410eeb4 Allow handing custom field names to toggle fieldset component 2023-04-04 12:03:00 +02:00
Râu Cao a1b238e86b Fix email default URL options missing
continuous-integration/drone/push Build is passing
2023-04-04 09:11:06 +02:00
Râu Cao 334b47353e WIP Add notifications preferences page
continuous-integration/drone/push Build is passing
2023-04-03 13:55:58 +02:00
Râu Cao 6848bd739c Add horizontal layout option for fieldset component 2023-04-03 13:55:39 +02:00
Râu Cao 7f77ad5528 Refactor user settings
continuous-integration/drone/push Build is passing
Use resources instead of custom controllers, following the Rails way
and making things much cleaner in the process.
2023-04-03 13:19:07 +02:00
raucao 6f2160b479 Merge pull request 'Add solargraph in development, document usage with bundled gems' (#112) from feature/solargraph into master
continuous-integration/drone/push Build is passing
Reviewed-on: #112
2023-04-02 08:19:26 +00:00
Râu Cao f08bb56a7a 0.5.0
continuous-integration/drone/push Build is passing
2023-04-01 11:44:25 +02:00
Râu Cao fe1dfd8ec8 Add solargraph in development, document usage with bundled gems
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Update release notes draft
2023-03-31 18:07:38 +02:00
raucao c1f275463e Merge pull request 'Add Redis, Sidekiq to Docker Compose setup' (#110) from feature/docker-compose_sidekiq into master
continuous-integration/drone/push Build is passing
Reviewed-on: #110
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-03-31 09:09:46 +00:00
raucao 324809f77e Merge pull request 'Expire inactive sessions, optionally allow to stay signed in' (#82) from feature/8-session_timeouts into master
continuous-integration/drone/push Build is passing
Reviewed-on: #82
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-03-31 07:58:24 +00:00
Râu Cao f9b07bcb01 Use development branch of release drafter action
continuous-integration/drone/push Build is passing
2023-03-28 17:27:31 +02:00
Râu Cao 986eb5387c Use release drafter fork with PR ID fix
continuous-integration/drone/push Build is passing
2023-03-28 17:13:39 +02:00
raucao f76e2c2f14 Merge pull request 'Add Gitea Release Drafter as Gitea Action' (#111) from feature/release_drafter into master
continuous-integration/drone/push Build is passing
Reviewed-on: #111
2023-03-28 14:21:44 +00:00
Râu Cao 22a7bbe6eb Add Gitea Release Drafter as Gitea Action
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Update release notes draft
2023-03-28 16:17:19 +02:00
greg 18f4deb30f Merge pull request 'Add (optional) Sentry integration' (#108) from feature/sentry_integration into master
continuous-integration/drone/push Build is passing
Reviewed-on: #108
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-03-28 12:53:00 +00:00
Râu Cao 9f9bf6fd80 Add Redis and Sidekiq to Docker Compose setup
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Update release notes draft
2023-03-28 12:24:58 +02:00
Râu Cao d2987da70a Send Devise emails via Sidekiq 2023-03-28 12:22:17 +02:00
Râu Cao 6b7a80e23a Make Redis URL configurable 2023-03-28 12:21:54 +02:00
Râu Cao 42b9b27561 Allow external network access
continuous-integration/drone/push Build is passing
Useful for connecting to services on private networks for example.
2023-03-28 11:38:56 +02:00
Râu Cao c17c980b69 Prepare for multiple akkounts containers
continuous-integration/drone/push Build is passing
Initially "web" and "sidekiq"
2023-03-28 11:25:10 +02:00
Râu Cao f199d5d12a Add (optional) Sentry integration
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
A Sentry DSN can be set via `SENTRY_DSN` and authenticated users will be
tagged with ID and username (cn) in events.
2023-03-27 12:47:28 +02:00
Râu Cao 4b17afa93d Fix typo
continuous-integration/drone/push Build is passing
2023-03-27 11:55:02 +02:00
Râu Cao 6d52af53ae Add basic storage config
continuous-integration/drone/push Build is passing
2023-03-27 11:46:39 +02:00
Râu Cao 4c5ad67652 Require action_mailbox
continuous-integration/drone/push Build is passing
2023-03-27 11:40:59 +02:00
Râu Cao 3437a756eb Only create LNDHub accounts when feature is enabled
continuous-integration/drone/push Build is passing
2023-03-24 16:01:53 +07:00
greg 0d9fc4aa74 Merge pull request 'Make email settings configurable, add custom mailer for one-off emails' (#107) from feature/custom_mailer into master
continuous-integration/drone/push Build is passing
Reviewed-on: #107
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-03-23 15:52:43 +00:00
greg 82475161a9 Merge branch 'master' into feature/custom_mailer
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-23 15:38:43 +00:00
Râu Cao fb3b9af3e5 Add custom mailer for one-off emails
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-22 14:54:23 +07:00
Râu Cao b1a0268e6b Make email settings configurable 2023-03-22 14:53:44 +07:00
raucao e1e7d8f87d Merge pull request 'Move exchanging of XMPP contacts to account confirmation' (#105) from chore/exchange_xmpp_contacts_after_confirmation into master
continuous-integration/drone/push Build is passing
Reviewed-on: #105
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-03-22 06:45:30 +00:00
Râu Cao 5b46f3adf5 Move exchanging of XMPP contacts to account confirmation
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Since the ejabberd service is now being enabled after the confirmation,
we also need to move the exchanging of roster contacts to that point.
2023-03-20 17:59:43 +07:00
Râu Cao a8a8fba14c Change styling of Devise shared links
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Update release notes draft
2023-03-19 18:07:09 +07:00
Râu Cao 8a7016a30b Add remember-me function for sign-in
When checked, remember user for 2 weeks. Otherwise expire session after
30 minutes.
2023-03-19 18:06:18 +07:00
Râu Cao e2618de7c6 Add time limit for inactive sessions
closes #8
2023-03-19 16:16:36 +07:00
raucao 90680368fb Merge pull request 'Complete admin pages for service settings' (#104) from feature/admin_user_service_settings into master
continuous-integration/drone/push Build is passing
Reviewed-on: #104
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-03-19 06:33:13 +00:00
Râu Cao 8d90847896 Add setting for contact roster name
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
And only exchange contacts when ejabberd integration is enabled
2023-03-15 09:03:39 +00:00
Râu Cao 8da297811b Mark settings as readonly, allow params for editable ones 2023-03-15 09:03:39 +00:00
Râu Cao fa56d6b772 Refactor toggles to work without JS, add specs 2023-03-15 09:03:39 +00:00
Râu Cao ca1221e9f3 Refactor admin settings, add all service settings 2023-03-15 09:03:39 +00:00
Râu Cao 295d486761 Disable toggles on admin user page
They are purely informational
2023-03-15 09:03:39 +00:00
Râu Cao e00390d102 Add cached settings for all current services 2023-03-15 09:03:39 +00:00
Râu Cao b947480190 Refactor sidenav link component, allow multiple levels 2023-03-15 09:03:39 +00:00
Râu Cao fa07978aac Add form field update capability to toggle components 2023-03-15 09:03:39 +00:00
Râu Cao e758e258a8 Allow disabling toggles, add toggle fieldset component 2023-03-15 09:03:39 +00:00
Râu Cao 805733939c Add toggle switch component, service configs, admin profile links 2023-03-15 09:03:39 +00:00
Râu Cao f050d010fd Refactor admin donation pages, fix errors
continuous-integration/drone/push Build is passing
Not sending the right response codes for Turbo to handle.
2023-03-15 15:24:00 +07:00
Râu Cao 95fac38b53 Show email address on account settings page
continuous-integration/drone/push Build is passing
2023-03-12 11:01:22 +07:00
raucao cb80465297 Merge pull request 'Upgrade Devise, remove custom Turbo integration' (#102) from chore/87-upgrade_devise into master
continuous-integration/drone/push Build is passing
Reviewed-on: #102
2023-03-09 04:43:03 +00:00
Râu Cao c7550b4f64 Upgrade Devise, remove custom Turbo integration
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-09 11:34:42 +07:00
raucao 341284aa99 Merge pull request 'Refactor form input styles/layouts' (#100) from ui/form_inputs into master
continuous-integration/drone/push Build is passing
Reviewed-on: #100
2023-03-09 03:42:22 +00:00
Râu Cao b34d040ce3 Refactor form input styles
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
* Fix issue where button is rendered taller in flexbox, due to default
  margin on input elements
* Refactor/improve all login and signup views
2023-03-09 10:23:16 +07:00
raucao 1142a4e2d5 Merge pull request 'Add keysend support for Lightning Addresses, specs for address/lnurlp responses' (#84) from feature/ln_address_keysend into master
continuous-integration/drone/push Build is passing
Reviewed-on: #84
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2023-03-03 13:29:02 +00:00
Râu Cao f2c7aa2f09 Fix typos
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-03 21:27:18 +08:00
greg cca44d7542 Merge branch 'master' into feature/ln_address_keysend
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-02 15:49:13 +00:00
greg cdad7546fb Merge pull request 'Improve design of service grid on dashboard' (#97) from feature/dashboard_layout into master
continuous-integration/drone/push Build is passing
Reviewed-on: #97
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-03-02 15:48:27 +00:00
greg feb7833533 Merge branch 'master' into feature/dashboard_layout
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-02 15:41:51 +00:00
Râu Cao dfb12b8f62 Fix typo
continuous-integration/drone/push Build is passing
2023-03-02 15:54:03 +08:00
Râu Cao 6c2a97e7e5 Improve design of service grid on dashboard
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-01 22:48:23 +08:00
greg c8b65de7f6 Merge pull request 'Add service attribute to LDAP user entry' (#91) from feature/ldap_services into master
continuous-integration/drone/push Build is passing
Reviewed-on: #91
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-03-01 09:57:53 +00:00
greg 2861254adf Merge branch 'master' into feature/ldap_services
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-01 09:35:53 +00:00
greg 1d2910dadb Merge pull request 'Add pagination features, paginate admin pages' (#95) from feature/89-pagination into master
continuous-integration/drone/push Build is passing
Reviewed-on: #95
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-03-01 09:34:58 +00:00
Râu Cao 251a170f2b Add documentation link for Pagy
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-01 17:14:44 +08:00
Râu Cao cbbb4c6e47 Add pagination to admin pages
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-01 17:08:36 +08:00
Râu Cao 3aad27c7bd Add Pagy gem, config, styles 2023-03-01 17:08:24 +08:00
Râu Cao 7cff849d79 Add more users when seeding db 2023-03-01 17:07:13 +08:00
Râu Cao 75ffd4e2f1 Add service attribute to LDAP user entry
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-27 23:36:23 +08:00
raucao b84f9109f6 Merge pull request 'Fix broken database seed' (#90) from bugfix/reserved_admin_username into master
continuous-integration/drone/push Build is passing
Reviewed-on: #90
Reviewed-by: raucao <raucao@noreply.kosmos.org>
2023-02-26 14:20:45 +00:00
raucao 7fd564726f Merge pull request 'Add user page to admin panel, improve other admin pages' (#88) from feature/admin_user_details into master
continuous-integration/drone/push Build is passing
Reviewed-on: #88
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-02-26 14:16:41 +00:00
galfert b2a1b8caf5 Remove "admin" from default reserved usernames
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Blocking admin prevents seeding the DB, which creates an admin user
2023-02-26 13:15:33 +01:00
galfert 52cc2a8151 Fix numbering in quickstart steps 2023-02-26 13:10:49 +01:00
Râu Cao c8e405d93a Fix inline tailwind styles not being applied
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-26 18:41:18 +08:00
Râu Cao 5f74212603 Improve admin donation pages
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-26 11:33:11 +08:00
Râu Cao 1c3e893b6b Fix height of link element buttons 2023-02-26 11:32:26 +08:00
Râu Cao eec4533fea Improve markup 2023-02-26 11:32:03 +08:00
Râu Cao 6d20ac9a1c Add lndhub info to admin user page
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-25 15:33:03 +08:00
Râu Cao 27dd4163f0 Add more data to admin user page 2023-02-25 15:32:50 +08:00
Râu Cao 1a55e5e895 Link users everywhere in admin panel 2023-02-25 15:32:13 +08:00
Râu Cao 8eb487600c Switch admin users index from pure LDAP to database 2023-02-25 15:31:19 +08:00
Râu Cao 678e80a25d Retrieve ldap entry from user model 2023-02-25 15:30:23 +08:00
Râu Cao 30fb9805e5 Add associations between users via invitations 2023-02-25 15:29:46 +08:00
Râu Cao e675970f4c Add view helper for colored badges 2023-02-25 15:28:02 +08:00
Râu Cao a0727e709f Add table class for rows with dividers 2023-02-25 15:27:28 +08:00
Râu Cao 55abbcc5ad WIP user page 2023-02-23 23:55:32 +08:00
Râu Cao ffed398024 Add admin user details page 2023-02-23 22:09:23 +08:00
Râu Cao 1a2482434c Rename admin users controller/route
continuous-integration/drone/push Build is passing
Started out as a simple helper page to list LDAP users, but turning into
proper user management now.
2023-02-23 21:53:12 +08:00
raucao b530ad2f0f Merge pull request 'Remove ln_login from users' (#86) from chore/remove_ln_login into master
continuous-integration/drone/push Build is passing
Reviewed-on: #86
2023-02-23 12:16:06 +00:00
Râu Cao 3c2fe7c15d Remove ln_login from users
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Not needed anymore, removing in favor of unencrypted `ln_account`.
2023-02-23 20:13:08 +08:00
raucao aa7044dea7 Merge pull request 'Fix deprecation warnings' (#85) from chore/fix_deprecation_warnings into master
continuous-integration/drone/push Build is passing
Reviewed-on: #85
2023-02-23 11:03:56 +00:00
Râu Cao a3f0d0f2cf Fix deprecation warnings
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-23 19:00:03 +08:00
186 changed files with 3437 additions and 766 deletions
+23 -1
View File
@@ -1,10 +1,32 @@
AKKOUNTS_DOMAIN=accounts.example.com
SMTP_SERVER=smtp.example.com
SMTP_PORT=587
SMTP_LOGIN=accounts
SMTP_PASSWORD=123abc
SMTP_FROM_ADDRESS=accounts@example.com
SMTP_DOMAIN=example.com
SMTP_AUTH_METHOD=plain
SMTP_ENABLE_STARTTLS=auto
REDIS_URL='redis://localhost:6379/1'
LDAP_HOST=localhost LDAP_HOST=localhost
LDAP_PORT=389 LDAP_PORT=389
LDAP_ADMIN_PASSWORD=passthebutter LDAP_ADMIN_PASSWORD=passthebutter
LDAP_SUFFIX="dc=kosmos,dc=org" LDAP_SUFFIX='dc=kosmos,dc=org'
WEBHOOKS_ALLOWED_IPS='10.1.1.163' WEBHOOKS_ALLOWED_IPS='10.1.1.163'
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
MASTODON_PUBLIC_URL='https://kosmos.social'
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
RS_STORAGE_URL='https://storage.kosmos.org'
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
EJABBERD_API_URL='https://xmpp.kosmos.org/api' EJABBERD_API_URL='https://xmpp.kosmos.org/api'
BTCPAY_API_URL='http://localhost:23001/api/v1' BTCPAY_API_URL='http://localhost:23001/api/v1'
+5 -1
View File
@@ -1,10 +1,14 @@
DISCOURSE_PUBLIC_URL='http://discourse.example.com'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
EJABBERD_API_URL='http://xmpp.example.com/api' EJABBERD_API_URL='http://xmpp.example.com/api'
BTCPAY_API_URL='http://btcpay.example.com/api/v1' BTCPAY_API_URL='http://btcpay.example.com/api/v1'
LNDHUB_LEGACY_API_URL='http://localhost:3023'
LNDHUB_API_URL='http://localhost:3026' LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946' LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
RS_STORAGE_URL='https://storage.kosmos.org'
WEBHOOKS_ALLOWED_IPS='10.1.1.23' WEBHOOKS_ALLOWED_IPS='10.1.1.23'
+13
View File
@@ -0,0 +1,13 @@
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
version-resolver:
major:
labels:
- 'release/major'
minor:
labels:
- 'release/minor'
patch:
labels:
- 'release/patch'
default: patch
+11
View File
@@ -0,0 +1,11 @@
name: Release Drafter
on:
pull_request:
types: [closed]
jobs:
release_drafter_job:
name: Update release notes draft
runs-on: ubuntu-latest
steps:
- name: Release Drafter
uses: https://github.com/raucao/gitea-release-drafter@dev
+7 -8
View File
@@ -1,8 +1,13 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM ruby:2.7.6 FROM ruby:2.7.6
RUN apt-get update -qq && apt-get install -y curl ldap-utils
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
ldap-utils tini
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get install -y nodejs RUN apt-get update && apt-get install -y nodejs
WORKDIR /akkounts WORKDIR /akkounts
COPY Gemfile /akkounts/Gemfile COPY Gemfile /akkounts/Gemfile
COPY Gemfile.lock /akkounts/Gemfile.lock COPY Gemfile.lock /akkounts/Gemfile.lock
@@ -12,11 +17,5 @@ RUN gem install foreman
RUN npm install -g yarn RUN npm install -g yarn
RUN yarn install RUN yarn install
# Add a script to be executed every time the container starts. ENTRYPOINT ["/usr/bin/tini", "--"]
COPY docker/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000 EXPOSE 3000
# Configure the main process to run when running the image
CMD ["bin", "dev"]
+13 -1
View File
@@ -32,13 +32,17 @@ gem 'lockbox'
# Authentication # Authentication
gem 'warden' gem 'warden'
gem 'devise' gem 'devise', '~> 4.9.0'
gem 'devise_ldap_authenticatable' gem 'devise_ldap_authenticatable'
gem 'net-ldap' gem 'net-ldap'
# Utilities # Utilities
gem "rqrcode", "~> 2.0" gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3' gem 'rails-settings-cached', '~> 2.8.3'
gem 'pagy', '~> 6.0', '>= 6.0.2'
gem 'flipper'
gem 'flipper-active_record'
gem 'flipper-ui'
# HTTP requests # HTTP requests
gem 'faraday' gem 'faraday'
@@ -47,6 +51,13 @@ gem 'faraday'
gem 'sidekiq', '< 7' gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler' gem 'sidekiq-scheduler'
# Service integrations
gem 'discourse_api'
# Monitoring
gem "sentry-ruby"
gem "sentry-rails"
group :development, :test do group :development, :test do
# Use sqlite3 as the database for Active Record # Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4' gem 'sqlite3', '~> 1.4'
@@ -61,6 +72,7 @@ group :development do
gem 'letter_opener' gem 'letter_opener'
gem 'letter_opener_web' gem 'letter_opener_web'
gem 'faker' gem 'faker'
gem 'solargraph'
end end
group :test do group :test do
+93 -5
View File
@@ -68,7 +68,10 @@ GEM
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.1) addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
backport (1.2.0)
bcrypt (3.1.18) bcrypt (3.1.18)
benchmark (0.2.1)
bindex (0.8.1) bindex (0.8.1)
builder (3.2.4) builder (3.2.4)
byebug (11.1.3) byebug (11.1.3)
@@ -95,7 +98,7 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
devise (4.8.1) devise (4.9.0)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@@ -105,10 +108,16 @@ GEM
devise (>= 3.4.1) devise (>= 3.4.1)
net-ldap (>= 0.16.0) net-ldap (>= 0.16.0)
diff-lcs (1.5.0) diff-lcs (1.5.0)
discourse_api (2.0.0)
faraday (~> 2.7)
faraday-follow_redirects
faraday-multipart
rack (>= 1.6)
dotenv (2.8.1) dotenv (2.8.1)
dotenv-rails (2.8.1) dotenv-rails (2.8.1)
dotenv (= 2.8.1) dotenv (= 2.8.1)
railties (>= 3.2) railties (>= 3.2)
e2mmap (0.1.0)
erubi (1.11.0) erubi (1.11.0)
et-orbi (1.2.7) et-orbi (1.2.7)
tzinfo tzinfo
@@ -122,8 +131,23 @@ GEM
faraday (2.7.1) faraday (2.7.1)
faraday-net_http (>= 2.0, < 3.1) faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4) ruby2_keywords (>= 0.0.4)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.0.2) faraday-net_http (3.0.2)
ffi (1.15.5) ffi (1.15.5)
flipper (0.28.0)
concurrent-ruby (< 2)
flipper-active_record (0.28.0)
activerecord (>= 4.2, < 8)
flipper (~> 0.28.0)
flipper-ui (0.28.0)
erubi (>= 1.0.0, < 2.0.0)
flipper (~> 0.28.0)
rack (>= 1.4, < 3)
rack-protection (>= 1.5.3, <= 4.0.0)
sanitize (< 7)
fugit (1.7.2) fugit (1.7.2)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
@@ -135,9 +159,15 @@ GEM
importmap-rails (1.1.5) importmap-rails (1.1.5)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
jaro_winkler (1.5.4)
jbuilder (2.11.5) jbuilder (2.11.5)
actionview (>= 5.0.0) actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
json (2.6.3)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
launchy (2.5.0) launchy (2.5.0)
addressable (~> 2.7) addressable (~> 2.7)
letter_opener (1.8.1) letter_opener (1.8.1)
@@ -162,6 +192,7 @@ GEM
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.0) mini_portile2 (2.8.0)
minitest (5.16.3) minitest (5.16.3)
multipart-post (2.3.0)
net-imap (0.3.1) net-imap (0.3.1)
net-protocol net-protocol
net-ldap (0.17.1) net-ldap (0.17.1)
@@ -178,6 +209,10 @@ GEM
nokogiri (1.13.9-x86_64-linux) nokogiri (1.13.9-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pagy (6.0.2)
parallel (1.22.1)
parser (3.2.1.1)
ast (~> 2.4.1)
pg (1.2.3) pg (1.2.3)
public_suffix (5.0.0) public_suffix (5.0.0)
puma (4.3.12) puma (4.3.12)
@@ -185,6 +220,8 @@ GEM
raabro (1.4.0) raabro (1.4.0)
racc (1.6.0) racc (1.6.0)
rack (2.2.4) rack (2.2.4)
rack-protection (3.0.6)
rack
rack-test (2.0.2) rack-test (2.0.2)
rack (>= 1.3) rack (>= 1.3)
rails (7.0.4) rails (7.0.4)
@@ -216,6 +253,7 @@ GEM
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6) rake (13.0.6)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.10.1) rb-inotify (0.10.1)
@@ -225,9 +263,11 @@ GEM
redis-client (0.11.2) redis-client (0.11.2)
connection_pool connection_pool
regexp_parser (2.6.1) regexp_parser (2.6.1)
responders (3.0.1) responders (3.1.0)
actionpack (>= 5.0) actionpack (>= 5.2)
railties (>= 5.0) railties (>= 5.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.2.5) rexml (3.2.5)
rqrcode (2.1.2) rqrcode (2.1.2)
chunky_png (~> 1.0) chunky_png (~> 1.0)
@@ -250,9 +290,30 @@ GEM
rspec-mocks (~> 3.11) rspec-mocks (~> 3.11)
rspec-support (~> 3.11) rspec-support (~> 3.11)
rspec-support (3.12.0) rspec-support (3.12.0)
rubocop (1.48.1)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.26.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.0)
parser (>= 3.2.1.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rufus-scheduler (3.8.2) rufus-scheduler (3.8.2)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
sanitize (6.0.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
sentry-rails (5.8.0)
railties (>= 5.0)
sentry-ruby (~> 5.8.0)
sentry-ruby (5.8.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (6.5.5) sidekiq (6.5.5)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
@@ -262,6 +323,21 @@ GEM
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 4, < 7) sidekiq (>= 4, < 7)
tilt (>= 1.4.0) tilt (>= 1.4.0)
solargraph (0.48.0)
backport (~> 1.2)
benchmark
bundler (>= 1.17.2)
diff-lcs (~> 1.4)
e2mmap
jaro_winkler (~> 1.5)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1)
parser (~> 3.0)
reverse_markdown (>= 1.0.5, < 3)
rubocop (>= 0.52)
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
sprockets (4.1.1) sprockets (4.1.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
@@ -283,6 +359,7 @@ GEM
railties (>= 6.0.0) railties (>= 6.0.0)
tzinfo (2.0.5) tzinfo (2.0.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2)
view_component (2.78.0) view_component (2.78.0)
activesupport (>= 5.0.0, < 8.0) activesupport (>= 5.0.0, < 8.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
@@ -298,11 +375,14 @@ GEM
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.7.0)
websocket-driver (0.7.5) websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yard (0.9.28)
webrick (~> 1.7.0)
zeitwerk (2.6.6) zeitwerk (2.6.6)
PLATFORMS PLATFORMS
@@ -314,12 +394,16 @@ DEPENDENCIES
capybara capybara
cssbundling-rails cssbundling-rails
database_cleaner database_cleaner
devise devise (~> 4.9.0)
devise_ldap_authenticatable devise_ldap_authenticatable
discourse_api
dotenv-rails dotenv-rails
factory_bot_rails factory_bot_rails
faker faker
faraday faraday
flipper
flipper-active_record
flipper-ui
importmap-rails importmap-rails
jbuilder (~> 2.7) jbuilder (~> 2.7)
letter_opener letter_opener
@@ -327,14 +411,18 @@ DEPENDENCIES
listen (~> 3.2) listen (~> 3.2)
lockbox lockbox
net-ldap net-ldap
pagy (~> 6.0, >= 6.0.2)
pg (~> 1.2.3) pg (~> 1.2.3)
puma (~> 4.1) puma (~> 4.1)
rails (~> 7.0.2) rails (~> 7.0.2)
rails-settings-cached (~> 2.8.3) rails-settings-cached (~> 2.8.3)
rqrcode (~> 2.0) rqrcode (~> 2.0)
rspec-rails rspec-rails
sentry-rails
sentry-ruby
sidekiq (< 7) sidekiq (< 7)
sidekiq-scheduler sidekiq-scheduler
solargraph
sprockets-rails sprockets-rails
sqlite3 (~> 1.4) sqlite3 (~> 1.4)
stimulus-rails stimulus-rails
+20 -5
View File
@@ -14,18 +14,17 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in 1. Make sure [Docker Compose is installed][1] and Docker is running (included in
Docker Desktop) Docker Desktop)
2. Uncomment the `web` section in `docker-compose.yml` 2. Uncomment the `redis`, `web`, and `sidekiq` sections in `docker-compose.yml`
3. Run `docker compose up` and wait until 389ds announces its successful start 3. Run `docker compose up` and wait until 389ds announces its successful start
in the log output in the log output
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"` 4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
5. `docker compose run web rails ldap:setup` 5. `docker compose run web rails ldap:setup`
5. `docker compose run web rails db:setup` 6. `docker compose run web rails db:setup`
After these steps, you should have a working Rails app with a handful of test After these steps, you should have a working Rails app with a handful of test
users running on [http://localhost:3000](http://localhost:3000). users running on [http://localhost:3000](http://localhost:3000).
Log in with username "admin" and password "admin is admin". All users listed on Log in with username "admin" and password "admin is admin". All users listed on
[http://localhost:3000/admin/ldap_users](http://localhost:3000/admin/ldap_users) [http://localhost:3000/admin/users](http://localhost:3000/admin/users)
have the password "user is user". have the password "user is user".
### Rails app ### Rails app
@@ -79,14 +78,26 @@ The setup task will first delete any existing entries in the directory tree
Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
with a fresh installation, delete both that directory as well as the container. with a fresh installation, delete both that directory as well as the container.
### Solargraph
[Solargraph](https://solargraph.org/) is a Ruby language server, which you may
use with your editor to add features like auto-completion and syntax
validation. You can add inline documentation for bundled gems with this
command:
bundle exec yard gems
## Documentation ## Documentation
### Rails
* [Ruby on Rails](https://guides.rubyonrails.org/) * [Ruby on Rails](https://guides.rubyonrails.org/)
* [Sass](https://sass-lang.com/documentation) * [Pagination](https://ddnexus.github.io/pagy/)
### Front-end ### Front-end
* [Tailwind CSS](https://tailwindcss.com/) * [Tailwind CSS](https://tailwindcss.com/)
* [Sass](https://sass-lang.com/documentation)
### Testing ### Testing
@@ -103,6 +114,10 @@ with a fresh installation, delete both that directory as well as the container.
* [Sidekiq](https://github.com/mperham/sidekiq/wiki/) * [Sidekiq](https://github.com/mperham/sidekiq/wiki/)
* [ActiveJob](https://github.com/mperham/sidekiq/wiki/Active-Job) * [ActiveJob](https://github.com/mperham/sidekiq/wiki/Active-Job)
### Feature Flags
* [Flipper](https://www.flippercloud.io/docs/get-started/self-hosted)
## License ## License
[GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/) [GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/)
@@ -4,7 +4,9 @@
@import "components/base"; @import "components/base";
@import "components/buttons"; @import "components/buttons";
@import "components/dashboard_services";
@import "components/forms"; @import "components/forms";
@import "components/links"; @import "components/links";
@import "components/notifications"; @import "components/notifications";
@import "components/pagination";
@import "components/tables"; @import "components/tables";
@@ -36,10 +36,18 @@
@apply mb-4 leading-6; @apply mb-4 leading-6;
} }
main p:last-child {
@apply mb-0;
}
main ul { main ul {
@apply mb-6; @apply mb-6;
} }
main ul:last-child {
@apply mb-0;
}
main ul li { main ul li {
@apply leading-6; @apply leading-6;
} }
@@ -1,6 +1,6 @@
@layer components { @layer components {
.btn { .btn {
@apply font-semibold rounded-md leading-none cursor-pointer text-center @apply inline-block font-semibold rounded-md leading-none cursor-pointer text-center
transition-colors duration-75 focus:outline-none focus:ring-4; transition-colors duration-75 focus:outline-none focus:ring-4;
} }
@@ -32,8 +32,4 @@
@apply bg-red-600 hover:bg-red-700 text-white @apply bg-red-600 hover:bg-red-700 text-white
focus:ring-red-500 focus:ring-opacity-75; focus:ring-red-500 focus:ring-opacity-75;
} }
input[type=text]:disabled {
@apply text-gray-700;
}
} }
@@ -0,0 +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%);
}
}
+8 -3
View File
@@ -1,13 +1,18 @@
@layer components { @layer components {
input[type=text], input[type=email], input[type=password], input[type=text], input[type=email], input[type=password],
input[type=number], select, textarea { input[type=number], select, textarea {
@apply mt-1 rounded-md bg-gray-100 focus:bg-white @apply rounded-md bg-gray-100 focus:bg-white
border-transparent focus:border-transparent focus:ring-2 border-transparent focus:border-transparent focus:ring-2
focus:ring-blue-600 focus:ring-opacity-75; focus:ring-blue-600 focus:ring-opacity-75;
} }
.field_with_errors { input[type=text]:disabled,
@apply inline-block; input[type=email]:disabled {
@apply text-gray-700;
}
input.field_with_errors {
@apply border-b-red-600;
} }
.error-msg { .error-msg {
@@ -5,10 +5,4 @@
&:visited { @apply text-indigo-600; } &:visited { @apply text-indigo-600; }
&:active { @apply text-red-600; } &:active { @apply text-red-600; }
} }
.devise-links {
a {
@apply ks-text-link;
}
}
} }
@@ -0,0 +1,45 @@
@layer components {
.pagy-nav.pagination {
@apply isolate inline-flex -space-x-px rounded-md shadow-sm;
}
.pagy-nav .page:not(.prev):not(.next) {
@apply hidden sm:inline-block;
}
.pagy-nav .page.next a {
@apply relative inline-flex items-center rounded-r-md border
border-gray-300 bg-white px-3 py-2 text-sm font-medium
text-gray-500 hover:bg-gray-100 focus:z-20;
}
.pagy-nav .page.prev a {
@apply relative inline-flex items-center rounded-l-md border
border-gray-300 bg-white px-3 py-2 text-sm font-medium
text-gray-500 hover:bg-gray-100 focus:z-20;
}
.pagy-nav .page.next.disabled {
@apply relative inline-flex items-center rounded-r-md border
border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium
text-gray-400 focus:z-20;
}
.pagy-nav .page.prev.disabled {
@apply relative inline-flex items-center rounded-l-md border
border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium
text-gray-400 focus:z-20;
}
.pagy-nav .page a, .page.gap {
@apply bg-white border-gray-300 text-gray-500 hover:bg-gray-100 relative
inline-flex items-center border px-4 py-2 text-sm font-medium
focus:z-20;
}
.pagy-nav .page.active {
@apply z-10 border-indigo-500 bg-indigo-50 text-indigo-600 relative
inline-flex items-center border px-4 py-2 text-sm font-medium
focus:z-20;
}
}
+16 -2
View File
@@ -7,16 +7,30 @@
@apply text-left; @apply text-left;
} }
table th { table thead th {
@apply pb-3.5 text-sm font-normal uppercase text-gray-500; @apply pb-3.5 text-sm font-normal uppercase text-gray-500;
} }
table tbody th {
@apply text-left font-normal text-gray-500;
}
table th:not(:last-of-type), table th:not(:last-of-type),
table td:not(:last-of-type) { table td:not(:last-of-type) {
@apply pr-2; @apply pr-2;
} }
table td { table td, tbody th {
@apply py-2; @apply py-2;
} }
table.divided {
@apply divide-y divide-gray-300;
}
table.divided tbody {
@apply divide-y divide-gray-200;
}
table.divided td, table.divided tbody th {
@apply py-3;
}
} }
@@ -0,0 +1,29 @@
<%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
<% if @positioning == :vertical %>
<label class="block">
<p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>">
<%= @title %>
</p>
<% if @descripton.present? %>
<p class="text-gray-500">
<%= @descripton %>
</p>
<% end %>
<%= content %>
</label>
<% elsif @positioning == :horizontal %>
<label class="block flex items-center justify-between">
<div class="flex flex-col">
<label class="font-bold mb-1"><%= @title %></label>
<% if @descripton.present? %>
<p class="text-gray-500"><%= @descripton %></p>
<% end %>
</div>
<div class="relative ml-4 inline-flex flex-shrink-0">
<%= content %>
</div>
</label>
<% else %>
<p>Invalid <code>positioning<code> argument for <code>FieldsetComponent</code>.</p>
<% end %>
<% end %>
@@ -0,0 +1,12 @@
# frozen_string_literal: true
module FormElements
class FieldsetComponent < ViewComponent::Base
def initialize(tag: "li", positioning: :vertical, title:, description: nil)
@tag = tag
@positioning = positioning
@title = title
@descripton = description
end
end
end
@@ -0,0 +1,33 @@
<%= tag.public_send @tag, class: "flex items-center justify-between mb-6 last:mb-0",
data: @form_enabled ? {
controller: "settings--toggle",
:'settings--toggle-switch-enabled-value' => @enabled.to_s
} : nil do %>
<div class="flex flex-col">
<label class="font-bold mb-1"><%= @title %></label>
<p class="text-gray-500"><%= @descripton %></p>
</div>
<div class="relative ml-4 inline-flex flex-shrink-0">
<%= render FormElements::ToggleComponent.new(
enabled: @enabled,
input_enabled: @input_enabled,
class_names: @form_enabled ? "hidden" : nil,
data: {
:'settings--toggle-target' => "button",
action: "settings--toggle#toggleSwitch"
}) %>
<% if @form_enabled %>
<% if @attribute.present? %>
<%= @form.check_box @attribute, {
checked: @enabled,
data: { :'settings--toggle-target' => "checkbox" }
}, "true", "false" %>
<% else %>
<input name="<%= @field_name %>" type="hidden" value="false" autocomplete="off">
<%= check_box_tag @field_name, "true", @enabled, {
data: { :'settings--toggle-target' => "checkbox" }
} %>
<% end %>
<% end %>
</div>
<% end %>
@@ -0,0 +1,19 @@
# frozen_string_literal: true
module FormElements
class FieldsetToggleComponent < ViewComponent::Base
def initialize(tag: "li", form: nil, attribute: nil, field_name: nil,
enabled: false, input_enabled: true, title:, description:)
@tag = tag
@form = form
@attribute = attribute
@field_name = field_name
@form_enabled = @form.present? || @field_name.present?
@enabled = enabled
@input_enabled = input_enabled
@title = title
@descripton = description
@button_text = @enabled ? "Switch off" : "Switch on"
end
end
end
@@ -0,0 +1,15 @@
<%= button_tag type: "button", name: "toggle", data: @data,
role: "switch", aria: { checked: @enabled.to_s },
tabindex: @tabindex, disabled: !@input_enabled,
class: "#{ @enabled ? 'bg-blue-600' : 'bg-gray-200' }
#{ @class_names.present? ? @class_names : '' }
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer
rounded-full border-2 border-transparent transition-colors
duration-200 ease-in-out focus:outline-none focus:ring-2
focus:ring-blue-600 focus:ring-offset-2" do %>
<span class="sr-only"><%= @button_text %></span>
<span aria-hidden="true" data-settings--toggle-target="switch"
class="<%= @enabled ? 'translate-x-5' : 'translate-x-0' %>
pointer-events-none inline-block h-5 w-5 transform rounded-full
bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
<% end %>
@@ -0,0 +1,13 @@
# frozen_string_literal: true
module FormElements
class ToggleComponent < ViewComponent::Base
def initialize(enabled:, input_enabled: true, data: nil, class_names: nil, tabindex: nil)
@enabled = !!enabled
@input_enabled = input_enabled
@data = data
@class_names = class_names
@tabindex = tabindex
end
end
end
@@ -0,0 +1,3 @@
<%= link_to @path, class: @link_class do %>
<%= @name %>
<% end %>
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class HeaderTabLinkComponent < ViewComponent::Base
def initialize(name:, path:, active: false, disabled: false)
@name = name
@path = path
@active = active
@disabled = disabled
@link_class = class_names_link(path)
end
def class_names_link(path)
common = "block md:inline-block px-5 py-2 rounded-md font-medium text-base md:text-xl"
if @active
"#{common} bg-gray-900/50 text-white"
else
"#{common} text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white"
end
end
end
@@ -0,0 +1,12 @@
<header class="py-10">
<div class="max-w-6xl md:flex md:gap-x-10 mx-auto px-4 sm:px-6 lg:px-8">
<% if @title.present? %>
<h1 class="text-3xl font-bold text-white">
<%= @title %>
</h1>
<% end %>
<nav class="md:grow flex gap-x-4 <%= @title.present? ? "justify-end" : "justify-start" %>" aria-label="Tabs">
<%= render partial: @tabnav_partial %>
</nav>
</div>
</header>
@@ -0,0 +1,8 @@
# frozen_string_literal: true
class HeaderWithTabsComponent < ViewComponent::Base
def initialize(title: nil, tabnav_partial:)
@title = title
@tabnav_partial = tabnav_partial
end
end
+8 -4
View File
@@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class SidenavLinkComponent < ViewComponent::Base class SidenavLinkComponent < ViewComponent::Base
def initialize(name:, path:, icon:, active: false, disabled: false) def initialize(name:, level: 1, path:, icon:, active: false, disabled: false)
@name = name @name = name
@level = level
@path = path @path = path
@icon = icon @icon = icon
@active = active @active = active
@@ -12,12 +13,15 @@ class SidenavLinkComponent < ViewComponent::Base
end end
def class_names_link(path) def class_names_link(path)
px = @level == 1 ? "px-4" : "pl-8 pr-4"
base = "#{px} py-2 group border-l-4 flex items-center text-base font-medium"
if @active if @active
"bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700 group border-l-4 px-4 py-2 flex items-center text-base font-medium" "#{base} bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
elsif @disabled elsif @disabled
"border-transparent text-gray-400 hover:bg-gray-50 group border-l-4 px-4 py-2 flex items-center text-base font-medium" "#{base} border-transparent text-gray-400 hover:bg-gray-50"
else else
"border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-4 py-2 flex items-center text-base font-medium" "#{base} border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900"
end end
end end
+1 -1
View File
@@ -1,5 +1,5 @@
class AccountController < ApplicationController class AccountController < ApplicationController
before_action :require_user_signed_in before_action :authenticate_user!
def index def index
@current_section = :account @current_section = :account
+1 -1
View File
@@ -1,4 +1,5 @@
class Admin::BaseController < ApplicationController class Admin::BaseController < ApplicationController
include Pagy::Backend
before_action :authenticate_user! before_action :authenticate_user!
before_action :authorize_admin before_action :authorize_admin
@@ -7,5 +8,4 @@ class Admin::BaseController < ApplicationController
def set_context def set_context
@context = :admin @context = :admin
end end
end end
@@ -5,7 +5,8 @@ class Admin::DonationsController < Admin::BaseController
# GET /donations # GET /donations
# GET /donations.json # GET /donations.json
def index def index
@donations = Donation.all.order('created_at desc') @pagy, @donations = pagy(Donation.all.order('created_at desc'))
@stats = { @stats = {
overall_sats: @donations.all.sum("amount_sats"), overall_sats: @donations.all.sum("amount_sats"),
donor_count: Donation.distinct.count(:user_id) donor_count: Donation.distinct.count(:user_id)
@@ -40,7 +41,7 @@ class Admin::DonationsController < Admin::BaseController
end end
format.json { render :show, status: :created, location: @donation } format.json { render :show, status: :created, location: @donation }
else else
format.html { render :new } format.html { render :new, status: :unprocessable_entity }
format.json { render json: @donation.errors, status: :unprocessable_entity } format.json { render json: @donation.errors, status: :unprocessable_entity }
end end
end end
@@ -58,7 +59,7 @@ class Admin::DonationsController < Admin::BaseController
end end
format.json { render :show, status: :ok, location: @donation } format.json { render :show, status: :ok, location: @donation }
else else
format.html { render :edit } format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @donation.errors, status: :unprocessable_entity } format.json { render json: @donation.errors, status: :unprocessable_entity }
end end
end end
@@ -1,7 +1,8 @@
class Admin::InvitationsController < Admin::BaseController class Admin::InvitationsController < Admin::BaseController
def index def index
@current_section = :invitations @current_section = :invitations
@invitations_used = Invitation.used.order('used_at desc') @pagy, @invitations_used = pagy(Invitation.used.order('used_at desc'))
@stats = { @stats = {
available: Invitation.unused.count, available: Invitation.unused.count,
accepted: @invitations_used.length, accepted: @invitations_used.length,
@@ -1,20 +0,0 @@
class Admin::LdapUsersController < Admin::BaseController
before_action :set_current_section
def index
ldap = LdapService.new
@ou = params[:ou] || "kosmos.org"
@orgs = ldap.fetch_organizations
@entries = ldap.fetch_users(ou: @ou)
@stats = {
users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: @ou).pending.count
}
end
private
def set_current_section
@current_section = :ldap_users
end
end
@@ -1,38 +1,12 @@
class Admin::Settings::RegistrationsController < Admin::SettingsController class Admin::Settings::RegistrationsController < Admin::SettingsController
def index def index
end end
def create def create
@errors = ActiveModel::Errors.new(Setting.new) update_settings
setting_params.keys.each do |key|
next if setting_params[key].nil?
setting = Setting.new(var: key)
setting.value = setting_params[key].strip
unless setting.valid?
@errors.merge!(setting.errors)
end
end
if @errors.any?
render :index
end
setting_params.keys.each do |key|
Setting.send("#{key}=", setting_params[key].strip) unless setting_params[key].nil?
end
redirect_to admin_settings_registrations_path, flash: { redirect_to admin_settings_registrations_path, flash: {
success: "Settings saved" success: "Settings saved"
} }
end end
private
def setting_params
params.require(:setting).permit(:reserved_usernames)
end
end end
@@ -1,9 +1,19 @@
class Admin::Settings::ServicesController < Admin::SettingsController class Admin::Settings::ServicesController < Admin::SettingsController
def index def index
@service = params[:s]
if @service.blank?
redirect_to admin_settings_services_path(params: { s: "discourse" })
end
end end
def update def create
end service = params.require(:service)
update_settings
redirect_to admin_settings_services_path(params: { s: service }), flash: {
success: "Settings saved"
}
end
end end
@@ -4,9 +4,37 @@ class Admin::SettingsController < Admin::BaseController
def index def index
end end
def update_settings
@errors = ActiveModel::Errors.new(Setting.new)
changed_keys = []
setting_params.keys.each do |key|
next if setting_params[key].nil? ||
(Setting.send(key).to_s == setting_params[key].strip)
changed_keys.push(key)
setting = Setting.new(var: key)
setting.value = setting_params[key].strip
unless setting.valid?
@errors.merge!(setting.errors)
end
end
if @errors.any?
render :index and return
end
changed_keys.each do |key|
Setting.send("#{key}=", setting_params[key].strip)
end
end
private private
def set_current_section def set_current_section
@current_section = :settings @current_section = :settings
end end
def setting_params
params.require(:setting).permit(Setting.editable_keys.map(&:to_sym))
end
end end
+35
View File
@@ -0,0 +1,35 @@
class Admin::UsersController < Admin::BaseController
before_action :set_user, only: [:show]
before_action :set_current_section
def index
ldap = LdapService.new
@ou = params[:ou] || "kosmos.org"
@orgs = ldap.fetch_organizations
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
@stats = {
users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: @ou).pending.count
}
end
def show
if Setting.lndhub_admin_enabled?
@lndhub_user = @user.lndhub_user
end
@services_enabled = @user.services_enabled
end
private
def set_user
address = params[:address].split("@")
@user = User.where(cn: address.first, ou: address.last).first
end
def set_current_section
@current_section = :users
end
end
+12
View File
@@ -3,6 +3,18 @@ class ApplicationController < ActionController::Base
render :text => exception, :status => 500 render :text => exception, :status => 500
end end
before_action :sentry_set_user
def sentry_set_user
return unless Setting.sentry_enabled
if user_signed_in?
Sentry.set_user(id: current_user.id, username: current_user.cn)
else
Sentry.set_user({})
end
end
def require_user_signed_in def require_user_signed_in
unless user_signed_in? unless user_signed_in?
redirect_to welcome_path and return redirect_to welcome_path and return
@@ -1,5 +1,5 @@
class Contributions::DonationsController < ApplicationController class Contributions::DonationsController < ApplicationController
before_action :require_user_signed_in before_action :authenticate_user!
# GET /donations # GET /donations
# GET /donations.json # GET /donations.json
@@ -1,5 +1,5 @@
class Contributions::ProjectsController < ApplicationController class Contributions::ProjectsController < ApplicationController
before_action :require_user_signed_in before_action :authenticate_user!
# GET /contributions # GET /contributions
def index def index
+1 -1
View File
@@ -2,6 +2,6 @@ class DashboardController < ApplicationController
before_action :require_user_signed_in before_action :require_user_signed_in
def index def index
@current_section = :dashboard @current_section = :services
end end
end end
@@ -0,0 +1,17 @@
class Discourse::SsoController < ApplicationController
before_action :authenticate_user!
def connect
secret = Setting.discourse_connect_secret
sso = DiscourseApi::SingleSignOn.parse(request.query_string, secret)
sso.external_id = current_user.id
sso.email = current_user.email
sso.username = current_user.cn
sso.name = current_user.display_name
sso.admin = current_user.is_admin?
sso.sso_secret = secret
redirect_to sso.to_url("#{Setting.discourse_public_url}/session/sso_login"),
allow_other_host: true
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
class InvitationsController < ApplicationController class InvitationsController < ApplicationController
before_action :require_user_signed_in, except: ["show"] before_action :authenticate_user!, except: ["show"]
before_action :require_user_signed_out, only: ["show"] before_action :require_user_signed_out, only: ["show"]
# GET /invitations # GET /invitations
@@ -1,13 +1,13 @@
require "rqrcode" require "rqrcode"
class WalletController < ApplicationController class Services::LightningController < ApplicationController
before_action :require_user_signed_in before_action :authenticate_user!
before_action :authenticate_with_lndhub before_action :authenticate_with_lndhub
before_action :set_current_section before_action :set_current_section
before_action :fetch_balance before_action :fetch_balance
def index def index
@wallet_url = "lndhub://#{current_user.ln_login}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}" @wallet_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
qrcode = RQRCode::QRCode.new(@wallet_url) qrcode = RQRCode::QRCode.new(@wallet_url)
@svg = qrcode.as_svg( @svg = qrcode.as_svg(
@@ -37,21 +37,21 @@ class WalletController < ApplicationController
session[:ln_auth_token] = auth_token session[:ln_auth_token] = auth_token
@ln_auth_token = auth_token @ln_auth_token = auth_token
end end
rescue rescue => e
# TODO add exception tracking Sentry.capture_exception(e) if Setting.sentry_enabled?
end end
def set_current_section def set_current_section
@current_section = :wallet @current_section = :services
end end
def fetch_balance def fetch_balance
lndhub = Lndhub.new lndhub = Lndhub.new
data = lndhub.balance @ln_auth_token data = lndhub.balance @ln_auth_token
@balance = data["BTC"]["AvailableBalance"] rescue nil @balance = data["BTC"]["AvailableBalance"] rescue nil
rescue rescue AuthError
authenticate_with_lndhub(force_reauth: true) authenticate_with_lndhub(force_reauth: true)
return nil if @fetch_balance_retried raise if @fetch_balance_retried
@fetch_balance_retried = true @fetch_balance_retried = true
fetch_balance fetch_balance
end end
@@ -61,9 +61,9 @@ class WalletController < ApplicationController
txs = lndhub.gettxs @ln_auth_token txs = lndhub.gettxs @ln_auth_token
invoices = lndhub.getuserinvoices(@ln_auth_token).select{|i| i["ispaid"]} invoices = lndhub.getuserinvoices(@ln_auth_token).select{|i| i["ispaid"]}
process_transactions(txs + invoices) process_transactions(txs + invoices)
rescue rescue AuthError
authenticate_with_lndhub(force_reauth: true) authenticate_with_lndhub(force_reauth: true)
return [] if @fetch_transactions_retried raise if @fetch_transactions_retried
@fetch_transactions_retried = true @fetch_transactions_retried = true
fetch_transactions fetch_transactions
end end
@@ -78,6 +78,7 @@ class WalletController < ApplicationController
tx["received"] = true tx["received"] = true
else else
tx["amount_sats"] = tx["value"] || tx["amt"] tx["amount_sats"] = tx["value"] || tx["amt"]
tx["fee"] = tx["type"] == "paid_invoice" ? tx["fee"] : nil
tx["datetime"] = Time.at(tx["timestamp"].to_i) tx["datetime"] = Time.at(tx["timestamp"].to_i)
tx["title"] = tx["type"] == "paid_invoice" ? "Sent" : "Received" tx["title"] = tx["type"] == "paid_invoice" ? "Sent" : "Received"
tx["description"] = tx["memo"] || tx["description"] tx["description"] = tx["memo"] || tx["description"]
@@ -85,6 +86,10 @@ class WalletController < ApplicationController
end end
end end
# Handle an edge case where lndhub.go includes a failed payment in the
# list, which wasn't actually booked
txs.reject!{ |tx| tx["type"] == "paid_invoice" && tx["payment_preimage"].blank? }
txs.sort{ |a,b| b["datetime"] <=> a["datetime"] } txs.sort{ |a,b| b["datetime"] <=> a["datetime"] }
end end
end end
@@ -0,0 +1,30 @@
class Services::RemotestorageController < ApplicationController
before_action :require_user_signed_in
before_action :require_service_enabled
before_action :require_feature_enabled
before_action :set_current_section
def dashboard
# unless current_user.services_enabled.include?(:remotestorage)
# redirect_to service_remotestorage_info_path
# end
end
private
def require_feature_enabled
unless Flipper.enabled?(:remotestorage, current_user)
http_status :forbidden
end
end
def require_service_enabled
unless Setting.remotestorage_enabled?
http_status :not_found
end
end
def set_current_section
@current_section = :services
end
end
@@ -1,13 +0,0 @@
class Settings::AccountController < SettingsController
def index
end
def reset_password
current_user.send_reset_password_instructions
sign_out current_user
msg = "We have sent you an email with a link to reset your password."
redirect_to check_your_email_path, notice: msg
end
end
@@ -1,11 +0,0 @@
class Settings::ProfileController < SettingsController
def index
@user = current_user
end
def update
end
end
+75 -3
View File
@@ -1,13 +1,85 @@
class SettingsController < ApplicationController class SettingsController < ApplicationController
before_action :require_user_signed_in before_action :authenticate_user!
before_action :set_current_section before_action :set_main_nav_section
before_action :set_settings_section, only: [:show, :update, :update_email]
before_action :set_user, only: [:show, :update, :update_email]
def index def index
redirect_to setting_path(:profile)
end
def show
end
def update
@user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name]
if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
LdapManager::UpdateDisplayName.call(@user.dn, user_params[:display_name])
end
redirect_to setting_path(@settings_section), flash: {
success: 'Settings saved.'
}
else
@validation_errors = @user.errors
render :show, status: :unprocessable_entity
end
end
def update_email
if @user.valid_ldap_authentication?(email_params[:current_password])
if @user.update email: email_params[:email]
redirect_to setting_path(:account), flash: {
notice: 'Please confirm your new address using the confirmation link we just sent you.'
}
else
@validation_errors = @user.errors
render :show, status: :unprocessable_entity
end
else
redirect_to setting_path(:account), flash: {
error: 'Password did not match your current password. Try again.'
}
end
end
def reset_password
current_user.send_reset_password_instructions
sign_out current_user
msg = "We have sent you an email with a link to reset your password."
redirect_to check_your_email_path, notice: msg
end end
private private
def set_current_section def set_main_nav_section
@current_section = :settings @current_section = :settings
end end
def set_settings_section
@settings_section = params[:section]
allowed_sections = [:profile, :account, :lightning, :xmpp]
unless allowed_sections.include?(@settings_section.to_sym)
redirect_to setting_path(:profile)
end
end
def set_user
@user = current_user
end
def user_params
params.require(:user).permit(:display_name, preferences: [
:lightning_notify_sats_received,
:xmpp_exchange_contacts_with_invitees
])
end
def email_params
params.require(:user).permit(:email, :current_password)
end
end end
@@ -0,0 +1,17 @@
# frozen_string_literal: true
class Users::ConfirmationsController < Devise::ConfirmationsController
# GET /resource/confirmation?confirmation_token=abcdef
def show
self.resource = resource_class.confirm_by_token(params[:confirmation_token])
yield resource if block_given?
if resource.errors.empty?
set_flash_message!(:success, :confirmed)
resource.devise_after_confirmation
respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) }
else
respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new }
end
end
end
+57
View File
@@ -0,0 +1,57 @@
class WebfingerController < ApplicationController
before_action :allow_cross_origin_requests, only: [:show]
layout false
def show
resource = params[:resource]
if resource && resource.match(/acct:\w+/)
useraddress = resource.split(":").last
username, org = useraddress.split("@")
username.downcase!
unless User.where(cn: username, ou: org).any?
head 404 and return
end
render json: webfinger(useraddress).to_json,
content_type: "application/jrd+json"
else
head 422 and return
end
end
private
def webfinger(useraddress)
links = [];
links << remotestorage_link(useraddress) if Setting.remotestorage_enabled
{ "links" => links }
end
def remotestorage_link(useraddress)
# TODO use when OAuth routes are available
# auth_url = new_rs_oauth_url(useraddress)
auth_url = "https://example.com/rs/oauth"
storage_url = "#{Setting.rs_storage_url}/#{useraddress}"
{
"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
"http://tools.ietf.org/html/rfc7233": "GET", # content range requests
"http://remotestorage.io/spec/web-authoring": nil
}
}
end
def allow_cross_origin_requests
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
end
end
+9 -3
View File
@@ -12,22 +12,28 @@ class WebhooksController < ApplicationController
end end
user = User.find_by!(ln_account: payload[:user_login]) user = User.find_by!(ln_account: payload[:user_login])
notify = user.preferences[:lightning_notify_sats_received]
# TODO make configurable case notify
when "xmpp"
notify_xmpp(user.address, payload[:amount], payload[:memo]) notify_xmpp(user.address, payload[:amount], payload[:memo])
when "email"
NotificationMailer.with(user: user, amount_sats: payload[:amount])
.lightning_sats_received.deliver_later
end
head :ok head :ok
end end
private private
# TODO refactor into mailer-like generic class/service
def notify_xmpp(address, amt_sats, memo) def notify_xmpp(address, amt_sats, memo)
payload = { payload = {
type: "normal", type: "normal",
from: "kosmos.org", # TODO domain config from: "kosmos.org", # TODO domain config
to: address, to: address,
subject: "Sats received!", subject: "Sats received!",
body: "#{amt_sats} sats received in your Lightning wallet:\n> #{memo}" body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"
} }
XmppSendMessageJob.perform_later(payload) XmppSendMessageJob.perform_later(payload)
end end
+1
View File
@@ -0,0 +1 @@
class AuthError < StandardError; end
+8 -1
View File
@@ -1,4 +1,6 @@
module ApplicationHelper module ApplicationHelper
include Pagy::Frontend
def sats_to_btc(sats) def sats_to_btc(sats)
sats.to_f / 100000000 sats.to_f / 100000000
end end
@@ -10,5 +12,10 @@ module ApplicationHelper
"text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block" "text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
end end
end end
end
# Colors available: gray, red, yellow, green, blue, purple, pink
# (Add more colors by adding classes to the safelist in tailwind.config.js)
def badge(text, color)
tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800"
end
end
-2
View File
@@ -1,2 +0,0 @@
module LdapUsersHelper
end
+2
View File
@@ -0,0 +1,2 @@
module UsersHelper
end
@@ -4,6 +4,10 @@ export default class extends Controller {
static targets = ["buttons", "countdown"] static targets = ["buttons", "countdown"]
connect() { connect() {
// Devise timeoutable ends up adding a second flash message without content
// TODO investigate bug
if (this.element.textContent.trim() == "true") return;
const timeoutSeconds = parseInt(this.data.get("timeout")); const timeoutSeconds = parseInt(this.data.get("timeout"));
setTimeout(() => { setTimeout(() => {
@@ -0,0 +1,27 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "emailField", "editEmailButton" ]
static values = { validationFailed: Boolean }
connect () {
if (this.validationFailedValue) return;
this.emailFieldTarget.disabled = true;
this.element.querySelectorAll(".initial-hidden").forEach(el => {
el.classList.add("hidden");
})
this.element.querySelectorAll(".initial-visible").forEach(el => {
el.classList.remove("hidden");
})
}
editEmail () {
this.emailFieldTarget.disabled = false;
this.emailFieldTarget.select();
this.editEmailButtonTarget.classList.add("hidden");
this.element.querySelectorAll(".initial-hidden").forEach(el => {
el.classList.remove("hidden");
})
}
}
@@ -0,0 +1,30 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "button", "switch", "checkbox" ]
static values = { switchEnabled: Boolean }
connect () {
this.buttonTarget.classList.remove("hidden")
this.checkboxTarget.classList.add("hidden")
}
toggleSwitch () {
this.switchEnabledValue = !this.switchEnabledValue
this.checkboxTarget.checked = this.switchEnabledValue
if (this.switchEnabledValue) {
this.buttonTarget.setAttribute("aria-checked", "true");
this.buttonTarget.classList.remove("bg-gray-200")
this.buttonTarget.classList.add("bg-blue-600")
this.switchTarget.classList.remove("translate-x-0")
this.switchTarget.classList.add("translate-x-5")
} else {
this.buttonTarget.setAttribute("aria-checked", "false");
this.buttonTarget.classList.remove("bg-blue-600")
this.buttonTarget.classList.add("bg-gray-200")
this.switchTarget.classList.remove("translate-x-5")
this.switchTarget.classList.add("translate-x-0")
}
}
}
+1 -2
View File
@@ -2,13 +2,12 @@ class CreateLndhubAccountJob < ApplicationJob
queue_as :default queue_as :default
def perform(user) def perform(user)
return if user.ln_login.present? && user.ln_password.present? return if user.ln_account.present? && user.ln_password.present?
lndhub = LndhubV2.new lndhub = LndhubV2.new
credentials = lndhub.create_account credentials = lndhub.create_account
user.update! ln_account: credentials["login"], user.update! ln_account: credentials["login"],
ln_login: credentials["login"], # TODO remove when production is migrated
ln_password: credentials["password"] ln_password: credentials["password"]
end end
end end
+9 -5
View File
@@ -1,18 +1,22 @@
class XmppExchangeContactsJob < ApplicationJob class XmppExchangeContactsJob < ApplicationJob
queue_as :default queue_as :default
def perform(inviter, username, domain) def perform(inviter, invitee)
return unless inviter.services_enabled.include?("xmpp") &&
invitee.services_enabled.include?("xmpp") &&
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new ejabberd = EjabberdApiClient.new
ejabberd.add_rosteritem({ ejabberd.add_rosteritem({
"localuser": username, "localhost": domain, "localuser": invitee.cn, "localhost": invitee.ou,
"user": inviter.cn, "host": inviter.ou, "user": inviter.cn, "host": inviter.ou,
"nick": inviter.cn, "group": "Friends", "subs": "both" "nick": inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
}) })
ejabberd.add_rosteritem({ ejabberd.add_rosteritem({
"localuser": inviter.cn, "localhost": inviter.ou, "localuser": inviter.cn, "localhost": inviter.ou,
"user": username, "host": domain, "user": invitee.cn, "host": invitee.ou,
"nick": username, "group": "Friends", "subs": "both" "nick": invitee.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
}) })
end end
end end
@@ -0,0 +1,26 @@
class XmppSetDefaultBookmarksJob < ApplicationJob
queue_as :default
def perform(user)
return unless Setting.xmpp_default_rooms.any?
@user = user
ejabberd = EjabberdApiClient.new
ejabberd.private_set user, storage_content
end
def storage_content
bookmarks = ""
Setting.xmpp_default_rooms.each do |r|
bookmarks << conference_element(
jid: r[/<(.+)>/, 1], name: r[/^(.+)\s/, 1], nick: @user.cn,
autojoin: Setting.xmpp_autojoin_default_rooms
)
end
"<storage xmlns='storage:bookmarks'>#{bookmarks}</storage>"
end
def conference_element(jid:, name:, autojoin: false, nick:)
"<conference jid='#{jid}' name='#{name}' autojoin='#{autojoin.to_s}'><nick>#{nick}</nick></conference>"
end
end
-1
View File
@@ -1,4 +1,3 @@
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer' layout 'mailer'
end end
+23
View File
@@ -0,0 +1,23 @@
# A custom mailer that can be used from the Rails console for one-off emails
# today, and later connected from an admin panel mailing page.
#
# Assign any template variables you want to use:
#
# user = User.first
#
# Create the email body from a custom email template file:
#
# body = ERB.new(File.read('./tmp/mailer-1.txt.erb')).result binding
#
# Send email via Sidekiq:
#
# CustomMailer.with(user: user, subject: "Important announcement", body: body).custom_message.deliver_later
#
class CustomMailer < ApplicationMailer
def custom_message
@user = params[:user]
@subject = params[:subject]
@body = params[:body]
mail(to: @user.email, subject: @subject)
end
end
+34
View File
@@ -0,0 +1,34 @@
# frozen_string_literal: true
if defined?(ActionMailer)
class Devise::Mailer < Devise.parent_mailer.constantize
include Devise::Mailers::Helpers
def confirmation_instructions(record, token, opts = {})
@token = token
if record.pending_reconfirmation?
devise_mail(record, :reconfirmation_instructions, opts)
else
devise_mail(record, :confirmation_instructions, opts)
end
end
def reset_password_instructions(record, token, opts = {})
@token = token
devise_mail(record, :reset_password_instructions, opts)
end
def unlock_instructions(record, token, opts = {})
@token = token
devise_mail(record, :unlock_instructions, opts)
end
def email_changed(record, opts = {})
devise_mail(record, :email_changed, opts)
end
def password_change(record, opts = {})
devise_mail(record, :password_change, opts)
end
end
end
+8
View File
@@ -0,0 +1,8 @@
class NotificationMailer < ApplicationMailer
def lightning_sats_received
@user = params[:user]
@amount_sats = params[:amount_sats]
@subject = "Sats received"
mail to: @user.email, subject: @subject
end
end
+2
View File
@@ -3,7 +3,9 @@ class Donation < ApplicationRecord
belongs_to :user belongs_to :user
# Validations # Validations
validates_presence_of :user
validates_presence_of :amount_sats validates_presence_of :amount_sats
validates_presence_of :paid_at
# Hooks # Hooks
# TODO before_create :store_fiat_value # TODO before_create :store_fiat_value
+1
View File
@@ -1,6 +1,7 @@
class Invitation < ApplicationRecord class Invitation < ApplicationRecord
# Relations # Relations
belongs_to :user belongs_to :user
belongs_to :invitee, class_name: "User", foreign_key: 'invited_user_id', optional: true
# Validations # Validations
validates_presence_of :user validates_presence_of :user
+13 -1
View File
@@ -10,6 +10,18 @@ class LndhubUser < LndhubBase
foreign_key: "login" foreign_key: "login"
def balance def balance
accounts.current.first.ledgers.sum("account_ledgers.amount") accounts.current.first.ledgers.sum("account_ledgers.amount").to_i.abs
end
def sum_outgoing
accounts.outgoing.first.ledgers.sum("account_ledgers.amount").to_i.abs
end
def sum_incoming
accounts.incoming.first.ledgers.sum("account_ledgers.amount").to_i.abs
end
def sum_fees
accounts.fees.first.ledgers.sum("account_ledgers.amount").to_i.abs
end end
end end
+104 -2
View File
@@ -2,27 +2,129 @@
class Setting < RailsSettings::Base class Setting < RailsSettings::Base
cache_prefix { "v1" } cache_prefix { "v1" }
field :accounts_domain, type: :string,
default: ENV["AKKOUNTS_DOMAIN"].presence
#
# Internal services
#
field :redis_url, type: :string, readonly: true,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
# #
# Registrations # Registrations
# #
field :reserved_usernames, type: :array, default: %w[ field :reserved_usernames, type: :array, default: %w[
account accounts admin donations mail webmaster support account accounts donations mail webmaster support
] ]
#
# XMPP
#
field :xmpp_default_rooms, type: :array, default: []
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
#
# Sentry
#
field :sentry_enabled, type: :boolean, readonly: true,
default: (ENV["SENTRY_DSN"].present?.to_s || false)
#
# Discourse
#
field :discourse_public_url, type: :string, readonly: true,
default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean,
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
field :discourse_connect_secret, type: :string, readonly: true,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
#
# ejabberd
#
field :ejabberd_enabled, type: :boolean,
default: (ENV["EJABBERD_API_URL"].present?.to_s || false)
field :ejabberd_api_url, type: :string, readonly: true,
default: ENV["EJABBERD_API_URL"].presence
field :ejabberd_admin_url, type: :string, readonly: true,
default: ENV["EJABBERD_ADMIN_URL"].presence
field :ejabberd_buddy_roster, type: :string,
default: "Buddies"
#
# Gitea
#
field :gitea_public_url, type: :string, readonly: true,
default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean,
default: (ENV["GITEA_PUBLIC_URL"].present?.to_s || false)
# #
# Lightning Network # Lightning Network
# #
field :lndhub_api_url, type: :string, readonly: true,
default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean, field :lndhub_enabled, type: :boolean,
default: (ENV["LNDHUB_API_URL"].present?.to_s || false) default: (ENV["LNDHUB_API_URL"].present?.to_s || false)
field :lndhub_admin_enabled, type: :boolean, field :lndhub_admin_enabled, type: :boolean,
default: (ENV["LNDHUB_ADMIN_UI"] || false) default: (ENV["LNDHUB_ADMIN_UI"] || false)
field :lndhub_public_key, type: :string, field :lndhub_public_key, type: :string, readonly: true,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "") default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean, field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present?.to_s || false } default: -> { self.lndhub_public_key.present?.to_s || false }
#
# Mastodon
#
field :mastodon_public_url, type: :string, readonly: true,
default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean,
default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false)
#
# MediaWiki
#
field :mediawiki_public_url, type: :string, readonly: true,
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean,
default: (ENV["MEDIAWIKI_PUBLIC_URL"].present?.to_s || false)
#
# Nostr
#
field :nostr_enabled, type: :boolean, default: true
#
# RemoteStorage
#
field :remotestorage_enabled, type: :boolean,
default: (ENV["RS_STORAGE_URL"].present?.to_s || false)
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence
end end
+83 -7
View File
@@ -1,8 +1,16 @@
class User < ApplicationRecord class User < ApplicationRecord
include EmailValidatable include EmailValidatable
attr_accessor :display_name
serialize :preferences, UserPreferences
# Relations # Relations
has_many :invitations, dependent: :destroy has_many :invitations, dependent: :destroy
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 :donations, dependent: :nullify
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user", has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
@@ -11,37 +19,42 @@ class User < ApplicationRecord
has_many :accounts, through: :lndhub_user has_many :accounts, through: :lndhub_user
validates_uniqueness_of :cn validates_uniqueness_of :cn
validates_length_of :cn, :minimum => 3 validates_length_of :cn, minimum: 3
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/, validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
if: Proc.new{ |u| u.cn.present? }, if: Proc.new{ |u| u.cn.present? },
message: "is invalid. Please use only letters, numbers and -" message: "is invalid. Please use only letters, numbers and -"
validates_format_of :cn, without: /\A-/, validates_format_of :cn, without: /\A-/,
if: Proc.new{ |u| u.cn.present? }, if: Proc.new{ |u| u.cn.present? },
message: "is invalid. Usernames need to start with a letter." message: "is invalid. Usernames need to start with a letter."
# FIXME This needs a server restart to apply values
validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i, validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i,
message: "has already been taken" message: "has already been taken"
validates_uniqueness_of :email validates_uniqueness_of :email
validates :email, email: true validates :email, email: true
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) }
scope :confirmed, -> { where.not(confirmed_at: nil) } scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :pending, -> { where(confirmed_at: nil) } scope :pending, -> { where(confirmed_at: nil) }
lockbox_encrypts :ln_login has_encrypted :ln_login, :ln_password
lockbox_encrypts :ln_password
# Include default devise modules. Others available are: # Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :ldap_authenticatable, devise :ldap_authenticatable,
:confirmable, :confirmable,
:recoverable, :recoverable,
:validatable :validatable,
:timeoutable,
:rememberable
def ldap_before_save def ldap_before_save
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
self.ou = dn.split(',')
dn = Devise::LDAP::Adapter.get_ldap_param(self.cn, "dn") .select{|e| e[0..1] == "ou"}.first
self.ou = dn.split(',').select{|e| e[0..1] == "ou"}.first.delete_prefix("ou=") .delete_prefix("ou=")
if self.confirmed_at.blank? && self.confirmation_token.blank? if self.confirmed_at.blank? && self.confirmation_token.blank?
# User had an account with a trusted email address before akkounts was a thing # User had an account with a trusted email address before akkounts was a thing
@@ -49,6 +62,26 @@ class User < ApplicationRecord
end end
end end
def devise_after_confirmation
if ldap_entry[:mail] != self.email
# E-Mail update confirmed
LdapManager::UpdateEmail.call(self.dn, self.email)
else
# E-Mail from signup confirmed (i.e. account activation)
enable_service %w[ discourse gitea mediawiki xmpp ]
#TODO enable in development when we have easy setup of ejabberd etc.
return if Rails.env.development? || !Setting.ejabberd_enabled?
XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present?
XmppSetDefaultBookmarksJob.perform_later(self)
end
end
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
def reset_password(new_password, new_password_confirmation) def reset_password(new_password, new_password_confirmation)
self.password = new_password self.password = new_password
self.password_confirmation = new_password_confirmation self.password_confirmation = new_password_confirmation
@@ -81,4 +114,47 @@ class User < ApplicationRecord
lndhub.authenticate self lndhub.authenticate self
lndhub.addinvoice payload lndhub.addinvoice payload
end end
def dn
return @dn if defined?(@dn)
@dn = Devise::LDAP::Adapter.get_dn(self.cn)
end
def ldap_entry(reload: false)
return @ldap_entry if defined?(@ldap_entry) && !reload
@ldap_entry = ldap.fetch_users(uid: self.cn, ou: self.ou).first
end
def display_name
@display_name ||= ldap_entry[:display_name]
end
def services_enabled
ldap_entry[:service] || []
end
def enable_service(service)
current_services = services_enabled
new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq
ldap.replace_attribute(dn, :service, services)
end
def disable_service(service)
current_services = services_enabled
disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :service, services)
end
def disable_all_services
ldap.delete_attribute(dn,:service)
end
private
def ldap
return @ldap_service if defined?(@ldap_service)
@ldap_service = LdapService.new
end
end end
+29
View File
@@ -0,0 +1,29 @@
DEFAULT_PREFS = YAML.load_file("#{Rails.root}/config/default_preferences.yml")
class UserPreferences
def self.dump(value)
process(value).to_yaml
end
def self.load(string)
stored_prefs = YAML.load(string || "{}")
DEFAULT_PREFS.merge(stored_prefs).with_indifferent_access
end
def self.is_integer?(value)
value.to_i.to_s == value
end
def self.process(hash)
hash.each do |key, value|
if value == "true"
hash[key] = true
elsif value == "false"
hash[key] = false
elsif value.is_a?(String) && is_integer?(value)
hash[key] = value.to_i
end
end
hash.stringify_keys!.to_h
end
end
+1 -8
View File
@@ -11,11 +11,10 @@ class CreateAccount < ApplicationService
def call def call
user = create_user_in_database user = create_user_in_database
add_ldap_document add_ldap_document
create_lndhub_account(user) create_lndhub_account(user) if Setting.lndhub_enabled
if @invitation.present? if @invitation.present?
update_invitation(user.id) update_invitation(user.id)
exchange_xmpp_contacts
end end
end end
@@ -43,12 +42,6 @@ class CreateAccount < ApplicationService
CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw) CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw)
end end
def exchange_xmpp_contacts
#TODO enable in development when we have easy setup of ejabberd etc.
return if Rails.env.development?
XmppExchangeContactsJob.perform_later(@invitation.user, @username, @domain)
end
def create_lndhub_account(user) def create_lndhub_account(user)
#TODO enable in development when we have a local lndhub (mock?) API #TODO enable in development when we have a local lndhub (mock?) API
return if Rails.env.development? return if Rails.env.development?
+7 -2
View File
@@ -1,6 +1,6 @@
class EjabberdApiClient class EjabberdApiClient
def initialize def initialize
@base_url = ENV["EJABBERD_API_URL"] @base_url = Setting.ejabberd_api_url
end end
def post(endpoint, payload) def post(endpoint, payload)
@@ -10,7 +10,7 @@ class EjabberdApiClient
if res.status != 200 if res.status != 200
Rails.logger.error "[ejabberd] API request failed:" Rails.logger.error "[ejabberd] API request failed:"
Rails.logger.error res.body Rails.logger.error res.body
#TODO add some kind of exception tracking/notifications #TODO Send custom event to Sentry
end end
end end
@@ -21,4 +21,9 @@ class EjabberdApiClient
def send_message(payload) def send_message(payload)
post "send_message", payload post "send_message", payload
end end
def private_set(user, content)
payload = { user: user.cn, host: user.ou, element: content }
post "private_set", payload
end
end end
@@ -0,0 +1,12 @@
module LdapManager
class UpdateDisplayName < LdapManagerService
def initialize(dn, display_name)
@dn = dn
@display_name = display_name
end
def call
replace_attribute @dn, :displayName, @display_name
end
end
end
+12
View File
@@ -0,0 +1,12 @@
module LdapManager
class UpdateEmail < LdapManagerService
def initialize(dn, address)
@dn = dn
@address = address
end
def call
replace_attribute @dn, :mail, @address
end
end
end
+2
View File
@@ -0,0 +1,2 @@
class LdapManagerService < LdapService
end
+17 -10
View File
@@ -3,6 +3,18 @@ class LdapService < ApplicationService
@suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org" @suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end end
def add_attribute(dn, attr, values)
ldap_client.add_attribute dn, attr, values
end
def replace_attribute(dn, attr, values)
ldap_client.replace_attribute dn, attr, values
end
def delete_attribute(dn, attr)
ldap_client.delete_attribute dn, attr
end
def add_entry(dn, attrs, interactive=false) def add_entry(dn, attrs, interactive=false)
puts "Adding entry: #{dn}" if interactive puts "Adding entry: #{dn}" if interactive
res = ldap_client.add dn: dn, attributes: attrs res = ldap_client.add dn: dn, attributes: attrs
@@ -10,10 +22,6 @@ class LdapService < ApplicationService
res res
end end
def add_attribute(dn, attr, value)
ldap_client.add_attribute dn, attr, value
end
def delete_entry(dn, interactive=false) def delete_entry(dn, interactive=false)
puts "Deleting entry: #{dn}" if interactive puts "Deleting entry: #{dn}" if interactive
res = ldap_client.delete dn: dn res = ldap_client.delete dn: dn
@@ -42,18 +50,18 @@ class LdapService < ApplicationService
treebase = ldap_config["base"] treebase = ldap_config["base"]
end end
attributes = %w{dn cn uid mail admin} attributes = %w{dn cn uid mail displayName admin service}
filter = Net::LDAP::Filter.eq("uid", "*") filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes) entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] } entries.sort_by! { |e| e.cn[0] }
entries = entries.collect do |e| entries = entries.collect do |e|
{ {
uid: e.uid.first, uid: e.uid.first,
mail: e.try(:mail) ? e.mail.first : nil, mail: e.try(:mail) ? e.mail.first : nil,
admin: e.try(:admin) ? 'admin' : nil display_name: e.try(:displayName) ? e.displayName.first : nil,
# password: e.userpassword.first admin: e.try(:admin) ? 'admin' : nil,
service: e.try(:service)
} }
end end
end end
@@ -131,5 +139,4 @@ class LdapService < ApplicationService
def ldap_config def ldap_config
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env] ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
end end
end end
+13 -9
View File
@@ -2,7 +2,7 @@ class Lndhub
attr_accessor :auth_token attr_accessor :auth_token
def initialize def initialize
@base_url = ENV["LNDHUB_LEGACY_API_URL"] @base_url = ENV["LNDHUB_API_URL"]
end end
def post(endpoint, payload) def post(endpoint, payload)
@@ -12,12 +12,7 @@ class Lndhub
end end
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
log_error(res) if res.status != 200
if res.status != 200
Rails.logger.error "[lndhub] API request failed:"
Rails.logger.error res.body
#TODO add some kind of exception tracking/notifications
end
JSON.parse(res.body) JSON.parse(res.body)
end end
@@ -31,7 +26,7 @@ class Lndhub
data = JSON.parse(res.body) data = JSON.parse(res.body)
if data.is_a?(Hash) && data["error"] && data["message"] == "bad auth" if data.is_a?(Hash) && data["error"] && data["message"] == "bad auth"
raise "BAD_AUTH" raise AuthError
else else
data data
end end
@@ -42,7 +37,7 @@ class Lndhub
end end
def authenticate(user) def authenticate(user)
credentials = post "auth?type=auth", { login: user.ln_login, password: user.ln_password } credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password }
self.auth_token = credentials["access_token"] self.auth_token = credentials["access_token"]
self.auth_token self.auth_token
end end
@@ -68,4 +63,13 @@ class Lndhub
invoice["payment_request"] invoice["payment_request"]
end end
def log_error(res)
Rails.logger.error "[lndhub] API request failed:"
Rails.logger.error res.body
if Setting.sentry_enabled?
Sentry.capture_message("Lndhub API request failed: #{res.body}")
end
end
end end
+3 -59
View File
@@ -1,9 +1,4 @@
class LndhubV2 class LndhubV2 < Lndhub
attr_accessor :auth_token
def initialize
@base_url = ENV["LNDHUB_API_URL"]
end
def post(endpoint, payload, options={}) def post(endpoint, payload, options={})
headers = { "Content-Type" => "application/json" } headers = { "Content-Type" => "application/json" }
@@ -12,64 +7,12 @@ class LndhubV2
elsif options[:admin_token] elsif options[:admin_token]
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" }) headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
end end
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
log_error(res) if res.status != 200
if res.status != 200
Rails.logger.error "[lndhub] API request failed:"
Rails.logger.error res.body
#TODO add some kind of exception tracking/notifications
end
JSON.parse(res.body) JSON.parse(res.body)
end end
def get(endpoint, auth_token)
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "Bearer #{auth_token}"
})
JSON.parse(res.body)
end
def create(payload)
post "create", payload
end
def authenticate(user)
credentials = post "auth?type=auth", { login: user.ln_login, password: user.ln_password }
self.auth_token = credentials["access_token"]
self.auth_token
end
def balance(user_token=nil)
get "balance", user_token || auth_token
end
def gettxs(user_token)
get "gettxs", user_token || auth_token
end
def getuserinvoices(user_token)
get "getuserinvoices", user_token || auth_token
end
def addinvoice(payload)
invoice = post "addinvoice", {
amt: payload[:amount],
memo: payload[:memo],
description_hash: payload[:description_hash]
}
invoice["payment_request"]
end
#
# V2
#
def create_account(payload={}) def create_account(payload={})
post "v2/users", payload, admin_token: Rails.application.credentials.lndhub[:admin_token] post "v2/users", payload, admin_token: Rails.application.credentials.lndhub[:admin_token]
end end
@@ -78,4 +21,5 @@ class LndhubV2
# Payload: { amount: 1000, description: "", description_hash: "" } # Payload: { amount: 1000, description: "", description_hash: "" }
post "v2/invoices", payload post "v2/invoices", payload
end end
end end
+13 -30
View File
@@ -1,58 +1,41 @@
<%= form_with(url: url, model: donation, local: true) do |form| %> <%= form_with(url: url, model: donation, local: true) do |form| %>
<% if donation.errors.any? %> <% if donation.errors.any? %>
<div id="error_explanation"> <section id="error_explanation">
<h3><%= pluralize(donation.errors.count, "error") %> prohibited this donation from being saved:</h3> <h3><%= pluralize(donation.errors.count, "error") %> prohibited this donation from being saved:</h3>
<ul> <ul class="list-disc list-inside">
<% donation.errors.full_messages.each do |message| %> <% donation.errors.full_messages.each do |message| %>
<li><%= message %></li> <li><%= message %></li>
<% end %> <% end %>
</ul> </ul>
</div> </section>
<% end %> <% end %>
<div class="field"> <section class="sm:w-1/2 grid grid-cols-2 items-center gap-y-2">
<p>
<%= form.label :user_id %> <%= form.label :user_id %>
<%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn %> <%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn, {} %>
</p>
</div>
<div class="field">
<p>
<%= form.label :amount_sats, "Amount BTC (sats)" %> <%= form.label :amount_sats, "Amount BTC (sats)" %>
<%= form.number_field :amount_sats %> <%= form.number_field :amount_sats %>
</p>
</div>
<div class="field">
<p>
<%= form.label :amount_eur, "Amount EUR (cents)" %> <%= form.label :amount_eur, "Amount EUR (cents)" %>
<%= form.number_field :amount_eur %> <%= form.number_field :amount_eur %>
</p>
</div>
<div class="field">
<p>
<%= form.label :amount_usd, "Amount USD (cents)"%> <%= form.label :amount_usd, "Amount USD (cents)"%>
<%= form.number_field :amount_usd %> <%= form.number_field :amount_usd %>
</p>
</div>
<div class="field">
<p>
<%= form.label :public_name %> <%= form.label :public_name %>
<%= form.text_field :public_name %> <%= form.text_field :public_name %>
</p>
</div>
<div class="field">
<p>
<%= form.label :paid_at %> <%= form.label :paid_at %>
<%= form.text_field :paid_at %> <%= form.text_field :paid_at %>
</p> </section>
</div>
<p class="mt-8"> <section>
<%= form.submit class: 'btn-md btn-blue' %> <p class="pt-6 border-t border-gray-200 text-right">
<%= link_to 'Cancel',
@donation.id.present? ? admin_donation_path(@donation) : admin_donations_path,
class: 'btn-md btn-gray' %>
<%= form.submit class: 'ml-2 btn-md btn-blue' %>
</p> </p>
</section>
<% end %> <% end %>
+1 -8
View File
@@ -1,12 +1,5 @@
<%= render HeaderComponent.new(title: "Donations") %> <%= render HeaderComponent.new(title: "Donation ##{@donation.id}") %>
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<h2>Editing Donation</h2>
<%= render 'form', donation: @donation, url: admin_donation_path(@donation) %> <%= render 'form', donation: @donation, url: admin_donation_path(@donation) %>
<p class="mt-8">
<%= link_to 'Show', admin_donation_path(@donation), class: 'ks-text-link' %> |
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
<p>
<% end %> <% end %>
+3 -3
View File
@@ -21,7 +21,7 @@
<section> <section>
<% if @donations.any? %> <% if @donations.any? %>
<h3>Recent Donations</h3> <h3>Recent Donations</h3>
<table> <table class="divided mb-8">
<thead> <thead>
<tr> <tr>
<th>User</th> <th>User</th>
@@ -33,11 +33,10 @@
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% @donations.each do |donation| %> <% @donations.each do |donation| %>
<tr> <tr>
<td><%= donation.user.address %></td> <td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td>
<td class="text-right"><%= sats_to_btc donation.amount_sats %></td> <td class="text-right"><%= sats_to_btc donation.amount_sats %></td>
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td> <td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td> <td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
@@ -53,6 +52,7 @@
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<%== pagy_nav @pagy %>
<% else %> <% else %>
<p> <p>
No donations yet. No donations yet.
+1 -7
View File
@@ -1,11 +1,5 @@
<%= render HeaderComponent.new(title: "Donations") %> <%= render HeaderComponent.new(title: "Add Donation") %>
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<h2>New Donation</h2>
<%= render 'form', donation: @donation, url: admin_donations_path %> <%= render 'form', donation: @donation, url: admin_donations_path %>
<p class="mt-8">
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
</p>
<% end %> <% end %>
+36 -33
View File
@@ -1,38 +1,41 @@
<%= render HeaderComponent.new(title: "Donations") %> <%= render HeaderComponent.new(title: "Donation ##{@donation.id}") %>
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<p> <section>
<strong>User:</strong> <table class="w-1/2 divided">
<%= @donation.user.address %> <tbody>
</p> <tr>
<th>User</th>
<td><%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td>
</tr>
<tr>
<th>Amount sats</th>
<td><%= @donation.amount_sats %></td>
</tr>
<tr>
<th>Amount EUR</th>
<td><%= @donation.amount_eur %></td>
</tr>
<tr>
<th>Amount USD</th>
<td><%= @donation.amount_usd %></td>
</tr>
<tr>
<th>Public name</th>
<td><%= @donation.public_name %></td>
</tr>
<tr>
<th>Date</th>
<td><%= @donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
</tbody>
</table>
</section>
<p> <section>
<strong>Amount sats:</strong> <p class="pt-6 border-t border-gray-200 text-right">
<%= @donation.amount_sats %> <%= link_to 'Back', admin_donations_path, class: 'btn-md btn-gray' %>
</p> <%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'ml-2 btn-md btn-blue mr-1' %>
<p>
<strong>Amount eur:</strong>
<%= @donation.amount_eur %>
</p>
<p>
<strong>Amount usd:</strong>
<%= @donation.amount_usd %>
</p>
<p>
<strong>Public name:</strong>
<%= @donation.public_name %>
</p>
<p>
<strong>Date:</strong>
<%= @donation.paid_at %>
</p>
<p class="mt-8">
<%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'ks-text-link' %> |
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
</p> </p>
</section>
<% end %> <% end %>
+4 -3
View File
@@ -24,7 +24,7 @@
<% if @invitations_used.any? %> <% if @invitations_used.any? %>
<section> <section>
<h3>Recently Accepted</h3> <h3>Recently Accepted</h3>
<table> <table class="divided mb-8">
<thead> <thead>
<tr> <tr>
<th>Token</th> <th>Token</th>
@@ -38,12 +38,13 @@
<tr> <tr>
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td> <td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td> <td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
<td><%= invitation.user.address %></td> <td><%= link_to invitation.user.address, admin_user_path(invitation.user.address), class: "ks-text-link" %></td>
<td><%= User.find(invitation.invited_user_id).address %></td> <td><%= link_to invitation.invitee.address, admin_user_path(invitation.invitee.address), class: "ks-text-link" %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<%== pagy_nav @pagy %>
</section> </section>
<% end %> <% end %>
<% end %> <% end %>
+2 -2
View File
@@ -20,7 +20,7 @@
<section> <section>
<h3>Accounts</h3> <h3>Accounts</h3>
<table> <table class="divided">
<thead> <thead>
<tr> <tr>
<th>LN Account</th> <th>LN Account</th>
@@ -36,7 +36,7 @@
</td> </td>
<td> <td>
<% if user = @users.find{ |u| u[2] == account.login } %> <% if user = @users.find{ |u| u[2] == account.login } %>
<%= "#{user[0]}@#{user[1]}" %> <%= link_to "#{user[0]}@#{user[1]}", admin_user_path("#{user[0]}@#{user[1]}"), class: "ks-text-link" %>
<% end %> <% end %>
</td> </td>
<td><%= number_with_delimiter account.balance.to_i.to_s %></td> <td><%= number_with_delimiter account.balance.to_i.to_s %></td>
@@ -0,0 +1,7 @@
<section>
<ul>
<% errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</section>
@@ -4,14 +4,9 @@
<%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %> <%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %>
<section> <section>
<h3>Registrations</h3> <h3>Registrations</h3>
<% if @errors && @errors.any? %> <% if @errors && @errors.any? %>
<div> <%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
<ul>
<% @errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %> <% end %>
<label class="block"> <label class="block">
@@ -22,14 +17,14 @@
<%= f.text_area :reserved_usernames, <%= f.text_area :reserved_usernames,
value: Setting.reserved_usernames.join("\n"), value: Setting.reserved_usernames.join("\n"),
class: "h-44 mb-2" %> class: "h-44 mb-2" %>
<p class="mb-0 text-sm text-gray-500"> <p class="text-sm text-gray-500">
One username per line One username per line
</p> </p>
</label> </label>
</section> </section>
<section> <section>
<p class="mb-0 pt-6 border-t border-gray-200"> <p class="pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %> <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p> </p>
</section> </section>
@@ -0,0 +1,52 @@
<h3>Discourse</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :discourse_enabled,
enabled: Setting.discourse_enabled?,
title: "Enable Discourse integration",
description: "Discourse configuration present and features enabled"
) %>
<% if Setting.discourse_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
<%= f.text_field :discourse_public_url,
value: Setting.discourse_public_url,
class: "w-full", disabled: true %>
<% end %>
<%= render FormElements::FieldsetComponent.new(title: "Connect secret") do %>
<%= f.password_field :discourse_connect_secret,
value: Setting.discourse_connect_secret,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>
<% if Setting.discourse_enabled? %>
<% content_for :documentation do %>
<h3 class="mt-8">How to configure Discourse</h3>
<ol class="list-decimal list-inside">
<li class="mb-6">
Set the <strong>Discourse Connect URL</strong> to the following URL:
</li>
<li data-controller="clipboard" class="mb-6 flex gap-1">
<input type="text" class="grow" disabled="disabled"
value="https://<%= Setting.accounts_domain %>/discourse/connect"
data-clipboard-target="source" />
<button class="btn-md btn-icon btn-blue 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-white h-4 w-4 inline" } %>
</span>
<span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
</span>
</button>
</li>
<li class="mb-6">
Set the <strong>Discourse Connect Secret</strong> to the value above.
</li>
<li>
Enable Discourse Connect.
</li>
<% end %>
<% end %>
@@ -0,0 +1,49 @@
<h3>ejabberd (XMPP)</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :ejabberd_enabled,
enabled: Setting.ejabberd_enabled?,
title: "Enable ejabberd integration",
description: "ejabberd configuration present and features enabled"
) %>
<% if Setting.ejabberd_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
<%= f.text_field :ejabberd_api_url,
value: Setting.ejabberd_api_url,
class: "w-full", disabled: true %>
<% end %>
<%= render FormElements::FieldsetComponent.new(title: "Admin URL") do %>
<%= f.text_field :ejabberd_admin_url,
value: Setting.ejabberd_admin_url,
class: "w-full", disabled: true %>
<% end %>
</ul>
<h3 class="mt-10">User default settings</h3>
<ul role="list">
<%= render FormElements::FieldsetComponent.new(
title: "Default rooms",
description: "Add these default rooms to new users' bookmarks"
) do %>
<%= f.text_area :xmpp_default_rooms,
value: Setting.xmpp_default_rooms.join("\n"),
placeholder: "Welcome <welcome@kosmos.chat>\nKosmos <kosmos@kosmos.chat>",
class: "h-24 w-full" %>
<% end %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :xmpp_autojoin_default_rooms,
enabled: Setting.xmpp_autojoin_default_rooms?,
title: "Auto-join default rooms",
description: "Automatically join above default rooms in chat clients"
) %>
<%= render FormElements::FieldsetComponent.new(
title: "Contact roster name",
description: "Used when exchanging contacts after signup from invitation"
) do %>
<%= f.text_field :ejabberd_buddy_roster,
value: Setting.ejabberd_buddy_roster,
class: "w-full" %>
<% end %>
<% end %>
</ul>
@@ -0,0 +1,17 @@
<h3>Gitea</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :gitea_enabled,
enabled: Setting.gitea_enabled?,
title: "Enable Gitea integration",
description: "Gitea configuration present and features enabled"
) %>
<% if Setting.gitea_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
<%= f.text_field :gitea_public_url,
value: Setting.gitea_public_url,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>
@@ -0,0 +1,38 @@
<h3>Lightning Network</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_enabled,
enabled: Setting.lndhub_enabled?,
title: "Enable LNDHub integration",
description: "LNDHub configuration present and wallet features enabled"
) %>
<% if Setting.lndhub_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
<%= f.text_field :lndhub_api_url,
value: Setting.lndhub_api_url,
class: "w-full", disabled: true %>
<% end %>
<% end %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_admin_enabled,
enabled: Setting.lndhub_admin_enabled?,
title: "Enable LNDHub admin panel",
description: "LNDHub database configuration present and admin panel enabled"
) %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_keysend_enabled,
enabled: Setting.lndhub_keysend_enabled?,
title: "Enable keysend payments",
description: "Allow users to receive invoice-less payments to their Lightning Address"
) %>
<% if Setting.lndhub_keysend_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Public key", description: "The public key of the Lightning node used by LNDHub") do %>
<%= f.text_field :lndhub_public_key,
value: Setting.lndhub_public_key,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>
@@ -0,0 +1,17 @@
<h3>Mastodon</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :mastodon_enabled,
enabled: Setting.mastodon_enabled?,
title: "Enable Mastodon integration",
description: "Mastodon configuration present and features enabled"
) %>
<% if Setting.mastodon_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
<%= f.text_field :mastodon_public_url,
value: Setting.mastodon_public_url,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>
@@ -0,0 +1,17 @@
<h3>MediaWiki</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :mediawiki_enabled,
enabled: Setting.mediawiki_enabled?,
title: "Enable MediaWiki integration",
description: "MediaWiki configuration present and features enabled"
) %>
<% if Setting.mediawiki_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
<%= f.text_field :mediawiki_public_url,
value: Setting.mediawiki_public_url,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>
@@ -0,0 +1,10 @@
<h3>Nostr</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :nostr_enabled,
enabled: Setting.nostr_enabled?,
title: "Enable Nostr integration (experimental)",
description: "Allow adding nostr pubkeys and resolve user addresses via NIP-05"
) %>
</ul>
@@ -0,0 +1,17 @@
<h3>RemoteStorage</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :remotestorage_enabled,
enabled: Setting.remotestorage_enabled?,
title: "Enable RemoteStorage integration",
description: "RemoteStorage configuration present and features enabled"
) %>
<% if Setting.remotestorage_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Storage URL") do %>
<%= f.text_field :rs_storage_url,
value: Setting.rs_storage_url,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>
@@ -1,39 +1,29 @@
<%= render HeaderComponent.new(title: "Settings") %> <%= render HeaderComponent.new(title: "Settings") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %> <%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
<section>
<h3>Lightning Network</h3>
<%= form_for(Setting.new, url: admin_settings_services_path) do |f| %> <%= form_for(Setting.new, url: admin_settings_services_path) do |f| %>
<% if @errors && @errors.any? %> <%= hidden_field_tag :service, @service %>
<div>
<ul>
<% @errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<ul role="list" class="mt-2 divide-y divide-gray-200"> <% if @errors && @errors.any? %>
<li class="flex items-center justify-between py-6"> <section>
<div class="flex flex-col"> <%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
<label class="font-bold mb-1">Enable LNDHub integration</label>
<p class="text-gray-500 mb-0">LNDHub configuration present and wallet features enabled</p>
</div>
<%= f.check_box :lndhub_enabled, checked: Setting.lndhub_enabled?,
disabled: true,
class: "relative ml-4 inline-flex flex-shrink-0" %>
</li>
<li class="flex items-center justify-between py-6">
<div class="flex flex-col">
<label class="font-bold mb-1">Enable LNDHub admin panel</label>
<p class="text-gray-500 mb-0">LNDHub database configuration present and admin panel enabled</p>
</div>
<%= f.check_box :lndhub_admin_enabled, checked: Setting.lndhub_admin_enabled?,
disabled: true,
class: "relative ml-4 inline-flex flex-shrink-0" %>
</li>
</ul>
<% end %>
</section> </section>
<% end %> <% end %>
<section>
<%= render partial: @service, locals: { f: f } %>
</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 %>
<% if content_for?(:documentation) %>
<section>
<%= yield :documentation %>
</section>
<% end %>
<% end %>
@@ -1,4 +1,4 @@
<%= render HeaderComponent.new(title: "LDAP Users: #{@ou}") %> <%= render HeaderComponent.new(title: "Users: #{@ou}") %>
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<section> <section>
@@ -22,7 +22,7 @@
<ul> <ul>
<% @orgs.each do |org| %> <% @orgs.each do |org| %>
<li class="inline-block"> <li class="inline-block">
<%= link_to org[:ou], admin_ldap_users_path(ou: org[:ou]), class: "ks-text-link" %> <%= link_to org[:ou], admin_users_path(ou: org[:ou]), class: "ks-text-link" %>
</li> </li>
<% end %> <% end %>
</ul> </ul>
@@ -30,25 +30,25 @@
<% end %> <% end %>
<section> <section>
<table> <table class="divided mb-8">
<thead> <thead>
<tr> <tr>
<th>UID</th> <th>UID</th>
<th>E-Mail</th> <th>Status</th>
<th>Admin</th> <th>Roles</th>
<!-- <th>Password</th> --> <!-- <th>Password</th> -->
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% @entries.each do |entry| %> <% @users.each do |user| %>
<tr> <tr>
<td><%= entry[:uid] %></td> <td><%= link_to(user.cn, admin_user_path(user.address), class: 'ks-text-link') %></td>
<td><%= entry[:mail] %></td> <td><%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %></td>
<td><%= entry[:admin] %></td> <td><%= user.is_admin? ? badge("admin", :red) : "" %></td>
<!-- <td><%= entry[:password] %></td> -->
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<%== pagy_nav @pagy %>
</section> </section>
<% end %> <% end %>
+186
View File
@@ -0,0 +1,186 @@
<%= render HeaderComponent.new(title: "User: #{@user.address}") %>
<%= render MainSimpleComponent.new do %>
<div class="mb-12 sm:flex sm:flex-row sm:gap-x-8">
<section class="sm:flex-1">
<h3>Account</h3>
<table class="divided">
<tbody>
<tr>
<th>ID</th>
<td><%= @user.id %></td>
</tr>
<tr>
<th>Created at</th>
<td><%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
<tr>
<th>Confirmed at</th>
<td>
<% if @user.confirmed_at %>
<%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %>
<% else %>
<%= badge "pending", :yellow %>
<% end %>
</td>
</tr>
<tr>
<th>Email</th>
<td><%= @user.email %></td>
</tr>
<tr>
<th>Roles</th>
<td><%= @user.is_admin? ? badge("admin", :red) : "—" %></td>
</tr>
<tr>
<th>Invited by</th>
<td>
<% if @user.inviter %>
<%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %>
<% else %>&mdash;<% end %>
</td>
</tr>
<tr>
<th>Invitations available</th>
<td>
<%= @user.invitations.count %>
</td>
</tr>
<tr>
<th class="align-top">Invited users</th>
<td class="align-top">
<% 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>
<% end %>
</ul>
<% else %>&mdash;<% end %>
</td>
</tr>
</tbody>
</table>
</section>
<section class="sm:flex-1 sm:pt-0">
<!-- <h3>Actions</h3> -->
</section>
</div>
<section>
<h3>Services</h3>
<table class="divided">
<thead>
<tr>
<th>Name</th>
<th>Enabled</th>
<th></th>
</tr>
</thead>
<tbody>
<% if Setting.discourse_enabled %>
<tr>
<td>Discourse</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("discourse"),
input_enabled: false
) %>
</td>
<td class="text-right">
<%= link_to "Open profile", "#{Setting.discourse_public_url}/u/#{@user.cn}/summary", class: "btn-sm btn-gray" %>
</td>
</tr>
<% end %>
<% if Setting.gitea_enabled %>
<tr>
<td>Gitea</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("gitea"),
input_enabled: false
) %>
</td>
<td class="text-right">
<%= link_to "Open profile", "#{Setting.gitea_public_url}/#{@user.cn}", class: "btn-sm btn-gray" %>
</td>
</tr>
<% end %>
<% if Setting.mastodon_enabled %>
<tr>
<td>Mastodon</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("mastodon"),
input_enabled: false
) %>
</td>
<td class="text-right">
<%= link_to "Open profile", "#{Setting.mastodon_public_url}/@#{@user.cn}", class: "btn-sm btn-gray" %>
</td>
</tr>
<% end %>
<% if Setting.mediawiki_enabled %>
<tr>
<td>MediaWiki</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("mediawiki"),
input_enabled: false
) %>
</td>
<td class="text-right">
<%= link_to "Open profile", "#{Setting.mediawiki_public_url}/Special:Contributions/#{@user.cn}", class: "btn-sm btn-gray" %>
</td>
</tr>
<% end %>
<% if Setting.ejabberd_enabled %>
<tr>
<td>XMPP (ejabberd)</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("xmpp"),
input_enabled: false
) %>
</td>
<td class="text-right">
<% if Setting.ejabberd_admin_url.present? %>
<%= link_to "Open profile", "#{Setting.ejabberd_admin_url}/server/#{@user.ou}/user/#{@user.cn}/", class: "btn-sm btn-gray" %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</section>
<% if Setting.lndhub_admin_enabled? && @user.confirmed? %>
<section>
<h3>LndHub</h3>
<% if @lndhub_user %>
<table>
<thead>
<tr>
<th>Account</th>
<th>Balance</th>
<th>Incoming</th>
<th>Outgoing</th>
<th>Fees</th>
</tr>
</thead>
<tbody>
<tr>
<td><%= @user.ln_account %></td>
<td><%= number_with_delimiter @lndhub_user.balance %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_incoming %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_outgoing %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_fees %> sats</td>
</tr>
</tbody>
</table>
<% else %>
<p>No LndHub user found for account <strong class="font-mono"><%= @user.ln_account %></strong>.
<% end %>
</section>
<% end %>
<% end %>
@@ -1,6 +1,10 @@
<%= render HeaderComponent.new(title: "Contributions") %> <%# <%= render HeaderComponent.new(title: "Contributions") %>
<%= render HeaderWithTabsComponent.new(
# title: "Contributions",
tabnav_partial: "shared/tabnav_contributions"
) %>
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %> <%= render MainSimpleComponent.new do %>
<section> <section>
<% if @donations.any? %> <% if @donations.any? %>
<p class="mb-12"> <p class="mb-12">
@@ -1,6 +1,9 @@
<%= render HeaderComponent.new(title: "Contributions") %> <%= render HeaderWithTabsComponent.new(
# title: "Contributions",
tabnav_partial: "shared/tabnav_contributions"
) %>
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %> <%= render MainSimpleComponent.new do %>
<section> <section>
<p class="mb-8"> <p class="mb-8">
Project contributions are how we develop and run all Kosmos software and Project contributions are how we develop and run all Kosmos software and

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