82 Commits

Author SHA1 Message Date
Râu Cao
f08bb56a7a 0.5.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-01 11:44:25 +02:00
c1f275463e Merge pull request 'Add Redis, Sidekiq to Docker Compose setup' (#110) from feature/docker-compose_sidekiq into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #110
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-03-31 09:09:46 +00:00
324809f77e Merge pull request 'Expire inactive sessions, optionally allow to stay signed in' (#82) from feature/8-session_timeouts into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #82
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-03-31 07:58:24 +00:00
Râu Cao
f9b07bcb01 Use development branch of release drafter action
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-28 17:27:31 +02:00
Râu Cao
986eb5387c Use release drafter fork with PR ID fix
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-28 17:13:39 +02:00
f76e2c2f14 Merge pull request 'Add Gitea Release Drafter as Gitea Action' (#111) from feature/release_drafter into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #111
2023-03-28 14:21:44 +00:00
Râu Cao
22a7bbe6eb Add Gitea Release Drafter as Gitea Action
All checks were successful
Update release notes draft
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-28 16:17:19 +02:00
18f4deb30f Merge pull request 'Add (optional) Sentry integration' (#108) from feature/sentry_integration into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #108
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-03-28 12:53:00 +00:00
Râu Cao
9f9bf6fd80 Add Redis and Sidekiq to Docker Compose setup
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Update release notes draft
2023-03-28 12:24:58 +02:00
Râu Cao
d2987da70a Send Devise emails via Sidekiq 2023-03-28 12:22:17 +02:00
Râu Cao
6b7a80e23a Make Redis URL configurable 2023-03-28 12:21:54 +02:00
Râu Cao
42b9b27561 Allow external network access
All checks were successful
continuous-integration/drone/push Build is passing
Useful for connecting to services on private networks for example.
2023-03-28 11:38:56 +02:00
Râu Cao
c17c980b69 Prepare for multiple akkounts containers
All checks were successful
continuous-integration/drone/push Build is passing
Initially "web" and "sidekiq"
2023-03-28 11:25:10 +02:00
Râu Cao
f199d5d12a Add (optional) Sentry integration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
A Sentry DSN can be set via `SENTRY_DSN` and authenticated users will be
tagged with ID and username (cn) in events.
2023-03-27 12:47:28 +02:00
Râu Cao
4b17afa93d Fix typo
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-27 11:55:02 +02:00
Râu Cao
6d52af53ae Add basic storage config
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-27 11:46:39 +02:00
Râu Cao
4c5ad67652 Require action_mailbox
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-27 11:40:59 +02:00
Râu Cao
3437a756eb Only create LNDHub accounts when feature is enabled
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-24 16:01:53 +07:00
0d9fc4aa74 Merge pull request 'Make email settings configurable, add custom mailer for one-off emails' (#107) from feature/custom_mailer into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #107
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-03-23 15:52:43 +00:00
82475161a9 Merge branch 'master' into feature/custom_mailer
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-23 15:38:43 +00:00
Râu Cao
fb3b9af3e5 Add custom mailer for one-off emails
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-22 14:54:23 +07:00
Râu Cao
b1a0268e6b Make email settings configurable 2023-03-22 14:53:44 +07:00
e1e7d8f87d Merge pull request 'Move exchanging of XMPP contacts to account confirmation' (#105) from chore/exchange_xmpp_contacts_after_confirmation into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #105
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-03-22 06:45:30 +00:00
Râu Cao
5b46f3adf5 Move exchanging of XMPP contacts to account confirmation
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Since the ejabberd service is now being enabled after the confirmation,
we also need to move the exchanging of roster contacts to that point.
2023-03-20 17:59:43 +07:00
Râu Cao
a8a8fba14c Change styling of Devise shared links
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Update release notes draft
2023-03-19 18:07:09 +07:00
Râu Cao
8a7016a30b Add remember-me function for sign-in
When checked, remember user for 2 weeks. Otherwise expire session after
30 minutes.
2023-03-19 18:06:18 +07:00
Râu Cao
e2618de7c6 Add time limit for inactive sessions
closes #8
2023-03-19 16:16:36 +07:00
90680368fb Merge pull request 'Complete admin pages for service settings' (#104) from feature/admin_user_service_settings into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #104
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-03-19 06:33:13 +00:00
Râu Cao
8d90847896 Add setting for contact roster name
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
And only exchange contacts when ejabberd integration is enabled
2023-03-15 09:03:39 +00:00
Râu Cao
8da297811b Mark settings as readonly, allow params for editable ones 2023-03-15 09:03:39 +00:00
Râu Cao
fa56d6b772 Refactor toggles to work without JS, add specs 2023-03-15 09:03:39 +00:00
Râu Cao
ca1221e9f3 Refactor admin settings, add all service settings 2023-03-15 09:03:39 +00:00
Râu Cao
295d486761 Disable toggles on admin user page
They are purely informational
2023-03-15 09:03:39 +00:00
Râu Cao
e00390d102 Add cached settings for all current services 2023-03-15 09:03:39 +00:00
Râu Cao
b947480190 Refactor sidenav link component, allow multiple levels 2023-03-15 09:03:39 +00:00
Râu Cao
fa07978aac Add form field update capability to toggle components 2023-03-15 09:03:39 +00:00
Râu Cao
e758e258a8 Allow disabling toggles, add toggle fieldset component 2023-03-15 09:03:39 +00:00
Râu Cao
805733939c Add toggle switch component, service configs, admin profile links 2023-03-15 09:03:39 +00:00
Râu Cao
f050d010fd Refactor admin donation pages, fix errors
All checks were successful
continuous-integration/drone/push Build is passing
Not sending the right response codes for Turbo to handle.
2023-03-15 15:24:00 +07:00
Râu Cao
95fac38b53 Show email address on account settings page
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-12 11:01:22 +07:00
cb80465297 Merge pull request 'Upgrade Devise, remove custom Turbo integration' (#102) from chore/87-upgrade_devise into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #102
2023-03-09 04:43:03 +00:00
Râu Cao
c7550b4f64 Upgrade Devise, remove custom Turbo integration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-09 11:34:42 +07:00
341284aa99 Merge pull request 'Refactor form input styles/layouts' (#100) from ui/form_inputs into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #100
2023-03-09 03:42:22 +00:00
Râu Cao
b34d040ce3 Refactor form input styles
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
* Fix issue where button is rendered taller in flexbox, due to default
  margin on input elements
* Refactor/improve all login and signup views
2023-03-09 10:23:16 +07:00
1142a4e2d5 Merge pull request 'Add keysend support for Lightning Addresses, specs for address/lnurlp responses' (#84) from feature/ln_address_keysend into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #84
Reviewed-by: bumi <bumi@noreply.kosmos.org>
2023-03-03 13:29:02 +00:00
Râu Cao
f2c7aa2f09 Fix typos
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-03 21:27:18 +08:00
cca44d7542 Merge branch 'master' into feature/ln_address_keysend
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-02 15:49:13 +00:00
cdad7546fb Merge pull request 'Improve design of service grid on dashboard' (#97) from feature/dashboard_layout into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #97
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-03-02 15:48:27 +00:00
feb7833533 Merge branch 'master' into feature/dashboard_layout
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-02 15:41:51 +00:00
Râu Cao
dfb12b8f62 Fix typo
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-02 15:54:03 +08:00
Râu Cao
6c2a97e7e5 Improve design of service grid on dashboard
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-01 22:48:23 +08:00
c8b65de7f6 Merge pull request 'Add service attribute to LDAP user entry' (#91) from feature/ldap_services into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #91
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-03-01 09:57:53 +00:00
2861254adf Merge branch 'master' into feature/ldap_services
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-01 09:35:53 +00:00
1d2910dadb Merge pull request 'Add pagination features, paginate admin pages' (#95) from feature/89-pagination into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #95
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-03-01 09:34:58 +00:00
Râu Cao
251a170f2b Add documentation link for Pagy
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-01 17:14:44 +08:00
Râu Cao
cbbb4c6e47 Add pagination to admin pages
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-01 17:08:36 +08:00
Râu Cao
3aad27c7bd Add Pagy gem, config, styles 2023-03-01 17:08:24 +08:00
Râu Cao
7cff849d79 Add more users when seeding db 2023-03-01 17:07:13 +08:00
Râu Cao
75ffd4e2f1 Add service attribute to LDAP user entry
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-27 23:36:23 +08:00
b84f9109f6 Merge pull request 'Fix broken database seed' (#90) from bugfix/reserved_admin_username into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #90
Reviewed-by: raucao <raucao@noreply.kosmos.org>
2023-02-26 14:20:45 +00:00
7fd564726f Merge pull request 'Add user page to admin panel, improve other admin pages' (#88) from feature/admin_user_details into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #88
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-02-26 14:16:41 +00:00
b2a1b8caf5 Remove "admin" from default reserved usernames
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Blocking admin prevents seeding the DB, which creates an admin user
2023-02-26 13:15:33 +01:00
52cc2a8151 Fix numbering in quickstart steps 2023-02-26 13:10:49 +01:00
Râu Cao
c8e405d93a Fix inline tailwind styles not being applied
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-26 18:41:18 +08:00
Râu Cao
5f74212603 Improve admin donation pages
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-26 11:33:11 +08:00
Râu Cao
1c3e893b6b Fix height of link element buttons 2023-02-26 11:32:26 +08:00
Râu Cao
eec4533fea Improve markup 2023-02-26 11:32:03 +08:00
Râu Cao
6d20ac9a1c Add lndhub info to admin user page
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-25 15:33:03 +08:00
Râu Cao
27dd4163f0 Add more data to admin user page 2023-02-25 15:32:50 +08:00
Râu Cao
1a55e5e895 Link users everywhere in admin panel 2023-02-25 15:32:13 +08:00
Râu Cao
8eb487600c Switch admin users index from pure LDAP to database 2023-02-25 15:31:19 +08:00
Râu Cao
678e80a25d Retrieve ldap entry from user model 2023-02-25 15:30:23 +08:00
Râu Cao
30fb9805e5 Add associations between users via invitations 2023-02-25 15:29:46 +08:00
Râu Cao
e675970f4c Add view helper for colored badges 2023-02-25 15:28:02 +08:00
Râu Cao
a0727e709f Add table class for rows with dividers 2023-02-25 15:27:28 +08:00
Râu Cao
55abbcc5ad WIP user page 2023-02-23 23:55:32 +08:00
Râu Cao
ffed398024 Add admin user details page 2023-02-23 22:09:23 +08:00
Râu Cao
dc63506102 Add ln node public key to test env
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-02-23 17:56:38 +08:00
Râu Cao
b87b9c2437 Prevent double render
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-02-23 17:54:34 +08:00
Râu Cao
e580cc9991 Add specs for Lightning Address and lnurlpay requests
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-02-23 17:46:36 +08:00
Râu Cao
68ab88c481 Add names for lnurl routes 2023-02-23 17:46:19 +08:00
Râu Cao
c7fe1bc3bc Add keysend support for Lightning Address
Allow keysend payments to user addresses. Useful for Podcasting 2.0/v4v.
2023-02-23 15:47:16 +08:00
111 changed files with 2181 additions and 485 deletions

View File

@@ -1,16 +1,34 @@
SMTP_SERVER=smtp.example.com
SMTP_PORT=587
SMTP_LOGIN=accounts
SMTP_PASSWORD=123abc
SMTP_FROM_ADDRESS=accounts@example.com
SMTP_DOMAIN=example.com
SMTP_AUTH_METHOD=plain
SMTP_ENABLE_STARTTLS=auto
REDIS_URL='redis://localhost:6379/1'
LDAP_HOST=localhost LDAP_HOST=localhost
LDAP_PORT=389 LDAP_PORT=389
LDAP_ADMIN_PASSWORD=passthebutter LDAP_ADMIN_PASSWORD=passthebutter
LDAP_SUFFIX="dc=kosmos,dc=org" LDAP_SUFFIX='dc=kosmos,dc=org'
WEBHOOKS_ALLOWED_IPS='10.1.1.163' WEBHOOKS_ALLOWED_IPS='10.1.1.163'
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
MASTODON_PUBLIC_URL='https://kosmos.social'
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
EJABBERD_API_URL='https://xmpp.kosmos.org/api' EJABBERD_API_URL='https://xmpp.kosmos.org/api'
BTCPAY_API_URL='http://localhost:23001/api/v1' BTCPAY_API_URL='http://localhost:23001/api/v1'
LNDHUB_API_URL='http://localhost:3023' LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
LNDHUB_ADMIN_UI=true LNDHUB_ADMIN_UI=true
LNDHUB_PG_HOST=localhost LNDHUB_PG_HOST=localhost
LNDHUB_PG_PORT=5432 LNDHUB_PG_PORT=5432

View File

@@ -4,5 +4,6 @@ BTCPAY_API_URL='http://btcpay.example.com/api/v1'
LNDHUB_API_URL='http://localhost:3026' LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
WEBHOOKS_ALLOWED_IPS='10.1.1.23' WEBHOOKS_ALLOWED_IPS='10.1.1.23'

View File

@@ -0,0 +1,13 @@
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
version-resolver:
major:
labels:
- 'release/major'
minor:
labels:
- 'release/minor'
patch:
labels:
- 'release/patch'
default: patch

View File

@@ -0,0 +1,11 @@
name: Release Drafter
on:
pull_request:
types: [closed]
jobs:
release_drafter_job:
name: Update release notes draft
runs-on: ubuntu-latest
steps:
- name: Release Drafter
uses: https://github.com/raucao/gitea-release-drafter@dev

View File

@@ -1,8 +1,13 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM ruby:2.7.6 FROM ruby:2.7.6
RUN apt-get update -qq && apt-get install -y curl ldap-utils
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
ldap-utils tini
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get install -y nodejs RUN apt-get update && apt-get install -y nodejs
WORKDIR /akkounts WORKDIR /akkounts
COPY Gemfile /akkounts/Gemfile COPY Gemfile /akkounts/Gemfile
COPY Gemfile.lock /akkounts/Gemfile.lock COPY Gemfile.lock /akkounts/Gemfile.lock
@@ -12,11 +17,5 @@ RUN gem install foreman
RUN npm install -g yarn RUN npm install -g yarn
RUN yarn install RUN yarn install
# Add a script to be executed every time the container starts. ENTRYPOINT ["/usr/bin/tini", "--"]
COPY docker/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000 EXPOSE 3000
# Configure the main process to run when running the image
CMD ["bin", "dev"]

View File

@@ -32,13 +32,14 @@ gem 'lockbox'
# Authentication # Authentication
gem 'warden' gem 'warden'
gem 'devise' gem 'devise', '~> 4.9.0'
gem 'devise_ldap_authenticatable' gem 'devise_ldap_authenticatable'
gem 'net-ldap' gem 'net-ldap'
# Utilities # Utilities
gem "rqrcode", "~> 2.0" gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3' gem 'rails-settings-cached', '~> 2.8.3'
gem 'pagy', '~> 6.0', '>= 6.0.2'
# HTTP requests # HTTP requests
gem 'faraday' gem 'faraday'
@@ -47,6 +48,10 @@ gem 'faraday'
gem 'sidekiq', '< 7' gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler' gem 'sidekiq-scheduler'
# Monitoring
gem "sentry-ruby"
gem "sentry-rails"
group :development, :test do group :development, :test do
# Use sqlite3 as the database for Active Record # Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4' gem 'sqlite3', '~> 1.4'

View File

@@ -95,7 +95,7 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
devise (4.8.1) devise (4.9.0)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@@ -178,6 +178,7 @@ GEM
nokogiri (1.13.9-x86_64-linux) nokogiri (1.13.9-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pagy (6.0.2)
pg (1.2.3) pg (1.2.3)
public_suffix (5.0.0) public_suffix (5.0.0)
puma (4.3.12) puma (4.3.12)
@@ -225,9 +226,9 @@ GEM
redis-client (0.11.2) redis-client (0.11.2)
connection_pool connection_pool
regexp_parser (2.6.1) regexp_parser (2.6.1)
responders (3.0.1) responders (3.1.0)
actionpack (>= 5.0) actionpack (>= 5.2)
railties (>= 5.0) railties (>= 5.2)
rexml (3.2.5) rexml (3.2.5)
rqrcode (2.1.2) rqrcode (2.1.2)
chunky_png (~> 1.0) chunky_png (~> 1.0)
@@ -253,6 +254,11 @@ GEM
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rufus-scheduler (3.8.2) rufus-scheduler (3.8.2)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
sentry-rails (5.8.0)
railties (>= 5.0)
sentry-ruby (~> 5.8.0)
sentry-ruby (5.8.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (6.5.5) sidekiq (6.5.5)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
@@ -314,7 +320,7 @@ DEPENDENCIES
capybara capybara
cssbundling-rails cssbundling-rails
database_cleaner database_cleaner
devise devise (~> 4.9.0)
devise_ldap_authenticatable devise_ldap_authenticatable
dotenv-rails dotenv-rails
factory_bot_rails factory_bot_rails
@@ -327,12 +333,15 @@ DEPENDENCIES
listen (~> 3.2) listen (~> 3.2)
lockbox lockbox
net-ldap net-ldap
pagy (~> 6.0, >= 6.0.2)
pg (~> 1.2.3) pg (~> 1.2.3)
puma (~> 4.1) puma (~> 4.1)
rails (~> 7.0.2) rails (~> 7.0.2)
rails-settings-cached (~> 2.8.3) rails-settings-cached (~> 2.8.3)
rqrcode (~> 2.0) rqrcode (~> 2.0)
rspec-rails rspec-rails
sentry-rails
sentry-ruby
sidekiq (< 7) sidekiq (< 7)
sidekiq-scheduler sidekiq-scheduler
sprockets-rails sprockets-rails

View File

@@ -14,12 +14,12 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in 1. Make sure [Docker Compose is installed][1] and Docker is running (included in
Docker Desktop) Docker Desktop)
2. Uncomment the `web` section in `docker-compose.yml` 2. Uncomment the `redis`, `web`, and `sidekiq` sections in `docker-compose.yml`
3. Run `docker compose up` and wait until 389ds announces its successful start 3. Run `docker compose up` and wait until 389ds announces its successful start
in the log output in the log output
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"` 4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
5. `docker compose run web rails ldap:setup` 5. `docker compose run web rails ldap:setup`
5. `docker compose run web rails db:setup` 6. `docker compose run web rails db:setup`
After these steps, you should have a working Rails app with a handful of test After these steps, you should have a working Rails app with a handful of test
users running on [http://localhost:3000](http://localhost:3000). users running on [http://localhost:3000](http://localhost:3000).
@@ -81,12 +81,15 @@ with a fresh installation, delete both that directory as well as the container.
## Documentation ## Documentation
### Rails
* [Ruby on Rails](https://guides.rubyonrails.org/) * [Ruby on Rails](https://guides.rubyonrails.org/)
* [Sass](https://sass-lang.com/documentation) * [Pagination](https://ddnexus.github.io/pagy/)
### Front-end ### Front-end
* [Tailwind CSS](https://tailwindcss.com/) * [Tailwind CSS](https://tailwindcss.com/)
* [Sass](https://sass-lang.com/documentation)
### Testing ### Testing

View File

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

View File

@@ -36,10 +36,18 @@
@apply mb-4 leading-6; @apply mb-4 leading-6;
} }
main p:last-child {
@apply mb-0;
}
main ul { main ul {
@apply mb-6; @apply mb-6;
} }
main ul:last-child {
@apply mb-0;
}
main ul li { main ul li {
@apply leading-6; @apply leading-6;
} }

View File

@@ -1,6 +1,6 @@
@layer components { @layer components {
.btn { .btn {
@apply font-semibold rounded-md leading-none cursor-pointer text-center @apply inline-block font-semibold rounded-md leading-none cursor-pointer text-center
transition-colors duration-75 focus:outline-none focus:ring-4; transition-colors duration-75 focus:outline-none focus:ring-4;
} }

View File

@@ -0,0 +1,5 @@
@layer components {
.services > div > a {
background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 0, rgba(255,255,255,0.88) 100%);
}
}

View File

@@ -1,7 +1,7 @@
@layer components { @layer components {
input[type=text], input[type=email], input[type=password], input[type=text], input[type=email], input[type=password],
input[type=number], select, textarea { input[type=number], select, textarea {
@apply mt-1 rounded-md bg-gray-100 focus:bg-white @apply rounded-md bg-gray-100 focus:bg-white
border-transparent focus:border-transparent focus:ring-2 border-transparent focus:border-transparent focus:ring-2
focus:ring-blue-600 focus:ring-opacity-75; focus:ring-blue-600 focus:ring-opacity-75;
} }
@@ -10,6 +10,10 @@
@apply inline-block; @apply inline-block;
} }
.field_with_errors input {
@apply w-full bg-red-100;
}
.error-msg { .error-msg {
@apply text-red-700; @apply text-red-700;
} }

View File

@@ -5,10 +5,4 @@
&:visited { @apply text-indigo-600; } &:visited { @apply text-indigo-600; }
&:active { @apply text-red-600; } &:active { @apply text-red-600; }
} }
.devise-links {
a {
@apply ks-text-link;
}
}
} }

View File

@@ -0,0 +1,45 @@
@layer components {
.pagy-nav.pagination {
@apply isolate inline-flex -space-x-px rounded-md shadow-sm;
}
.pagy-nav .page:not(.prev):not(.next) {
@apply hidden sm:inline-block;
}
.pagy-nav .page.next a {
@apply relative inline-flex items-center rounded-r-md border
border-gray-300 bg-white px-3 py-2 text-sm font-medium
text-gray-500 hover:bg-gray-100 focus:z-20;
}
.pagy-nav .page.prev a {
@apply relative inline-flex items-center rounded-l-md border
border-gray-300 bg-white px-3 py-2 text-sm font-medium
text-gray-500 hover:bg-gray-100 focus:z-20;
}
.pagy-nav .page.next.disabled {
@apply relative inline-flex items-center rounded-r-md border
border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium
text-gray-400 focus:z-20;
}
.pagy-nav .page.prev.disabled {
@apply relative inline-flex items-center rounded-l-md border
border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium
text-gray-400 focus:z-20;
}
.pagy-nav .page a, .page.gap {
@apply bg-white border-gray-300 text-gray-500 hover:bg-gray-100 relative
inline-flex items-center border px-4 py-2 text-sm font-medium
focus:z-20;
}
.pagy-nav .page.active {
@apply z-10 border-indigo-500 bg-indigo-50 text-indigo-600 relative
inline-flex items-center border px-4 py-2 text-sm font-medium
focus:z-20;
}
}

View File

@@ -7,16 +7,30 @@
@apply text-left; @apply text-left;
} }
table th { table thead th {
@apply pb-3.5 text-sm font-normal uppercase text-gray-500; @apply pb-3.5 text-sm font-normal uppercase text-gray-500;
} }
table tbody th {
@apply text-left font-normal text-gray-500;
}
table th:not(:last-of-type), table th:not(:last-of-type),
table td:not(:last-of-type) { table td:not(:last-of-type) {
@apply pr-2; @apply pr-2;
} }
table td { table td, tbody th {
@apply py-2; @apply py-2;
} }
table.divided {
@apply divide-y divide-gray-300;
}
table.divided tbody {
@apply divide-y divide-gray-200;
}
table.divided td, table.divided tbody th {
@apply py-3;
}
} }

View File

@@ -0,0 +1,13 @@
<%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
<label class="block">
<p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>">
<%= @title %>
</p>
<% if @descripton.present? %>
<p class="text-gray-500">
<%= @descripton %>
</p>
<% end %>
<%= content %>
</label>
<% end %>

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
module FormElements
class FieldsetComponent < ViewComponent::Base
def initialize(tag: "li", title:, description: nil)
@tag = tag
@title = title
@descripton = description
end
end
end

View File

@@ -0,0 +1,26 @@
<%= tag.public_send @tag, class: "flex items-center justify-between mb-6 last:mb-0",
data: @form.present? ? {
controller: "settings--toggle",
:'settings--toggle-switch-enabled-value' => @enabled.to_s
} : nil do %>
<div class="flex flex-col">
<label class="font-bold mb-1"><%= @title %></label>
<p class="text-gray-500"><%= @descripton %></p>
</div>
<div class="relative ml-4 inline-flex flex-shrink-0">
<%= render FormElements::ToggleComponent.new(
enabled: @enabled,
input_enabled: @input_enabled,
class_names: @form.present? ? "hidden" : nil,
data: {
:'settings--toggle-target' => "button",
action: "settings--toggle#toggleSwitch"
}) %>
<% if @form.present? %>
<%= @form.check_box @attribute, {
checked: @enabled,
data: { :'settings--toggle-target' => "checkbox" }
}, "true", "false" %>
<% end %>
</div>
<% end %>

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
module FormElements
class FieldsetToggleComponent < ViewComponent::Base
def initialize(form: nil, attribute: nil, tag: "li", enabled: false,
input_enabled: true, title:, description:)
@form = form
@attribute = attribute
@tag = tag
@enabled = enabled
@input_enabled = input_enabled
@title = title
@descripton = description
@button_text = @enabled ? "Switch off" : "Switch on"
end
end
end

View File

@@ -0,0 +1,15 @@
<%= button_tag type: "button", name: "toggle", data: @data,
role: "switch", aria: { checked: @enabled.to_s },
tabindex: @tabindex, disabled: !@input_enabled,
class: "#{ @enabled ? 'bg-blue-600' : 'bg-gray-200' }
#{ @class_names.present? ? @class_names : '' }
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer
rounded-full border-2 border-transparent transition-colors
duration-200 ease-in-out focus:outline-none focus:ring-2
focus:ring-blue-600 focus:ring-offset-2" do %>
<span class="sr-only"><%= @button_text %></span>
<span aria-hidden="true" data-settings--toggle-target="switch"
class="<%= @enabled ? 'translate-x-5' : 'translate-x-0' %>
pointer-events-none inline-block h-5 w-5 transform rounded-full
bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
<% end %>

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module FormElements
class ToggleComponent < ViewComponent::Base
def initialize(enabled:, input_enabled: true, data: nil, class_names: nil, tabindex: nil)
@enabled = !!enabled
@input_enabled = input_enabled
@data = data
@class_names = class_names
@tabindex = tabindex
end
end
end

View File

@@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class SidenavLinkComponent < ViewComponent::Base class SidenavLinkComponent < ViewComponent::Base
def initialize(name:, path:, icon:, active: false, disabled: false) def initialize(name:, level: 1, path:, icon:, active: false, disabled: false)
@name = name @name = name
@level = level
@path = path @path = path
@icon = icon @icon = icon
@active = active @active = active
@@ -12,12 +13,15 @@ class SidenavLinkComponent < ViewComponent::Base
end end
def class_names_link(path) def class_names_link(path)
px = @level == 1 ? "px-4" : "pl-8 pr-4"
base = "#{px} py-2 group border-l-4 flex items-center text-base font-medium"
if @active if @active
"bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700 group border-l-4 px-4 py-2 flex items-center text-base font-medium" "#{base} bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
elsif @disabled elsif @disabled
"border-transparent text-gray-400 hover:bg-gray-50 group border-l-4 px-4 py-2 flex items-center text-base font-medium" "#{base} border-transparent text-gray-400 hover:bg-gray-50"
else else
"border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-4 py-2 flex items-center text-base font-medium" "#{base} border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900"
end end
end end

View File

@@ -1,4 +1,5 @@
class Admin::BaseController < ApplicationController class Admin::BaseController < ApplicationController
include Pagy::Backend
before_action :authenticate_user! before_action :authenticate_user!
before_action :authorize_admin before_action :authorize_admin
@@ -7,5 +8,4 @@ class Admin::BaseController < ApplicationController
def set_context def set_context
@context = :admin @context = :admin
end end
end end

View File

@@ -5,7 +5,8 @@ class Admin::DonationsController < Admin::BaseController
# GET /donations # GET /donations
# GET /donations.json # GET /donations.json
def index def index
@donations = Donation.all.order('created_at desc') @pagy, @donations = pagy(Donation.all.order('created_at desc'))
@stats = { @stats = {
overall_sats: @donations.all.sum("amount_sats"), overall_sats: @donations.all.sum("amount_sats"),
donor_count: Donation.distinct.count(:user_id) donor_count: Donation.distinct.count(:user_id)
@@ -40,7 +41,7 @@ class Admin::DonationsController < Admin::BaseController
end end
format.json { render :show, status: :created, location: @donation } format.json { render :show, status: :created, location: @donation }
else else
format.html { render :new } format.html { render :new, status: :unprocessable_entity }
format.json { render json: @donation.errors, status: :unprocessable_entity } format.json { render json: @donation.errors, status: :unprocessable_entity }
end end
end end
@@ -58,7 +59,7 @@ class Admin::DonationsController < Admin::BaseController
end end
format.json { render :show, status: :ok, location: @donation } format.json { render :show, status: :ok, location: @donation }
else else
format.html { render :edit } format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @donation.errors, status: :unprocessable_entity } format.json { render json: @donation.errors, status: :unprocessable_entity }
end end
end end

View File

@@ -1,7 +1,8 @@
class Admin::InvitationsController < Admin::BaseController class Admin::InvitationsController < Admin::BaseController
def index def index
@current_section = :invitations @current_section = :invitations
@invitations_used = Invitation.used.order('used_at desc') @pagy, @invitations_used = pagy(Invitation.used.order('used_at desc'))
@stats = { @stats = {
available: Invitation.unused.count, available: Invitation.unused.count,
accepted: @invitations_used.length, accepted: @invitations_used.length,

View File

@@ -1,38 +1,12 @@
class Admin::Settings::RegistrationsController < Admin::SettingsController class Admin::Settings::RegistrationsController < Admin::SettingsController
def index def index
end end
def create def create
@errors = ActiveModel::Errors.new(Setting.new) update_settings
setting_params.keys.each do |key|
next if setting_params[key].nil?
setting = Setting.new(var: key)
setting.value = setting_params[key].strip
unless setting.valid?
@errors.merge!(setting.errors)
end
end
if @errors.any?
render :index
end
setting_params.keys.each do |key|
Setting.send("#{key}=", setting_params[key].strip) unless setting_params[key].nil?
end
redirect_to admin_settings_registrations_path, flash: { redirect_to admin_settings_registrations_path, flash: {
success: "Settings saved" success: "Settings saved"
} }
end end
private
def setting_params
params.require(:setting).permit(:reserved_usernames)
end
end end

View File

@@ -1,9 +1,19 @@
class Admin::Settings::ServicesController < Admin::SettingsController class Admin::Settings::ServicesController < Admin::SettingsController
def index def index
@service = params[:s]
if @service.blank?
redirect_to admin_settings_services_path(params: { s: "discourse" })
end
end end
def update def create
end service = params.require(:service)
update_settings
redirect_to admin_settings_services_path(params: { s: service }), flash: {
success: "Settings saved"
}
end
end end

View File

@@ -4,9 +4,37 @@ class Admin::SettingsController < Admin::BaseController
def index def index
end end
def update_settings
@errors = ActiveModel::Errors.new(Setting.new)
changed_keys = []
setting_params.keys.each do |key|
next if setting_params[key].nil? ||
(Setting.send(key).to_s == setting_params[key].strip)
changed_keys.push(key)
setting = Setting.new(var: key)
setting.value = setting_params[key].strip
unless setting.valid?
@errors.merge!(setting.errors)
end
end
if @errors.any?
render :index and return
end
changed_keys.each do |key|
Setting.send("#{key}=", setting_params[key].strip)
end
end
private private
def set_current_section def set_current_section
@current_section = :settings @current_section = :settings
end end
def setting_params
params.require(:setting).permit(Setting.editable_keys.map(&:to_sym))
end
end end

View File

@@ -1,19 +1,34 @@
class Admin::UsersController < Admin::BaseController class Admin::UsersController < Admin::BaseController
before_action :set_user, only: [:show]
before_action :set_current_section before_action :set_current_section
def index def index
ldap = LdapService.new ldap = LdapService.new
@ou = params[:ou] || "kosmos.org" @ou = params[:ou] || "kosmos.org"
@orgs = ldap.fetch_organizations @orgs = ldap.fetch_organizations
@entries = ldap.fetch_users(ou: @ou) @pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
@stats = { @stats = {
users_confirmed: User.where(ou: @ou).confirmed.count, users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: @ou).pending.count users_pending: User.where(ou: @ou).pending.count
} }
end end
def show
if Setting.lndhub_admin_enabled?
@lndhub_user = @user.lndhub_user
end
@services_enabled = @user.services_enabled
end
private private
def set_user
address = params[:address].split("@")
@user = User.where(cn: address.first, ou: address.last).first
end
def set_current_section def set_current_section
@current_section = :users @current_section = :users
end end

View File

@@ -3,6 +3,18 @@ class ApplicationController < ActionController::Base
render :text => exception, :status => 500 render :text => exception, :status => 500
end end
before_action :sentry_set_user
def sentry_set_user
return unless Setting.sentry_enabled
if user_signed_in?
Sentry.set_user(id: current_user.id, username: current_user.cn)
else
Sentry.set_user({})
end
end
def require_user_signed_in def require_user_signed_in
unless user_signed_in? unless user_signed_in?
redirect_to welcome_path and return redirect_to welcome_path and return

View File

@@ -1,4 +1,5 @@
class LnurlpayController < ApplicationController class LnurlpayController < ApplicationController
before_action :check_feature_enabled
before_action :find_user_by_address before_action :find_user_by_address
MIN_SATS = 10 MIN_SATS = 10
@@ -17,6 +18,20 @@ class LnurlpayController < ApplicationController
} }
end end
def keysend
http_status :not_found and return unless Setting.lndhub_keysend_enabled?
render json: {
status: "OK",
tag: "keysend",
pubkey: Setting.lndhub_public_key,
customData: [{
customKey: "696969",
customValue: @user.ln_account
}]
}
end
def invoice def invoice
amount = params[:amount].to_i / 1000 # msats amount = params[:amount].to_i / 1000 # msats
address = params[:address] address = params[:address]
@@ -72,4 +87,9 @@ class LnurlpayController < ApplicationController
comment.length <= MAX_COMMENT_CHARS comment.length <= MAX_COMMENT_CHARS
end end
private
def check_feature_enabled
http_status :not_found unless Setting.lndhub_enabled?
end
end end

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
class Users::ConfirmationsController < Devise::ConfirmationsController
# GET /resource/confirmation?confirmation_token=abcdef
def show
self.resource = resource_class.confirm_by_token(params[:confirmation_token])
yield resource if block_given?
if resource.errors.empty?
set_flash_message!(:success, :confirmed)
resource.devise_after_confirmation
respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) }
else
respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new }
end
end
end

View File

@@ -1,4 +1,6 @@
module ApplicationHelper module ApplicationHelper
include Pagy::Frontend
def sats_to_btc(sats) def sats_to_btc(sats)
sats.to_f / 100000000 sats.to_f / 100000000
end end
@@ -10,5 +12,10 @@ module ApplicationHelper
"text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block" "text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
end end
end end
end
# Colors available: gray, red, yellow, green, blue, purple, pink
# (Add more colors by adding classes to the safelist in tailwind.config.js)
def badge(text, color)
tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800"
end
end

View File

@@ -4,6 +4,10 @@ export default class extends Controller {
static targets = ["buttons", "countdown"] static targets = ["buttons", "countdown"]
connect() { connect() {
// Devise timeoutable ends up adding a second flash message without content
// TODO investigate bug
if (this.element.textContent.trim() == "true") return;
const timeoutSeconds = parseInt(this.data.get("timeout")); const timeoutSeconds = parseInt(this.data.get("timeout"));
setTimeout(() => { setTimeout(() => {

View File

@@ -0,0 +1,30 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "button", "switch", "checkbox" ]
static values = { switchEnabled: Boolean }
connect () {
this.buttonTarget.classList.remove("hidden")
this.checkboxTarget.classList.add("hidden")
}
toggleSwitch () {
this.switchEnabledValue = !this.switchEnabledValue
this.checkboxTarget.checked = this.switchEnabledValue
if (this.switchEnabledValue) {
this.buttonTarget.setAttribute("aria-checked", "true");
this.buttonTarget.classList.remove("bg-gray-200")
this.buttonTarget.classList.add("bg-blue-600")
this.switchTarget.classList.remove("translate-x-0")
this.switchTarget.classList.add("translate-x-5")
} else {
this.buttonTarget.setAttribute("aria-checked", "false");
this.buttonTarget.classList.remove("bg-blue-600")
this.buttonTarget.classList.add("bg-gray-200")
this.switchTarget.classList.remove("translate-x-5")
this.switchTarget.classList.add("translate-x-0")
}
}
}

View File

@@ -7,12 +7,12 @@ class XmppExchangeContactsJob < ApplicationJob
ejabberd.add_rosteritem({ ejabberd.add_rosteritem({
"localuser": username, "localhost": domain, "localuser": username, "localhost": domain,
"user": inviter.cn, "host": inviter.ou, "user": inviter.cn, "host": inviter.ou,
"nick": inviter.cn, "group": "Friends", "subs": "both" "nick": inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
}) })
ejabberd.add_rosteritem({ ejabberd.add_rosteritem({
"localuser": inviter.cn, "localhost": inviter.ou, "localuser": inviter.cn, "localhost": inviter.ou,
"user": username, "host": domain, "user": username, "host": domain,
"nick": username, "group": "Friends", "subs": "both" "nick": username, "group": Setting.ejabberd_buddy_roster, "subs": "both"
}) })
end end
end end

View File

@@ -1,4 +1,3 @@
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer' layout 'mailer'
end end

View File

@@ -0,0 +1,23 @@
# A custom mailer that can be used from the Rails console for one-off emails
# today, and later connected from an admin panel mailing page.
#
# Assign any template variables you want to use:
#
# user = User.first
#
# Create the email body from a custom email template file:
#
# body = ERB.new(File.read('./tmp/mailer-1.txt.erb')).result binding
#
# Send email via Sidekiq:
#
# CustomMailer.with(user: user, subject: "Important announcement", body: body).custom_message.deliver_later
#
class CustomMailer < ApplicationMailer
def custom_message
@user = params[:user]
@subject = params[:subject]
@body = params[:body]
mail(to: @user.email, subject: @subject)
end
end

View File

@@ -3,7 +3,9 @@ class Donation < ApplicationRecord
belongs_to :user belongs_to :user
# Validations # Validations
validates_presence_of :user
validates_presence_of :amount_sats validates_presence_of :amount_sats
validates_presence_of :paid_at
# Hooks # Hooks
# TODO before_create :store_fiat_value # TODO before_create :store_fiat_value

View File

@@ -1,6 +1,7 @@
class Invitation < ApplicationRecord class Invitation < ApplicationRecord
# Relations # Relations
belongs_to :user belongs_to :user
belongs_to :invitee, class_name: "User", foreign_key: 'invited_user_id', optional: true
# Validations # Validations
validates_presence_of :user validates_presence_of :user

View File

@@ -10,6 +10,18 @@ class LndhubUser < LndhubBase
foreign_key: "login" foreign_key: "login"
def balance def balance
accounts.current.first.ledgers.sum("account_ledgers.amount") accounts.current.first.ledgers.sum("account_ledgers.amount").to_i.abs
end
def sum_outgoing
accounts.outgoing.first.ledgers.sum("account_ledgers.amount").to_i.abs
end
def sum_incoming
accounts.incoming.first.ledgers.sum("account_ledgers.amount").to_i.abs
end
def sum_fees
accounts.fees.first.ledgers.sum("account_ledgers.amount").to_i.abs
end end
end end

View File

@@ -2,10 +2,106 @@
class Setting < RailsSettings::Base class Setting < RailsSettings::Base
cache_prefix { "v1" } cache_prefix { "v1" }
#
# Internal services
#
field :redis_url, type: :string, readonly: true,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
#
# Registrations
#
field :reserved_usernames, type: :array, default: %w[ field :reserved_usernames, type: :array, default: %w[
account accounts admin donations mail webmaster support account accounts donations mail webmaster support
] ]
field :lndhub_enabled, default: (ENV["LNDHUB_API_URL"].present?.to_s || "false"), type: :boolean #
field :lndhub_admin_enabled, default: (ENV["LNDHUB_ADMIN_UI"] || "false"), type: :boolean # Sentry
#
field :sentry_enabled, type: :boolean, readonly: true,
default: (ENV["SENTRY_DSN"].present?.to_s || false)
#
# Discourse
#
field :discourse_public_url, type: :string, readonly: true,
default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean,
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
#
# ejabberd
#
field :ejabberd_enabled, type: :boolean,
default: (ENV["EJABBERD_API_URL"].present?.to_s || false)
field :ejabberd_api_url, type: :string, readonly: true,
default: ENV["EJABBERD_API_URL"].presence
field :ejabberd_admin_url, type: :string, readonly: true,
default: ENV["EJABBERD_ADMIN_URL"].presence
field :ejabberd_buddy_roster, type: :string,
default: "Buddies"
#
# Gitea
#
field :gitea_public_url, type: :string, readonly: true,
default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean,
default: (ENV["GITEA_PUBLIC_URL"].present?.to_s || false)
#
# Lightning Network
#
field :lndhub_api_url, type: :string, readonly: true,
default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean,
default: (ENV["LNDHUB_API_URL"].present?.to_s || false)
field :lndhub_admin_enabled, type: :boolean,
default: (ENV["LNDHUB_ADMIN_UI"] || false)
field :lndhub_public_key, type: :string, readonly: true,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present?.to_s || false }
#
# Mastodon
#
field :mastodon_public_url, type: :string, readonly: true,
default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean,
default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false)
#
# MediaWiki
#
field :mediawiki_public_url, type: :string, readonly: true,
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean,
default: (ENV["MEDIAWIKI_PUBLIC_URL"].present?.to_s || false)
#
# Nostr
#
field :nostr_enabled, type: :boolean, default: true
end end

View File

@@ -3,6 +3,10 @@ class User < ApplicationRecord
# Relations # Relations
has_many :invitations, dependent: :destroy has_many :invitations, dependent: :destroy
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user
has_many :invitees, through: :invitations
has_many :donations, dependent: :nullify has_many :donations, dependent: :nullify
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user", has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
@@ -34,13 +38,15 @@ class User < ApplicationRecord
devise :ldap_authenticatable, devise :ldap_authenticatable,
:confirmable, :confirmable,
:recoverable, :recoverable,
:validatable :validatable,
:timeoutable,
:rememberable
def ldap_before_save def ldap_before_save
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
self.ou = dn.split(',')
dn = Devise::LDAP::Adapter.get_ldap_param(self.cn, "dn") .select{|e| e[0..1] == "ou"}.first
self.ou = dn.split(',').select{|e| e[0..1] == "ou"}.first.delete_prefix("ou=") .delete_prefix("ou=")
if self.confirmed_at.blank? && self.confirmation_token.blank? if self.confirmed_at.blank? && self.confirmation_token.blank?
# User had an account with a trusted email address before akkounts was a thing # User had an account with a trusted email address before akkounts was a thing
@@ -48,6 +54,21 @@ class User < ApplicationRecord
end end
end end
def devise_after_confirmation
enable_service %w[ discourse ejabberd gitea mediawiki ]
#TODO enable in development when we have easy setup of ejabberd etc.
return if Rails.env.development?
if inviter.present?
exchange_xmpp_contact_with_inviter if Setting.ejabberd_enabled?
end
end
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
def reset_password(new_password, new_password_confirmation) def reset_password(new_password, new_password_confirmation)
self.password = new_password self.password = new_password
self.password_confirmation = new_password_confirmation self.password_confirmation = new_password_confirmation
@@ -80,4 +101,48 @@ class User < ApplicationRecord
lndhub.authenticate self lndhub.authenticate self
lndhub.addinvoice payload lndhub.addinvoice payload
end end
def dn
return @dn if defined?(@dn)
@dn = Devise::LDAP::Adapter.get_dn(self.cn)
end
def ldap_entry
ldap.fetch_users(uid: self.cn, ou: self.ou).first
end
def services_enabled
ldap_entry[:service] || []
end
def enable_service(service)
current_services = services_enabled
new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq
ldap.replace_attribute(dn, :service, services)
end
def disable_service(service)
current_services = services_enabled
disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :service, services)
end
def disable_all_services
ldap.delete_attribute(dn,:service)
end
def exchange_xmpp_contact_with_inviter
return unless inviter.services_enabled.include?("ejabberd") &&
services_enabled.include?("ejabberd")
XmppExchangeContactsJob.perform_later(inviter, self.cn, self.ou)
end
private
def ldap
return @ldap_service if defined?(@ldap_service)
@ldap_service = LdapService.new
end
end end

View File

@@ -11,11 +11,10 @@ class CreateAccount < ApplicationService
def call def call
user = create_user_in_database user = create_user_in_database
add_ldap_document add_ldap_document
create_lndhub_account(user) create_lndhub_account(user) if Setting.lndhub_enabled
if @invitation.present? if @invitation.present?
update_invitation(user.id) update_invitation(user.id)
exchange_xmpp_contacts
end end
end end
@@ -43,12 +42,6 @@ class CreateAccount < ApplicationService
CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw) CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw)
end end
def exchange_xmpp_contacts
#TODO enable in development when we have easy setup of ejabberd etc.
return if Rails.env.development?
XmppExchangeContactsJob.perform_later(@invitation.user, @username, @domain)
end
def create_lndhub_account(user) def create_lndhub_account(user)
#TODO enable in development when we have a local lndhub (mock?) API #TODO enable in development when we have a local lndhub (mock?) API
return if Rails.env.development? return if Rails.env.development?

View File

@@ -3,6 +3,18 @@ class LdapService < ApplicationService
@suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org" @suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end end
def add_attribute(dn, attr, values)
ldap_client.add_attribute dn, attr, values
end
def replace_attribute(dn, attr, values)
ldap_client.replace_attribute dn, attr, values
end
def delete_attribute(dn, attr)
ldap_client.delete_attribute dn, attr
end
def add_entry(dn, attrs, interactive=false) def add_entry(dn, attrs, interactive=false)
puts "Adding entry: #{dn}" if interactive puts "Adding entry: #{dn}" if interactive
res = ldap_client.add dn: dn, attributes: attrs res = ldap_client.add dn: dn, attributes: attrs
@@ -10,10 +22,6 @@ class LdapService < ApplicationService
res res
end end
def add_attribute(dn, attr, value)
ldap_client.add_attribute dn, attr, value
end
def delete_entry(dn, interactive=false) def delete_entry(dn, interactive=false)
puts "Deleting entry: #{dn}" if interactive puts "Deleting entry: #{dn}" if interactive
res = ldap_client.delete dn: dn res = ldap_client.delete dn: dn
@@ -42,18 +50,17 @@ class LdapService < ApplicationService
treebase = ldap_config["base"] treebase = ldap_config["base"]
end end
attributes = %w{dn cn uid mail admin} attributes = %w{dn cn uid mail admin service}
filter = Net::LDAP::Filter.eq("uid", "*") filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes) entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] } entries.sort_by! { |e| e.cn[0] }
entries = entries.collect do |e| entries = entries.collect do |e|
{ {
uid: e.uid.first, uid: e.uid.first,
mail: e.try(:mail) ? e.mail.first : nil, mail: e.try(:mail) ? e.mail.first : nil,
admin: e.try(:admin) ? 'admin' : nil admin: e.try(:admin) ? 'admin' : nil,
# password: e.userpassword.first service: e.try(:service)
} }
end end
end end
@@ -131,5 +138,4 @@ class LdapService < ApplicationService
def ldap_config def ldap_config
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env] ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
end end
end end

View File

@@ -1,58 +1,41 @@
<%= form_with(url: url, model: donation, local: true) do |form| %> <%= form_with(url: url, model: donation, local: true) do |form| %>
<% if donation.errors.any? %> <% if donation.errors.any? %>
<div id="error_explanation"> <section id="error_explanation">
<h3><%= pluralize(donation.errors.count, "error") %> prohibited this donation from being saved:</h3> <h3><%= pluralize(donation.errors.count, "error") %> prohibited this donation from being saved:</h3>
<ul> <ul class="list-disc list-inside">
<% donation.errors.full_messages.each do |message| %> <% donation.errors.full_messages.each do |message| %>
<li><%= message %></li> <li><%= message %></li>
<% end %> <% end %>
</ul> </ul>
</div> </section>
<% end %> <% end %>
<div class="field"> <section class="sm:w-1/2 grid grid-cols-2 items-center gap-y-2">
<p> <%= form.label :user_id %>
<%= form.label :user_id %> <%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn, {} %>
<%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn %>
</p>
</div>
<div class="field"> <%= form.label :amount_sats, "Amount BTC (sats)" %>
<p> <%= form.number_field :amount_sats %>
<%= form.label :amount_sats, "Amount BTC (sats)" %>
<%= form.number_field :amount_sats %>
</p>
</div>
<div class="field"> <%= form.label :amount_eur, "Amount EUR (cents)" %>
<p> <%= form.number_field :amount_eur %>
<%= form.label :amount_eur, "Amount EUR (cents)" %>
<%= form.number_field :amount_eur %>
</p>
</div>
<div class="field"> <%= form.label :amount_usd, "Amount USD (cents)"%>
<p> <%= form.number_field :amount_usd %>
<%= form.label :amount_usd, "Amount USD (cents)"%>
<%= form.number_field :amount_usd %>
</p>
</div>
<div class="field"> <%= form.label :public_name %>
<p> <%= form.text_field :public_name %>
<%= form.label :public_name %>
<%= form.text_field :public_name %>
</p>
</div>
<div class="field"> <%= form.label :paid_at %>
<p> <%= form.text_field :paid_at %>
<%= form.label :paid_at %> </section>
<%= form.text_field :paid_at %>
</p>
</div>
<p class="mt-8"> <section>
<%= form.submit class: 'btn-md btn-blue' %> <p class="pt-6 border-t border-gray-200 text-right">
</p> <%= link_to 'Cancel',
@donation.id.present? ? admin_donation_path(@donation) : admin_donations_path,
class: 'btn-md btn-gray' %>
<%= form.submit class: 'ml-2 btn-md btn-blue' %>
</p>
</section>
<% end %> <% end %>

View File

@@ -1,12 +1,5 @@
<%= render HeaderComponent.new(title: "Donations") %> <%= render HeaderComponent.new(title: "Donation ##{@donation.id}") %>
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<h2>Editing Donation</h2>
<%= render 'form', donation: @donation, url: admin_donation_path(@donation) %> <%= render 'form', donation: @donation, url: admin_donation_path(@donation) %>
<p class="mt-8">
<%= link_to 'Show', admin_donation_path(@donation), class: 'ks-text-link' %> |
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
<p>
<% end %> <% end %>

View File

@@ -21,7 +21,7 @@
<section> <section>
<% if @donations.any? %> <% if @donations.any? %>
<h3>Recent Donations</h3> <h3>Recent Donations</h3>
<table> <table class="divided mb-8">
<thead> <thead>
<tr> <tr>
<th>User</th> <th>User</th>
@@ -33,11 +33,10 @@
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% @donations.each do |donation| %> <% @donations.each do |donation| %>
<tr> <tr>
<td><%= donation.user.address %></td> <td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td>
<td class="text-right"><%= sats_to_btc donation.amount_sats %></td> <td class="text-right"><%= sats_to_btc donation.amount_sats %></td>
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td> <td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td> <td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
@@ -53,6 +52,7 @@
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<%== pagy_nav @pagy %>
<% else %> <% else %>
<p> <p>
No donations yet. No donations yet.

View File

@@ -1,11 +1,5 @@
<%= render HeaderComponent.new(title: "Donations") %> <%= render HeaderComponent.new(title: "Add Donation") %>
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<h2>New Donation</h2>
<%= render 'form', donation: @donation, url: admin_donations_path %> <%= render 'form', donation: @donation, url: admin_donations_path %>
<p class="mt-8">
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
</p>
<% end %> <% end %>

View File

@@ -1,38 +1,41 @@
<%= render HeaderComponent.new(title: "Donations") %> <%= render HeaderComponent.new(title: "Donation ##{@donation.id}") %>
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<p> <section>
<strong>User:</strong> <table class="w-1/2 divided">
<%= @donation.user.address %> <tbody>
</p> <tr>
<th>User</th>
<td><%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td>
</tr>
<tr>
<th>Amount sats</th>
<td><%= @donation.amount_sats %></td>
</tr>
<tr>
<th>Amount EUR</th>
<td><%= @donation.amount_eur %></td>
</tr>
<tr>
<th>Amount USD</th>
<td><%= @donation.amount_usd %></td>
</tr>
<tr>
<th>Public name</th>
<td><%= @donation.public_name %></td>
</tr>
<tr>
<th>Date</th>
<td><%= @donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
</tbody>
</table>
</section>
<p> <section>
<strong>Amount sats:</strong> <p class="pt-6 border-t border-gray-200 text-right">
<%= @donation.amount_sats %> <%= link_to 'Back', admin_donations_path, class: 'btn-md btn-gray' %>
</p> <%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'ml-2 btn-md btn-blue mr-1' %>
</p>
<p> </section>
<strong>Amount eur:</strong>
<%= @donation.amount_eur %>
</p>
<p>
<strong>Amount usd:</strong>
<%= @donation.amount_usd %>
</p>
<p>
<strong>Public name:</strong>
<%= @donation.public_name %>
</p>
<p>
<strong>Date:</strong>
<%= @donation.paid_at %>
</p>
<p class="mt-8">
<%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'ks-text-link' %> |
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
</p>
<% end %> <% end %>

View File

@@ -24,7 +24,7 @@
<% if @invitations_used.any? %> <% if @invitations_used.any? %>
<section> <section>
<h3>Recently Accepted</h3> <h3>Recently Accepted</h3>
<table> <table class="divided mb-8">
<thead> <thead>
<tr> <tr>
<th>Token</th> <th>Token</th>
@@ -38,12 +38,13 @@
<tr> <tr>
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td> <td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td> <td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
<td><%= invitation.user.address %></td> <td><%= link_to invitation.user.address, admin_user_path(invitation.user.address), class: "ks-text-link" %></td>
<td><%= User.find(invitation.invited_user_id).address %></td> <td><%= link_to invitation.invitee.address, admin_user_path(invitation.invitee.address), class: "ks-text-link" %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<%== pagy_nav @pagy %>
</section> </section>
<% end %> <% end %>
<% end %> <% end %>

View File

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

View File

@@ -0,0 +1,7 @@
<section>
<ul>
<% errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</section>

View File

@@ -4,14 +4,9 @@
<%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %> <%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %>
<section> <section>
<h3>Registrations</h3> <h3>Registrations</h3>
<% if @errors && @errors.any? %> <% if @errors && @errors.any? %>
<div> <%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
<ul>
<% @errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %> <% end %>
<label class="block"> <label class="block">
@@ -22,14 +17,14 @@
<%= f.text_area :reserved_usernames, <%= f.text_area :reserved_usernames,
value: Setting.reserved_usernames.join("\n"), value: Setting.reserved_usernames.join("\n"),
class: "h-44 mb-2" %> class: "h-44 mb-2" %>
<p class="mb-0 text-sm text-gray-500"> <p class="text-sm text-gray-500">
One username per line One username per line
</p> </p>
</label> </label>
</section> </section>
<section> <section>
<p class="mb-0 pt-6 border-t border-gray-200"> <p class="pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %> <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p> </p>
</section> </section>

View File

@@ -0,0 +1,17 @@
<h3>Discourse</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :discourse_enabled,
enabled: Setting.discourse_enabled?,
title: "Enable Discourse integration",
description: "Discourse configuration present and features enabled"
) %>
<% if Setting.discourse_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
<%= f.text_field :discourse_public_url,
value: Setting.discourse_public_url,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>

View File

@@ -0,0 +1,30 @@
<h3>ejabberd (XMPP)</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :ejabberd_enabled,
enabled: Setting.ejabberd_enabled?,
title: "Enable ejabberd integration",
description: "ejabberd configuration present and features enabled"
) %>
<% if Setting.ejabberd_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
<%= f.text_field :ejabberd_api_url,
value: Setting.ejabberd_api_url,
class: "w-full", disabled: true %>
<% end %>
<%= render FormElements::FieldsetComponent.new(title: "Admin URL") do %>
<%= f.text_field :ejabberd_admin_url,
value: Setting.ejabberd_admin_url,
class: "w-full", disabled: true %>
<% end %>
<%= render FormElements::FieldsetComponent.new(
title: "Contact roster name",
description: "Used when exchanging contacts after signup from invitation"
) do %>
<%= f.text_field :ejabberd_buddy_roster,
value: Setting.ejabberd_buddy_roster,
class: "w-full" %>
<% end %>
<% end %>
</ul>

View File

@@ -0,0 +1,17 @@
<h3>Gitea</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :gitea_enabled,
enabled: Setting.gitea_enabled?,
title: "Enable Gitea integration",
description: "Gitea configuration present and features enabled"
) %>
<% if Setting.gitea_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
<%= f.text_field :gitea_public_url,
value: Setting.gitea_public_url,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>

View File

@@ -0,0 +1,38 @@
<h3>Lightning Network</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_enabled,
enabled: Setting.lndhub_enabled?,
title: "Enable LNDHub integration",
description: "LNDHub configuration present and wallet features enabled"
) %>
<% if Setting.lndhub_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
<%= f.text_field :lndhub_api_url,
value: Setting.lndhub_api_url,
class: "w-full", disabled: true %>
<% end %>
<% end %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_admin_enabled,
enabled: Setting.lndhub_admin_enabled?,
title: "Enable LNDHub admin panel",
description: "LNDHub database configuration present and admin panel enabled"
) %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_keysend_enabled,
enabled: Setting.lndhub_keysend_enabled?,
title: "Enable keysend payments",
description: "Allow users to receive invoice-less payments to their Lightning Address"
) %>
<% if Setting.lndhub_keysend_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Public key", description: "The public key of the Lightning node used by LNDHub") do %>
<%= f.text_field :lndhub_public_key,
value: Setting.lndhub_public_key,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>

View File

@@ -0,0 +1,17 @@
<h3>Mastodon</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :mastodon_enabled,
enabled: Setting.mastodon_enabled?,
title: "Enable Mastodon integration",
description: "Mastodon configuration present and features enabled"
) %>
<% if Setting.mastodon_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
<%= f.text_field :mastodon_public_url,
value: Setting.mastodon_public_url,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>

View File

@@ -0,0 +1,17 @@
<h3>MediaWiki</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :mediawiki_enabled,
enabled: Setting.mediawiki_enabled?,
title: "Enable MediaWiki integration",
description: "MediaWiki configuration present and features enabled"
) %>
<% if Setting.mediawiki_enabled? %>
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
<%= f.text_field :mediawiki_public_url,
value: Setting.mediawiki_public_url,
class: "w-full", disabled: true %>
<% end %>
<% end %>
</ul>

View File

@@ -0,0 +1,10 @@
<h3>Nostr</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :nostr_enabled,
enabled: Setting.nostr_enabled?,
title: "Enable Nostr integration (experimental)",
description: "Allow adding nostr pubkeys and resolve user addresses via NIP-05"
) %>
</ul>

View File

@@ -1,39 +1,23 @@
<%= render HeaderComponent.new(title: "Settings") %> <%= render HeaderComponent.new(title: "Settings") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %> <%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
<section> <%= form_for(Setting.new, url: admin_settings_services_path) do |f| %>
<h3>Lightning Network</h3> <%= hidden_field_tag :service, @service %>
<%= form_for(Setting.new, url: admin_settings_services_path) do |f| %>
<% if @errors && @errors.any? %>
<div>
<ul>
<% @errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<ul role="list" class="mt-2 divide-y divide-gray-200"> <% if @errors && @errors.any? %>
<li class="flex items-center justify-between py-6"> <section>
<div class="flex flex-col"> <%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
<label class="font-bold mb-1">Enable LNDHub integration</label> </section>
<p class="text-gray-500 mb-0">LNDHub configuration present and wallet features enabled</p>
</div>
<%= f.check_box :lndhub_enabled, checked: Setting.lndhub_enabled?,
disabled: true,
class: "relative ml-4 inline-flex flex-shrink-0" %>
</li>
<li class="flex items-center justify-between py-6">
<div class="flex flex-col">
<label class="font-bold mb-1">Enable LNDHub admin panel</label>
<p class="text-gray-500 mb-0">LNDHub database configuration present and admin panel enabled</p>
</div>
<%= f.check_box :lndhub_admin_enabled, checked: Setting.lndhub_admin_enabled?,
disabled: true,
class: "relative ml-4 inline-flex flex-shrink-0" %>
</li>
</ul>
<% end %> <% end %>
</section>
<section>
<%= render partial: @service, locals: { f: f } %>
</section>
<section>
<p class="pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p>
</section>
<% end %>
<% end %> <% end %>

View File

@@ -30,25 +30,25 @@
<% end %> <% end %>
<section> <section>
<table> <table class="divided mb-8">
<thead> <thead>
<tr> <tr>
<th>UID</th> <th>UID</th>
<th>E-Mail</th> <th>Status</th>
<th>Admin</th> <th>Roles</th>
<!-- <th>Password</th> --> <!-- <th>Password</th> -->
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% @entries.each do |entry| %> <% @users.each do |user| %>
<tr> <tr>
<td><%= entry[:uid] %></td> <td><%= link_to(user.cn, admin_user_path(user.address), class: 'ks-text-link') %></td>
<td><%= entry[:mail] %></td> <td><%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %></td>
<td><%= entry[:admin] %></td> <td><%= user.is_admin? ? badge("admin", :red) : "" %></td>
<!-- <td><%= entry[:password] %></td> -->
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<%== pagy_nav @pagy %>
</section> </section>
<% end %> <% end %>

View File

@@ -0,0 +1,182 @@
<%= render HeaderComponent.new(title: "User: #{@user.address}") %>
<%= render MainSimpleComponent.new do %>
<div class="mb-12 sm:flex sm:flex-row sm:gap-x-8">
<section class="sm:flex-1">
<h3>Account</h3>
<table class="divided">
<tbody>
<tr>
<th>Created at</th>
<td><%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
<tr>
<th>Confirmed at</th>
<td>
<% if @user.confirmed_at %>
<%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %>
<% else %>
<%= badge "pending", :yellow %>
<% end %>
</td>
</tr>
<tr>
<th>Email</th>
<td><%= @user.email %></td>
</tr>
<tr>
<th>Roles</th>
<td><%= @user.is_admin? ? badge("admin", :red) : "—" %></td>
</tr>
<tr>
<th>Invited by</th>
<td>
<% if @user.inviter %>
<%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %>
<% else %>&mdash;<% end %>
</td>
</tr>
<tr>
<th>Invitations available</th>
<td>
<%= @user.invitations.count %>
</td>
</tr>
<tr>
<th class="align-top">Invited users</th>
<td class="align-top">
<% if @user.invitees.length > 0 %>
<ul class="mb-0">
<% @user.invitees.order(cn: :asc).each do |invitee| %>
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %></li>
<% end %>
</ul>
<% else %>&mdash;<% end %>
</td>
</tr>
</tbody>
</table>
</section>
<section class="sm:flex-1 sm:pt-0">
<!-- <h3>Actions</h3> -->
</section>
</div>
<section>
<h3>Services</h3>
<table class="divided">
<thead>
<tr>
<th>Name</th>
<th>Enabled</th>
<th></th>
</tr>
</thead>
<tbody>
<% if Setting.discourse_enabled %>
<tr>
<td>Discourse</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("discourse"),
input_enabled: false
) %>
</td>
<td class="text-right">
<%= link_to "Open profile", "#{Setting.discourse_public_url}/u/#{@user.cn}/summary", class: "btn-sm btn-gray" %>
</td>
</tr>
<% end %>
<% if Setting.gitea_enabled %>
<tr>
<td>Gitea</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("gitea"),
input_enabled: false
) %>
</td>
<td class="text-right">
<%= link_to "Open profile", "#{Setting.gitea_public_url}/#{@user.cn}", class: "btn-sm btn-gray" %>
</td>
</tr>
<% end %>
<% if Setting.mastodon_enabled %>
<tr>
<td>Mastodon</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("mastodon"),
input_enabled: false
) %>
</td>
<td class="text-right">
<%= link_to "Open profile", "#{Setting.mastodon_public_url}/@#{@user.cn}", class: "btn-sm btn-gray" %>
</td>
</tr>
<% end %>
<% if Setting.mediawiki_enabled %>
<tr>
<td>MediaWiki</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("mediawiki"),
input_enabled: false
) %>
</td>
<td class="text-right">
<%= link_to "Open profile", "#{Setting.mediawiki_public_url}/Special:Contributions/#{@user.cn}", class: "btn-sm btn-gray" %>
</td>
</tr>
<% end %>
<% if Setting.ejabberd_enabled %>
<tr>
<td>XMPP (ejabberd)</td>
<td>
<%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("ejabberd"),
input_enabled: false
) %>
</td>
<td class="text-right">
<% if Setting.ejabberd_admin_url.present? %>
<%= link_to "Open profile", "#{Setting.ejabberd_admin_url}/server/#{@user.ou}/user/#{@user.cn}/", class: "btn-sm btn-gray" %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</section>
<% if Setting.lndhub_admin_enabled? && @user.confirmed? %>
<section>
<h3>LndHub</h3>
<% if @lndhub_user %>
<table>
<thead>
<tr>
<th>Account</th>
<th>Balance</th>
<th>Incoming</th>
<th>Outgoing</th>
<th>Fees</th>
</tr>
</thead>
<tbody>
<tr>
<td><%= @user.ln_account %></td>
<td><%= number_with_delimiter @lndhub_user.balance %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_incoming %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_outgoing %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_fees %> sats</td>
</tr>
</tbody>
</table>
<% else %>
<p>No LndHub user found for account <strong class="font-mono"><%= @user.ln_account %></strong>.
<% end %>
</section>
<% end %>
<% end %>

View File

@@ -0,0 +1 @@
<%= @body %>

View File

@@ -2,60 +2,87 @@
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<section> <section>
<p> <p class="mb-8">
Your Kosmos account and password currently give you access to these Your Kosmos account and password currently give you access to these
services: services:
</p> </p>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 services mt-12"> <div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div> <div class="border border-gray-300 rounded-md hover:border-gray-400
<h3 class="mb-3.5"> bg-cover bg-[center_top_-50px] bg-no-repeat
<%= link_to "Chat", "https://wiki.kosmos.org/Services:Chat", class: "ks-text-link" %> bg-[url(/img/logos/icon_xmpp.svg)]">
</h3> <%= link_to "https://wiki.kosmos.org/Services:Chat",
<p class="text-gray-500"> class: "block h-full px-6 py-6 rounded-md" do %>
Chat rooms and instant messaging (XMPP/Jabber) <h3 class="mb-3.5">Chat</h3>
</p> <p class="text-gray-600">
Federated chat rooms and instant messaging
</p>
<% end %>
</div> </div>
<div> <div class="border border-gray-300 rounded-md hover:border-gray-400
<h3 class="mb-3.5"> bg-[length:95%] bg-center bg-no-repeat
<%= link_to "Discourse", "https://community.kosmos.org", class: "ks-text-link" %> bg-[url(/img/logos/icon_discourse.svg)]">
</h3> <%= link_to "https://community.kosmos.org",
<p class="text-gray-500"> class: "block h-full px-6 py-6 rounded-md" do %>
Kosmos community forums and user support/help site <h3 class="mb-3.5">Discourse</h3>
</p> <p class="text-gray-600">
Kosmos community forums and user support/help site
</p>
<% end %>
</div> </div>
<div> <div class="border border-gray-300 rounded-md hover:border-gray-400
<h3 class="mb-3.5"> bg-cover bg-[center_top_-20px] bg-no-repeat
<%= render partial: "icons/zap", locals: { custom_class: "text-amber-500 h-4 w-4 inline" } %> bg-[url(/img/logos/icon_mediawiki.svg)]">
<%= link_to "Lightning Wallet", wallet_path, class: "ks-text-link" %> <%= link_to "https://wiki.kosmos.org",
</h3> class: "block h-full px-6 py-6 rounded-md" do %>
<p class="text-gray-500"> <h3 class="mb-3.5">Wiki</h3>
Send and receive sats over the Bitcoin Lightning Network <p class="text-gray-600">
</p> Kosmos documentation and knowledge base
</p>
<% end %>
</div> </div>
<div> <div class="border border-gray-300 rounded-md hover:border-gray-400
<h3 class="mb-3.5"> bg-cover bg-center sm:bg-[center_top_-140px] bg-no-repeat
<%= link_to "Wiki", "https://wiki.kosmos.org", class: "ks-text-link" %> bg-[url(/img/logos/icon_lightning.svg)]">
</h3> <%= link_to wallet_path,
<p class="text-gray-500"> class: "block h-full px-6 py-6 rounded-md" do %>
Kosmos documentation and knowledge base <h3 class="mb-3.5">Wallet</h3>
</p> <p class="text-gray-600">
Send and receive sats over the Bitcoin Lightning Network
</p>
<% end %>
</div> </div>
<div> <div class="border border-gray-300 rounded-md hover:border-gray-400
<h3 class="mb-3.5"> bg-cover bg-center bg-no-repeat
<%= link_to "Gitea", "https://gitea.kosmos.org", class: "ks-text-link" %> bg-[url(/img/logos/icon_gitea.png)]">
</h3> <%= link_to "https://gitea.kosmos.org",
<p class="text-gray-500"> class: "block h-full px-6 py-6 rounded-md" do %>
Code hosting and collaboration for software projects <h3 class="mb-3.5">Gitea</h3>
</p> <p class="text-gray-600">
Code hosting and collaboration for software projects
</p>
<% end %>
</div> </div>
<div> <div class="border border-gray-300 rounded-md hover:border-gray-400
<h3 class="mb-3.5"> bg-cover bg-[center_top_-70px] bg-no-repeat
<%= link_to "Drone CI", "https://drone.kosmos.org", class: "ks-text-link" %> bg-[url(/img/logos/icon_droneci.svg)]">
</h3> <%= link_to "https://drone.kosmos.org",
<p class="text-gray-500"> class: "block h-full px-6 py-6 rounded-md" do %>
Continuous integration for software projects on Gitea <h3 class="mb-3.5">Drone CI</h3>
</p> <p class="text-gray-600">
Continuous integration for software projects on Gitea
</p>
<% end %>
</div> </div>
<!-- <div class="border border&#45;gray&#45;300 rounded&#45;md hover:border&#45;gray&#45;400 -->
<!-- bg&#45;[length:80%] bg&#45;[right_top_&#45;30px] bg&#45;no&#45;repeat -->
<!-- bg&#45;[url(/img/logos/icon_mastodon.svg)]"> -->
<!-- <%= link_to "https://kosmos.social", class: "block h&#45;full px&#45;6 py&#45;6 rounded&#45;md" do %> -->
<!-- <h3 class="mb&#45;3.5">Mastodon</h3> -->
<!-- <p class="text&#45;gray&#45;400"> -->
<!-- Your account on the Open Social Web -->
<!-- </p> -->
<!-- <% end %> -->
<!-- </div> -->
</div> </div>
</section> </section>
<% end %> <% end %>

View File

@@ -6,7 +6,7 @@
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %> <%= render "devise/shared/error_messages", resource: resource %>
<p> <p>
<%= f.label :email, 'Email address', class: 'block mb-1 w-full' %> <%= f.label :email, 'Email address', class: 'block mb-2 font-bold' %>
<%= f.email_field :email, <%= f.email_field :email,
required: true, autofocus: true, autocomplete: "email", required: true, autofocus: true, autocomplete: "email",
value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email), value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email),
@@ -14,7 +14,7 @@
</p> </p>
<p class="mt-8"> <p class="mt-8">
<%= f.submit "Resend confirmation link", <%= f.submit "Resend confirmation link",
class: 'btn-md btn-blue w-full sm:w-auto' %> class: 'btn-md btn-blue w-full' %>
</p> </p>
<% end %> <% end %>

View File

@@ -5,19 +5,21 @@
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %> <%= render "devise/shared/error_messages", resource: resource %>
<div class="mb-6">
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
<p class="flex gap-2 items-center">
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
required: true, class: "relative grow"%>
<span class="relative shrink-0 text-gray-500">@ kosmos.org</span>
</p>
</div>
<p> <p>
<%= f.label :cn, 'User', class: 'block' %> <%= f.label :email, 'Email address', class: 'block mb-2 font-bold' %>
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
required: true, class: "w-full md:w-3/5"%>
<span class="ml-1 text-gray-500">@ kosmos.org</span>
</p>
<p>
<%= f.label :email, 'Email address', class: 'block' %>
<%= f.email_field :email, autocomplete: "email", required: true, <%= f.email_field :email, autocomplete: "email", required: true,
class: "w-full md:w-3/5"%> class: "w-full"%>
</p> </p>
<p class="mt-8"> <p class="mt-8">
<%= f.submit "Send me a reset link", class: 'btn-md btn-blue w-full sm:w-auto' %> <%= f.submit "Send me a reset link", class: 'btn-md btn-blue w-full' %>
</p> </p>
<% end %> <% end %>

View File

@@ -3,21 +3,47 @@
<%= render MainCompactComponent.new do %> <%= render MainCompactComponent.new do %>
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %> <%= render "devise/shared/error_messages", resource: resource %>
<p> <div class="mb-6">
<%= f.label :cn, 'User', class: 'block' %> <%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
<%= f.text_field :cn, autofocus: true, autocomplete: "username", <p class="flex gap-2 items-center">
class: "w-full md:w-3/5"%> <%= f.text_field :cn, autofocus: true, autocomplete: "username",
<span class="ml-1 text-gray-500">@ kosmos.org</span> required: true, class: "relative grow", tabindex: "1" %>
</p> <span class="relative shrink-0 text-gray-500">@ kosmos.org</span>
<p> </p>
<%= f.label :password, class: 'block' %> </div>
<p class="mb-8">
<%= f.label :password, class: 'block mb-2 font-bold' %>
<%= f.password_field :password, autocomplete: "current-password", <%= f.password_field :password, autocomplete: "current-password",
class: "w-full md:w-3/5"%> required: true, class: "w-full", tabindex: "2" %>
</p> </p>
<p class="mt-8">
<%= f.submit "Log in", class: 'btn-md btn-blue w-full sm:w-auto' %> <%= tag.div class: "flex items-center mb-8 gap-x-3", data: {
controller: "settings--toggle",
:'settings--toggle-switch-enabled-value' => "false"
} do %>
<div class="relative inline-flex flex-shrink-0">
<%= render FormElements::ToggleComponent.new(
enabled: false, input_enabled: true, class_names: "hidden",
tabindex: "3", data: {
:'settings--toggle-target' => "button",
action: "settings--toggle#toggleSwitch"
}) %>
<%= f.check_box :remember_me, {
checked: false,
data: { :'settings--toggle-target' => "checkbox" }
}, "true", "false" %>
</div>
<%= f.label :remember_me,
class: "text-gray-500 flex flex-col",
data: { action: "click->settings--toggle#toggleSwitch" } %>
<p class="grow text-sm text-right">
<%= link_to "Forgot your password?", new_password_path(resource_name),
class: "text-gray-500 underline" %><br />
</p>
<% end %>
<p>
<%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %>
</p> </p>
<% end %> <% end %>
<%= render "devise/shared/links" %>
<% end %> <% end %>

View File

@@ -1,25 +1,29 @@
<div class="devise-links mt-8 text-sm"> <div class="devise-links mt-8 text-sm">
<%- if controller_name != 'sessions' %> <%- if controller_name != 'sessions' %>
<p class="mb-1.5"> <p class="mb-2">
<%= link_to "Log in", new_session_path(resource_name) %><br /> <%= link_to "Log in", new_session_path(resource_name),
class: "text-gray-500 underline" %>
</p> </p>
<% end %> <% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<p class="mb-1.5"> <p class="mb-2">
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br /> <%= link_to "Forgot your password?", new_password_path(resource_name),
class: "text-gray-500 underline" %>
</p> </p>
<% end %> <% end %>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> <%- if devise_mapping.confirmable? && !controller_name.match(/^(confirmations|sessions)$/) %>
<p class="mb-1.5"> <p class="mb-2">
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br /> <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name),
class: "text-gray-500 underline" %>
</p> </p>
<% end %> <% end %>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<p class="mb-1.5"> <p class="mb-2">
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br /> <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name),
class: "text-gray-500 underline" %>
</p> </p>
<% end %> <% end %>
</div> </div>

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x <%= custom_class %>"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 320 B

View File

@@ -6,14 +6,13 @@
<p class="mb-8"> <p class="mb-8">
Invite your friends to a Kosmos account by sharing an invitation URL with them: Invite your friends to a Kosmos account by sharing an invitation URL with them:
</p> </p>
<ul> <ul class="md:w-3/4">
<% @invitations_unused.each do |invitation| %> <% @invitations_unused.each do |invitation| %>
<li class="font-mono mb-1 flex gap-1 md:block" <li class="font-mono mb-2 flex gap-1" data-controller="clipboard">
data-controller="clipboard"> <input type="text" disabled class="relative grow"
<input type="text" disabled class="md:w-3/4 flex-1"
value="<%= invitation_url(invitation.token) %>" value="<%= invitation_url(invitation.token) %>"
data-clipboard-target="source" /> data-clipboard-target="source" />
<button id="copy-user-address" class="btn-md btn-icon btn-blue flex-none w-auto" <button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0 w-auto"
data-clipboard-target="trigger" data-action="clipboard#copy" data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard"> title="Copy to clipboard">
<span class="content-initial"> <span class="content-initial">

View File

@@ -2,9 +2,7 @@
<html> <html>
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style> <style></style>
/* Email styles need to be inline */
</style>
</head> </head>
<body> <body>

View File

@@ -1,6 +1,16 @@
<%= render HeaderComponent.new(title: "Settings") %> <%= render HeaderComponent.new(title: "Settings") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %> <%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %>
<section>
<h3>E-Mail</h3>
<p class="mb-2">
<%= label :email, 'Address', class: 'font-bold' %>
</p>
<p class="flex gap-1 mb-2 sm:w-3/5">
<input type="text" id="email" class="grow"
value=<%= current_user.email %> disabled="disabled" />
</p>
</section>
<section> <section>
<h3>Password</h3> <h3>Password</h3>
<p class="mb-8">Use the following button to request an email with a password reset link:</p> <p class="mb-8">Use the following button to request an email with a password reset link:</p>

View File

@@ -3,14 +3,14 @@
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %> <%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %>
<section> <section>
<h3>Profile</h3> <h3>Profile</h3>
<p class="mb-1"> <p class="mb-2">
<%= label :user_address, 'User address', class: 'font-bold' %> <%= label :user_address, 'User address', class: 'font-bold' %>
</p> </p>
<p data-controller="clipboard" class="flex gap-1 mb-2 sm:block"> <p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
<input type="text" id="user_address" class="flex-1 sm:w-3/5" <input type="text" id="user_address" class="grow"
value=<%= @user.address %> disabled="disabled" value=<%= @user.address %> disabled="disabled"
data-clipboard-target="source" /> data-clipboard-target="source" />
<button id="copy-user-address" class="btn-md btn-icon btn-blue flex-none w-auto" <button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy" data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard"> title="Copy to clipboard">
<span class="content-initial"> <span class="content-initial">

View File

@@ -6,6 +6,9 @@
name: "Services", path: admin_settings_services_path, icon: "grid", name: "Services", path: admin_settings_services_path, icon: "grid",
active: current_page?(admin_settings_services_path) active: current_page?(admin_settings_services_path)
) %> ) %>
<% if current_page?(admin_settings_services_path) %>
<%= render partial: "shared/admin_sidenav_settings_services" %>
<% end %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
name: "Security", path: "#", icon: "shield", disabled: true name: "Security", path: "#", icon: "shield", disabled: true
) %> ) %>

View File

@@ -0,0 +1,49 @@
<%= render SidenavLinkComponent.new(
level: 2,
name: "Discourse",
path: admin_settings_services_path(params: { s: "discourse" }),
icon: Setting.discourse_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "discourse" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "ejabberd",
path: admin_settings_services_path(params: { s: "ejabberd" }),
icon: Setting.ejabberd_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "ejabberd" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "Gitea",
path: admin_settings_services_path(params: { s: "gitea" }),
icon: Setting.gitea_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "gitea" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "LNDHub",
path: admin_settings_services_path(params: { s: "lndhub" }),
icon: Setting.lndhub_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "lndhub" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "Mastodon",
path: admin_settings_services_path(params: { s: "mastodon" }),
icon: Setting.mastodon_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "mastodon" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "MediaWiki",
path: admin_settings_services_path(params: { s: "mediawiki" }),
icon: Setting.mediawiki_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "mediawiki" })),
) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "Nostr",
path: admin_settings_services_path(params: { s: "nostr" }),
icon: Setting.nostr_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "nostr" })),
) %>

View File

@@ -11,6 +11,6 @@
</p> </p>
<p class="mt-12"> <p class="mt-12">
<%= link_to "Get started", signup_steps_path(1), <%= link_to "Get started", signup_steps_path(1),
class: "btn-md btn-blue block w-full md:inline-block sm:w-auto" %> class: "btn-md btn-blue block w-full" %>
</p> </p>
<% end %> <% end %>

View File

@@ -5,19 +5,20 @@
when 1 %> when 1 %>
<h2>Choose a username</h2> <h2>Choose a username</h2>
<%= form_for @user, :url => signup_validate_url do |f| %> <%= form_for @user, :url => signup_validate_url do |f| %>
<p> <div class="mb-6">
<%= f.label :cn, 'Username', class: 'hidden' %> <p class="flex gap-2 items-center">
<%= f.text_field :cn, autofocus: true, autocomplete: "username", <%= f.text_field :cn, autofocus: true, autocomplete: "username",
class: 'text-xl w-full md:w-3/5 mb-1' %> required: true, class: "relative grow text-xl"%>
<span class="text-base md:text-xl text-gray-500 ml-1">@</span> <span class="relative shrink-0 text-gray-500 md:text-xl">
<span class="text-base md:text-xl text-gray-500">kosmos.org</span> @ kosmos.org
</p> </span>
</p>
</div>
<% if @validation_error.present? %> <% if @validation_error.present? %>
<p class="error-msg">Username <%= @validation_error %></p> <p class="error-msg">Username <%= @validation_error %></p>
<% end %> <% end %>
<p class="mt-12"> <p class="mt-12">
<%= f.submit "Continue", <%= f.submit "Continue", class: "btn-md btn-blue block w-full" %>
class: "btn-md btn-blue block w-full md:inline-block sm:w-auto" %>
</p> </p>
<% end %> <% end %>
@@ -27,14 +28,13 @@
<p> <p>
<%= f.label :email, 'Email address', class: 'hidden' %> <%= f.label :email, 'Email address', class: 'hidden' %>
<%= f.email_field :email, autofocus: true, autocomplete: 'email', <%= f.email_field :email, autofocus: true, autocomplete: 'email',
class: 'text-xl w-full' %> required: true, class: 'text-xl w-full' %>
</p> </p>
<% if @validation_error.present? %> <% if @validation_error.present? %>
<p class="error-msg">Email <%= @validation_error %></p> <p class="error-msg">Email <%= @validation_error %></p>
<% end %> <% end %>
<p class="mt-12"> <p class="mt-12">
<%= f.submit "Continue", <%= f.submit "Continue", class: "btn-md btn-blue block w-full" %>
class: "btn-md btn-blue block w-full md:inline-block sm:w-auto" %>
</p> </p>
<% end %> <% end %>
@@ -44,8 +44,7 @@
<%= form_for @user, :url => signup_validate_url do |f| %> <%= form_for @user, :url => signup_validate_url do |f| %>
<p> <p>
<%= f.label :password, 'Password', class: 'hidden' %> <%= f.label :password, 'Password', class: 'hidden' %>
<%= f.password_field :password, autofocus: true, <%= f.password_field :password, autofocus: true, class: 'text-xl w-full' %>
class: 'text-xl w-full' %>
</p> </p>
<% if @validation_error.present? %> <% if @validation_error.present? %>
<p class="error-msg">Password <%= @validation_error %></p> <p class="error-msg">Password <%= @validation_error %></p>
@@ -55,8 +54,7 @@
and Privacy Policy. Don't worry, they will be excellent! and Privacy Policy. Don't worry, they will be excellent!
</p> </p>
<p class="mt-8"> <p class="mt-8">
<%= f.submit "Create account", <%= f.submit "Create account", class: "btn-md btn-blue block w-full" %>
class: "btn-md btn-blue block w-full sm:inline-block sm:w-auto" %>
</p> </p>
<% end %> <% end %>
<% end %> <% end %>

View File

@@ -8,7 +8,7 @@ require "active_record/railtie"
# require "active_storage/engine" # require "active_storage/engine"
require "action_controller/railtie" require "action_controller/railtie"
require "action_mailer/railtie" require "action_mailer/railtie"
# require "action_mailbox/engine" require "action_mailbox/engine"
# require "action_text/engine" # require "action_text/engine"
require "action_view/railtie" require "action_view/railtie"
require "action_cable/engine" require "action_cable/engine"

View File

@@ -57,10 +57,14 @@ Rails.application.configure do
# routes, locales, etc. This feature depends on the listen gem. # routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker config.file_watcher = ActiveSupport::EventedFileUpdateChecker
config.action_mailer.default_options = {
from: "accounts@localhost"
}
# Don't actually send emails, cache them for viewing via letter opener # Don't actually send emails, cache them for viewing via letter opener
config.action_mailer.delivery_method = :letter_opener config.action_mailer.delivery_method = :letter_opener
# Don't care if the mailer can't send # Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = true
# Base URL to be used by email template link helpers # Base URL to be used by email template link helpers
config.action_mailer.default_url_options = { host: "localhost:3000", protocol: "http" } config.action_mailer.default_url_options = { host: "localhost:3000", protocol: "http" }

View File

@@ -57,28 +57,53 @@ Rails.application.configure do
# config.active_job.queue_adapter = :resque # config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "akkounts_production" # config.active_job.queue_name_prefix = "akkounts_production"
config.action_mailer.perform_caching = false # E-mail settings, adapted from https://github.com/mastodon/mastodon
config.action_mailer.delivery_method = :smtp outgoing_email_address = ENV.fetch('SMTP_FROM_ADDRESS', 'accounts@localhost')
outgoing_email_domain = Mail::Address.new(outgoing_email_address).domain
config.action_mailer.default_options = {
from: outgoing_email_address,
message_id: -> { "<#{Mail.random_tag}@#{outgoing_email_domain}>" },
}
config.action_mailer.default_options[:reply_to] = ENV['SMTP_REPLY_TO'] if ENV['SMTP_REPLY_TO'].present?
config.action_mailer.default_options[:return_path] = ENV['SMTP_RETURN_PATH'] if ENV['SMTP_RETURN_PATH'].present?
enable_starttls = nil
enable_starttls_auto = nil
case ENV['SMTP_ENABLE_STARTTLS']
when 'always'
enable_starttls = true
when 'never'
enable_starttls = false
when 'auto'
enable_starttls_auto = true
else
enable_starttls_auto = ENV['SMTP_ENABLE_STARTTLS_AUTO'] != 'false'
end
config.action_mailer.smtp_settings = { config.action_mailer.smtp_settings = {
address: "mail.gandi.net", port: ENV['SMTP_PORT'],
port: "587", address: ENV['SMTP_SERVER'],
authentication: "plain", user_name: ENV['SMTP_LOGIN'].presence,
enable_starttls_auto: true, password: ENV['SMTP_PASSWORD'].presence,
user_name: Rails.application.credentials.smtp[:username], domain: ENV['SMTP_DOMAIN'] || ENV['LOCAL_DOMAIN'],
password: Rails.application.credentials.smtp[:password] authentication: ENV['SMTP_AUTH_METHOD'] == 'none' ? nil : ENV['SMTP_AUTH_METHOD'] || :plain,
ca_file: ENV['SMTP_CA_FILE'].presence || '/etc/ssl/certs/ca-certificates.crt',
openssl_verify_mode: ENV['SMTP_OPENSSL_VERIFY_MODE'],
enable_starttls: enable_starttls,
enable_starttls_auto: enable_starttls_auto,
tls: ENV['SMTP_TLS'].presence && ENV['SMTP_TLS'] == 'true',
ssl: ENV['SMTP_SSL'].presence && ENV['SMTP_SSL'] == 'true',
} }
config.action_mailer.default_url_options = { config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym
host: "accounts.kosmos.org",
protocol: "https",
from: "accounts@kosmos.org"
}
# Ignore bad email addresses and do not raise email delivery errors. # Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = true
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found). # the I18n.default_locale when a translation cannot be found).

View File

@@ -3,30 +3,12 @@ require 'digest'
require 'securerandom' require 'securerandom'
# frozen_string_literal: true # frozen_string_literal: true
# Create custom failure for turbo
class TurboFailureApp < Devise::FailureApp
def respond
if request_format == :turbo_stream
redirect
else
super
end
end
def skip_format?
%w(html turbo_stream */*).include? request_format.to_s
end
end
# Assuming you have not yet modified this file, each configuration option below
# is set to its default value. Note that some are commented out while others
# are not: uncommented lines are intended to protect your configuration from
# breaking changes in upgrades (i.e., in the event that future versions of
# Devise change the default values for those options).
#
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model. # Many of these configuration options can be set straight in your model.
Devise.setup do |config| Devise.setup do |config|
# Hotwire/Turbo
config.responder.error_status = :unprocessable_entity
config.responder.redirect_status = :see_other
# ==> LDAP Configuration # ==> LDAP Configuration
config.ldap_logger = true config.ldap_logger = true
config.ldap_create_user = true config.ldap_create_user = true
@@ -59,7 +41,6 @@ Devise.setup do |config|
# ==> Controller configuration # ==> Controller configuration
# Configure the parent class to the devise controllers. # Configure the parent class to the devise controllers.
# config.parent_controller = 'DeviseController' # config.parent_controller = 'DeviseController'
config.parent_controller = 'TurboController'
# ==> Mailer Configuration # ==> Mailer Configuration
# Configure the e-mail address which will be shown in Devise::Mailer, # Configure the e-mail address which will be shown in Devise::Mailer,
@@ -205,13 +186,13 @@ Devise.setup do |config|
# ==> Configuration for :rememberable # ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again. # The time the user will be remembered without asking for credentials again.
# config.remember_for = 2.weeks config.remember_for = 2.weeks
# Invalidates all the remember me tokens when the user signs out. # Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true config.expire_all_remember_me_on_sign_out = true
# If true, extends the user's remember period when remembered via cookie. # If true, extends the user's remember period when remembered via cookie.
# config.extend_remember_period = false config.extend_remember_period = true
# Options to be passed to the created cookie. For instance, you can set # Options to be passed to the created cookie. For instance, you can set
# secure: true in order to force SSL only cookies. # secure: true in order to force SSL only cookies.
@@ -229,7 +210,7 @@ Devise.setup do |config|
# ==> Configuration for :timeoutable # ==> Configuration for :timeoutable
# The time you want to timeout the user session without activity. After this # The time you want to timeout the user session without activity. After this
# time the user will be asked for credentials again. Default is 30 minutes. # time the user will be asked for credentials again. Default is 30 minutes.
# config.timeout_in = 30.minutes config.timeout_in = 30.minutes
# ==> Configuration for :lockable # ==> Configuration for :lockable
# Defines which strategy will be used to lock an account. # Defines which strategy will be used to lock an account.
@@ -319,11 +300,10 @@ Devise.setup do |config|
# If you want to use other strategies, that are not supported by Devise, or # If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block. # change the failure app, you can configure them inside the config.warden block.
# #
config.warden do |manager| # config.warden do |manager|
manager.failure_app = TurboFailureApp
# manager.intercept_401 = false # manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy # manager.default_strategies(scope: :user).unshift :some_external_strategy
end # end
# ==> Mountable engine configurations # ==> Mountable engine configurations
# When using Devise inside an engine, let's call it `MyEngine`, and this engine # When using Devise inside an engine, let's call it `MyEngine`, and this engine
@@ -339,13 +319,6 @@ Devise.setup do |config|
# so you need to do it manually. For the users scope, it would be: # so you need to do it manually. For the users scope, it would be:
# config.omniauth_path_prefix = '/my_engine/users/auth' # config.omniauth_path_prefix = '/my_engine/users/auth'
# ==> Turbolinks configuration
# If your app is using Turbolinks, Turbolinks::Controller needs to be included to make redirection work correctly:
#
# ActiveSupport.on_load(:devise_failure_app) do
# include Turbolinks::Controller
# end
# ==> Configuration for :registerable # ==> Configuration for :registerable
# When set to false, does not sign a user in automatically after their password is # When set to false, does not sign a user in automatically after their password is

250
config/initializers/pagy.rb Normal file
View File

@@ -0,0 +1,250 @@
# frozen_string_literal: true
# Pagy initializer file (6.0.2)
# Customize only what you really need and notice that the core Pagy works also without any of the following lines.
# Should you just cherry pick part of this file, please maintain the require-order of the extras
# Pagy DEFAULT Variables
# See https://ddnexus.github.io/pagy/docs/api/pagy#variables
# All the Pagy::DEFAULT are set for all the Pagy instances but can be overridden per instance by just passing them to
# Pagy.new|Pagy::Countless.new|Pagy::Calendar::*.new or any of the #pagy* controller methods
# Instance variables
# See https://ddnexus.github.io/pagy/docs/api/pagy#instance-variables
# Pagy::DEFAULT[:page] = 1 # default
# Pagy::DEFAULT[:items] = 20 # default
# Pagy::DEFAULT[:outset] = 0 # default
# Other Variables
# See https://ddnexus.github.io/pagy/docs/api/pagy#other-variables
# Pagy::DEFAULT[:size] = [1,4,4,1] # default
# Pagy::DEFAULT[:page_param] = :page # default
# The :params can be also set as a lambda e.g ->(params){ params.exclude('useless').merge!('custom' => 'useful') }
# Pagy::DEFAULT[:params] = {} # default
# Pagy::DEFAULT[:fragment] = '#fragment' # example
# Pagy::DEFAULT[:link_extra] = 'data-remote="true"' # example
# Pagy::DEFAULT[:i18n_key] = 'pagy.item_name' # default
# Pagy::DEFAULT[:cycle] = true # example
# Pagy::DEFAULT[:request_path] = "/foo" # example
# Extras
# See https://ddnexus.github.io/pagy/categories/extra
# Backend Extras
# Arel extra: For better performance utilizing grouped ActiveRecord collections:
# See: https://ddnexus.github.io/pagy/docs/extras/arel
# require 'pagy/extras/arel'
# Array extra: Paginate arrays efficiently, avoiding expensive array-wrapping and without overriding
# See https://ddnexus.github.io/pagy/docs/extras/array
# require 'pagy/extras/array'
# Calendar extra: Add pagination filtering by calendar time unit (year, quarter, month, week, day)
# See https://ddnexus.github.io/pagy/docs/extras/calendar
# require 'pagy/extras/calendar'
# Default for each unit
# Pagy::Calendar::Year::DEFAULT[:order] = :asc # Time direction of pagination
# Pagy::Calendar::Year::DEFAULT[:format] = '%Y' # strftime format
#
# Pagy::Calendar::Quarter::DEFAULT[:order] = :asc # Time direction of pagination
# Pagy::Calendar::Quarter::DEFAULT[:format] = '%Y-Q%q' # strftime format
#
# Pagy::Calendar::Month::DEFAULT[:order] = :asc # Time direction of pagination
# Pagy::Calendar::Month::DEFAULT[:format] = '%Y-%m' # strftime format
#
# Pagy::Calendar::Week::DEFAULT[:order] = :asc # Time direction of pagination
# Pagy::Calendar::Week::DEFAULT[:format] = '%Y-%W' # strftime format
#
# Pagy::Calendar::Day::DEFAULT[:order] = :asc # Time direction of pagination
# Pagy::Calendar::Day::DEFAULT[:format] = '%Y-%m-%d' # strftime format
#
# Uncomment the following lines, if you need calendar localization without using the I18n extra
# module LocalizePagyCalendar
# def localize(time, opts)
# ::I18n.l(time, **opts)
# end
# end
# Pagy::Calendar.prepend LocalizePagyCalendar
# Countless extra: Paginate without any count, saving one query per rendering
# See https://ddnexus.github.io/pagy/docs/extras/countless
# require 'pagy/extras/countless'
# Pagy::DEFAULT[:countless_minimal] = false # default (eager loading)
# Elasticsearch Rails extra: Paginate `ElasticsearchRails::Results` objects
# See https://ddnexus.github.io/pagy/docs/extras/elasticsearch_rails
# Default :pagy_search method: change only if you use also
# the searchkick or meilisearch extra that defines the same
# Pagy::DEFAULT[:elasticsearch_rails_pagy_search] = :pagy_search
# Default original :search method called internally to do the actual search
# Pagy::DEFAULT[:elasticsearch_rails_search] = :search
# require 'pagy/extras/elasticsearch_rails'
# Headers extra: http response headers (and other helpers) useful for API pagination
# See http://ddnexus.github.io/pagy/extras/headers
# require 'pagy/extras/headers'
# Pagy::DEFAULT[:headers] = { page: 'Current-Page',
# items: 'Page-Items',
# count: 'Total-Count',
# pages: 'Total-Pages' } # default
# Meilisearch extra: Paginate `Meilisearch` result objects
# See https://ddnexus.github.io/pagy/docs/extras/meilisearch
# Default :pagy_search method: change only if you use also
# the elasticsearch_rails or searchkick extra that define the same method
# Pagy::DEFAULT[:meilisearch_pagy_search] = :pagy_search
# Default original :search method called internally to do the actual search
# Pagy::DEFAULT[:meilisearch_search] = :ms_search
# require 'pagy/extras/meilisearch'
# Metadata extra: Provides the pagination metadata to Javascript frameworks like Vue.js, react.js, etc.
# See https://ddnexus.github.io/pagy/docs/extras/metadata
# you must require the frontend helpers internal extra (BEFORE the metadata extra) ONLY if you need also the :sequels
# require 'pagy/extras/frontend_helpers'
# require 'pagy/extras/metadata'
# For performance reasons, you should explicitly set ONLY the metadata you use in the frontend
# Pagy::DEFAULT[:metadata] = %i[scaffold_url page prev next last] # example
# Searchkick extra: Paginate `Searchkick::Results` objects
# See https://ddnexus.github.io/pagy/docs/extras/searchkick
# Default :pagy_search method: change only if you use also
# the elasticsearch_rails or meilisearch extra that defines the same
# DEFAULT[:searchkick_pagy_search] = :pagy_search
# Default original :search method called internally to do the actual search
# Pagy::DEFAULT[:searchkick_search] = :search
# require 'pagy/extras/searchkick'
# uncomment if you are going to use Searchkick.pagy_search
# Searchkick.extend Pagy::Searchkick
# Frontend Extras
# Bootstrap extra: Add nav, nav_js and combo_nav_js helpers and templates for Bootstrap pagination
# See https://ddnexus.github.io/pagy/docs/extras/bootstrap
# require 'pagy/extras/bootstrap'
# Bulma extra: Add nav, nav_js and combo_nav_js helpers and templates for Bulma pagination
# See https://ddnexus.github.io/pagy/docs/extras/bulma
# require 'pagy/extras/bulma'
# Foundation extra: Add nav, nav_js and combo_nav_js helpers and templates for Foundation pagination
# See https://ddnexus.github.io/pagy/docs/extras/foundation
# require 'pagy/extras/foundation'
# Materialize extra: Add nav, nav_js and combo_nav_js helpers for Materialize pagination
# See https://ddnexus.github.io/pagy/docs/extras/materialize
# require 'pagy/extras/materialize'
# Navs extra: Add nav_js and combo_nav_js javascript helpers
# Notice: the other frontend extras add their own framework-styled versions,
# so require this extra only if you need the unstyled version
# See https://ddnexus.github.io/pagy/docs/extras/navs
# require 'pagy/extras/navs'
# Semantic extra: Add nav, nav_js and combo_nav_js helpers for Semantic UI pagination
# See https://ddnexus.github.io/pagy/docs/extras/semantic
# require 'pagy/extras/semantic'
# UIkit extra: Add nav helper and templates for UIkit pagination
# See https://ddnexus.github.io/pagy/docs/extras/uikit
# require 'pagy/extras/uikit'
# Multi size var used by the *_nav_js helpers
# See https://ddnexus.github.io/pagy/docs/extras/navs#steps
# Pagy::DEFAULT[:steps] = { 0 => [2,3,3,2], 540 => [3,5,5,3], 720 => [5,7,7,5] } # example
# Feature Extras
# Gearbox extra: Automatically change the number of items per page depending on the page number
# See https://ddnexus.github.io/pagy/docs/extras/gearbox
# require 'pagy/extras/gearbox'
# set to false only if you want to make :gearbox_extra an opt-in variable
# Pagy::DEFAULT[:gearbox_extra] = false # default true
# Pagy::DEFAULT[:gearbox_items] = [15, 30, 60, 100] # default
# Items extra: Allow the client to request a custom number of items per page with an optional selector UI
# See https://ddnexus.github.io/pagy/docs/extras/items
# require 'pagy/extras/items'
# set to false only if you want to make :items_extra an opt-in variable
# Pagy::DEFAULT[:items_extra] = false # default true
# Pagy::DEFAULT[:items_param] = :items # default
# Pagy::DEFAULT[:max_items] = 100 # default
# Overflow extra: Allow for easy handling of overflowing pages
# See https://ddnexus.github.io/pagy/docs/extras/overflow
# require 'pagy/extras/overflow'
# Pagy::DEFAULT[:overflow] = :empty_page # default (other options: :last_page and :exception)
# Support extra: Extra support for features like: incremental, infinite, auto-scroll pagination
# See https://ddnexus.github.io/pagy/docs/extras/support
# require 'pagy/extras/support'
# Trim extra: Remove the page=1 param from links
# See https://ddnexus.github.io/pagy/docs/extras/trim
# require 'pagy/extras/trim'
# set to false only if you want to make :trim_extra an opt-in variable
# Pagy::DEFAULT[:trim_extra] = false # default true
# Standalone extra: Use pagy in non Rack environment/gem
# See https://ddnexus.github.io/pagy/docs/extras/standalone
# require 'pagy/extras/standalone'
# Pagy::DEFAULT[:url] = 'http://www.example.com/subdir' # optional default
# Rails
# Enable the .js file required by the helpers that use javascript
# (pagy*_nav_js, pagy*_combo_nav_js, and pagy_items_selector_js)
# See https://ddnexus.github.io/pagy/docs/api/javascript
# With the asset pipeline
# Sprockets need to look into the pagy javascripts dir, so add it to the assets paths
# Rails.application.config.assets.paths << Pagy.root.join('javascripts')
# I18n
# Pagy internal I18n: ~18x faster using ~10x less memory than the i18n gem
# See https://ddnexus.github.io/pagy/docs/api/i18n
# Notice: No need to configure anything in this section if your app uses only "en"
# or if you use the i18n extra below
#
# Examples:
# load the "de" built-in locale:
# Pagy::I18n.load(locale: 'de')
#
# load the "de" locale defined in the custom file at :filepath:
# Pagy::I18n.load(locale: 'de', filepath: 'path/to/pagy-de.yml')
#
# load the "de", "en" and "es" built-in locales:
# (the first passed :locale will be used also as the default_locale)
# Pagy::I18n.load({ locale: 'de' },
# { locale: 'en' },
# { locale: 'es' })
#
# load the "en" built-in locale, a custom "es" locale,
# and a totally custom locale complete with a custom :pluralize proc:
# (the first passed :locale will be used also as the default_locale)
# Pagy::I18n.load({ locale: 'en' },
# { locale: 'es', filepath: 'path/to/pagy-es.yml' },
# { locale: 'xyz', # not built-in
# filepath: 'path/to/pagy-xyz.yml',
# pluralize: lambda{ |count| ... } )
# I18n extra: uses the standard i18n gem which is ~18x slower using ~10x more memory
# than the default pagy internal i18n (see above)
# See https://ddnexus.github.io/pagy/docs/extras/i18n
# require 'pagy/extras/i18n'
# Default i18n key
# Pagy::DEFAULT[:i18n_key] = 'pagy.item_name' # default
# When you are done setting your own default freeze it, so it will not get changed accidentally
Pagy::DEFAULT.freeze

View File

@@ -0,0 +1,9 @@
if ENV["SENTRY_DSN"].present?
Sentry.init do |config|
config.dsn = ENV["SENTRY_DSN"]
config.breadcrumbs_logger = [:active_support_logger, :http_logger]
config.traces_sampler = lambda do |context|
true
end
end
end

View File

@@ -0,0 +1,5 @@
require_relative "../../app/models/setting"
Sidekiq.configure_server do |config|
config.redis = { url: Setting.redis_url }
end

View File

@@ -1,7 +1,7 @@
require 'sidekiq/web' require 'sidekiq/web'
Rails.application.routes.draw do Rails.application.routes.draw do
devise_for :users devise_for :users, controllers: { confirmations: "users/confirmations" }
get 'welcome', to: 'welcome#index' get 'welcome', to: 'welcome#index'
get 'check_your_email', to: 'welcome#check_your_email' get 'check_your_email', to: 'welcome#check_your_email'
@@ -28,8 +28,12 @@ Rails.application.routes.draw do
get 'wallet', to: 'wallet#index' get 'wallet', to: 'wallet#index'
get 'wallet/transactions', to: 'wallet#transactions' get 'wallet/transactions', to: 'wallet#transactions'
get 'lnurlpay/:address', to: 'lnurlpay#index', constraints: { address: /[^\/]+/} get 'lnurlpay/:address', to: 'lnurlpay#index',
get 'lnurlpay/:address/invoice', to: 'lnurlpay#invoice', constraints: { address: /[^\/]+/} as: 'lightning_address', constraints: { address: /[^\/]+/}
get 'lnurlpay/:address/invoice', to: 'lnurlpay#invoice',
as: 'lnurlpay_invoice', constraints: { address: /[^\/]+/}
get 'keysend/:address', to: 'lnurlpay#keysend',
as: 'lightning_address_keysend', constraints: { address: /[^\/]+/}
post 'webhooks/lndhub', to: 'webhooks#lndhub' post 'webhooks/lndhub', to: 'webhooks#lndhub'
@@ -39,7 +43,7 @@ Rails.application.routes.draw do
namespace :admin do namespace :admin do
root to: 'dashboard#index' root to: 'dashboard#index'
resources 'users', only: ['index'] resources 'users', param: 'address', only: ['index', 'show'], constraints: { address: /.*/ }
get 'invitations', to: 'invitations#index' get 'invitations', to: 'invitations#index'
resources :donations resources :donations
get 'lightning', to: 'lightning#index' get 'lightning', to: 'lightning#index'

View File

@@ -1,3 +1,4 @@
:concurrency: 2 :concurrency: 2
:queues: :queues:
- default - default
- mailers

7
config/storage.yml Normal file
View File

@@ -0,0 +1,7 @@
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>

View File

@@ -0,0 +1,5 @@
class RemoveLnLoginCiphertextFromUsers < ActiveRecord::Migration[7.0]
def change
remove_column :users, :ln_login_ciphertext
end
end

View File

@@ -1,5 +0,0 @@
class RemoveLnLoginFromUsers < ActiveRecord::Migration[7.0]
def change
remove_column :users, :ln_login_cyphertext
end
end

View File

@@ -0,0 +1,6 @@
class AddRememberCreatedAtToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :remember_created_at, :datetime
add_column :users, :remember_token, :string
end
end

View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_02_23_115536) do ActiveRecord::Schema[7.0].define(version: 2023_03_19_101128) do
create_table "donations", force: :cascade do |t| create_table "donations", force: :cascade do |t|
t.integer "user_id" t.integer "user_id"
t.integer "amount_sats" t.integer "amount_sats"
@@ -57,6 +57,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_23_115536) do
t.text "ln_login_ciphertext" t.text "ln_login_ciphertext"
t.text "ln_password_ciphertext" t.text "ln_password_ciphertext"
t.string "ln_account" t.string "ln_account"
t.datetime "remember_created_at"
t.string "remember_token"
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end end

View File

@@ -10,7 +10,7 @@ Sidekiq::Testing.inline! do
ldap.add_attribute "cn=admin,ou=kosmos.org,cn=users,dc=kosmos,dc=org", :admin, "true" ldap.add_attribute "cn=admin,ou=kosmos.org,cn=users,dc=kosmos,dc=org", :admin, "true"
5.times do |n| 35.times do |n|
username = Faker::Name.unique.first_name.downcase username = Faker::Name.unique.first_name.downcase
email = Faker::Internet.unique.email email = Faker::Internet.unique.email

View File

@@ -3,11 +3,67 @@ services:
image: 4teamwork/389ds:latest image: 4teamwork/389ds:latest
volumes: volumes:
- ./tmp/389ds:/data - ./tmp/389ds:/data
networks:
- external_network
- internal_network
ports: ports:
- "389:3389" - "389:3389"
environment: environment:
DS_DM_PASSWORD: passthebutter DS_DM_PASSWORD: passthebutter
SUFFIX_NAME: "dc=kosmos,dc=org" SUFFIX_NAME: "dc=kosmos,dc=org"
# redis:
# restart: always
# image: redis:7-alpine
# networks:
# - internal_network
# healthcheck:
# test: ['CMD', 'redis-cli', 'ping']
# volumes:
# - ./tmp/redis:/data
# web:
# build: .
# tty: true
# command: bash -c "rm -f /akkounts/tmp/pids/server.pid; bin/dev"
# volumes:
# - .:/akkounts
# networks:
# - external_network
# - internal_network
# ports:
# - "3000:3000"
# environment:
# RAILS_ENV: development
# REDIS_URL: redis://redis:6379/0
# LDAP_HOST: ldap
# LDAP_PORT: 3389
# LDAP_ADMIN_PASSWORD: passthebutter
# LDAP_USE_TLS: "false"
# depends_on:
# - ldap
# - redis
# sidekiq:
# build: .
# command: bash -c "bundle exec sidekiq -C config/sidekiq.yml"
# volumes:
# - .:/akkounts
# networks:
# - internal_network
# environment:
# RAILS_ENV: development
# REDIS_URL: redis://redis:6379/0
# LDAP_HOST: ldap
# LDAP_PORT: 3389
# LDAP_ADMIN_PASSWORD: passthebutter
# LDAP_USE_TLS: "false"
# LAUNCHY_DRY_RUN: true
# BROWSER: /dev/null
# depends_on:
# - ldap
# - redis
# phpldapadmin: # phpldapadmin:
# image: osixia/phpldapadmin:0.9.0 # image: osixia/phpldapadmin:0.9.0
# ports: # ports:
@@ -16,19 +72,8 @@ services:
# PHPLDAPADMIN_HTTPS: false # PHPLDAPADMIN_HTTPS: false
# PHPLDAPADMIN_LDAP_HOSTS: "#PYTHON2BASH:[{'ldap': [{'server': [{'tls': False}, {'port': 3389}]}, {'login': [{'bind_id': 'cn=Directory Manager'}, {'bind_pass': 'passthebutter'}]}]}]" # PHPLDAPADMIN_LDAP_HOSTS: "#PYTHON2BASH:[{'ldap': [{'server': [{'tls': False}, {'port': 3389}]}, {'login': [{'bind_id': 'cn=Directory Manager'}, {'bind_pass': 'passthebutter'}]}]}]"
# PHPLDAPADMIN_LDAP_CLIENT_TLS: false # PHPLDAPADMIN_LDAP_CLIENT_TLS: false
# web:
# build: . networks:
# tty: true external_network:
# command: bash -c "sleep 5 && rm -f tmp/pids/server.pid && bin/dev" internal_network:
# volumes: internal: true
# - .:/akkounts
# ports:
# - "3000:3000"
# environment:
# RAILS_ENV: development
# LDAP_HOST: ldap
# LDAP_PORT: 3389
# LDAP_ADMIN_PASSWORD: passthebutter
# LDAP_USE_TLS: "false"
# depends_on:
# - ldap

View File

@@ -1,8 +0,0 @@
#!/bin/bash
set -e
# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid
# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

View File

@@ -11,7 +11,7 @@
"postcss-preset-env": "^7.8.3", "postcss-preset-env": "^7.8.3",
"tailwindcss": "^3.2.4" "tailwindcss": "^3.2.4"
}, },
"version": "0.4.0", "version": "0.5.0",
"scripts": { "scripts": {
"build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css", "build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css",
"build:css": "yarn run build:css:tailwind" "build:css": "yarn run build:css:tailwind"

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -1 104 106">
<path fill="#231f20" d="M51.87 0C23.71 0 0 22.83 0 51v52.81l51.86-.05c28.16 0 51-23.71 51-51.87S80 0 51.87 0Z"/>
<path fill="#fff9ae" d="M52.37 19.74a31.62 31.62 0 0 0-27.79 46.67l-5.72 18.4 20.54-4.64a31.61 31.61 0 1 0 13-60.43Z"/>
<path fill="#00aeef" d="M77.45 32.12a31.6 31.6 0 0 1-38.05 48l-20.54 4.7 20.91-2.47a31.6 31.6 0 0 0 37.68-50.23Z"/>
<path fill="#00a94f" d="M71.63 26.29A31.6 31.6 0 0 1 38.8 78l-19.94 6.82 20.54-4.65a31.6 31.6 0 0 0 32.23-53.88Z"/>
<path fill="#f15d22" d="M26.47 67.11a31.61 31.61 0 0 1 51-35 31.61 31.61 0 0 0-52.89 34.3l-5.72 18.4Z"/>
<path fill="#e31b23" d="M24.58 66.41a31.61 31.61 0 0 1 47.05-40.12 31.61 31.61 0 0 0-49 39.63l-3.76 18.9Z"/>
</svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@@ -0,0 +1,9 @@
<svg width="33" height="34" viewBox="0 0 33 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.8125 17.2932C32.8125 19.6071 32.3208 21.8488 31.4779 23.8735L25.6476 17.944C26.35 16.787 26.7012 15.4131 26.7012 13.9669C26.7715 9.84522 23.47 6.44662 19.3958 6.44662C17.9207 6.44662 16.586 6.88048 15.4621 7.60359L10.0533 2.10799C12.0904 1.16795 14.2679 0.661774 16.6562 0.661774C25.5773 0.661774 32.8125 8.10976 32.8125 17.2932ZM7.80543 3.40958L13.6357 9.41135C12.6523 10.7129 12.0904 12.3038 12.0904 14.0392C12.0904 18.2332 15.3918 21.6318 19.466 21.6318C21.1519 21.6318 22.6973 21.0534 23.9617 20.041L30.2134 26.4767C27.2632 30.9599 22.3461 33.9246 16.6562 33.9246C7.73519 33.9246 0.5 26.4767 0.5 17.2932C0.5 11.436 3.38003 6.37431 7.80543 3.40958ZM19.466 18.7394C22.2056 18.7394 24.3832 16.4978 24.3832 13.6777C24.3832 10.8576 22.2056 8.61594 19.466 8.61594C16.7265 8.61594 14.5489 10.8576 14.5489 13.6777C14.5489 16.4978 16.7265 18.7394 19.466 18.7394Z" fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.88952" y1="1.73016" x2="27.8689" y2="33.2773" gradientUnits="userSpaceOnUse">
<stop stop-color="#73DFE7"/>
<stop offset="1" stop-color="#0095F7"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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