203 Commits

Author SHA1 Message Date
8728684c21 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-12 16:10:06 +04:00
3bd07472b2 Fix pages views when signed out
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-12 16:09:42 +04:00
dfb5b6b794 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-12 15:39:15 +04:00
32b1c2748a Fix wrong variable
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-30 20:22:04 +04:00
fc6cac8368 Remove superfluous link
All checks were successful
continuous-integration/drone/push Build is passing
Already linked in the same paragraph
2025-05-30 16:53:05 +04:00
eefdc88a47 Merge pull request 'Editable content' (#229) from feature/186-content_editing into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #229
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-30 11:14:50 +00:00
f2e8ca790c Add Privacy and ToS pages, footer menu
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2025-05-30 13:27:15 +04:00
32cd4d896d Fix link color for Devise links
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-29 17:26:18 +04:00
67c450860a Fix tab links
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-29 16:24:33 +04:00
f1d9cf1e3d Remove special link class
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
This cleans up the code quite a bit, but also allows links in editable
content to be rendered with the default style.
2025-05-29 16:10:34 +04:00
ab1490f472 Remove Kosmos name from wording
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
refs #222
2025-05-29 14:24:43 +04:00
6014134396 Finish MVP for content editing 2025-05-29 14:18:14 +04:00
6713665a61 WIP Rename "projects" page, make content editable
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-28 18:42:10 +04:00
315cf4dd9f Add editable content helpers 2025-05-28 18:41:53 +04:00
2f86b3c16f Add admin/editable_contents controller 2025-05-28 18:40:54 +04:00
55c63be9e2 Memoize instance variable 2025-05-28 18:39:48 +04:00
5c8ffc2630 Add editable contents table 2025-05-28 18:39:25 +04:00
c7a21c7a69 Add top margin to h3 within content 2025-05-28 18:37:59 +04:00
252b0f1792 Revert "Add ActionText configs, update spec helpers/configs"
This reverts commit c9d23f829d.
2025-05-28 16:53:31 +04:00
57246ea76d Fix navbar current link 2025-05-28 15:35:57 +04:00
c9d23f829d Add ActionText configs, update spec helpers/configs 2025-05-28 14:52:31 +04:00
8cf138b035 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-28 14:51:31 +04:00
55111f1b8b Allow using icons without custom class
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-28 14:50:59 +04:00
d48f1caa28 Merge branch 'master' into live
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-28 14:29:09 +04:00
4c6e64095f Fix unused invitations count
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-28 14:28:59 +04:00
450ccff65b Add custom class to all remaining icons
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-28 13:57:01 +04:00
6a2ad475be Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-28 13:26:51 +04:00
0778f29a8e Merge pull request 'Refactor ejabberd API integration' (#226) from core/refactor_ejabberd_integration into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #226
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-28 09:22:39 +00:00
3dbde86cdf Merge pull request 'Introduce membership statuses' (#227) from feature/contributor_status into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #227
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-28 09:16:02 +00:00
0dcfefd66c Merge pull request 'Improve admin pages for invitations' (#228) from feature/admin_invitations into feature/contributor_status
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
Reviewed-on: #228
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-28 09:00:11 +00:00
c6a187b25a Limit invitees on admin user page, link to invitations for more
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2025-05-28 12:50:10 +04:00
c99d8545c1 Add username filter to admin invitations index
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-28 12:34:52 +04:00
e8f912360b Fix wrong stats number
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-28 12:11:26 +04:00
c94a0e34d1 Add donations to user details, link to filtered list
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-27 19:04:35 +04:00
04094efbdb Add username filter with UI to admin donations page 2025-05-27 18:43:45 +04:00
71352d13d2 Add pending donations to admin donations index
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
And add more info to the details page
2025-05-27 18:08:22 +04:00
fff7527694 Don't show njump link when no pubkey set 2025-05-27 17:35:48 +04:00
7a8ca0707a Add missing dash for no member status
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-27 17:18:47 +04:00
b657a25d4d Wording 2025-05-27 17:16:26 +04:00
e48132cf5f Set member status to sustainer upon payment
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Introduces a state machine for the payment status as well.

refs #213
2025-05-27 16:39:03 +04:00
463bf34cdf Add link for icon library to README
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-27 15:12:31 +04:00
f313686b13 Add settings for member statuses
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-27 14:59:10 +04:00
0b4bc4ef5c Improve color shade of sidebar link icon
Was a bit bright
2025-05-27 14:58:45 +04:00
393f85e45c WIP Add member/contributor status to users
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-27 13:32:58 +04:00
d737d9f6b8 Refactor ejabberd API integration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2025-05-26 14:10:27 +04:00
942ae25f09 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-23 14:10:16 +04:00
4bf6985b87 Fix wrong matcher for custom LDAP attribute
All checks were successful
continuous-integration/drone/push Build is passing
389ds doesn't like case-insensitive matches for 7-bit ASCII strings
2025-05-23 14:08:41 +04:00
308cac5a39 Merge pull request 'Add Mastodon API client, service for syncing avatars and display names' (#225) from feature/mastodon_api into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #225
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-23 08:48:15 +00:00
7f766473ab Fix typo
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-22 13:21:37 +04:00
c1bac2625c Only log exception to stdout
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-05-21 16:42:49 +04:00
c5c6765d67 Log LDAP exceptions
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-21 16:29:52 +04:00
171524fb83 Use production link
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-18 14:58:55 +04:00
3538067da6 Use production link
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2025-05-18 14:58:34 +04:00
b6968f1742 Merge branch 'feature/mastodon_api' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-18 14:56:52 +04:00
c374bcd3bc Merge branch 'master' into feature/mastodon_api
Some checks are pending
continuous-integration/drone/push Build is running
2025-05-18 14:56:42 +04:00
655009ad7a Add example link for PGP pubkey
Some checks are pending
continuous-integration/drone/push Build is running
2025-05-18 14:56:29 +04:00
4f4e086518 Merge branch 'feature/mastodon_api' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-18 14:46:33 +04:00
71c9bd29ab Merge branch 'master' into feature/mastodon_api 2025-05-18 14:46:28 +04:00
e66d134550 Log missing l param for WKD requests, return 400
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-18 14:46:04 +04:00
a04a1479f8 Merge branch 'feature/mastodon_api' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-18 14:38:00 +04:00
11167e3e43 Merge branch 'master' into feature/mastodon_api 2025-05-18 14:37:47 +04:00
ebbd87368c Handle l param missing for WKD request
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-18 14:37:22 +04:00
7b0ebb761f Allow display name to be removed
All checks were successful
continuous-integration/drone/push Build is passing
When form field is empty
2025-05-18 14:26:09 +04:00
613090d38a Merge branch 'feature/mastodon_api' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-17 18:57:10 +04:00
fb03427d59 Allow syncing a single Mastodon profile
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-17 18:56:34 +04:00
ad138f715c Update doc 2025-05-17 18:56:34 +04:00
6730aae2dc Only update other avatars in one place
Prevent future mistakes
2025-05-17 18:56:33 +04:00
a71aa3fda2 Don't queue job when service isn't enabled 2025-05-17 18:56:33 +04:00
92e6b1395a Add avatar to admin user page 2025-05-17 18:56:33 +04:00
37c59b7b0c Sync Mastodon IDs/profiles to local accounts
Add a new service to import some data from Mastodon accounts:

* Find users by username, store Mastodon account ID in local db when
  found
* Import display name (don't overwrite existing)
* Import avatar (don't overwrite existing)
2025-05-17 18:56:30 +04:00
f1c540537a Merge branch 'feature/mastodon_api' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-17 18:22:38 +04:00
1dcdc2b032 Allow syncing a single Mastodon profile
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-17 18:22:11 +04:00
4545c3ba19 Merge branch 'feature/mastodon_api' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-17 18:12:57 +04:00
bfa1514181 Update doc
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-17 18:11:17 +04:00
f0846308da Only update other avatars in one place
All checks were successful
continuous-integration/drone/push Build is passing
Prevent future mistakes
2025-05-17 18:02:13 +04:00
b3b7fe6359 Don't queue job when service isn't enabled 2025-05-17 18:02:13 +04:00
f0b541ee50 Add avatar to admin user page 2025-05-17 18:02:13 +04:00
df9077e3c1 Sync Mastodon IDs/profiles to local accounts
Add a new service to import some data from Mastodon accounts:

* Find users by username, store Mastodon account ID in local db when
  found
* Import display name (don't overwrite existing)
* Import avatar (don't overwrite existing)
2025-05-17 18:02:07 +04:00
c291765777 Add mastodon_id to users 2025-05-17 16:44:13 +04:00
f0cfde560b Add Mastodon API service class, auth token config
Add a new REST API service class to keep things DRY
2025-05-17 14:18:16 +04:00
74666130c0 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-16 17:30:30 +04:00
c43e43d89c Open RS apps in new tab
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-16 17:30:11 +04:00
7c43a4d919 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-16 16:01:31 +04:00
dbbf116c52 Fix RS storage-first auth work in dev, remove token
All checks were successful
continuous-integration/drone/push Build is passing
See https://github.com/remotestorage/remotestorage.js/issues/900
2025-05-16 15:59:40 +04:00
520552ec70 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-16 15:38:20 +04:00
208b1f04ae Fix web app icon component
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-16 15:38:03 +04:00
8049f81b73 Merge pull request 'Set XMPP avatar when new avatar is uploaded' (#224) from feature/ejabberd_pep into master
Some checks are pending
continuous-integration/drone/push Build is running
Reviewed-on: #224
2025-05-16 11:37:29 +00:00
307095bfd0 WTF
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 22:21:33 +04:00
ae7291d4f1 Merge branch 'feature/ejabberd_pep' into live
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-15 22:07:26 +04:00
5f276ff349 Queue XmppSetAvatarJob when new avatar is uploaded
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Failing after 10m9s
And let job do nothing in development for now
2025-05-15 22:04:25 +04:00
5916969447 Add job for setting avatar via XMPP 2025-05-15 20:05:53 +04:00
322ca98da9 Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 20:05:30 +04:00
66f3950b83 Add job for setting avatar via XMPP 2025-05-15 20:04:55 +04:00
4e115eb514 Merge branch 'feature/ejabberd_pep' into live
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-15 19:50:30 +04:00
fc9b471a10 Add job for setting avatar via XMPP 2025-05-15 19:50:05 +04:00
b33e47e60f Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:54:25 +04:00
382c5ad10e Return response for ejabberd API calls 2025-05-15 12:53:58 +04:00
9878e4a3e8 Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:38:58 +04:00
9c35323bcd Return response for ejabberd API calls 2025-05-15 12:38:40 +04:00
e2716d94c0 Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:22:29 +04:00
8b3243af6b Sort API methods alphabetically
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:19:09 +04:00
fc36fbf10c Add get_vcard2 to ejabberd client
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:16:53 +04:00
9394f649a6 Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:02:05 +04:00
06d2705c4c Add private_get to ejabberd service
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 12:01:10 +04:00
41b9cb722b Merge branch 'feature/ejabberd_pep' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 11:48:10 +04:00
f1c13d7bd9 Add private_get to ejabberd service
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 11:47:23 +04:00
c6cb9caa6d Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-14 18:58:38 +04:00
03be2e09e6 Merge pull request 'User avatars' (#223) from feature/user_avatars into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #223
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-14 14:58:15 +00:00
582d339c0a Remove feature gate for avatar upload
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2025-05-14 18:55:26 +04:00
208977177a Merge branch 'feature/user_avatars' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-14 15:40:47 +04:00
a098ea43bb Add avatar URL to Webfinger when available
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-14 15:39:50 +04:00
9a406b8381 Merge branch 'feature/user_avatars' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-14 14:44:14 +04:00
417e346074 Do not use ActiveStorage variants, process original avatar
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Variants are currently broken. So we process the original file with the
most common avatar dimensions and stripping metadata, then hash and
upload only that version.
2025-05-14 14:42:03 +04:00
1884f082ee Add note about variants not working when not generated ad-hoc
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-12 18:07:10 +04:00
4c972bfe7a Merge branch 'feature/user_avatars' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-12 16:40:44 +04:00
51a3652fc8 Fix S3 keys/paths for user avatars
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Also fixes the avatars controller to work with all back-ends
2025-05-12 16:39:53 +04:00
0191d248f8 Merge branch 'feature/user_avatars' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-12 15:11:39 +04:00
46b908839d Add avatar URL to Discourse Connect
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Discourse should download and set the avatar if the user doesn't have
one set yet.
2025-05-12 15:04:56 +04:00
512f0ccca1 Add controller for rendering avatars on simple URL 2025-05-12 15:04:01 +04:00
17ffbde03a WIP Store avatars as ActiveStorage attachments
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Also push to LDAP as jpegPhoto
2025-05-11 18:43:21 +04:00
9e2210c45b Store avatars as binary instead of base64
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-10 20:58:36 +04:00
6d7d722c5d Add inetOrgPerson objectclass to user entries
refs #174
2025-05-08 16:52:54 +04:00
ae5d63c613 Merge pull request 'Move remaining credentials from Rails credentials store to ENV' (#221) from chore/215-configs into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #221
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-06 17:16:32 +00:00
710afb6c78 Merge branch 'chore/215-configs' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-06 20:14:42 +04:00
93aa26f430 Remove lockbox column
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2025-05-06 20:14:25 +04:00
69e9662133 Merge branch 'chore/215-configs' into live
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-06 20:01:15 +04:00
50110c12b9 Remove lockbox gem
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-06 20:01:01 +04:00
a73d0f29bd Merge branch 'chore/215-configs' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-06 19:53:52 +04:00
95843aee6d Remove credentials files
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-06 19:50:27 +04:00
84ed4b2de2 Remove old ln columns from users table
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-06 19:47:58 +04:00
7836805570 Merge branch 'chore/215-configs' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-06 19:04:03 +04:00
931624cf95 Add encryption credentials to test env
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-06 18:14:26 +04:00
eae370b737 Migrate from lockbox to ActiveRecord encryption (1/2)
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-06 18:09:27 +04:00
15a9fdec3e Make RS auth work by default in dev with Docker Compose 2025-05-06 18:07:52 +04:00
3d8619532b Refactor LDAP config
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
* Move credentials to ENV vars in prod
* Use same configs in dev and prod
* Make UID attribute and admin DN configurable
2025-05-06 15:32:59 +04:00
d56edb34f1 Remove SMTP credentials from Rails credentials
Already unused
2025-05-06 15:08:46 +04:00
1fc434cb3a Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-05 17:39:06 +04:00
a97bbf61a8 Fix postgresql query for deleting auth expiry job
All checks were successful
continuous-integration/drone/push Build is passing
Solid Queue uses a text column, instead of a jsonb, so we need to cast
it as jsonb on the fly.
2025-05-05 17:37:58 +04:00
5a523fd220 Merge pull request 'Refactor database configs' (#220) from chore/db_configs into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #220
2025-05-05 12:54:22 +00:00
dad7d5195e Merge branch 'chore/db_configs' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-05 15:28:16 +04:00
889c9ae824 Refactor database configs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
* Move postgres credentials to ENV vars
* Allow postgres in development
* Allow SQlite in production
* Refactor optional lndhub db config

Co-authored-by: Greg Karékinian <greg@karekinian.com>
2025-05-05 15:25:25 +04:00
e686cf42e8 Merge pull request 'Switch from Sidekiq to Solid Queue' (#219) from dev/sidekiq_to_solidqueue into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #219
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-05-05 11:24:56 +00:00
906468d156 Allow to immediately expire auth via job
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
When running the job before its schedule
2025-05-05 12:46:46 +04:00
ee5c6d86d0 Port RS auth job removal to Solid Queue
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-05-05 11:07:30 +04:00
d1eea85b04 Add Redis gem explicitly, remove sidekiq require
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-04 18:14:49 +04:00
ecd814641a Remove Sidekiq initializer
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-04 17:44:37 +04:00
b1dd5800b2 Update lockfile
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-04 17:42:31 +04:00
0cad4cdcfe WIP Switch from Sidekiq to Solid Queue
Some checks failed
continuous-integration/drone/push Build is running
continuous-integration/drone/pr Build is failing
2025-05-04 17:40:33 +04:00
b61906059c Merge pull request 'Upgrade Rails to 8.0' (#216) from chore/upgrade_rails into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #216
Reviewed-by: Greg <greg@noreply.kosmos.org>
2025-04-30 08:36:16 +00:00
596cad34da Merge branch 'chore/upgrade_rails' into live 2025-04-29 17:25:25 +04:00
aef779a59c Switch from Sprockets to Propshaft
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 1s
2025-04-29 17:11:21 +04:00
aaee5cb4ed Merge branch 'chore/upgrade_rails' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-28 17:56:57 +04:00
1ddecab2c3 Upgrade Rails to 8.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-04-28 17:49:54 +04:00
ebaca5ba65 Merge branch 'chore/upgrade_rails' into live 2025-04-28 15:58:17 +04:00
74b4bc3875 Upgrade Rails to 7.2
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-28 00:17:25 +04:00
646c95ecc2 Fix local/development RS auth URL
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-27 16:09:32 +04:00
fb054ae455 Add task for generating ctags
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-26 12:37:10 +04:00
b6bcfa2ee3 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-18 14:54:01 +04:00
536052e9bf Merge pull request 'Upgrade strfry/deno, port strfry policies to @nostrify/policies' (#214) from chore/upgrade_strfry_deno into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #214
2025-04-18 10:51:35 +00:00
b29a0abb0b Document strfry integration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2025-04-16 17:34:10 +04:00
29ff486683 Port strfry policies to @nostrify/policies
Use packages from JSR and adapt code for new policy APIs
2025-04-15 19:01:22 +04:00
e53b9dd186 Upgrade strfry docker image
Contains latest strfry (1.0.4) and deno (2.2.10)
2025-04-15 19:00:52 +04:00
a2921297fe Fix seeds
The CreateAccount service has moved to a namespace
2025-04-11 16:14:44 +04:00
9082ee45d8 Merge branch 'master' into live 2025-01-02 08:32:15 -05:00
7df56479a4 Fix 500 when pubkey is nil 2025-01-02 08:30:58 -05:00
29264aad98 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-16 13:33:35 +02:00
8aa3ca9e23 Merge pull request 'Let users upload their OpenPGP public key, and serve WKD response' (#205) from feature/191-gpg_keys_wkd into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #205
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-10-14 14:08:31 +00:00
3ad1d03785 Merge pull request 'Encrypt all system emails for users with PGP key' (#207) from feature/encrypted_system_emails into feature/191-gpg_keys_wkd
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
Reviewed-on: #207
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-10-14 13:39:01 +00:00
e258a8bd27 Merge pull request 'Use ASCII format for nostrKey LDAP schema' (#206) from chore/nostr_key_ldap_schema into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #206
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-10-10 14:18:31 +00:00
339462f320 Refactor mailer options usage
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 5s
2024-10-08 14:06:10 +02:00
c4c2d16342 Encrypt outgoing emails when possible 2024-10-08 14:05:50 +02:00
8ad6adbaeb Use ASCII format for nostrKey LDAP schema
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Failing after 10m11s
No need for UTF-8
2024-09-25 18:35:48 +02:00
259b51a95e Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-24 21:40:25 +02:00
fb369530e3 Merge branch 'master' into live
Some checks failed
continuous-integration/drone/push Build is failing
2024-09-14 17:18:09 +02:00
5dc10a4d33 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-14 16:46:27 +02:00
2297c68046 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-14 16:41:01 +02:00
b82ab45c99 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-14 14:57:35 +02:00
d12c63db26 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-10 16:07:24 +02:00
e6a9ef84ce Merge branch 'master' into live
Some checks failed
continuous-integration/drone/push Build is failing
2024-08-19 15:13:46 +02:00
b7e91344a0 Merge branch 'master' into live
Some checks failed
continuous-integration/drone/push Build is failing
2024-08-19 14:48:35 +02:00
0f07e32781 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 14:49:29 +02:00
1311b5ed6a Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 14:46:00 +02:00
12f82061e8 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 14:42:32 +02:00
a07b4369ab Hide njump link for users without pubkey
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-23 17:33:34 +02:00
2605c06807 Merge branch 'feature/admin_pages' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-23 17:30:22 +02:00
1db768fb15 Merge branch 'feature/admin_pages' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-23 17:27:03 +02:00
8a7403df32 Merge branch 'feature/own_relay' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 15:52:03 +02:00
f0295fef7a Merge branch 'feature/own_relay' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 15:28:55 +02:00
090affd304 Merge branch 'feature/own_relay' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 14:51:20 +02:00
bafddd436b Merge branch 'feature/own_relay' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 13:59:59 +02:00
560f193c4b Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-20 13:56:11 +02:00
8aabbad5bb Merge branch 'feature/strfry_zap_receipts' into live 2024-06-20 13:56:00 +02:00
ba8d21eb7a Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-09 13:29:57 +02:00
53df455d53 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-09 13:17:51 +02:00
9f1af3a9aa Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-07 14:13:53 +02:00
1d09008ce2 Merge branch 'master' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-04 17:45:13 +02:00
57c5317c38 Merge branch 'feature/170-nostr_zaps' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-21 18:29:13 +02:00
41bd920060 Merge branch 'feature/170-nostr_zaps' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-21 18:09:09 +02:00
0815fa6040 Merge branch 'feature/170-nostr_zaps' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 17:07:58 +02:00
af0e99aa50 Merge branch 'feature/170-nostr_zaps' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 16:55:10 +02:00
f05eec5255 Merge branch 'feature/170-nostr_zaps' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 16:48:28 +02:00
66ca2dc6b0 Merge branch 'feature/173-nostr_ldap' into live
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 13:44:24 +02:00
800183e9da Increase sidekiq concurrency in prod
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 13:44:01 +02:00
499 changed files with 4011 additions and 1748 deletions

View File

@@ -1,6 +1,23 @@
# PRIMARY_DOMAIN=kosmos.org
# AKKOUNTS_DOMAIN=accounts.example.com
# Generate this using `rails secret`
# SECRET_KEY_BASE=
# Generate these using `rails db:encryption:init`
# (Optional, needed for LndHub integration)
# ENCRYPTION_PRIMARY_KEY=
# ENCRYPTION_KEY_DERIVATION_SALT=
# The default backend is SQLite
# DB_ADAPTER=postgresql
# PG_HOST=localhost
# PG_PORT=5432
# PG_DATABASE=akkounts
# PG_DATABASE_QUEUE=akkounts_queue
# PG_USERNAME=akkounts
# PG_PASSWORD=
# SMTP_SERVER=smtp.example.com
# SMTP_PORT=587
# SMTP_LOGIN=accounts
@@ -20,8 +37,12 @@
# LDAP_HOST=localhost
# LDAP_PORT=389
# LDAP_USE_TLS=false
# LDAP_UID_ATTR=cn
# LDAP_BASE="ou=kosmos.org,cn=users,dc=kosmos,dc=org"
# LDAP_ADMIN_USER="cn=Directory Manager"
# LDAP_ADMIN_PASSWORD=passthebutter
# LDAP_SUFFIX='dc=kosmos,dc=org'
# LDAP_SUFFIX="dc=kosmos,dc=org"
# REDIS_URL='redis://localhost:6379/1'

View File

@@ -1,6 +1,9 @@
PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.kosmos.org
ENCRYPTION_PRIMARY_KEY=YhNLBgCFMAzw5dV3gISxnGrhNDMQwRdn
ENCRYPTION_KEY_DERIVATION_SALT=h28g16MRZ1sghF2jTCos1DiLZXUswinR
REDIS_URL='redis://localhost:6379/0'
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
@@ -21,7 +24,8 @@ LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de55648
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/1'
RS_STORAGE_URL='https://storage.kosmos.org'
RS_AKKOUNTS_DOMAIN=localhost
WEBHOOKS_ALLOWED_IPS='10.1.1.23'

4
.gitignore vendored
View File

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

22
Gemfile
View File

@@ -2,13 +2,13 @@ source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 7.1'
gem 'rails', '~> 8.0'
# Use Puma as the app server
gem 'puma', '~> 4.1'
gem 'puma', '~> 6.6'
# View components
gem "view_component"
# Separate dependency since Rails 7.0
gem 'sprockets-rails'
# Asset bundler
gem 'propshaft'
# Allows custom JS build tasks to integrate with the asset pipeline
gem 'cssbundling-rails'
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
@@ -19,17 +19,12 @@ gem "turbo-rails"
gem "stimulus-rails"
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1'
# Configuration
gem 'dotenv-rails'
# Security
gem 'lockbox'
# Authentication
gem 'warden'
gem 'devise', '~> 4.9.0'
@@ -37,6 +32,7 @@ gem 'devise_ldap_authenticatable'
gem 'net-ldap'
# Utilities
gem 'aasm'
gem "image_processing", "~> 1.12.2"
gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3'
@@ -46,6 +42,7 @@ gem 'flipper-active_record'
gem 'flipper-ui'
gem 'gpgme', '~> 2.0.24'
gem 'zbase32', '~> 0.1.1'
gem 'kramdown'
# HTTP requests
gem 'faraday'
@@ -53,8 +50,8 @@ gem 'down'
gem 'aws-sdk-s3', require: false
# Background/scheduled jobs
gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler'
gem 'solid_queue'
gem "mission_control-jobs"
# Monitoring
gem "sentry-ruby"
@@ -65,10 +62,11 @@ gem 'discourse_api'
gem "lnurl"
gem 'manifique', '~> 1.1.0'
gem 'nostr', '~> 0.6.0'
gem "redis", "~> 5.4"
group :development, :test do
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.7.2'
gem 'sqlite3', '>= 2.1'
gem 'rspec-rails'
gem 'rails-controller-testing'
end

View File

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

View File

@@ -57,7 +57,7 @@ Running the test suite:
Running the test suite with Docker Compose requires overriding the Rails
environment:
docker-compose run -e "RAILS_ENV=test" web rspec
docker-compose exec -e "RAILS_ENV=test" web rspec
### Docker Compose
@@ -128,6 +128,7 @@ command:
### Front-end
* [Icons](https://feathericons.com)
* [Tailwind CSS](https://tailwindcss.com/)
* [Sass](https://sass-lang.com/documentation)
* [Stimulus](https://stimulus.hotwired.dev/handbook/)

View File

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

View File

@@ -7,7 +7,6 @@
@import "components/buttons";
@import "components/dashboard_services";
@import "components/forms";
@import "components/links";
@import "components/notifications";
@import "components/pagination";
@import "components/tables";

View File

@@ -6,6 +6,7 @@
body {
@apply leading-none bg-cover bg-fixed;
background-image: linear-gradient(35deg, rgba(255,0,255,0.2) 0, rgba(13,79,153,0.8) 100%), url('/img/bg-1.jpg');
color: black;
}
body#admin {
@@ -32,6 +33,10 @@
@apply pt-8 sm:pt-12;
}
main section h3:not(:first-child) {
@apply mt-8;
}
main section:first-of-type {
@apply pt-0;
}
@@ -55,4 +60,11 @@
main ul li {
@apply leading-6;
}
main a:not(nav > *) {
@apply text-blue-600;
&:hover { @apply underline; }
&:visited { @apply text-indigo-600; }
&:active { @apply text-red-600; }
}
}

View File

@@ -1,5 +1,15 @@
@layer components {
.btn-text-dark { @apply text-black; }
.btn-text-dark:hover { @apply text-black no-underline; }
.btn-text-dark:visited { @apply text-black; }
.btn-text-dark:active { @apply text-black; }
.btn-text-light { @apply text-white; }
.btn-text-light:hover { @apply text-white no-underline; }
.btn-text-light:visited { @apply text-white; }
.btn-text-light:active { @apply text-white; }
.btn {
@apply btn-text-dark;
@apply inline-block font-semibold rounded-md leading-none cursor-pointer text-center
transition-colors duration-75 focus:outline-none focus:ring-4;
}
@@ -28,17 +38,20 @@
}
.btn-blue {
@apply bg-blue-500 hover:bg-blue-600 text-white
@apply btn-text-light;
@apply bg-blue-500 hover:bg-blue-600
focus:ring-blue-400 focus:ring-opacity-75;
}
.btn-emerald {
@apply bg-emerald-500 hover:bg-emerald-600 text-white
@apply btn-text-light;
@apply bg-emerald-500 hover:bg-emerald-600
focus:ring-emerald-400 focus:ring-opacity-75;
}
.btn-red {
@apply bg-red-600 hover:bg-red-700 text-white
@apply btn-text-light;
@apply bg-red-600 hover:bg-red-700
focus:ring-red-500 focus:ring-opacity-75;
}

View File

@@ -1,8 +0,0 @@
@layer components {
.ks-text-link {
@apply text-blue-600;
&:hover { @apply underline; }
&:visited { @apply text-indigo-600; }
&:active { @apply text-red-600; }
}
}

View File

@@ -34,7 +34,7 @@
.pagy-nav .page a, .page.gap {
@apply bg-white border-gray-300 text-gray-500 hover:bg-gray-100 relative
inline-flex items-center border px-4 py-2 text-sm font-medium
focus:z-20;
no-underline focus:z-20;
}
.pagy-nav .page.active {

View File

@@ -2,6 +2,8 @@
module AppCatalog
class WebAppIconComponent < ViewComponent::Base
include ApplicationHelper
def initialize(web_app:)
if web_app&.icon&.attached?
@image_url = image_url_for(web_app.icon)
@@ -9,13 +11,5 @@ module AppCatalog
@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

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

View File

@@ -1,8 +1,9 @@
# frozen_string_literal: true
class DropdownLinkComponent < ViewComponent::Base
def initialize(href:, separator: false, add_class: nil)
def initialize(href:, open_in_new_tab: false, separator: false, add_class: nil)
@href = href
@target = open_in_new_tab ? "_blank" : nil
@class = class_str(separator, add_class)
end

View File

@@ -0,0 +1,30 @@
<div class="inline-block text-left" data-controller="modal" data-action="keydown.esc->modal#close">
<button class="btn-md btn-outline text-red-600" data-action="click->modal#open" title="Edit">
<%= content || "Edit" %>
</button>
<%= render ModalComponent.new(show_close_button: false) do %>
<%= form_with model: [:admin, @editable_content],
html: { autocomplete: "off" } do |form| %>
<%= form.hidden_field :redirect_to, value: @redirect_to %>
<p class="mb-2">
<%= form.label :content, @editable_content.key.capitalize, class: 'font-bold' %>
</p>
<% if @editable_content.rich_text %>
<p>
<%= form.textarea :content, class: "md:w-[56rem] md:h-[28rem]" %>
</p>
<p class="text-right">
<%= form.submit "Save", class: "ml-2 btn-md btn-blue" %>
</p>
<% else %>
<p class="">
<%= form.text_field :content, class: "w-80" %>
</p>
<p>
<%= form.submit "Save", class: "btn-md btn-blue w-full" %>
</p>
<% end %>
<% end %>
<% end %>
</div>

View File

@@ -0,0 +1,6 @@
class EditContentButtonComponent < ViewComponent::Base
def initialize(context:, key:, rich_text: false, redirect_to: nil)
@editable_content = EditableContent.find_or_create_by(context:, key:, rich_text:)
@redirect_to = redirect_to
end
end

View File

@@ -0,0 +1,9 @@
<% if @editable_content.has_content? %>
<% if @editable_content.rich_text %>
<%= helpers.markdown_to_html @editable_content.content %>
<% else %>
<%= @editable_content.content %>
<% end %>
<% else %>
<%= @default %>
<% end %>

View File

@@ -0,0 +1,6 @@
class EditableContentComponent < ViewComponent::Base
def initialize(context:, key:, rich_text: false, default: nil)
@editable_content = EditableContent.find_or_create_by(context:, key:, rich_text:)
@default = default
end
end

View File

@@ -1,4 +1,4 @@
<main class="w-full max-w-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
<main class="w-full max-w-xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white rounded-lg shadow px-6 sm:px-12 py-8 sm:py-12">
<%= content %>
</div>

View File

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

View File

@@ -1,4 +1,4 @@
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
<main class="w-full max-w-6xl mx-auto px-4 md:px-6 lg:px-8">
<div class="bg-white rounded-lg shadow">
<div class="md:min-h-[50vh] divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<aside class="py-6 sm:py-8 lg:col-span-3">

View File

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

View File

@@ -12,7 +12,8 @@
</div>
<%= render DropdownComponent.new do %>
<%= render DropdownLinkComponent.new(
href: launch_app_services_storage_rs_auth_url(@auth)
href: launch_app_services_storage_rs_auth_url(@auth),
open_in_new_tab: true
) do %>
Launch app
<% end %>

View File

@@ -29,7 +29,7 @@ class SidenavLinkComponent < ViewComponent::Base
def class_names_icon(path)
if @active
"text-teal-500 group-hover:text-teal-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
"text-teal-600 group-hover:text-teal-600 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
elsif @disabled
"text-gray-300 group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
else

View File

@@ -4,11 +4,22 @@ class Admin::DonationsController < Admin::BaseController
# GET /donations
def index
@pagy, @donations = pagy(Donation.completed.order('paid_at desc'))
@username = params[:username].presence
pending_scope = Donation.incomplete.joins(:user).order('paid_at desc')
completed_scope = Donation.completed.joins(:user).order('paid_at desc')
if @username
pending_scope = pending_scope.where(users: { cn: @username })
completed_scope = completed_scope.where(users: { cn: @username })
end
@pending_donations = pending_scope
@pagy, @donations = pagy(completed_scope)
@stats = {
overall_sats: @donations.sum("amount_sats"),
donor_count: Donation.completed.count(:user_id)
overall_sats: completed_scope.sum("amount_sats"),
donor_count: completed_scope.distinct.count(:user_id)
}
end

View File

@@ -0,0 +1,45 @@
class Admin::EditableContentsController < Admin::BaseController
before_action :set_content, only: [:show, :edit, :update]
before_action :set_current_section, only: [:index, :show, :edit]
def index
@path = params[:path].presence
scope = EditableContent.order(path: :asc)
scope = scope.where(path: @path) if @path
@pagy, @contents = pagy(scope)
end
def show
end
def edit
end
def update
return_to = params[:editable_content][:redirect_to].presence
if @editable_content.update(content_params)
if return_to
redirect_to return_to
else
render status: :ok
end
else
render :edit, status: :unprocessable_entity
end
end
private
def set_content
@editable_content = EditableContent.find(params[:id])
end
def content_params
params.require(:editable_content).permit(:path, :key, :lang, :content, :rich_text)
end
def set_current_section
@current_section = :content
end
end

View File

@@ -1,12 +1,28 @@
class Admin::InvitationsController < Admin::BaseController
before_action :set_current_section
def index
@current_section = :invitations
@pagy, @invitations_used = pagy(Invitation.used.order('used_at desc'))
@username = params[:username].presence
accepted_scope = Invitation.used.order('used_at desc')
unused_scope = Invitation.unused
if @username
accepted_scope = accepted_scope.joins(:user).where(users: { cn: @username })
unused_scope = unused_scope.joins(:user).where(users: { cn: @username })
end
@pagy, @invitations_used = pagy(accepted_scope)
@stats = {
available: Invitation.unused.count,
accepted: @invitations_used.length,
users_with_referrals: Invitation.used.distinct.count(:user_id)
available: unused_scope.count,
accepted: accepted_scope.count,
users_with_referrals: accepted_scope.distinct.count(:user_id)
}
end
private
def set_current_section
@current_section = :invitations
end
end

View File

@@ -4,7 +4,7 @@ class Admin::LightningController < Admin::BaseController
def index
@current_section = :lightning
@users = User.pluck(:cn, :ou, :ln_account)
@users = User.pluck(:cn, :ou, :lndhub_username)
@accounts = LndhubAccount.with_balances.order(balance: :desc).to_a
@ln = {}

View File

@@ -0,0 +1,23 @@
class Admin::Settings::MembershipController < Admin::SettingsController
def show
end
def update
update_settings
redirect_to admin_settings_membership_path, flash: {
success: "Settings saved"
}
end
private
def setting_params
params.require(:setting).permit([
:member_status_contributor,
:member_status_sustainer,
:user_index_show_contributors,
:user_index_show_sustainers
])
end
end

View File

@@ -4,25 +4,37 @@ class Admin::UsersController < Admin::BaseController
# GET /admin/users
def index
ldap = LdapService.new
@ou = Setting.primary_domain
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
ldap = LdapService.new
ou = Setting.primary_domain
@show_contributors = Setting.user_index_show_contributors
@show_sustainers = Setting.user_index_show_sustainers
@contributors = ldap.search_users(:memberStatus, :contributor, :cn) if @show_contributors
@sustainers = ldap.search_users(:memberStatus, :sustainer, :cn) if @show_sustainers
@admins = ldap.search_users(:admin, true, :cn)
@pagy, @users = pagy(User.where(ou: ou).order(cn: :asc))
@stats = {
users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: @ou).pending.count
users_confirmed: User.where(ou: ou).confirmed.count,
users_pending: User.where(ou: ou).pending.count
}
@stats[:users_contributing] = @contributors.size if @show_contributors
@stats[:users_paying] = @sustainers.size if @show_sustainers
end
# GET /admin/users/:username
def show
@invitees = @user.invitees
@recent_invitees = @user.invitees.order(created_at: :desc).limit(5)
@more_invitees = (@invitees - @recent_invitees).count
if Setting.lndhub_admin_enabled?
@lndhub_user = @user.lndhub_user
end
@services_enabled = @user.services_enabled
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
@ldap_avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
end
# POST /admin/users/:username/invitations

View File

@@ -0,0 +1,27 @@
class AvatarsController < ApplicationController
def show
if user = User.find_by(cn: params[:username])
http_status :not_found and return unless user.avatar.attached?
sha256_hash = params[:hash]
format = params[:format]&.to_sym || :png
# size = params[:size]&.to_sym || :original
unless user.avatar.filename.to_s == "#{sha256_hash}.#{format}"
http_status :not_found and return
end
# TODO See note for avatar_variant in user model
# blob = if size == :original
# user.avatar.blob
# else
# user.avatar_variant(size: size)&.blob
# end
data = user.avatar.blob.download
send_data data, type: "image/#{format}", disposition: "inline"
else
http_status :not_found
end
end
end

View File

@@ -11,7 +11,7 @@ class Contributions::DonationsController < ApplicationController
def index
@current_section = :contributions
@donations_completed = current_user.donations.completed.order('paid_at desc')
@donations_pending = current_user.donations.processing.order('created_at desc')
@donations_processing = current_user.donations.processing.order('created_at desc')
if Setting.lndhub_enabled?
begin
@@ -81,14 +81,11 @@ class Contributions::DonationsController < ApplicationController
case invoice["status"]
when "Settled"
@donation.paid_at = DateTime.now
@donation.payment_status = "settled"
@donation.save!
@donation.complete!
flash_message = { success: "Thank you!" }
when "Processing"
unless @donation.processing?
@donation.payment_status = "processing"
@donation.save!
@donation.start_processing!
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

View File

@@ -0,0 +1,16 @@
class Contributions::OtherController < ApplicationController
before_action :authenticate_user!
before_action :set_content_editing
# GET /contributions/other
def index
@current_section = :contributions
end
private
def set_content_editing
return unless params[:edit] && current_user.is_admin?
@edit_content = true
end
end

View File

@@ -1,8 +0,0 @@
class Contributions::ProjectsController < ApplicationController
before_action :authenticate_user!
# GET /contributions
def index
@current_section = :contributions
end
end

View File

@@ -8,6 +8,9 @@ class Discourse::SsoController < ApplicationController
sso.email = current_user.email
sso.username = current_user.cn
sso.name = current_user.display_name
if current_user.avatar.attached?
sso.avatar_url = helpers.image_url_for(current_user.avatar)
end
sso.admin = current_user.is_admin?
sso.sso_secret = secret

View File

@@ -37,7 +37,7 @@ class LnurlpayController < ApplicationController
pubkey: Setting.lndhub_public_key,
customData: [{
customKey: "696969",
customValue: @user.ln_account
customValue: @user.lndhub_username
}]
}
end

View File

@@ -0,0 +1,9 @@
class PagesController < ApplicationController
def privacy
@current_section = :privacy
end
def tos
@current_section = :tos
end
end

View File

@@ -9,7 +9,7 @@ class Services::LightningController < ApplicationController
before_action :lndhub_fetch_balance
def index
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
@wallet_setup_url = "lndhub://#{current_user.lndhub_username}:#{current_user.lndhub_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
end
def transactions

View File

@@ -23,7 +23,11 @@ class Services::RsAuthsController < Services::BaseController
end
def launch_app
launch_url = "#{@auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{@auth.token}"
user_address = Rails.env.development? ?
"#{current_user.cn}@localhost:3000" :
current_user.address
launch_url = "#{@auth.launch_url}#remotestorage=#{user_address}"
redirect_to launch_url, allow_other_host: true
end

View File

@@ -25,7 +25,7 @@ class SettingsController < ApplicationController
def update
@user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name]
@user.avatar_new = user_params[:avatar]
@user.avatar_new = user_params[:avatar_new]
@user.pgp_pubkey = user_params[:pgp_pubkey]
if @user.save
@@ -34,7 +34,12 @@ class SettingsController < ApplicationController
end
if @user.avatar_new.present?
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
if store_user_avatar
UserManager::UpdateAvatar.call(user: @user)
else
@validation_errors = @user.errors
render :show, status: :unprocessable_entity and return
end
end
if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key])
@@ -162,7 +167,7 @@ class SettingsController < ApplicationController
def user_params
params.require(:user).permit(
:display_name, :avatar, :pgp_pubkey,
:display_name, :avatar_new, :pgp_pubkey,
preferences: UserPreferences.pref_keys
)
end
@@ -184,4 +189,30 @@ class SettingsController < ApplicationController
salt = BCrypt::Engine.generate_salt
BCrypt::Engine.hash_secret(password, salt)
end
def store_user_avatar
io = @user.avatar_new.tempfile
img_data = UserManager::ProcessAvatar.call(io: io)
if img_data.blank?
@user.errors.add(:avatar, "failed to process file")
false
end
tempfile = Tempfile.create
tempfile.binmode
tempfile.write(img_data)
tempfile.rewind
hash = Digest::SHA256.hexdigest(img_data)
ext = @user.avatar_new.content_type == "image/png" ? "png" : "jpg"
filename = "#{hash}.#{ext}"
if filename == @user.avatar.filename.to_s
@user.errors.add(:avatar, "must be a new file/picture")
false
else
key = "users/#{@user.cn}/avatars/#{filename}"
@user.avatar.attach io: tempfile, key: key, filename: filename
@user.save
end
end
end

View File

@@ -1,12 +1,20 @@
class WebKeyDirectoryController < WellKnownController
before_action :allow_cross_origin_requests
# /.well-known/openpgpkey/hu/:hashed_username(.txt)
# /.well-known/openpgpkey/hu/:hashed_username(.txt)?l=username
def show
if params[:l].blank?
# TODO store hashed username in db if existing implementations trigger
# this a lot
msg = "WKD request with \"l\" param omitted for hu: #{params[:hashed_username]}"
Sentry.capture_message(msg) if Setting.sentry_enabled?
http_status :bad_request and return
end
@user = User.find_by(cn: params[:l].downcase)
if @user.nil? ||
@user.pgp_pubkey.empty? ||
@user.pgp_pubkey.blank? ||
!@user.pgp_pubkey_contains_user_address?
http_status :not_found and return
end

View File

@@ -33,6 +33,10 @@ class WebfingerController < WellKnownController
links: []
}
if @user.avatar.attached?
jrd[:links] += avatar_link
end
if Setting.mastodon_enabled && @user.service_enabled?(:mastodon)
# https://docs.joinmastodon.org/spec/webfinger/
jrd[:aliases] += mastodon_aliases
@@ -47,6 +51,16 @@ class WebfingerController < WellKnownController
jrd
end
def avatar_link
[
{
rel: "http://webfinger.net/rel/avatar",
type: @user.avatar.content_type,
href: helpers.image_url_for(@user.avatar)
}
]
end
def mastodon_aliases
[
"#{Setting.mastodon_public_url}/@#{@user.cn}",
@@ -74,7 +88,7 @@ class WebfingerController < WellKnownController
end
def remotestorage_link
auth_url = new_rs_oauth_url(@username, host: Setting.accounts_domain)
auth_url = new_rs_oauth_url(@username, host: Setting.rs_accounts_domain)
storage_url = "#{Setting.rs_storage_url}/#{@username}"
{

View File

@@ -5,7 +5,7 @@ class WebhooksController < ApplicationController
before_action :process_payload
def lndhub
@user = User.find_by!(ln_account: @payload[:user_login])
@user = User.find_by!(lndhub_username: @payload[:user_login])
if @zap = @user.zaps.find_by(payment_request: @payload[:payment_request])
settled_at = Time.parse(@payload[:settled_at])

View File

@@ -14,4 +14,23 @@ module ApplicationHelper
def badge(text, color)
tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800"
end
def markdown_to_html(string)
raw Kramdown::Document.new(string, { input: "GFM" }).to_html
end
def image_url_for(attachment)
return s3_image_url(attachment) if Setting.s3_enabled?
if attachment.record.is_a?(User) && attachment.name == "avatar"
hash, format = attachment.blob.filename.to_s.split(".", 2)
user_avatar_url(
username: attachment.record.cn,
hash: hash,
format: format
)
else
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
end
end
end

View File

@@ -10,9 +10,7 @@ class BtcpayCheckDonationJob < ApplicationJob
case invoice["status"]
when "Settled"
donation.paid_at = DateTime.now
donation.payment_status = "settled"
donation.save!
donation.complete!
NotificationMailer.with(user: donation.user)
.bitcoin_donation_confirmed

View File

@@ -4,7 +4,7 @@ class CreateLdapUserJob < ApplicationJob
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"],
objectclass: ["top", "account", "person", "inetOrgPerson", "extensibleObject"],
cn: username,
sn: username,
uid: username,

View File

@@ -2,12 +2,12 @@ class CreateLndhubAccountJob < ApplicationJob
queue_as :default
def perform(user)
return if user.ln_account.present? && user.ln_password.present?
return if user.lndhub_username.present? && user.lndhub_password.present?
lndhub = LndhubV2.new
credentials = lndhub.create_account
user.update! ln_account: credentials["login"],
ln_password: credentials["password"]
user.update! lndhub_username: credentials["login"],
lndhub_password: credentials["password"]
end
end

View File

@@ -3,8 +3,6 @@ class RemoteStorageExpireAuthorizationJob < ApplicationJob
def perform(rs_auth_id)
rs_auth = RemoteStorageAuthorization.find rs_auth_id
return unless rs_auth.expire_at.nil? || rs_auth.expire_at <= DateTime.now
rs_auth.destroy!
end
end

View File

@@ -2,21 +2,6 @@ class XmppExchangeContactsJob < ApplicationJob
queue_as :default
def perform(inviter, invitee)
return unless inviter.service_enabled?(:ejabberd) &&
invitee.service_enabled?(:ejabberd) &&
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new
ejabberd.add_rosteritem({
"localuser": invitee.cn, "localhost": invitee.ou,
"user": inviter.cn, "host": inviter.ou,
"nick": inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
ejabberd.add_rosteritem({
"localuser": inviter.cn, "localhost": inviter.ou,
"user": invitee.cn, "host": invitee.ou,
"nick": invitee.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
EjabberdManager::ExchangeContacts.call(inviter:, invitee:)
end
end

View File

@@ -2,7 +2,6 @@ class XmppSendMessageJob < ApplicationJob
queue_as :default
def perform(payload)
ejabberd = EjabberdApiClient.new
ejabberd.send_message payload
EjabberdManager::SendMessage.call(payload:)
end
end

View File

@@ -0,0 +1,7 @@
class XmppSetAvatarJob < ApplicationJob
queue_as :default
def perform(user:, overwrite: false)
EjabberdManager::SetAvatar.call(user:, overwrite:)
end
end

View File

@@ -2,25 +2,6 @@ class XmppSetDefaultBookmarksJob < ApplicationJob
queue_as :default
def perform(user)
return unless Setting.xmpp_default_rooms.any?
@user = user
ejabberd = EjabberdApiClient.new
ejabberd.private_set user, storage_content
end
def storage_content
bookmarks = ""
Setting.xmpp_default_rooms.each do |r|
bookmarks << conference_element(
jid: r[/<(.+)>/, 1], name: r[/^(.+)\s/, 1], nick: @user.cn,
autojoin: Setting.xmpp_autojoin_default_rooms
)
end
"<storage xmlns='storage:bookmarks'>#{bookmarks}</storage>"
end
def conference_element(jid:, name:, autojoin: false, nick:)
"<conference jid='#{jid}' name='#{name}' autojoin='#{autojoin.to_s}'><nick>#{nick}</nick></conference>"
EjabberdManager::SetDefaultBookmarks.call(user:)
end
end

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,9 @@ module Settings
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
field :mastodon_auth_token, type: :string,
default: ENV["MASTODON_AUTH_TOKEN"].presence
end
end
end

View File

@@ -0,0 +1,18 @@
module Settings
module MembershipSettings
extend ActiveSupport::Concern
included do
field :member_status_contributor, type: :string,
default: "Contributor"
field :member_status_sustainer, type: :string,
default: "Sustainer"
# Admin panel
field :user_index_show_contributors, type: :boolean,
default: false
field :user_index_show_sustainers, type: :boolean,
default: false
end
end
end

View File

@@ -6,6 +6,9 @@ module Settings
field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present?
field :rs_accounts_domain, type: :string,
default: ENV["RS_AKKOUNTS_DOMAIN"] || ENV["AKKOUNTS_DOMAIN"]
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence

View File

@@ -1,22 +1,42 @@
class Donation < ApplicationRecord
# Relations
include AASM
belongs_to :user
# Validations
validates_presence_of :user
validates_presence_of :donation_method,
inclusion: { in: %w[ custom btcpay lndhub ] }
validates_presence_of :payment_status, allow_nil: true,
inclusion: { in: %w[ processing settled ] }
inclusion: { in: %w[ pending 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 :pending, -> { where(payment_status: "pending") }
scope :processing, -> { where(payment_status: "processing") }
scope :completed, -> { where(payment_status: "settled") }
scope :completed, -> { where(payment_status: "settled") }
scope :incomplete, -> { where.not(payment_status: "settled") }
aasm column: :payment_status do
state :pending, initial: true
state :processing
state :settled
event :start_processing do
transitions from: :pending, to: :processing
end
event :complete do
transitions from: :processing, to: :settled, after: [:set_paid_at, :set_sustainer_status]
transitions from: :pending, to: :settled, after: [:set_paid_at, :set_sustainer_status]
end
end
def pending?
payment_status == "pending"
end
def processing?
payment_status == "processing"
@@ -25,4 +45,17 @@ class Donation < ApplicationRecord
def completed?
payment_status == "settled"
end
private
def set_paid_at
update paid_at: DateTime.now if paid_at.nil?
end
def set_sustainer_status
user.add_member_status :sustainer
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error("Failed to set memberStatus: #{e.message}")
end
end

View File

@@ -0,0 +1,12 @@
class EditableContent < ApplicationRecord
validates :key, presence: true,
uniqueness: { scope: :context }
def has_content?
content.present?
end
def is_empty?
content.blank?
end
end

View File

@@ -6,7 +6,7 @@ class LndhubUser < LndhubBase
foreign_key: "user_id"
belongs_to :user, class_name: "User",
primary_key: "ln_account",
primary_key: "lndhub_username",
foreign_key: "login"
def balance

View File

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

View File

@@ -16,6 +16,7 @@ class Setting < RailsSettings::Base
include Settings::LightningNetworkSettings
include Settings::MastodonSettings
include Settings::MediaWikiSettings
include Settings::MembershipSettings
include Settings::NostrSettings
include Settings::OpenCollectiveSettings
include Settings::RemoteStorageSettings

View File

@@ -4,8 +4,8 @@ class User < ApplicationRecord
include EmailValidatable
attr_accessor :current_password
attr_accessor :avatar_new
attr_accessor :display_name
attr_accessor :avatar_new
attr_accessor :pgp_pubkey
serialize :preferences, coder: UserPreferences
@@ -23,10 +23,16 @@ class User < ApplicationRecord
has_many :zaps
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
primary_key: "ln_account", foreign_key: "login"
primary_key: "lndhub_username", foreign_key: "login"
has_many :accounts, through: :lndhub_user
#
# Attachments
#
has_one_attached :avatar
#
# Validations
#
@@ -50,6 +56,7 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) }
validate :acceptable_avatar
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
@@ -66,7 +73,7 @@ class User < ApplicationRecord
# Encrypted database columns
#
has_encrypted :ln_login, :ln_password
encrypts :lndhub_password
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
@@ -77,6 +84,10 @@ class User < ApplicationRecord
:timeoutable,
:rememberable
#
# Methods
#
def ldap_before_save
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
self.ou = dn.split(',')
@@ -120,11 +131,11 @@ class User < ApplicationRecord
end
def is_admin?
admin ||= if admin = Devise::LDAP::Adapter.get_ldap_param(self.cn, :admin)
!!admin.first
else
false
end
@admin ||= if admin = Devise::LDAP::Adapter.get_ldap_param(self.cn, :admin)
!!admin.first
else
false
end
end
def address
@@ -152,13 +163,41 @@ class User < ApplicationRecord
def ldap_entry(reload: false)
return @ldap_entry if defined?(@ldap_entry) && !reload
@ldap_entry = ldap.fetch_users(uid: self.cn, ou: self.ou).first
@ldap_entry = ldap.fetch_users(cn: self.cn).first
end
def add_to_ldap_array(attr_key, ldap_attr, value)
current_entries = ldap_entry[attr_key.to_sym] || []
new_entries = Array(value).map(&:to_s)
entries = (current_entries + new_entries).uniq.sort
ldap.replace_attribute(dn, ldap_attr.to_sym, entries)
end
def remove_from_ldap_array(attr_key, ldap_attr, value)
current_entries = ldap_entry[attr_key.to_sym] || []
entries_to_remove = Array(value).map(&:to_s)
entries = (current_entries - entries_to_remove).uniq.sort
ldap.replace_attribute(dn, ldap_attr.to_sym, entries)
end
def display_name
@display_name ||= ldap_entry[:display_name]
end
# TODO Variant keys are currently broken for some reason
# (They use the same key as the main blob, when it should be
# "/variants/#{key)"
# def avatar_variant(size: :medium)
# dimensions = case size
# when :large then [400, 400]
# when :medium then [256, 256]
# when :small then [64, 64]
# else [256, 256]
# end
# format = avatar.content_type == "image/png" ? :png : :jpeg
# avatar.variant(resize_to_fill: dimensions, format: format)
# end
def nostr_pubkey
@nostr_pubkey ||= ldap_entry[:nostr_key]
end
@@ -186,10 +225,6 @@ class User < ApplicationRecord
ZBase32.encode(Digest::SHA1.digest(cn))
end
def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end
def services_enabled
ldap_entry[:services_enabled] || []
end
@@ -199,21 +234,39 @@ class User < ApplicationRecord
end
def enable_service(service)
current_services = services_enabled
new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq.sort
ldap.replace_attribute(dn, :serviceEnabled, services)
add_to_ldap_array :services_enabled, :serviceEnabled, service
ldap_entry(reload: true)[:services_enabled]
end
def disable_service(service)
current_services = services_enabled
disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq.sort
ldap.replace_attribute(dn, :serviceEnabled, services)
remove_from_ldap_array :services_enabled, :serviceEnabled, service
ldap_entry(reload: true)[:services_enabled]
end
def disable_all_services
ldap.delete_attribute(dn,:service)
ldap.delete_attribute(dn, :serviceEnabled)
end
def member_status
ldap_entry[:member_status] || []
end
def add_member_status(status)
add_to_ldap_array :member_status, :memberStatus, status
ldap_entry(reload: true)[:member_status]
end
def remove_member_status(status)
remove_from_ldap_array :member_status, :memberStatus, status
ldap_entry(reload: true)[:member_status]
end
def is_contributing_member?
member_status.map(&:to_sym).include?(:contributor)
end
def is_paying_member?
member_status.map(&:to_sym).include?(:sustainer)
end
private
@@ -227,7 +280,7 @@ class User < ApplicationRecord
return unless avatar_new.present?
if avatar_new.size > 1.megabyte
errors.add(:avatar, "file size is too large")
errors.add(:avatar, "must be less than 1MB file size")
end
acceptable_types = ["image/jpeg", "image/png"]

View File

@@ -1,36 +1,22 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcpayManagerService < ApplicationService
class BtcpayManagerService < RestApiService
private
def base_url
@base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
end
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 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
def headers
{
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{auth_token}"
}
end
end

View File

@@ -1,29 +0,0 @@
class EjabberdApiClient
def initialize
@base_url = Setting.ejabberd_api_url
end
def post(endpoint, payload)
res = Faraday.post("#{@base_url}/#{endpoint}", payload.to_json,
"Content-Type" => "application/json")
if res.status != 200
Rails.logger.error "[ejabberd] API request failed:"
Rails.logger.error res.body
#TODO Send custom event to Sentry
end
end
def add_rosteritem(payload)
post "add_rosteritem", payload
end
def send_message(payload)
post "send_message", payload
end
def private_set(user, content)
payload = { user: user.cn, host: user.ou, element: content }
post "private_set", payload
end
end

View File

@@ -0,0 +1,25 @@
module EjabberdManager
class ExchangeContacts < EjabberdManagerService
def initialize(inviter:, invitee:)
@inviter = inviter
@invitee = invitee
end
def call
return unless @inviter.service_enabled?(:ejabberd) &&
@invitee.service_enabled?(:ejabberd) &&
@inviter.preferences[:xmpp_exchange_contacts_with_invitees]
add_rosteritem({
"localuser": @invitee.cn, "localhost": @invitee.ou,
"user": @inviter.cn, "host": @inviter.ou,
"nick": @inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
add_rosteritem({
"localuser": @inviter.cn, "localhost": @inviter.ou,
"user": @invitee.cn, "host": @invitee.ou,
"nick": @invitee.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
})
end
end
end

View File

@@ -0,0 +1,25 @@
module EjabberdManager
class GetAvatar < EjabberdManagerService
def initialize(user:)
@user = user
end
def call
res = get_vcard2 @user, "PHOTO", "BINVAL"
if res.status == 200
# VCARD PHOTO/BINVAL prop exists
img_base64 = JSON.parse(res.body)["content"]
ct_res = get_vcard2 @user, "PHOTO", "TYPE"
content_type = JSON.parse(ct_res.body)["content"]
{ content_type:, img_base64: }
elsif res.status == 400
# VCARD or PHOTO/BINVAL prop does not exist
nil
else
# Unexpected error, let job fail
raise res.inspect
end
end
end
end

View File

@@ -0,0 +1,11 @@
module EjabberdManager
class SendMessage < EjabberdManagerService
def initialize(payload:)
@payload = payload
end
def call
send_message @payload
end
end
end

View File

@@ -0,0 +1,80 @@
require 'digest'
require "image_processing/vips"
module EjabberdManager
class SetAvatar < EjabberdManagerService
def initialize(user:, overwrite: false)
@user = user
@overwrite = overwrite
end
def call
unless @overwrite
current_avatar = EjabberdManager::GetAvatar.call(user: @user)
Rails.logger.info { "User #{@user.cn} already has an avatar set" }
return if current_avatar.present?
end
Rails.logger.debug { "Setting XMPP avatar for user #{@user.cn}" }
stanzas = build_xep0084_stanzas
stanzas.each do |stanza|
payload = { from: @user.address, to: @user.address, stanza: stanza }
res = send_stanza payload
raise res.inspect if res.status != 200
end
end
end
private
def process_avatar
@user.avatar.blob.open do |file|
processed = ImageProcessing::Vips
.source(file)
.resize_to_fill(256, 256)
.convert("png")
.call
processed.read
end
end
# See https://xmpp.org/extensions/xep-0084.html
def build_xep0084_stanzas
img_data = process_avatar
sha1_hash = Digest::SHA1.hexdigest(img_data)
base64_data = Base64.strict_encode64(img_data)
[
"""
<iq type='set' from='#{@user.address}' id='avatar-data-#{rand(101)}'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:avatar:data'>
<item id='#{sha1_hash}'>
<data xmlns='urn:xmpp:avatar:data'>#{base64_data}</data>
</item>
</publish>
</pubsub>
</iq>
""".strip,
"""
<iq type='set' from='#{@user.address}' id='avatar-metadata-#{rand(101)}'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:avatar:metadata'>
<item id='#{sha1_hash}'>
<metadata xmlns='urn:xmpp:avatar:metadata'>
<info bytes='#{img_data.size}'
id='#{sha1_hash}'
height='256'
type='image/png'
width='256'/>
</metadata>
</item>
</publish>
</pubsub>
</iq>
""".strip,
]
end
end

View File

@@ -0,0 +1,31 @@
module EjabberdManager
class SetDefaultBookmarks < EjabberdManagerService
def initialize(user:)
@user = user
end
def call
return unless Setting.xmpp_default_rooms.any?
private_set @user, storage_content
end
private
def storage_content
bookmarks = ""
Setting.xmpp_default_rooms.each do |r|
bookmarks << conference_element(
jid: r[/<(.+)>/, 1], name: r[/^(.+)\s/, 1], nick: @user.cn,
autojoin: Setting.xmpp_autojoin_default_rooms
)
end
"<storage xmlns='storage:bookmarks'>#{bookmarks}</storage>"
end
def conference_element(jid:, name:, autojoin: false, nick:)
"<conference jid='#{jid}' name='#{name}' autojoin='#{autojoin.to_s}'><nick>#{nick}</nick></conference>"
end
end
end

View File

@@ -0,0 +1,55 @@
class EjabberdManagerService < RestApiService
private
def base_url
@base_url ||= Setting.ejabberd_api_url
end
def headers
{ "Content-Type" => "application/json" }
end
def parse_responses?
false
end
#
# API endpoints
#
def add_rosteritem(payload)
post "add_rosteritem", payload
end
def send_message(payload)
post "send_message", payload
end
def send_stanza(payload)
post "send_stanza", payload
end
def get_vcard2(user, name, subname)
payload = {
user: user.cn, host: user.ou,
name: name, subname: subname
}
post "get_vcard2", payload
end
def private_get(user, element_name, namespace)
payload = {
user: user.cn, host: user.ou,
element: element_name, ns: namespace
}
post "private_get", payload
end
def private_set(user, content)
payload = {
user: user.cn, host: user.ou,
element: content
}
post "private_set", payload
end
end

View File

@@ -5,12 +5,12 @@ module LdapManager
end
def call
treebase = ldap_config["base"]
treebase = ldap_config["base"]
attributes = %w{ jpegPhoto }
filter = Net::LDAP::Filter.eq("cn", @cn)
filter = Net::LDAP::Filter.eq("cn", @cn)
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
entry[:jpegPhoto].present? ? entry.jpegPhoto.first : nil
end
end
end

View File

@@ -2,26 +2,41 @@ require "image_processing/vips"
module LdapManager
class UpdateAvatar < LdapManagerService
def initialize(dn:, file:)
@dn = dn
@img_data = process(file)
def initialize(user:)
@user = user
@dn = user.dn
end
def call
replace_attribute @dn, :jpegPhoto, @img_data
unless @user.avatar.attached?
Rails.logger.error { "Cannot store empty jpegPhoto for user #{@user.cn}" }
return false
end
img_data = @user.avatar.blob.download
jpg_data = process_avatar
Rails.logger.debug { "Storing new jpegPhoto for user #{@user.cn} in LDAP" }
result = replace_attribute(@dn, :jpegPhoto, jpg_data)
result == 0
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
def process_avatar
@user.avatar.blob.open do |file|
processed = ImageProcessing::Vips
.source(file)
.resize_to_fill(256, 256)
.convert("jpeg")
.saver(strip: true)
.call
processed.read
end
rescue Vips::Error => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error { "Image processing failed for LDAP avatar: #{e.message}" }
nil
end
end
end

View File

@@ -6,7 +6,11 @@ module LdapManager
end
def call
replace_attribute @dn, :displayName, @display_name
if @display_name.present?
replace_attribute @dn, :displayName, @display_name
else
delete_attribute @dn, :displayName
end
end
end
end

View File

@@ -50,19 +50,17 @@ class LdapService < ApplicationService
end
def fetch_users(args={})
if args[:ou]
treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}"
else
treebase = ldap_config["base"]
end
attributes = %w[
dn cn uid mail displayName admin serviceEnabled
dn cn uid mail displayName admin serviceEnabled memberStatus
mailRoutingAddress mailpassword nostrKey pgpKey
]
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
filter = Net::LDAP::Filter.eq('objectClass', 'person') &
Net::LDAP::Filter.eq("cn", args[:cn] || "*")
entries = client.search(base: treebase, filter: filter, attributes: attributes)
entries = client.search(
base: ldap_config["base"], filter: filter,
attributes: attributes
)
entries.sort_by! { |e| e.cn[0] }
entries = entries.collect do |e|
{
@@ -71,6 +69,7 @@ class LdapService < ApplicationService
display_name: e.try(:displayName) ? e.displayName.first : nil,
admin: e.try(:admin) ? 'admin' : nil,
services_enabled: e.try(:serviceEnabled),
member_status: e.try(:memberStatus),
email_maildrop: e.try(:mailRoutingAddress),
email_password: e.try(:mailpassword),
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil,
@@ -79,10 +78,20 @@ class LdapService < ApplicationService
end
end
def search_users(search_attr, value, return_attr)
filter = Net::LDAP::Filter.eq('objectClass', 'person') &
Net::LDAP::Filter.eq(search_attr.to_s, value.to_s) &
Net::LDAP::Filter.present('cn')
entries = client.search(
base: ldap_config["base"], filter: filter,
attributes: [return_attr]
)
entries.map { |entry| entry[return_attr].first }.compact
end
def fetch_organizations
attributes = %w{dn ou description}
filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit")
# filter = Net::LDAP::Filter.eq("objectClass", "*")
treebase = "cn=users,#{ldap_suffix}"
entries = client.search(base: treebase, filter: filter, attributes: attributes)

View File

@@ -33,7 +33,10 @@ class Lndhub < ApplicationService
end
def authenticate(user)
credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password }
credentials = post "auth?type=auth", {
login: user.lndhub_username,
password: user.lndhub_password
}
self.auth_token = credentials["access_token"]
self.auth_token
end

View File

@@ -0,0 +1,12 @@
module MastodonManager
class FetchUser < MastodonManagerService
def initialize(mastodon_id:)
@mastodon_id = mastodon_id
end
def call
user = get "v1/admin/accounts/#{@mastodon_id}"
user.with_indifferent_access
end
end
end

View File

@@ -0,0 +1,14 @@
module MastodonManager
class FindUser < MastodonManagerService
def initialize(username:)
@username = username
end
def call
users = get "v2/admin/accounts?username=#{@username}&origin=local"
users = users.map { |u| u.with_indifferent_access }
# Results may contain partial matches
users.find { |u| u.dig(:username).downcase == @username.downcase }
end
end
end

View File

@@ -0,0 +1,64 @@
module MastodonManager
class SyncAccountProfiles < MastodonManagerService
def initialize(direction: "down", overwrite: false, user: nil)
@direction = direction
@overwrite = overwrite
@user = user
if @direction != "down"
raise NotImplementedError
end
end
def call
if @user
Rails.logger.debug { "Syncing account profile for user #{@user.cn} (direction: #{@direction}, overwrite: #{@overwrite})"}
users = User.where(cn: @user.cn)
else
Rails.logger.debug { "Syncing account profiles (direction: #{@direction}, overwrite: #{@overwrite})"}
users = User
end
users.find_each do |user|
if user.mastodon_id.blank?
mastodon_user = MastodonManager::FindUser.call username: user.cn
if mastodon_user
Rails.logger.debug { "Setting mastodon_id for user #{user.cn}" }
user.update! mastodon_id: mastodon_user.dig(:account, :id).to_i
else
Rails.logger.debug { "No Mastodon user found for username #{user.cn}" }
next
end
end
next if user.avatar.attached? && user.display_name.present?
unless mastodon_user
Rails.logger.debug { "Fetching Mastodon account with ID #{user.mastodon_id} for #{user.cn}" }
mastodon_user = MastodonManager::FetchUser.call mastodon_id: user.mastodon_id
end
if user.display_name.blank?
if mastodon_display_name = mastodon_user.dig(:account, :display_name)
Rails.logger.debug { "Setting display name for user #{user.cn} from Mastodon" }
LdapManager::UpdateDisplayName.call(
dn: user.dn, display_name: mastodon_display_name
)
end
end
if !user.avatar.attached?
if avatar_url = mastodon_user.dig(:account, :avatar_static)
Rails.logger.debug { "Importing Mastodon avatar for user #{user.cn}" }
UserManager::ImportRemoteAvatar.call(
user: user, avatar_url: avatar_url
)
end
end
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error e
end
end
end
end

View File

@@ -0,0 +1,22 @@
#
# API Docs: https://docs.joinmastodon.org/methods/
#
class MastodonManagerService < RestApiService
private
def base_url
@base_url ||= "#{Setting.mastodon_public_url}/api"
end
def auth_token
@auth_token ||= Setting.mastodon_auth_token
end
def headers
{
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "Bearer #{auth_token}"
}
end
end

View File

@@ -0,0 +1,29 @@
class RestApiService < ApplicationService
private
def base_url
raise NotImplementedError
end
def headers
raise NotImplementedError
end
def endpoint_url(path)
"#{base_url}/#{path.gsub(/^\//, '')}"
end
def parse_responses?
true
end
def get(path, params = {})
res = Faraday.get endpoint_url(path), params, headers
parse_responses? ? JSON.parse(res.body) : res
end
def post(path, payload)
res = Faraday.post endpoint_url(path), payload.to_json, headers
parse_responses? ? JSON.parse(res.body) : res
end
end

View File

@@ -0,0 +1,42 @@
module UserManager
class ImportRemoteAvatar < UserManagerService
def initialize(user:, avatar_url:)
@user = user
@avatar_url = avatar_url
end
def call
if import_remote_avatar
UserManager::UpdateAvatar.call(user: @user)
end
end
private
def import_remote_avatar
tempfile = Down.download(@avatar_url)
content_type = tempfile.content_type
unless %w[image/jpeg image/png].include?(content_type)
Rails.logger.warn { "Wrong content type of remote avatar for user #{user.cn}: '#{content_type}'" }
return false
end
img_data = UserManager::ProcessAvatar.call(io: tempfile)
tempfile = Tempfile.create
tempfile.binmode
tempfile.write(img_data)
tempfile.rewind
hash = Digest::SHA256.hexdigest(img_data)
ext = content_type == "image/png" ? "png" : "jpg"
filename = "#{hash}.#{ext}"
key = "users/#{@user.cn}/avatars/#{filename}"
@user.avatar.attach io: tempfile, key: key, filename: filename
rescue => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.warn "Importing remote avatar failed: \"#{e.message}\""
false
end
end
end

View File

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

View File

@@ -0,0 +1,21 @@
module UserManager
class ProcessAvatar < UserManagerService
def initialize(io:)
@io = io
end
def call
processed = ImageProcessing::Vips
.source(@io)
.resize_to_fill(400, 400)
.saver(strip: true)
.call
@io.rewind
processed.read
rescue Vips::Error => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.warn { "Image processing failed for avatar: #{e.message}" }
nil
end
end
end

View File

@@ -0,0 +1,16 @@
module UserManager
class UpdateAvatar < UserManagerService
def initialize(user:)
@user = user
end
def call
LdapManager::UpdateAvatar.call(user: @user)
if Setting.ejabberd_enabled?
return if Rails.env.development?
XmppSetAvatarJob.perform_later(user: @user)
end
end
end
end

View File

@@ -0,0 +1,11 @@
<%= form_with url: path, method: :get, local: true, class: "flex gap-1" do %>
<%= text_field_tag :username, @username, placeholder: 'Filter by username' %>
<%= button_tag type: 'submit', name: nil, title: "Filter", class: 'btn-md btn-icon btn-outline' do %>
<%= render partial: "icons/filter", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
<% end %>
<% if @username %>
<%= link_to path, title: "Remove filter", class: 'btn-md btn-icon btn-outline' do %>
<%= render partial: "icons/x", locals: { custom_class: "text-red-600 h-4 w-4 inline" } %>
<% end %>
<% end %>
<% end %>

View File

@@ -38,8 +38,7 @@
<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>
target: "_blank", rel: "nofollow noopener" %></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">

View File

@@ -0,0 +1,34 @@
<table class="divided">
<thead>
<tr>
<th>User</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>
</tr>
</thead>
<tbody>
<% donations.each do |donation| %>
<tr>
<td><%= link_to donation.user.cn, admin_user_path(donation.user.cn) %></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)") : donation.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
<td class="text-right">
<%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% if defined?(pagy) %>
<div class="mt-8">
<%== pagy_nav pagy %>
</div>
<% end %>

View File

@@ -5,7 +5,7 @@
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Overall',
title: 'Received',
value: @stats[:overall_sats],
unit: 'sats'
) %>
@@ -19,41 +19,28 @@
</section>
<section>
<% if @donations.any? %>
<h3>Recent Donations</h3>
<table class="divided mb-8">
<thead>
<tr>
<th>User</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>
</tr>
</thead>
<tbody>
<% @donations.each do |donation| %>
<tr>
<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">
<%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%== pagy_nav @pagy %>
<%= render partial: "admin/username_search_form",
locals: { path: admin_donations_path } %>
</section>
<% if @pending_donations.present? %>
<section>
<h3>Pending</h3>
<%= render partial: "admin/donations/list", locals: {
donations: @pending_donations
} %>
</section>
<% end %>
<section>
<% if @donations.present? %>
<h3>Received</h3>
<%= render partial: "admin/donations/list", locals: {
donations: @donations, pagy: @pagy
} %>
<% else %>
<p>
No donations yet.
No donations received yet.
</p>
<% end %>
</section>

View File

@@ -6,7 +6,7 @@
<tbody>
<tr>
<th>User</th>
<td><%= link_to @donation.user.cn, admin_user_path(@donation.user.cn), class: 'ks-text-link' %></td>
<td><%= link_to @donation.user.cn, admin_user_path(@donation.user.cn) %></td>
</tr>
<tr>
<th>Donation Method</th>
@@ -25,7 +25,15 @@
<td><%= @donation.public_name %></td>
</tr>
<tr>
<th>Date</th>
<th>Payment status</th>
<td><%= @donation.payment_status %></td>
</tr>
<tr>
<th>Created at</th>
<td><%= @donation.created_at&.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
<tr>
<th>Paid at</th>
<td><%= @donation.paid_at&.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
</tbody>

View File

@@ -21,9 +21,15 @@
) %>
<% end %>
</section>
<section>
<%= render partial: "admin/username_search_form",
locals: { path: admin_invitations_path } %>
</section>
<% if @invitations_used.any? %>
<section>
<h3>Recently Accepted</h3>
<h3>Accepted</h3>
<table class="divided mb-8">
<thead>
<tr>
@@ -38,8 +44,8 @@
<tr>
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
<td><%= link_to invitation.user.cn, admin_user_path(invitation.user.cn), class: "ks-text-link" %></td>
<td><%= link_to invitation.invitee.cn, admin_user_path(invitation.invitee.cn), class: "ks-text-link" %></td>
<td><%= link_to invitation.user.cn, admin_user_path(invitation.user.cn) %></td>
<td><%= link_to invitation.invitee.cn, admin_user_path(invitation.invitee.cn) %></td>
</tr>
<% end %>
</tbody>

View File

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

View File

@@ -0,0 +1,53 @@
<%= render HeaderComponent.new(title: "Settings") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
<%= form_for(Setting.new, url: admin_settings_membership_path, method: :put) do |f| %>
<section>
<h3>Membership</h3>
<% if @errors && @errors.any? %>
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
<% end %>
<ul role="list">
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :member_status_contributor,
title: "Status name for contributing users",
description: "A contributing member of your organization/group"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :member_status_sustainer,
title: "Status name for paying users",
description: "A paying/donating member or customer"
) %>
</ul>
</section>
<section>
<h3>Admin panel</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :user_index_show_contributors,
enabled: Setting.user_index_show_contributors?,
title: "Show #{Setting.member_status_contributor.downcase} status in user list",
description: "Can slow down page rendering with large user base"
) %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :user_index_show_sustainers,
enabled: Setting.user_index_show_sustainers?,
title: "Show #{Setting.member_status_sustainer.downcase} status in user list",
description: "Can slow down page rendering with large user base"
) %>
</ul>
</section>
<section>
<p class="pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p>
</section>
<% end %>
<% end %>

View File

@@ -16,5 +16,10 @@
key: :mastodon_address_domain,
title: "User address domain"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :mastodon_auth_token,
type: :password,
title: "API auth token"
) %>
<% end %>
</ul>

View File

@@ -13,6 +13,20 @@
title: 'Pending',
value: @stats[:users_pending],
) %>
<% if @show_contributors %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: Setting.member_status_contributor.pluralize,
value: @stats[:users_contributing],
) %>
<% end %>
<% if @show_sustainers %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: Setting.member_status_sustainer.pluralize,
value: @stats[:users_paying],
) %>
<% end %>
<% end %>
</section>
@@ -28,9 +42,13 @@
<tbody>
<% @users.each do |user| %>
<tr>
<td><%= link_to(user.cn, admin_user_path(user.cn), class: 'ks-text-link') %></td>
<td><%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %></td>
<td><%= user.is_admin? ? badge("admin", :red) : "" %></td>
<td><%= link_to(user.cn, admin_user_path(user.cn)) %></td>
<td>
<%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %>
<% if @show_contributors %><%= @contributors.include?(user.cn) ? badge("contributor", :green) : "" %><% end %>
<% if @show_sustainers %><%= @sustainers.include?(user.cn) ? badge("sustainer", :green) : "" %><% end %>
</td>
<td><%= @admins.include?(user.cn) ? badge("admin", :red) : "" %></td>
</tr>
<% end %>
</tbody>

View File

@@ -32,11 +32,35 @@
<th>Roles</th>
<td><%= @user.is_admin? ? badge("admin", :red) : "—" %></td>
</tr>
<tr>
<th>Status</th>
<td>
<% if @user.is_contributing_member? || @user.is_paying_member? %>
<%= @user.is_contributing_member? ? badge("contributor", :green) : "" %>
<%= @user.is_paying_member? ? badge("sustainer", :green) : "" %>
<% else %>
<% end %>
</td>
</tr>
<tr>
<th>Donations</th>
<td>
<% if @user.donations.any? %>
<%= link_to admin_donations_path(username: @user.cn) do %>
<%= @user.donations.completed.count %> for
<%= number_with_delimiter @user.donations.completed.sum("amount_sats") %> sats
<% end %>
<% else %>
<% end %>
</td>
</tr>
<tr>
<th>Invited by</th>
<td>
<% if @user.inviter %>
<%= link_to @user.inviter.cn, admin_user_path(@user.inviter.cn), class: 'ks-text-link' %>
<%= link_to @user.inviter.cn, admin_user_path(@user.inviter.cn) %>
<% else %>&mdash;<% end %>
</td>
</tr>
@@ -45,7 +69,7 @@
<td data-controller="modal" data-action="keydown.esc->modal#close">
<div class="flex justify-between">
<span>
<%= @user.invitations.count %>
<%= @user.invitations.unused.count %>
</span>
<span>
<button id="add-invitations" data-action="click->modal#open">
@@ -75,10 +99,13 @@
<tr>
<th class="align-top">Invited users</th>
<td class="align-top">
<% if @user.invitees.length > 0 %>
<% if @invitees.any? %>
<ul class="mb-0">
<% @user.invitees.order(cn: :asc).each do |invitee| %>
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.cn, admin_user_path(invitee.cn), class: 'ks-text-link' %></li>
<% @recent_invitees.each do |invitee| %>
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.cn, admin_user_path(invitee.cn) %></li>
<% end %>
<% if @more_invitees > 0 %>
<li>and <%= link_to "#{@more_invitees} more", admin_invitations_path(username: @user.cn) %></li>
<% end %>
</ul>
<% else %>&mdash;<% end %>
@@ -89,14 +116,42 @@
</section>
<section class="sm:flex-1 sm:pt-0">
<h3>LDAP</h3>
<h3>Avatar</h3>
<% if @user.avatar.attached? %>
<table class="divided">
<tbody>
<tr>
<th class="align-top">Image</th>
<td class="align-top">
<%= image_tag image_url_for(@user.avatar), class: "h-20 w-20 rounded-lg" %>
</td>
</tr>
<tr>
<th>Content type</th>
<td>
<%= @user.avatar.content_type %>
</td>
</tr>
<tr>
<th>Size</th>
<td>
<%= number_to_human_size(@user.avatar.blob.byte_size) %>
</td>
</tr>
</tbody>
</table>
<% else %>
<p class="text-gray-500">No avatar uploaded</p>
<% end %>
<h3 class="mt-12">LDAP</h3>
<table class="divided">
<tbody>
<tr>
<th>Avatar</th>
<td>
<% if @avatar.present? %>
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
<% if @ldap_avatar.present? %>
JPEG size: <%= number_to_human_size(@ldap_avatar.size) %>
<% else %>
&mdash;
<% end %>
@@ -113,7 +168,7 @@
<span class="font-mono" title="<%= @user.pgp_fpr %>">
<% if @user.pgp_pubkey_contains_user_address? %>
<%= link_to wkd_key_url(hashed_username: @user.wkd_hash, l: @user.cn, format: :txt),
class: "ks-text-link", target: "_blank" do %>
target: "_blank" do %>
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
<% end %>
<% else %>
@@ -239,7 +294,9 @@
) %>
</td>
<td class="text-right">
<% if @user.nostr_pubkey.present? %>
<%= link_to "Open profile", "https://njump.me/#{@user.nostr_pubkey_bech32}", class: "btn-sm btn-gray" %>
<% end %>
</td>
</tr>
<% end %>
@@ -276,7 +333,7 @@
</thead>
<tbody>
<tr>
<td><%= @user.ln_account %></td>
<td><%= @user.lndhub_username %></td>
<td><%= number_with_delimiter @lndhub_user.balance %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_incoming %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_outgoing %> sats</td>
@@ -285,7 +342,7 @@
</tbody>
</table>
<% else %>
<p>No LndHub user found for account <strong class="font-mono"><%= @user.ln_account %></strong>.
<p>No LndHub user found for account <strong class="font-mono"><%= @user.lndhub_username %></strong>.
<% end %>
</section>
<% end %>

View File

@@ -3,7 +3,7 @@
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
<section>
<p class="mb-12">
Your financial contributions to the development and upkeep of Kosmos
Your financial contributions to the development and upkeep of our
software and services.
</p>
</section>
@@ -22,17 +22,17 @@
</div>
</section>
<% if @donations_pending.any? %>
<% if @donations_processing.any? %>
<section class="donation-list">
<h2>Pending</h2>
<%= render partial: "contributions/donations/list",
locals: { donations: @donations_pending } %>
locals: { donations: @donations_processing } %>
</section>
<% end %>
<% if @donations_completed.any? %>
<section class="donation-list">
<h2>Past contributions</h2>
<h2>Contributions</h2>
<%= render partial: "contributions/donations/list",
locals: { donations: @donations_completed } %>
</section>

View File

@@ -0,0 +1,20 @@
<%= render HeaderComponent.new(title: "Contributions") %>
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
<section>
<%= render EditableContentComponent.new(
context: "contributions/other", key: "body", rich_text: true,
default: "No content yet") %>
<% if current_user.is_admin? %>
<div class="mt-8 pt-6 border-t border-gray-200 text-right">
<%= render EditContentButtonComponent.new(
context: "contributions/other", key: "title",
redirect_to: request.path) do %>Edit title<% end %>
<%= render EditContentButtonComponent.new(
context: "contributions/other", key: "body", rich_text: true,
redirect_to: request.path) do %>Edit content<% end %>
</div>
<% end %>
</section>
<% end %>

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