Compare commits

...

188 Commits

Author SHA1 Message Date
Râu Cao 6ddeacb779 Merge pull request 'Add Mastodon aliases and links to Webfinger when enabled' (#189) from feature/mastodon_webfinger into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #189
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-04-14 10:18:15 +00:00
Râu Cao 78aff3d796
Fix spec
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
The test env has Mastodon enabled now
2024-04-04 17:22:57 +03:00
Râu Cao 8f600f44bd
Add Mastodon aliases and links to Webfinger when enabled
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details
Also requires "remotestorage" service to be enabled via attribute
2024-04-04 17:17:57 +03:00
Râu Cao 819ecf6ad8
Add `#service_enabled?` method to user model 2024-04-04 13:28:09 +03:00
Râu Cao 670b2da1ef
Ad-hoc content update
continuous-integration/drone/push Build is passing Details
Before #186 is implemented
2024-03-29 10:33:28 +04:00
Râu Cao ed5c5b3081
Add remotestorage queue to Sidekiq config
continuous-integration/drone/push Build is passing Details
2024-03-29 09:47:30 +04:00
Râu Cao 4ee6bfddfa Merge pull request 'Improvements/adjustments for Mastodon integration' (#185) from chore/mastodon into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #185
2024-03-29 05:24:10 +00:00
Râu Cao 8b60890061
Add Phanpy to recommended Mastodon apps
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 4s Details
It's too good not to.
2024-03-29 09:21:17 +04:00
Râu Cao 0367450c4b
Replace hyphen with underscore in Mastodon address
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Unfortunately, Mastodon only allows underscores for usernames, and
reversely, akkounts only allows hyphens and no underscores.
2024-03-29 09:08:15 +04:00
Râu Cao e6f5623c7f
Enable Mastodon service by default (for now) 2024-03-29 09:06:41 +04:00
Râu Cao 367f566ccb Merge pull request 'Add global setting for default services, enable for preconfirmed accounts' (#184) from feature/preconfirmed_accounts into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #184
2024-03-28 13:23:22 +00:00
Râu Cao 80e69df75c
Add global setting for default services, enable for preconfirmed accounts
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 4s Details
Co-authored-by: Greg Karékinian <greg@karekinian.com>
2024-03-28 17:21:20 +04:00
Râu Cao 02af69b055
Add missing env var to example config
continuous-integration/drone/push Build is passing Details
2024-03-28 10:56:42 +04:00
Râu Cao 5d459e7e7d
Fix LDAP attribute name
continuous-integration/drone/push Build is passing Details
2024-03-19 18:18:06 +01:00
Râu Cao 51a3cb60ec Merge pull request 'Add custom LDAP attributes to schema' (#181) from feature/custom_ldap_attributes into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #181
Reviewed-by: greg <greg@noreply.kosmos.org>
2024-03-19 14:46:44 +00:00
Râu Cao 43c57c128f Merge pull request 'Move nostr pubkeys to LDAP attribute' (#183) from feature/173-nostr_ldap into feature/custom_ldap_attributes
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
Reviewed-on: #183
Reviewed-by: greg <greg@noreply.kosmos.org>
2024-03-19 14:43:02 +00:00
Râu Cao 5a3adba603
Move nostr pubkeys to LDAP attribute
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 4s Details
closes #173
2024-03-17 11:04:11 +01:00
Râu Cao 3715cb518b
User Settings: Rename Experiments to Nostr
continuous-integration/drone/push Build is passing Details
And use a nostr icon
2024-03-16 16:03:15 +01:00
Râu Cao 2c9ecc1fef
Add nostr icons 2024-03-16 16:03:00 +01:00
Râu Cao 095747e89b
Fix broken admin links
continuous-integration/drone/push Build is passing Details
2024-03-13 18:19:25 +01:00
Râu Cao 2130369604
Update db schema
continuous-integration/drone/push Build is passing Details
2024-03-13 18:15:42 +01:00
Râu Cao c996351930
Fix PostgreSQL query issue 2024-03-13 18:13:17 +01:00
Râu Cao 8b897168cc Merge pull request 'Let users donate sats via BTCPay Server' (#176) from feature/donations_btcpay into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #176
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-03-13 16:31:54 +00:00
Râu Cao 4217ba52e0
Switch `service` LDAP attribute to `serviceEnabled`
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Improve internal naming on the way
2024-03-13 16:41:49 +01:00
Râu Cao de20931d30
Add tasks for modifying schema, first custom attributes
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
refs #172, #173
2024-03-13 14:30:03 +01:00
Râu Cao 8de0a2e26e
Improve seed output 2024-03-13 14:28:31 +01:00
Râu Cao 06521d1c34
LDAP: add delete_all_users method, use in seeds 2024-03-13 14:27:39 +01:00
Râu Cao 38b3d68fd5
LDAP: Rename client method, add modify method 2024-03-13 14:26:44 +01:00
Râu Cao eac8fa6edb
0.9.0
continuous-integration/drone/push Build is passing Details
2024-03-07 14:48:27 +01:00
Râu Cao 43f918a074
Update liquor-cabinet image, fix LC/redis networking issue on Linux
continuous-integration/drone/push Build is passing Details
2024-03-06 22:07:35 +01:00
Râu Cao e322867d79 Merge pull request 'Fix login redirect for existing RS auth' (#180) from bugfix/178-rs_login_redirect into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #180
2024-03-06 21:06:27 +00:00
Râu Cao 4d6fa318b7
Fix login redirect for existing RS auth
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
fixes #178
2024-03-06 22:00:15 +01:00
Râu Cao 7f2df3b025
Fix donation record for amounts given in sats
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 4s Details
2024-03-06 11:22:53 +01:00
Râu Cao da22a9d448
Add spec for reported regression
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details
2024-03-06 11:20:43 +01:00
Râu Cao e3b96d5cff
Merge branch 'master' into feature/donations_btcpay
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-03-03 12:50:16 +01:00
Râu Cao 4e8878a4b5 Merge pull request 'Allow running specs in Docker container, update README' (#177) from dev/docker_rspec into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #177
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-03-03 11:47:53 +00:00
Râu Cao e65b890880
Update db schema
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
2024-03-02 17:31:44 +01:00
Râu Cao f57edd4d3b
Update README to account for Docker Compose everywhere
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-03-02 16:57:07 +01:00
Râu Cao 1afd56fb80
Allow running specs in Docker (Web) container 2024-03-02 16:56:07 +01:00
Râu Cao 71669a4b96 Merge pull request 'Refactor admin settings routes' (#156) from feature/content_settings into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #156
2024-03-02 14:30:21 +00:00
Râu Cao c312e30c17
Fix link in admin settings/services sidenav
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 2s Details
2024-03-02 15:26:12 +01:00
Râu Cao 51f4556ede Refactor admin settings routes
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
This is much cleaner, and semantically more correct.
2024-03-02 14:22:08 +00:00
Râu Cao c36cf5eee6
Merge branch 'master' into feature/donations_btcpay
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-03-02 15:07:40 +01:00
Râu Cao 54220019bb
Send email confirmation when BTC payment is confirmed
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2024-03-02 14:31:48 +01:00
Râu Cao 079ee8833c
Implement bitcoin donations via BTCPay 2024-03-02 14:31:48 +01:00
Râu Cao 26d613bdca
Allow other controllers to access lndhub user balance 2024-03-02 14:31:48 +01:00
Râu Cao 69b3afb8f7
DRY up btcpay and lndhub services
Removing initialize methods from the main/manager class also allows for
different iniitalizers in specific task services
2024-03-02 14:31:48 +01:00
Râu Cao fee951c05c
Move past donations to partial 2024-03-02 14:31:45 +01:00
Râu Cao 4fa4ae6b54 Merge pull request 'Comment out settings in .env.example' (#175) from task/env-example into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #175
Reviewed-by: Râu Cao <raucao@kosmos.org>
2024-03-02 13:30:18 +00:00
galfert 869ff4691b Comment out settings in .env.example
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 2s Details
2024-03-02 12:43:59 +01:00
Râu Cao 822a2dc018
Fix specs
continuous-integration/drone/push Build is passing Details
2024-03-01 17:15:02 +01:00
Râu Cao 5b7fc3707b
Hide avatar settings behind feature flag
continuous-integration/drone/push Build is failing Details
In favor of #157
2024-03-01 11:13:49 +01:00
Râu Cao 0e2dc54dc6 Merge pull request 'Upgrade Rails to 7.1, update dependencies, require Ruby 3.x' (#160) from chore/update_dependencies into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #160
Reviewed-by: slvrbckt <slvrbckt@noreply.kosmos.org>
2024-02-27 18:56:59 +00:00
Greg 87f09c94d0 Merge pull request 'Fix/improve local ActiveStorage backend usage and handling of WebApp icons' (#162) from bugfix/local_web_app_icons into chore/update_dependencies
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 2s Details
Reviewed-on: #162
Reviewed-by: greg <greg@noreply.kosmos.org>
2024-02-27 16:07:55 +00:00
Râu Cao b33b8104a8
Fix typo
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
2024-02-27 14:33:37 +01:00
Râu Cao 4a4a222973 Merge branch 'chore/update_dependencies' into bugfix/local_web_app_icons
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-02-23 18:25:23 +00:00
Râu Cao 8c524abcf5 Merge pull request 'Fix Docker volume permissions on some host platforms' (#171) from bugfix/macos_docker_volumes into chore/update_dependencies
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Reviewed-on: #171
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-02-23 18:24:10 +00:00
Râu Cao a852ab75ae
Fix Docker volume permissions on some host platforms
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 2s Details
Use named volumes instead of bind mounts.
2024-02-23 16:43:56 +01:00
Râu Cao de1f234c15
Merge branch 'chore/update_dependencies' into bugfix/local_web_app_icons
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-02-22 15:13:18 +01:00
Râu Cao 4581900427 Merge pull request 'Fix Ruby in Docker container on Apple silicon' (#168) from chore/fix_docker_ruby_on_apple_silicon into chore/update_dependencies
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Reviewed-on: #168
Reviewed-by: slvrbckt <slvrbckt@noreply.kosmos.org>
2024-02-22 14:12:05 +00:00
Râu Cao 56d91083e5
Fix seeds for new keyword argument
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
2024-02-22 13:24:41 +01:00
Râu Cao ba7c3795f8
Add pkg-config
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-02-22 11:29:56 +01:00
Râu Cao bbf3fb91a0
Fix Ruby in Docker container on Apple silicon
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-02-22 10:47:21 +01:00
Râu Cao 1754df73cb Merge pull request 'Allow admins to add and remove invitations per account' (#167) from feature/164-invites into chore/update_dependencies
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Reviewed-on: #167
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-02-17 10:17:47 +00:00
Râu Cao 9a1f9abf84
Formatting
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
2024-02-10 12:53:26 +01:00
Râu Cao 2753388e1e
Add specs for admin user management 2024-02-10 12:53:11 +01:00
Râu Cao f3159d30f1
Allow admins to add and remove invitations per account
continuous-integration/drone/push Build is passing Details
2024-02-10 11:21:45 +01:00
Râu Cao ca238be6f4
Add option for hiding close button in modal windows 2024-02-10 10:24:09 +01:00
Râu Cao 8747ce4eb0
Remove multi-domain support on admin user pages
continuous-integration/drone/push Build is passing Details
refs #166
2024-02-10 08:55:15 +01:00
Râu Cao fcda3b9c8c
WIP Make dropdowns more configurable, add invitations menu to admin page 2024-02-09 18:57:07 +01:00
Râu Cao 67689dcce3
Add service for creating invites
continuous-integration/drone/push Build is passing Details
2024-02-09 17:59:07 +01:00
Râu Cao 22ffcd54db
Patch away a deprecation warning caused by Devise
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-02-09 17:58:28 +01:00
Râu Cao bd1b177993
Rescue all icon download/upload errors, send to Sentry
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-02-08 13:36:17 +01:00
Râu Cao 3f110995a4
Add timestamp to icon filenames
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
There can be race condition when a background job is supposed to delete
an icon while there is a new one being attached. Also, this encodes the
date/time when the icon has been added, for inspection and convenience.
2024-02-08 13:03:32 +01:00
Râu Cao a7410058fa
Save WebApp before fetching icons 2024-02-08 13:02:08 +01:00
Râu Cao 411587456b
Destroy dependent RS auths when destroying a WebApp 2024-02-08 13:01:19 +01:00
Râu Cao 84e915ece9
Allow custom path for ActiveStorage local/disk backend 2024-02-08 13:01:07 +01:00
Râu Cao 70ac3b0a70
Fix RS dashboard for auths without Web App
RS auths without a valid domain name will not fetch any metadata and
therefore not create a WebApp record. This fixes icons being looked up
anyway, resulting in exceptions
2024-02-08 12:51:53 +01:00
Râu Cao a7cbd8ce36
Allow disabling S3 explicitly, disable in Docker Compose
For example when there is a .env.development for running the app on a
host machine directly, but as a developer you also want to run it with
Docker Compose from time to time.
2024-02-08 12:50:34 +01:00
Râu Cao c9052b35f6
Database update for Flipper
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-02-08 12:29:11 +01:00
Râu Cao 3b96130491
Upgrade web-console, fix it for Docker
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Was failing silently in Docker, because the warnings were turned off.
2024-02-08 12:26:28 +01:00
Râu Cao 176b1a10c6
Remove obsolete closing tag 2024-02-08 12:10:14 +01:00
Râu Cao 1c54e4c0b5
New CI image Dockerfile
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-02-03 11:36:06 +02:00
Râu Cao 7796a22491
Switch to newly published manifique gem
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-02-02 17:55:20 +02:00
Râu Cao 7e6e917ae1
Use new CI image with Ruby 3.3.0
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-02-02 17:28:33 +02:00
Râu Cao 28cfe4b1e7
Fix deprecation warning 2024-02-02 16:58:04 +02:00
Râu Cao 179a82d2dd
Use keyword arguments for ApplicationService calls
Not all services are using keywords, which breaks those calls in Ruby 3
2024-02-02 15:50:25 +02:00
Râu Cao 420442c1c0
Update Ruby for Dockerfile/Compose 2024-02-02 14:34:09 +02:00
Râu Cao 68c5758ecc
Update dependencies, upgrade to Rails 7.1, require Ruby 3.x 2024-02-02 14:25:47 +02:00
Râu Cao c5dd3c30a6
Use full URL for S3 alias host
continuous-integration/drone/push Build is passing Details
2024-02-02 14:01:47 +02:00
Râu Cao 422d5c7cd2
Fix address missing in lightning address receive notifications
continuous-integration/drone/push Build is passing Details
2024-02-01 16:22:20 +02:00
Râu Cao 5a23d523a8
Add fallback icons for apps on RS app dashboard
continuous-integration/drone/push Build is passing Details
2024-01-29 18:33:06 +02:00
Râu Cao f8da034e66
Fail gracefully when remote icon is 404
continuous-integration/drone/push Build is passing Details
2024-01-29 14:54:18 +02:00
Râu Cao b0b56fcf92
Fix lnurlp route
continuous-integration/drone/push Build is passing Details
2024-01-29 11:18:51 +02:00
Râu Cao 0cf000c1b8 Merge pull request 'Only support primary domain for Lightning Address' (#158) from chore/well-known_routes into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #158
2024-01-29 09:03:37 +00:00
Râu Cao fa9a924b0a Merge pull request 'Fix RS auth array usage in production' (#159) from bugfix/postgresql_arrays into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #159
2024-01-29 08:58:02 +00:00
Râu Cao 50f91cc7d7
Fix RS auth array usage in production
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
Serialization into YAML breaks the native PostgreSQL array usage.

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

Also puts the Lightning Address discovery routes under the .well-known
path. Combined, these changes simplify reverse-proxying to the
.well-known endpoints.
2024-01-26 16:08:21 +02:00
Râu Cao eaf41e0835
Adjust spec for c32fc51
continuous-integration/drone/push Build is passing Details
2024-01-26 16:02:47 +02:00
Râu Cao 243cf9c08d
Don't add CORS headers for Webfinger in production
continuous-integration/drone/push Build is failing Details
The reverse proxy should handle it.
2024-01-26 11:01:45 +03:00
Râu Cao c32fc51aab
Do not enable email service by default
continuous-integration/drone/push Build is failing Details
2024-01-26 09:38:38 +03:00
Râu Cao aa9178d569
Sort service ENV vars alphabetically, add missing lndhub var
continuous-integration/drone/push Build is passing Details
2024-01-26 08:36:58 +03:00
Râu Cao 281938dd64
Only set API CORS headers in development
continuous-integration/drone/push Build is passing Details
In production, this is the reverse proxy's responsibility
2024-01-22 15:35:13 +03:00
Râu Cao fafc5d8f6f
Improve copy
continuous-integration/drone/push Build is passing Details
2024-01-22 12:10:17 +03:00
Râu Cao 1238359b5f
Remove superfluous header text
continuous-integration/drone/push Build is passing Details
2024-01-22 12:04:55 +03:00
Râu Cao 84220beb1c Merge pull request 'Add email service and settings' (#154) from feature/email_service into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #154
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-01-22 09:01:18 +00:00
Râu Cao 1e9ec9bb76
Fix wrong prefix for email QR code
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
2024-01-22 11:52:45 +03:00
Râu Cao 21e51a7c40 Merge pull request 'Update nostr gem, switch to Ruby for bech32 encoding' (#155) from chore/bech32_handling into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #155
2024-01-21 09:31:51 +00:00
Râu Cao e3c30f7b16
Remove obsolete function
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
2024-01-15 13:00:48 +03:00
Râu Cao b4f0c60ea0
Update nostr gem, switch to Ruby for bech32 encoding
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2024-01-15 12:54:58 +03:00
Râu Cao 1a5a2177b4
Update spec
continuous-integration/drone/push Build is failing Details
2024-01-15 12:38:27 +03:00
Râu Cao 7e8443c598
Change Lightning balance property
continuous-integration/drone/push Build is failing Details
... so that clients can use the same property with all balances
2024-01-15 11:39:24 +03:00
Râu Cao 7b71f2cf76
Revert "Fix fixture file"
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
This reverts commit c7b137e5eb.
2024-01-10 18:35:04 +03:00
Râu Cao c7b137e5eb
Fix fixture file
continuous-integration/drone Build is failing Details
2024-01-10 18:30:19 +03:00
Râu Cao 958d18d61a
Add email service and settings 2024-01-10 18:30:05 +03:00
Râu Cao 3aa0c49507
Set CORS headers for BTCPay API endpoints 2024-01-02 09:49:09 +03:00
Râu Cao 4e566a0607 Merge pull request 'Fetch/store Web App metadata and icons, finish RS integration' (#153) from feature/142-webapp_database into master
Reviewed-on: #153
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-01-01 13:18:47 +00:00
Râu Cao aab6793b86
Improve permission list in RS emails
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
2023-11-20 18:32:52 +01:00
Râu Cao cfd0935bdc
Notify user about new RS authorizations 2023-11-20 18:24:34 +01:00
Râu Cao c2dae105ff
Add settings page for Storage, add notification prefs 2023-11-20 18:22:06 +01:00
Râu Cao 2a70bf2fb9
Small refactoring
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-20 13:40:56 +01:00
Râu Cao 9a9947f9ad
Respect "start_url" from manifest when launching web apps
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-20 13:32:40 +01:00
Râu Cao bdf5a18ad4
Re-add more specs
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-20 12:21:57 +01:00
Râu Cao aa399b862a
Allow to launch RS apps from dashboard
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-19 19:10:13 +01:00
Râu Cao 713e91a720
Implement RS auth revocation
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-19 18:49:17 +01:00
Râu Cao 8ec2a6d7e4
Remove obsolete spec file 2023-11-19 18:49:06 +01:00
Râu Cao 4ecf2c4246
Improve app list 2023-11-19 18:48:44 +01:00
Râu Cao 4fdf8accd6
Add note
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-18 17:36:18 +01:00
Râu Cao f451adcb53
Try smaller icons if 256px not available 2023-11-18 17:35:57 +01:00
Râu Cao 721dccb499
Add dropdown components, menus for RS auth items 2023-11-18 17:13:55 +01:00
Râu Cao 27bb7d1bfe
Finish working liquor-cabinet setup for Docker Compose
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-16 12:46:19 +01:00
Râu Cao 1d44181fb5
Wording 2023-11-16 12:46:05 +01:00
Râu Cao de67f59d5c
Fail gracefully and log error when token missing in Redis 2023-11-16 12:45:26 +01:00
Râu Cao 1995e6dda2
Fix RS OAuth URL in Webfinger record
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-16 12:44:59 +01:00
Râu Cao 600cfe0f78
Update lockfile 2023-11-16 12:42:39 +01:00
Râu Cao e301ac8e2e
Fix title
continuous-integration/drone/push Build is passing Details
2023-11-01 22:47:59 +01:00
Râu Cao 03a1d9f277
Allow existing user records with reserved usernames to be saved
continuous-integration/drone/push Build is running Details
2023-11-01 22:26:53 +01:00
Râu Cao 00049f3743
Add info for running Minio/RS to README
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-11-01 22:01:14 +01:00
Râu Cao 60c0a43f33
Add minio to Docker Compose setup, configure Liquor Cabinet 2023-11-01 21:51:29 +01:00
Râu Cao 0c1b1b4afe
Adjust specs for web app metadata fetching 2023-11-01 21:49:08 +01:00
Râu Cao 92310d434a
Remove rs namespace from Redis keys
Superfluous, since the whole db should be RS only
2023-11-01 21:48:16 +01:00
Râu Cao 56c127ca0c
Only allow primary domain for RS
Replace user addresses with usernames in the respective URLs
2023-11-01 21:46:38 +01:00
Râu Cao 5075fef616
Only show avatar when available on admin user page
continuous-integration/drone/push Build is failing Details
2023-10-25 22:16:16 +02:00
Râu Cao 8e090daa9c
Fetch web app metadata when creating RS auth 2023-10-25 22:16:16 +02:00
Râu Cao def87a1621
Remove variants from attachment 2023-10-25 22:16:16 +02:00
Râu Cao 00ec7fa21c
WIP Add RS auths/apps to Storage dashboard 2023-10-25 22:16:13 +02:00
Râu Cao 2b8bfaaca8
Add admin page for web apps
continuous-integration/drone/push Build is passing Details
2023-10-24 22:42:16 +02:00
Râu Cao 3e9a08a266
Remove (long) obsolete edge case 2023-10-24 17:29:24 +02:00
Râu Cao fcea11f0e5
Associate RS authorizations with web apps 2023-10-24 17:29:24 +02:00
Râu Cao 261a782963
Only complete icon URLs when given relative or absolute paths 2023-10-24 17:29:24 +02:00
Râu Cao e964e7e52c
Save web app metadata explicitly 2023-10-24 17:29:24 +02:00
Râu Cao e508407df4
Remove debug statement 2023-10-24 17:29:23 +02:00
Râu Cao bec827acb1
Store web app icons with proper folder paths 2023-10-24 17:29:23 +02:00
Râu Cao 0a69603643
Update web app metadata when first creating a record 2023-10-24 17:29:23 +02:00
Râu Cao d4f71e98ed
Download and attach icons for web apps 2023-10-24 17:29:23 +02:00
Râu Cao e56c9bd0d5
Add web app model, service to fetch metadata 2023-10-24 17:29:23 +02:00
Râu Cao e1b7e1b2ef
Update dependencies, add manifique 2023-10-24 17:29:23 +02:00
Râu Cao 1056ffd08e
Add optional S3 config/backend for ActiveStorage 2023-10-24 17:29:23 +02:00
Râu Cao be5fe00f20 Merge pull request 'Fix XMPP from-address config not being used' (#150) from bugfix/xmpp_from_address into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #150
2023-10-19 10:47:45 +00:00
Râu Cao e9c4929726
Fix XMPP from-address config not being used
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
2023-10-17 15:21:57 +02:00
Râu Cao 14ff0c0e16 Merge pull request 'BTCPay settings, admin page, and new Lightning balance API' (#147) from feature/btcpay_configs into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #147
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-09-26 10:13:09 +00:00
Râu Cao d939f5d649
Merge branch 'master' into feature/btcpay_configs
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 4s Details
2023-09-20 19:12:24 +02:00
Râu Cao 69fffb29d8
Make publishing of BTCPay wallet balances optional
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2023-09-20 18:36:53 +02:00
Râu Cao 91d3b977e9
Fix spec 2023-09-20 18:26:50 +02:00
Râu Cao 7a5fd46835 Merge pull request 'Add user avatars to LDAP, upload on profile settings page' (#148) from feature/123-user_avatars into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #148
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-09-13 13:01:25 +00:00
Râu Cao 9c4c5c2553
Use correct content type for image
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
2023-09-13 14:49:16 +02:00
Râu Cao 8f819d12c0
Remove debug output 2023-09-13 14:48:51 +02:00
Râu Cao b810e27480
Use custom docker image with libvips installed in CI
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-09-07 19:40:43 +02:00
Râu Cao 1949f1876f
Use attr_reader instead of shared instance variables
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-09-07 19:22:15 +02:00
Râu Cao 2ba0116ca6
Fix wrong inheritance
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-09-07 19:17:46 +02:00
Râu Cao 2c2ddabdff
Fix code being silly
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-09-07 19:15:14 +02:00
Râu Cao dfcdbec0dd
Add specs for avatar upload
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details
2023-09-07 11:42:42 +02:00
Râu Cao 3b67a8791c
Add libvips package to Docker container 2023-09-07 11:42:24 +02:00
Râu Cao d5ab532947
Store and retrieve avatars in/from LDAP exclusively
continuous-integration/drone/push Build is failing Details
No need to keep them in two places at the same time. We can fetch them
from LDAP whenever we want to do something with them.
2023-09-06 20:42:26 +02:00
Râu Cao 50c63d5c38
Update user avatar in LDAP 2023-09-06 19:02:07 +02:00
Râu Cao 64d09cfb7f
Use variant declarations instead of custom methods 2023-09-06 12:38:47 +02:00
Râu Cao def44618ef
Comments
continuous-integration/drone/push Build is passing Details
2023-09-06 12:16:00 +02:00
Râu Cao 9e5aeaf572
Add user avatars 2023-09-06 12:15:53 +02:00
Râu Cao 86f85a90f4
Add/configure ActiveStorage 2023-09-06 12:14:28 +02:00
Râu Cao d8a35ac3fd Merge pull request 'Fix wrong redirect after sign-in for RS OAuth' (#146) from bugfix/rs_oauth_login into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #146
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-09-05 11:03:02 +00:00
Râu Cao 5a5f62e98a
Refactor BTCPay service and API, add lightning balance
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2023-09-04 16:02:54 +02:00
Râu Cao 074f9afcbb
Fix descriptions not being shown for resettable form fields 2023-09-04 15:37:02 +02:00
Râu Cao 725fd2e5ea
Move lndhub admin token to env var/setting 2023-09-04 15:36:22 +02:00
Râu Cao 8349ca5e12
Add admin settings page for BTCPay 2023-09-04 15:25:20 +02:00
Râu Cao 46d59e3371
Improve icons in admin service settings sidenav 2023-09-04 15:24:35 +02:00
Râu Cao e8e6ee0bc4
Add configurable settings for BTCPay 2023-09-04 15:23:27 +02:00
Râu Cao a91ee2bd0a
Fix generated usernames in seeds potentially being too short
continuous-integration/drone/push Build is passing Details
2023-09-04 11:35:51 +02:00
Râu Cao fcb6923c92
Fix wrong redirect after sign-in for RS OAuth
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Release Drafter / Update release notes draft (pull_request) Successful in 3s Details
We use a custom auth method to pre-fill the username when reaching the
RS OAuth while signed out. However, it needs to redirect back to the RS
OAuth page after sign-in, and not to the root path.
2023-09-04 11:33:16 +02:00
221 changed files with 4922 additions and 1203 deletions

View File

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

View File

@ -1,46 +1,66 @@
PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.example.com
# PRIMARY_DOMAIN=kosmos.org
# 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
# 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
LDAP_HOST=localhost
LDAP_PORT=389
LDAP_ADMIN_PASSWORD=passthebutter
LDAP_SUFFIX='dc=kosmos,dc=org'
# S3_ENABLED=true
# S3_ENDPOINT=https://s3.kosmos.org
# S3_REGION=garage
# S3_BUCKET=akkounts-production
# S3_ALIAS_HOST=https://accounts.web.s3.kosmos.org
# S3_ACCESS_KEY=123456abcdefg
# S3_SECRET_KEY=123456789123456789123456789
REDIS_URL='redis://localhost:6379/1'
# LDAP_HOST=localhost
# LDAP_PORT=389
# LDAP_ADMIN_PASSWORD=passthebutter
# LDAP_SUFFIX='dc=kosmos,dc=org'
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
# REDIS_URL='redis://localhost:6379/1'
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
# WEBHOOKS_ALLOWED_IPS='10.1.1.163'
DRONECI_PUBLIC_URL='https://drone.kosmos.org'
#
# Service Integrations
#
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'
RS_REDIS_URL='redis://localhost:6379/2'
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
# BTCPAY_API_URL='http://localhost:23001/api/v1'
# BTCPAY_STORE_ID=''
# BTCPAY_AUTH_TOKEN=''
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
# DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
# DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
BTCPAY_API_URL='http://localhost:23001/api/v1'
# DRONECI_PUBLIC_URL='https://drone.kosmos.org'
LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
LNDHUB_ADMIN_UI=true
LNDHUB_PG_HOST=localhost
LNDHUB_PG_PORT=5432
LNDHUB_PG_DATABASE=lndhub
LNDHUB_PG_USERNAME=lndhub
LNDHUB_PG_PASSWORD=''
# EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
# EJABBERD_API_URL='https://xmpp.kosmos.org/api'
# GITEA_PUBLIC_URL='https://gitea.kosmos.org'
# LNDHUB_API_URL='http://localhost:3023'
# LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
# LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
# LNDHUB_ADMIN_UI=true
# LNDHUB_ADMIN_TOKEN=123456789
# LNDHUB_PG_HOST=localhost
# LNDHUB_PG_PORT=5432
# LNDHUB_PG_DATABASE=lndhub
# LNDHUB_PG_USERNAME=lndhub
# LNDHUB_PG_PASSWORD=''
# MASTODON_PUBLIC_URL='https://kosmos.social'
# MASTODON_ADDRESS_DOMAIN='https://kosmos.org'
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
# RS_STORAGE_URL='https://storage.kosmos.org'
# RS_REDIS_URL='redis://localhost:6379/2'

View File

@ -2,12 +2,16 @@ PRIMARY_DOMAIN=kosmos.org
REDIS_URL='redis://localhost:6379/0'
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
BTCPAY_STORE_ID='123456'
DISCOURSE_PUBLIC_URL='http://discourse.example.com'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
EJABBERD_API_URL='http://xmpp.example.com/api'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
MASTODON_PUBLIC_URL='http://example.social'
LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'

1
.gitignore vendored
View File

@ -23,6 +23,7 @@
!/tmp/pids/
!/tmp/pids/.keep
/storage
/public/assets
.byebug_history

View File

@ -1 +1 @@
2.7.2
3.3.0

View File

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

19
Gemfile
View File

@ -2,7 +2,7 @@ source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 7.0.2'
gem 'rails', '~> 7.1'
# Use Puma as the app server
gem 'puma', '~> 4.1'
# View components
@ -22,7 +22,7 @@ gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
# gem 'bcrypt', '~> 3.1.7'
gem 'bcrypt', '~> 3.1'
# Configuration
gem 'dotenv-rails'
@ -37,6 +37,7 @@ gem 'devise_ldap_authenticatable'
gem 'net-ldap'
# Utilities
gem "image_processing", "~> 1.12.2"
gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3'
gem 'pagy', '~> 6.0', '>= 6.0.2'
@ -46,6 +47,8 @@ gem 'flipper-ui'
# HTTP requests
gem 'faraday'
gem 'down'
gem 'aws-sdk-s3', require: false
# Background/scheduled jobs
gem 'sidekiq', '< 7'
@ -58,19 +61,19 @@ gem "sentry-rails"
# Services
gem 'discourse_api'
gem "lnurl"
gem 'nostr', git: 'https://gitea.kosmos.org/kosmos/nostr-gem.git', branch: 'feature/ruby_2.7_compat'
gem 'manifique'
gem 'nostr'
group :development, :test do
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4'
gem 'sqlite3', '~> 1.7.2'
gem 'rspec-rails'
gem 'rails-controller-testing'
gem "byebug", "~> 11.1"
end
group :development do
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
gem 'web-console', '>= 3.3.0'
gem 'web-console', '~> 4.2'
gem 'listen', '~> 3.2'
gem 'letter_opener'
gem 'letter_opener_web'
@ -86,8 +89,8 @@ group :test do
end
group :production do
# Use postgresql as the database for Active Record
gem 'pg', '~> 1.2.3'
gem 'pg', '~> 1.5'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

View File

@ -1,113 +1,127 @@
GIT
remote: https://gitea.kosmos.org/kosmos/nostr-gem.git
revision: 596529d9eb50d13b3f385245636698fccf37b442
branch: feature/ruby_2.7_compat
specs:
nostr (0.4.0)
bech32 (~> 1.3)
bip-schnorr (~> 0.4)
ecdsa (~> 1.2)
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
json (~> 2.6)
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.5)
actionpack (= 7.0.5)
activesupport (= 7.0.5)
actioncable (7.1.3)
actionpack (= 7.1.3)
activesupport (= 7.1.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.5)
actionpack (= 7.0.5)
activejob (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
zeitwerk (~> 2.6)
actionmailbox (7.1.3)
actionpack (= 7.1.3)
activejob (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.5)
actionpack (= 7.0.5)
actionview (= 7.0.5)
activejob (= 7.0.5)
activesupport (= 7.0.5)
actionmailer (7.1.3)
actionpack (= 7.1.3)
actionview (= 7.1.3)
activejob (= 7.1.3)
activesupport (= 7.1.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.5)
actionview (= 7.0.5)
activesupport (= 7.0.5)
rack (~> 2.0, >= 2.2.4)
rails-dom-testing (~> 2.2)
actionpack (7.1.3)
actionview (= 7.1.3)
activesupport (= 7.1.3)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.5)
actionpack (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actiontext (7.1.3)
actionpack (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.5)
activesupport (= 7.0.5)
actionview (7.1.3)
activesupport (= 7.1.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.5)
activesupport (= 7.0.5)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.1.3)
activesupport (= 7.1.3)
globalid (>= 0.3.6)
activemodel (7.0.5)
activesupport (= 7.0.5)
activerecord (7.0.5)
activemodel (= 7.0.5)
activesupport (= 7.0.5)
activestorage (7.0.5)
actionpack (= 7.0.5)
activejob (= 7.0.5)
activerecord (= 7.0.5)
activesupport (= 7.0.5)
activemodel (7.1.3)
activesupport (= 7.1.3)
activerecord (7.1.3)
activemodel (= 7.1.3)
activesupport (= 7.1.3)
timeout (>= 0.4.0)
activestorage (7.1.3)
actionpack (= 7.1.3)
activejob (= 7.1.3)
activerecord (= 7.1.3)
activesupport (= 7.1.3)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.5)
activesupport (7.1.3)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.4)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.886.0)
aws-sdk-core (3.191.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.77.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.143.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
backport (1.2.0)
bcrypt (3.1.18)
bech32 (1.3.0)
base64 (0.2.0)
bcrypt (3.1.20)
bech32 (1.4.2)
thor (>= 1.1.0)
benchmark (0.2.1)
benchmark (0.3.0)
bigdecimal (3.1.6)
bindex (0.8.1)
bip-schnorr (0.6.0)
bip-schnorr (0.7.0)
ecdsa_ext (~> 0.5.0)
builder (3.2.4)
byebug (11.1.3)
capybara (3.39.2)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
chunky_png (1.4.0)
concurrent-ruby (1.2.2)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
crack (0.4.5)
crack (0.4.6)
bigdecimal
rexml
crass (1.0.6)
cssbundling-rails (1.1.2)
cssbundling-rails (1.4.0)
railties (>= 6.0.0)
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
@ -115,8 +129,8 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.3)
devise (4.9.2)
date (3.3.4)
devise (4.9.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@ -125,7 +139,7 @@ GEM
devise_ldap_authenticatable (0.8.7)
devise (>= 3.4.1)
net-ldap (>= 0.16.0)
diff-lcs (1.5.0)
diff-lcs (1.5.1)
discourse_api (2.0.1)
faraday (~> 2.7)
faraday-follow_redirects
@ -135,6 +149,10 @@ GEM
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
down (5.4.1)
addressable (~> 2.8)
drb (2.2.0)
ruby2_keywords
e2mmap (0.1.0)
ecdsa (1.2.0)
ecdsa_ext (0.5.0)
@ -144,56 +162,66 @@ GEM
tzinfo
event_emitter (0.2.6)
eventmachine (1.2.7)
factory_bot (6.2.1)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
faker (3.2.0)
faker (3.2.3)
i18n (>= 1.8.11, < 2)
faraday (2.7.6)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.0.2)
faye-websocket (0.11.2)
faraday-net_http (3.1.0)
net-http
faye-websocket (0.11.3)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.15.5)
flipper (0.28.0)
ffi (1.16.3)
flipper (1.2.2)
concurrent-ruby (< 2)
flipper-active_record (0.28.0)
flipper-active_record (1.2.2)
activerecord (>= 4.2, < 8)
flipper (~> 0.28.0)
flipper-ui (0.28.0)
flipper (~> 1.2.2)
flipper-ui (1.2.2)
erubi (>= 1.0.0, < 2.0.0)
flipper (~> 0.28.0)
rack (>= 1.4, < 3)
flipper (~> 1.2.2)
rack (>= 1.4, < 4)
rack-protection (>= 1.5.3, <= 4.0.0)
sanitize (< 7)
fugit (1.8.1)
fugit (1.9.0)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
globalid (1.1.0)
activesupport (>= 5.0)
hashdiff (1.0.1)
globalid (1.2.1)
activesupport (>= 6.1)
hashdiff (1.1.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
importmap-rails (1.1.6)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (2.0.1)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.7.2)
irb (1.11.1)
rdoc
reline (>= 0.4.2)
jaro_winkler (1.5.6)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.6.3)
jmespath (1.6.2)
json (2.7.1)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.3)
launchy (2.5.2)
addressable (~> 2.8)
letter_opener (1.8.1)
@ -206,10 +234,10 @@ GEM
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
lnurl (1.0.1)
lnurl (1.1.0)
bech32 (~> 1.1)
lockbox (1.2.0)
loofah (2.21.3)
lockbox (1.3.2)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@ -217,64 +245,92 @@ GEM
net-imap
net-pop
net-smtp
manifique (1.0.1)
faraday (~> 2.9.0)
faraday-follow_redirects (= 0.3.0)
nokogiri (~> 1.16.0)
marcel (1.0.2)
matrix (0.4.2)
method_source (1.0.0)
mini_mime (1.1.2)
minitest (5.18.0)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.21.2)
multipart-post (2.3.0)
net-imap (0.3.6)
mutex_m (0.2.0)
net-http (0.4.1)
uri
net-imap (0.4.9.1)
date
net-protocol
net-ldap (0.18.0)
net-ldap (0.19.0)
net-pop (0.1.2)
net-protocol
net-protocol (0.2.1)
net-protocol (0.2.2)
timeout
net-smtp (0.3.3)
net-smtp (0.4.0.1)
net-protocol
nio4r (2.5.9)
nokogiri (1.15.2-arm64-darwin)
nio4r (2.7.0)
nokogiri (1.16.0)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.15.2-x86_64-linux)
nokogiri (1.16.0-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.0-x86_64-linux)
racc (~> 1.4)
nostr (0.5.0)
bech32 (~> 1.4)
bip-schnorr (~> 0.6)
ecdsa (~> 1.2)
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
json (~> 2.6)
orm_adapter (0.5.0)
pagy (6.0.4)
parallel (1.23.0)
parser (3.2.2.3)
pagy (6.4.3)
parallel (1.24.0)
parser (3.3.0.5)
ast (~> 2.4.1)
racc
pg (1.2.3)
public_suffix (5.0.1)
pg (1.5.4)
psych (5.1.2)
stringio
public_suffix (5.0.4)
puma (4.3.12)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.7.1)
rack (2.2.7)
rack-protection (3.0.6)
rack
racc (1.7.3)
rack (2.2.8)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-session (1.0.2)
rack (< 3)
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.5)
actioncable (= 7.0.5)
actionmailbox (= 7.0.5)
actionmailer (= 7.0.5)
actionpack (= 7.0.5)
actiontext (= 7.0.5)
actionview (= 7.0.5)
activejob (= 7.0.5)
activemodel (= 7.0.5)
activerecord (= 7.0.5)
activestorage (= 7.0.5)
activesupport (= 7.0.5)
rackup (1.0.0)
rack (< 3)
webrick
rails (7.1.3)
actioncable (= 7.1.3)
actionmailbox (= 7.1.3)
actionmailer (= 7.1.3)
actionpack (= 7.1.3)
actiontext (= 7.1.3)
actionview (= 7.1.3)
activejob (= 7.1.3)
activemodel (= 7.1.3)
activerecord (= 7.1.3)
activestorage (= 7.1.3)
activesupport (= 7.1.3)
bundler (>= 1.15.0)
railties (= 7.0.5)
railties (= 7.1.3)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
@ -282,27 +338,32 @@ GEM
rails-settings-cached (2.8.3)
activerecord (>= 5.0.0)
railties (>= 5.0.0)
railties (7.0.5)
actionpack (= 7.0.5)
activesupport (= 7.0.5)
method_source
railties (7.1.3)
actionpack (= 7.1.3)
activesupport (= 7.1.3)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.0.6)
rake (13.1.0)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbs (2.8.4)
rdoc (6.6.2)
psych (>= 4.0.0)
redis (4.8.1)
regexp_parser (2.8.1)
responders (3.1.0)
regexp_parser (2.9.0)
reline (0.4.2)
io-console (~> 0.5)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.2.5)
rexml (3.2.6)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
@ -312,10 +373,10 @@ GEM
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.5)
rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.3)
rspec-rails (6.1.1)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
@ -323,32 +384,35 @@ GEM
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.0)
rubocop (1.52.1)
rspec-support (3.12.1)
rubocop (1.60.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.3)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
ruby-progressbar (1.13.0)
ruby-vips (2.2.0)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
rufus-scheduler (3.9.1)
fugit (~> 1.1, >= 1.1.6)
sanitize (6.0.1)
sanitize (6.1.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
sentry-rails (5.9.0)
sentry-rails (5.16.1)
railties (>= 5.0)
sentry-ruby (~> 5.9.0)
sentry-ruby (5.9.0)
sentry-ruby (~> 5.16.1)
sentry-ruby (5.16.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (6.5.9)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
@ -356,7 +420,7 @@ GEM
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0)
solargraph (0.49.0)
solargraph (0.50.0)
backport (~> 1.2)
benchmark
bundler (~> 2.0)
@ -372,56 +436,63 @@ GEM
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
sprockets (4.2.0)
sprockets (4.2.1)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.6.3-arm64-darwin)
sqlite3 (1.6.3-x86_64-linux)
stimulus-rails (1.2.1)
sqlite3 (1.7.2)
mini_portile2 (~> 2.8.0)
sqlite3 (1.7.2-arm64-darwin)
sqlite3 (1.7.2-x86_64-linux)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
thor (1.2.2)
tilt (2.2.0)
timeout (0.3.2)
turbo-rails (1.4.0)
stringio (3.1.0)
thor (1.3.0)
tilt (2.3.0)
timeout (0.4.1)
turbo-rails (1.5.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2)
view_component (3.2.0)
unicode-display_width (2.5.0)
uri (0.13.0)
view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.0)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.18.1)
webmock (3.19.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket-driver (0.7.5)
webrick (1.8.1)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.34)
zeitwerk (2.6.8)
zeitwerk (2.6.12)
PLATFORMS
arm64-darwin-22
ruby
x86_64-linux
DEPENDENCIES
byebug (~> 11.1)
aws-sdk-s3
bcrypt (~> 3.1)
capybara
cssbundling-rails
database_cleaner
@ -429,12 +500,14 @@ DEPENDENCIES
devise_ldap_authenticatable
discourse_api
dotenv-rails
down
factory_bot_rails
faker
faraday
flipper
flipper-active_record
flipper-ui
image_processing (~> 1.12.2)
importmap-rails
jbuilder (~> 2.7)
letter_opener
@ -442,12 +515,13 @@ DEPENDENCIES
listen (~> 3.2)
lnurl
lockbox
manifique
net-ldap
nostr!
nostr
pagy (~> 6.0, >= 6.0.2)
pg (~> 1.2.3)
pg (~> 1.5)
puma (~> 4.1)
rails (~> 7.0.2)
rails (~> 7.1)
rails-controller-testing
rails-settings-cached (~> 2.8.3)
rqrcode (~> 2.0)
@ -458,14 +532,14 @@ DEPENDENCIES
sidekiq-scheduler
solargraph
sprockets-rails
sqlite3 (~> 1.4)
sqlite3 (~> 1.7.2)
stimulus-rails
turbo-rails
tzinfo-data
view_component
warden
web-console (>= 3.3.0)
web-console (~> 4.2)
webmock
BUNDLED WITH
2.3.7
2.5.5

View File

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

View File

@ -32,6 +32,11 @@
focus:ring-blue-400 focus:ring-opacity-75;
}
.btn-emerald {
@apply bg-emerald-500 hover:bg-emerald-600 text-white
focus:ring-emerald-400 focus:ring-opacity-75;
}
.btn-red {
@apply bg-red-600 hover:bg-red-700 text-white
focus:ring-red-500 focus:ring-opacity-75;

View File

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

View File

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

View File

@ -0,0 +1,34 @@
<div data-controller="dropdown" data-action="click->dropdown#toggle click@window->dropdown#hide">
<div class="relative inline-block">
<div role="button" tabindex="0" data-dropdown-target="button"
class="inline-block select-none">
<% if @size == :large %>
<span class="appearance-none flex items-center inline-block">
<span class="p-2 bg-gray-50 hover:bg-gray-100 rounded-full">
<%= render partial: "icons/#{@icon_name}",
locals: { custom_class: "inline text-gray-500 h-6 w-6" } %>
</span>
</span>
<% elsif @size == :small %>
<span class="appearance-none flex items-center inline-block">
<span class="text-gray-500 hover:text-blue-600">
<%= render partial: "icons/#{@icon_name}",
locals: { custom_class: "inline h-4 w-4" } %>
</span>
</span>
<% end %>
</div>
<div data-dropdown-target="menu"
data-transition-enter="transition ease-out duration-200"
data-transition-enter-from="opacity-0 translate-y-1"
data-transition-enter-to="opacity-100 translate-y-0"
data-transition-leave="transition ease-in duration-150"
data-transition-leave-from="opacity-100 translate-y-0"
data-transition-leave-to="opacity-0 translate-y-1"
class="hidden absolute top-4 right-0 z-10 mt-5 flex w-screen max-w-max">
<div class="bg-white shadow-lg rounded border overflow-hidden w-auto">
<%= content %>
</div>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,6 @@
<%= link_to @href, class: @class, data: {
'dropdown-target': "menuItem",
'action': "keydown.up->dropdown#previousItem:prevent keydown.down->dropdown#nextItem:prevent"
} do %>
<%= content %>
<% end %>

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class DropdownLinkComponent < ViewComponent::Base
def initialize(href:, separator: false, add_class: nil)
@href = href
@class = class_str(separator, add_class)
end
private
def class_str(separator, add_class)
str = "no-underline block px-5 py-3 text-sm text-gray-900 bg-white
hover:bg-gray-100 focus:bg-gray-100 whitespace-no-wrap"
str = "#{str} border-t" if separator
str = "#{str} #{add_class}" if add_class
str
end
end

View File

@ -6,7 +6,7 @@ module FormElements
@tag = tag
@positioning = :vertical
@title = title
@descripton = description
@description = description
@key = key.to_sym
@type = type
@resettable = is_resettable?(@key)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
<div class="flex items-center gap-4">
<div class="h-16 w-16 flex-none">
<%= render AppCatalog::WebAppIconComponent.new(web_app: @web_app) %>
</div>
<div class="flex-grow">
<h4 class="mb-1 text-lg font-bold">
<%= @web_app&.name || @auth.app_name %>
</h4>
<p class="text-sm text-gray-500">
<%= @auth.client_id %>
</p>
</div>
<%= render DropdownComponent.new do %>
<%= render DropdownLinkComponent.new(
href: launch_app_services_storage_rs_auth_url(@auth)
) do %>
Launch app
<% end %>
<%= render DropdownLinkComponent.new(
href: revoke_services_storage_rs_auth_url(@auth),
separator: true, add_class: "text-red-700"
) do %>
Revoke access
<% end %>
<% end %>
</div>

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class RsAuthComponent < ViewComponent::Base
def initialize(auth:)
@auth = auth
@web_app = auth.web_app
end
end

View File

@ -1,4 +1,8 @@
<%= link_to @path, class: @link_class, title: (@disabled ? "Coming soon" : nil) do %>
<% if @icon.present? %>
<%= render partial: "icons/#{@icon}", locals: { custom_class: @icon_class } %>
<% elsif @text_icon.present? %>
<span class="mr-3"><%= @text_icon %></span>
<% end %>
<span class="truncate"><%= @name %></span>
<% end %>

View File

@ -1,11 +1,13 @@
# frozen_string_literal: true
class SidenavLinkComponent < ViewComponent::Base
def initialize(name:, level: 1, path:, icon:, active: false, disabled: false)
def initialize(name:, level: 1, path:, icon: nil, text_icon: nil,
active: false, disabled: false)
@name = name
@level = level
@path = path
@icon = icon
@text_icon = text_icon
@active = active
@disabled = disabled
@link_class = class_names_link(path)

View File

@ -0,0 +1,9 @@
class Admin::AppCatalog::WebAppsController < Admin::AppCatalogController
def index
@pagy, @web_apps = pagy(AppCatalog::WebApp.order('created_at desc'))
@stats = {
known_apps: AppCatalog::WebApp.count
}
end
end

View File

@ -0,0 +1,9 @@
class Admin::AppCatalogController < Admin::BaseController
before_action :set_current_section
private
def set_current_section
@current_section = :app_catalog
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,37 @@
class Api::BtcpayController < Api::BaseController
before_action :require_feature_enabled
before_action :set_cors_access_control_headers
def onchain_btc_balance
balance = BtcpayManager::FetchOnchainWalletBalance.call
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch BTC wallet balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
def lightning_btc_balance
balance = BtcpayManager::FetchLightningWalletBalance.call
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch BTC lightning balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
private
def require_feature_enabled
unless Setting.btcpay_publish_wallet_balances
http_status :not_found and return
end
end
def set_cors_access_control_headers
return unless Rails.env.development?
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Headers'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end

View File

@ -1,13 +0,0 @@
class Api::KreditsController < Api::BaseController
def onchain_btc_balance
btcpay = BtcPay.new
balance = btcpay.onchain_wallet_balance
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch kredits BTC wallet balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
end

View File

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

View File

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

View File

@ -1,6 +1,6 @@
class LnurlpayController < ApplicationController
before_action :check_feature_enabled
before_action :find_user_by_address
before_action :check_service_available
before_action :find_user
MIN_SATS = 10
MAX_SATS = 1_000_000
@ -9,7 +9,7 @@ class LnurlpayController < ApplicationController
def index
render json: {
status: "OK",
callback: "https://accounts.kosmos.org/lnurlpay/#{@user.address}/invoice",
callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice",
tag: "payRequest",
maxSendable: MAX_SATS * 1000, # msat
minSendable: MIN_SATS * 1000, # msat
@ -34,8 +34,8 @@ class LnurlpayController < ApplicationController
def invoice
amount = params[:amount].to_i / 1000 # msats
address = params[:address]
comment = params[:comment] || ""
address = @user.address
if !valid_amount?(amount)
render json: { status: "ERROR", reason: "Invalid amount" }
@ -69,9 +69,8 @@ class LnurlpayController < ApplicationController
private
def find_user_by_address
address = params[:address].split("@")
@user = User.where(cn: address.first, ou: address.last).first
def find_user
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first
http_status :not_found if @user.nil?
end
@ -89,7 +88,7 @@ class LnurlpayController < ApplicationController
private
def check_feature_enabled
def check_service_available
http_status :not_found unless Setting.lndhub_enabled?
end
end

View File

@ -3,8 +3,7 @@ class Rs::OauthController < ApplicationController
before_action :authenticate_user!, only: :create
def new
username, org = params[:useraddress].split("@")
@user = User.where(cn: username.downcase, ou: org).first
@user = User.where(cn: params[:username].downcase, ou: Setting.primary_domain).first
@scopes = parse_scopes params[:scope]
@redirect_uri = params[:redirect_uri]
@client_id = params[:client_id]
@ -22,7 +21,7 @@ class Rs::OauthController < ApplicationController
unless current_user == @user
sign_out :user
redirect_to new_rs_oauth_url(@user.address,
redirect_to new_rs_oauth_url(@user.cn,
scope: params[:scope],
redirect_uri: params[:redirect_uri],
client_id: params[:client_id],
@ -88,7 +87,7 @@ class Rs::OauthController < ApplicationController
permissions: permissions,
client_id: client_id,
redirect_uri: redirect_uri,
app_name: client_id, #TODO use user-defined name
app_name: client_id,
expire_at: expire_at
)
@ -96,28 +95,15 @@ class Rs::OauthController < ApplicationController
allow_other_host: true
end
# GET /rs/oauth/token/:id/launch_app
def launch_app
auth = current_user.remote_storage_authorizations.find(params[:id])
redirect_to app_auth_url(auth), allow_other_host: true
end
private
def require_signed_in_with_username
unless user_signed_in?
username, org = params[:useraddress].split("@")
redirect_to new_user_session_path(cn: username, ou: org)
session[:user_return_to] = request.url
redirect_to new_user_session_path(cn: params[:username], ou: Setting.primary_domain)
end
end
def app_auth_url(auth)
url = "#{auth.url}#remotestorage=#{current_user.address}"
url += "&access_token=#{auth.token}"
url
end
def hostname_of(uri)
uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0]
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,42 @@
class Services::RsAuthsController < Services::BaseController
before_action :authenticate_user!
before_action :require_feature_enabled
before_action :require_service_available
# before_action :require_service_enabled
before_action :find_rs_auth
def destroy
@auth.destroy!
respond_to do |format|
format.html do redirect_to services_storage_url, flash: {
success: 'App authorization revoked'
}
end
format.json { head :no_content }
end
end
def launch_app
launch_url = "#{@auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{@auth.token}"
redirect_to launch_url, allow_other_host: true
end
private
def require_feature_enabled
unless Flipper.enabled?(:remotestorage, current_user)
http_status :forbidden
end
end
def require_service_available
http_status :not_found unless Setting.remotestorage_enabled?
end
def find_rs_auth
@auth = current_user.remote_storage_authorizations.find(params[:id])
http_status :not_found unless @auth.present?
end
end

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ class WebhooksController < ApplicationController
def notify_xmpp(address, amt_sats, memo)
payload = {
type: "normal",
from: Setting.primary_domain,
from: Setting.xmpp_notifications_from_address,
to: address,
subject: "Sats received!",
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import { Application } from "@hotwired/stimulus"
import { Modal, Tabs } from "tailwindcss-stimulus-components"
import { Dropdown, Modal, Tabs } from "tailwindcss-stimulus-components"
const application = Application.start()
application.register('dropdown', Dropdown)
application.register('modal', Modal)
application.register('tabs', Tabs)

View File

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

View File

@ -1,13 +1,4 @@
import { Controller } from "@hotwired/stimulus"
import { bech32 } from "bech32"
function hexToBytes (hex) {
let bytes = []
for (let c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16))
}
return bytes
}
// Connects to data-controller="settings--nostr-pubkey"
export default class extends Controller {
@ -15,10 +6,6 @@ export default class extends Controller {
static values = { userAddress: String, pubkeyHex: String, sharedSecret: String }
connect () {
if (this.hasPubkeyHexValue && this.pubkeyHexValue.length > 0) {
this.pubkeyBech32InputTarget.value = this.pubkeyBech32
}
if (window.nostr) {
if (this.hasSetPubkeyTarget) {
this.setPubkeyTarget.disabled = false
@ -53,11 +40,6 @@ export default class extends Controller {
}
}
get pubkeyBech32 () {
const words = bech32.toWords(hexToBytes(this.pubkeyHexValue))
return bech32.encode('npub', words)
}
get csrfToken () {
const element = document.head.querySelector('meta[name="csrf-token"]')
return element.getAttribute("content")

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
module AppCatalog
def self.table_name_prefix
"app_catalog_"
end
end

View File

@ -0,0 +1,16 @@
class AppCatalog::WebApp < ApplicationRecord
store :metadata, coder: JSON
has_many :remote_storage_authorizations, dependent: :destroy
has_one_attached :icon
has_one_attached :apple_touch_icon
validates :url, presence: true, uniqueness: true
validates :url, format: { with: URI.regexp },
if: Proc.new { |a| a.url.present? }
def update_metadata
AppCatalogManager::UpdateMetadata.call(app: self)
end
end

View File

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

View File

@ -1,7 +1,8 @@
class RemoteStorageAuthorization < ApplicationRecord
belongs_to :user
belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true
serialize :permissions
serialize :permissions unless Rails.env.production?
validates_presence_of :permissions
validates_presence_of :client_id
@ -15,22 +16,36 @@ class RemoteStorageAuthorization < ApplicationRecord
before_create :generate_token
before_create :store_token_in_redis
before_create :find_or_create_web_app
after_create :schedule_token_expiry
after_create :notify_user
before_destroy :delete_token_from_redis
after_destroy :remove_token_expiry_job
def url
if self.redirect_uri
uri = URI.parse self.redirect_uri
"#{uri.scheme}://#{client_id}"
uri = URI.parse self.redirect_uri
"#{uri.scheme}://#{client_id}"
end
def launch_url
return url unless web_app && web_app.metadata[:start_url].present?
start_url = web_app.metadata[:start_url]
if start_url.match("^https?:\/\/")
return start_url.start_with?(url) ? start_url : url
else
"http://#{client_id}"
path = start_url.gsub(/^\.\.\//, "").gsub(/^\.\//, "").gsub(/^\//, "")
"#{url}/#{path}"
end
end
def delete_token_from_redis
key = "rs:authorizations:#{user.address}:#{token}"
key = "authorizations:#{user.cn}:#{token}"
redis.srem? key, redis.smembers(key)
rescue => e
Rails.logger.error e
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
private
@ -44,7 +59,7 @@ class RemoteStorageAuthorization < ApplicationRecord
end
def store_token_in_redis
redis.sadd "rs:authorizations:#{user.address}:#{token}", permissions
redis.sadd "authorizations:#{user.cn}:#{token}", permissions
end
def schedule_token_expiry
@ -60,4 +75,40 @@ class RemoteStorageAuthorization < ApplicationRecord
job.delete if job.display_args == [id]
end
end
def find_or_create_web_app
if looks_like_hosted_origin?
web_app = AppCatalog::WebApp.find_or_create_by!(url: self.url)
web_app.update_metadata unless web_app.name.present?
self.web_app = web_app
self.app_name = web_app.name.presence || client_id
else
self.app_name = client_id
end
end
def looks_like_hosted_origin?
uri = URI.parse self.redirect_uri
!!(uri.host =~ /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/)
rescue URI::InvalidURIError
false
end
def notify_user
notify = user.preferences[:remotestorage_notify_auth_created]
case notify
when "xmpp"
router = Router.new
payload = {
type: "normal", to: user.address,
from: Setting.xmpp_notifications_from_address,
body: "You have just granted '#{self.client_id}' access to your Kosmos Storage. Visit your Storage dashboard to check on your connected apps and revoke permissions anytime: #{router.services_storage_url}"
}
XmppSendMessageJob.perform_later(payload)
when "email"
NotificationMailer.with(user: user, auth: self)
.remotestorage_auth_created.deliver_later
end
end
end

View File

@ -15,6 +15,9 @@ class Setting < RailsSettings::Base
field :redis_url, type: :string,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
field :s3_enabled, type: :boolean,
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
#
# Registrations
#
@ -36,7 +39,28 @@ class Setting < RailsSettings::Base
#
field :sentry_enabled, type: :boolean, readonly: true,
default: (ENV["SENTRY_DSN"].present?.to_s || false)
default: ENV["SENTRY_DSN"].present?
#
# BTCPay Server
#
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_public_url, type: :string,
default: ENV["BTCPAY_PUBLIC_URL"].presence
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
#
# Discourse
@ -46,7 +70,7 @@ class Setting < RailsSettings::Base
default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean,
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
default: ENV["DISCOURSE_PUBLIC_URL"].present?
field :discourse_connect_secret, type: :string,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
@ -59,14 +83,14 @@ class Setting < RailsSettings::Base
default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean,
default: (ENV["DRONECI_PUBLIC_URL"].present?.to_s || false)
default: ENV["DRONECI_PUBLIC_URL"].present?
#
# ejabberd
#
field :ejabberd_enabled, type: :boolean,
default: (ENV["EJABBERD_API_URL"].present?.to_s || false)
default: ENV["EJABBERD_API_URL"].present?
field :ejabberd_api_url, type: :string,
default: ENV["EJABBERD_API_URL"].presence
@ -85,7 +109,7 @@ class Setting < RailsSettings::Base
default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean,
default: (ENV["GITEA_PUBLIC_URL"].present?.to_s || false)
default: ENV["GITEA_PUBLIC_URL"].present?
#
# Lightning Network
@ -95,16 +119,19 @@ class Setting < RailsSettings::Base
default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean,
default: (ENV["LNDHUB_API_URL"].present?.to_s || false)
default: ENV["LNDHUB_API_URL"].present?
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean,
default: (ENV["LNDHUB_ADMIN_UI"] || false)
default: ENV["LNDHUB_ADMIN_UI"] || false
field :lndhub_public_key, type: :string,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present?.to_s || false }
default: -> { self.lndhub_public_key.present? }
#
# Mastodon
@ -114,7 +141,7 @@ class Setting < RailsSettings::Base
default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean,
default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false)
default: ENV["MASTODON_PUBLIC_URL"].present?
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
@ -127,24 +154,61 @@ class Setting < RailsSettings::Base
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean,
default: (ENV["MEDIAWIKI_PUBLIC_URL"].present?.to_s || false)
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
#
# Nostr
#
field :nostr_enabled, type: :boolean, default: true
field :nostr_enabled, type: :boolean, default: false
#
# OpenCollective
#
field :opencollective_enabled, type: :boolean, default: true
#
# RemoteStorage
#
field :remotestorage_enabled, type: :boolean,
default: (ENV["RS_STORAGE_URL"].present?.to_s || false)
default: ENV["RS_STORAGE_URL"].present?
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence
field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
#
# E-Mail Service
#
field :email_enabled, type: :boolean,
default: ENV["EMAIL_SMTP_HOST"].present?
# field :email_smtp_host, type: :string,
# default: ENV["EMAIL_SMTP_HOST"].presence
#
# field :email_smtp_port, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || 587
#
# field :email_smtp_enable_starttls, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || true
#
# field :email_auth_method, type: :string,
# default: ENV["EMAIL_AUTH_METHOD"].presence || "plain"
#
# field :email_imap_host, type: :string,
# default: ENV["EMAIL_IMAP_HOST"].presence
#
# field :email_imap_port, type: :string,
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
def self.default_services
# TODO Make configurable from respective service settings page
%w[ discourse gitea mastodon mediawiki xmpp ]
end
end

View File

@ -1,11 +1,18 @@
require 'nostr'
class User < ApplicationRecord
include EmailValidatable
attr_accessor :display_name
attr_accessor :avatar_new
attr_accessor :current_password
serialize :preferences, UserPreferences
serialize :preferences, coder: UserPreferences
#
# Relations
#
has_many :invitations, dependent: :destroy
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user
@ -20,6 +27,10 @@ class User < ApplicationRecord
has_many :remote_storage_authorizations
#
# Validations
#
validates_uniqueness_of :cn, scope: :ou
validates_length_of :cn, minimum: 3
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
@ -30,7 +41,8 @@ class User < ApplicationRecord
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,
message: "has already been taken"
message: "has already been taken",
unless: Proc.new{ |u| u.persisted? }
validates_uniqueness_of :email
validates :email, email: true
@ -38,12 +50,20 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) }
validates_uniqueness_of :nostr_pubkey, allow_blank: true
validate :acceptable_avatar
#
# Scopes
#
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :pending, -> { where(confirmed_at: nil) }
scope :all_except, -> (user) { where.not(id: user) }
#
# Encrypted database columns
#
has_encrypted :ln_login, :ln_password
# Include default devise modules. Others available are:
@ -70,13 +90,12 @@ class User < ApplicationRecord
def devise_after_confirmation
if ldap_entry[:mail] != self.email
# E-Mail update confirmed
LdapManager::UpdateEmail.call(self.dn, self.email)
LdapManager::UpdateEmail.call(dn: self.dn, address: self.email)
else
# TODO Make configurable
# E-Mail from signup confirmed (i.e. account activation)
enable_service %w[ discourse gitea mediawiki xmpp ]
enable_default_services
#TODO enable in development when we have easy setup of ejabberd etc.
# TODO enable in development when we have easy setup of ejabberd etc.
return if Rails.env.development? || !Setting.ejabberd_enabled?
XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present?
@ -112,7 +131,7 @@ class User < ApplicationRecord
def mastodon_address
return nil unless Setting.mastodon_enabled?
"#{self.cn}@#{Setting.mastodon_address_domain}"
"#{self.cn.gsub("-", "_")}@#{Setting.mastodon_address_domain}"
end
def valid_attribute?(attribute_name)
@ -120,6 +139,10 @@ class User < ApplicationRecord
self.errors[attribute_name].blank?
end
def enable_default_services
enable_service Setting.default_services
end
def ln_create_invoice(payload)
lndhub = Lndhub.new
lndhub.authenticate self
@ -140,22 +163,39 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name]
end
def nostr_pubkey
@nostr_pubkey ||= ldap_entry[:nostr_key]
end
def nostr_pubkey_bech32
return nil unless nostr_pubkey.present?
Nostr::PublicKey.new(nostr_pubkey).to_bech32
end
def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end
def services_enabled
ldap_entry[:service] || []
ldap_entry[:services_enabled] || []
end
def service_enabled?(name)
services_enabled.map(&:to_sym).include?(name.to_sym)
end
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)
ldap.replace_attribute(dn, :serviceEnabled, services)
end
def disable_service(service)
current_services = services_enabled
disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :service, services)
ldap.replace_attribute(dn, :serviceEnabled, services)
end
def disable_all_services
@ -168,4 +208,17 @@ class User < ApplicationRecord
return @ldap_service if defined?(@ldap_service)
@ldap_service = LdapService.new
end
def acceptable_avatar
return unless avatar_new.present?
if avatar_new.size > 1.megabyte
errors.add(:avatar, "file size is too large")
end
acceptable_types = ["image/jpeg", "image/png"]
unless acceptable_types.include?(avatar_new.content_type)
errors.add(:avatar, "must be a JPEG or PNG file")
end
end
end

View File

@ -0,0 +1,63 @@
require "manifique"
require "down"
module AppCatalogManager
class UpdateMetadata < AppCatalogManagerService
def initialize(app:)
@app = app
end
def call
agent = Manifique::Agent.new(url: @app.url)
metadata = agent.fetch_metadata
@app.name = metadata.name
[:name, :short_name, :description, :theme_color, :background_color,
:display, :start_url, :scope, :share_target, :icons].each do |prop|
@app.metadata[prop] = metadata.send(prop) if prop
end
@app.save!
# TODO move icon downloads to separate, async job
if icon = metadata.select_icon(sizes: "256x256") ||
icon = metadata.select_icon(sizes: "192x192")
attach_remote_image(:icon, icon)
# TODO elsif get whatever is available
end
if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon")
attach_remote_image(:apple_touch_icon, apple_touch_icon)
end
rescue Manifique::Error => e
msg = "Fetching web app manifest failed for #{e.url}: #{e.type}"
Rails.logger.warn(msg)
Sentry.capture_message(msg) if Setting.sentry_enabled?
false
end
def attach_remote_image(attachment_name, icon)
if icon['src'].start_with?("http")
download_url = icon['src']
else
download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}"
end
filename = "#{attachment_name}-#{Time.now.to_i}.png"
key = "web_apps/#{@app.id}/icons/#{filename}"
begin
tempfile = Down.download(download_url)
@app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
rescue Down::NotFound
msg = "Download of \"#{attachment_name}\" failed: NotFound error for #{download_url}"
Rails.logger.warn(msg)
Sentry.capture_message(msg)
rescue => e
Rails.logger.warn "Saving attachment \"#{attachment_name}\" failed: \"#{e.message}\""
Sentry.capture_exception(e) if Setting.sentry_enabled?
end
end
end
end

View File

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

View File

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

View File

@ -1,32 +0,0 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcPay
def initialize
@base_url = ENV["BTCPAY_API_URL"]
@store_id = Rails.application.credentials.btcpay[:store_id]
@auth_token = Rails.application.credentials.btcpay[:auth_token]
end
def onchain_wallet_balance
res = get "stores/#{@store_id}/payment-methods/onchain/BTC/wallet"
{
balance: res["balance"].to_f,
unconfirmed_balance: res["unconfirmedBalance"].to_f,
confirmed_balance: res["confirmedBalance"].to_f
}
end
private
def get(endpoint)
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{@auth_token}"
})
JSON.parse(res.body)
end
end

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
module BtcpayManager
class FetchOnchainWalletBalance < BtcpayManagerService
def call
res = get "/payment-methods/onchain/BTC/wallet"
{
balance: (res["balance"].to_f * 100000000).to_i, # BTC to sats
unconfirmed_balance: (res["unconfirmedBalance"].to_f * 100000000).to_i,
confirmed_balance: (res["confirmedBalance"].to_f * 100000000).to_i
}
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
require "image_processing/vips"
module LdapManager
class UpdateAvatar < LdapManagerService
def initialize(dn:, file:)
@dn = dn
@img_data = process(file)
end
def call
replace_attribute @dn, :jpegPhoto, @img_data
end
private
def process(file)
processed = ImageProcessing::Vips
.resize_to_fill(512, 512)
.source(file)
.convert("jpeg")
.saver(strip: true)
.call
Base64.strict_encode64 processed.read
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
module NostrManager
class ValidateId < NostrManagerService
def initialize(event)
def initialize(event:)
@event = Nostr::Event.new(**event)
end

View File

@ -1,6 +1,6 @@
module NostrManager
class VerifySignature < NostrManagerService
def initialize(event)
def initialize(event:)
@event = Nostr::Event.new(**event)
end

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

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

View File

@ -0,0 +1,56 @@
<%= render HeaderComponent.new(title: "App Catalog") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_app_catalog') do %>
<section>
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Known Web Apps',
value: @stats[:known_apps],
) %>
<%# <%= render QuickstatsItemComponent.new(
<%# type: :number,
<%# title: 'Accepted',
<%# value: @stats[:accepted],
<%# ) %>
<%# <%= render QuickstatsItemComponent.new(
<%# type: :number,
<%# title: 'Users with referrals',
<%# value: @stats[:users_with_referrals],
<%# meta: "/ #{User.count}"
<%# ) %>
<% end %>
</section>
<% if @web_apps.any? %>
<section>
<h3>Web Apps</h3>
<table class="divided mb-8">
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th class="hidden md:table-cell">RS Auths</th>
<th class="hidden md:table-cell">Created at</th>
</tr>
</thead>
<tbody>
<% @web_apps.each do |web_app| %>
<tr>
<td><%= web_app.name %></td>
<td><%= link_to web_app.url, web_app.url,
target: "_blank", rel: "nofollow noopener",
class: "ks-text-link" %></td>
<td class="hidden md:table-cell"><%= web_app.remote_storage_authorizations.count %></td>
<td class="hidden md:table-cell">
<span title="<%= web_app.created_at %>" class="cursor-help">
<%= time_ago_in_words web_app.created_at, include_seconds: false %> ago
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
<%== pagy_nav @pagy %>
</section>
<% end %>
<% end %>

View File

@ -1,2 +0,0 @@
json.extract! donation, :id, :user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :created_at, :updated_at
json.url donation_url(donation, format: :json)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<%= render HeaderComponent.new(title: "Settings") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
<%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %>
<%= form_for(Setting.new, url: admin_settings_registrations_path, method: :put) do |f| %>
<section>
<h3>Registrations</h3>

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