diff --git a/.env.example b/.env.example
index e8dc0f3..155ec8a 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,5 @@
+AKKOUNTS_DOMAIN=accounts.example.com
+
SMTP_SERVER=smtp.example.com
SMTP_PORT=587
SMTP_LOGIN=accounts
@@ -7,6 +9,8 @@ SMTP_DOMAIN=example.com
SMTP_AUTH_METHOD=plain
SMTP_ENABLE_STARTTLS=auto
+REDIS_URL='redis://localhost:6379/1'
+
LDAP_HOST=localhost
LDAP_PORT=389
LDAP_ADMIN_PASSWORD=passthebutter
diff --git a/.env.test b/.env.test
index 0c94493..016655b 100644
--- a/.env.test
+++ b/.env.test
@@ -6,4 +6,6 @@ LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
+RS_STORAGE_URL='https://storage.kosmos.org'
+
WEBHOOKS_ALLOWED_IPS='10.1.1.23'
diff --git a/.gitea/release-drafter.yml b/.gitea/release-drafter.yml
new file mode 100644
index 0000000..9fcce2b
--- /dev/null
+++ b/.gitea/release-drafter.yml
@@ -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
diff --git a/.gitea/workflows/release_drafter.yml b/.gitea/workflows/release_drafter.yml
new file mode 100644
index 0000000..ba9dc39
--- /dev/null
+++ b/.gitea/workflows/release_drafter.yml
@@ -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
diff --git a/Dockerfile b/Dockerfile
index 0eead4c..f2692e7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,13 @@
# syntax=docker/dockerfile:1
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 apt-get update && apt-get install -y nodejs
+
WORKDIR /akkounts
COPY Gemfile /akkounts/Gemfile
COPY Gemfile.lock /akkounts/Gemfile.lock
@@ -12,11 +17,5 @@ RUN gem install foreman
RUN npm install -g yarn
RUN yarn install
-# Add a script to be executed every time the container starts.
-COPY docker/entrypoint.sh /usr/bin/
-RUN chmod +x /usr/bin/entrypoint.sh
-ENTRYPOINT ["entrypoint.sh"]
+ENTRYPOINT ["/usr/bin/tini", "--"]
EXPOSE 3000
-
-# Configure the main process to run when running the image
-CMD ["bin", "dev"]
diff --git a/Gemfile b/Gemfile
index a1918c1..1b6704e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -48,6 +48,10 @@ gem 'faraday'
gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler'
+# Monitoring
+gem "sentry-ruby"
+gem "sentry-rails"
+
group :development, :test do
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4'
@@ -62,6 +66,7 @@ group :development do
gem 'letter_opener'
gem 'letter_opener_web'
gem 'faker'
+ gem 'solargraph'
end
group :test do
diff --git a/Gemfile.lock b/Gemfile.lock
index e788bef..2d92414 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -68,7 +68,10 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
+ ast (2.4.2)
+ backport (1.2.0)
bcrypt (3.1.18)
+ benchmark (0.2.1)
bindex (0.8.1)
builder (3.2.4)
byebug (11.1.3)
@@ -109,6 +112,7 @@ GEM
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
+ e2mmap (0.1.0)
erubi (1.11.0)
et-orbi (1.2.7)
tzinfo
@@ -135,9 +139,15 @@ GEM
importmap-rails (1.1.5)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
+ jaro_winkler (1.5.4)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
+ json (2.6.3)
+ kramdown (2.4.0)
+ rexml
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
launchy (2.5.0)
addressable (~> 2.7)
letter_opener (1.8.1)
@@ -179,6 +189,9 @@ GEM
racc (~> 1.4)
orm_adapter (0.5.0)
pagy (6.0.2)
+ parallel (1.22.1)
+ parser (3.2.1.1)
+ ast (~> 2.4.1)
pg (1.2.3)
public_suffix (5.0.0)
puma (4.3.12)
@@ -217,6 +230,7 @@ GEM
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
+ rainbow (3.1.1)
rake (13.0.6)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
@@ -229,6 +243,8 @@ GEM
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
+ reverse_markdown (2.1.1)
+ nokogiri
rexml (3.2.5)
rqrcode (2.1.2)
chunky_png (~> 1.0)
@@ -251,9 +267,27 @@ GEM
rspec-mocks (~> 3.11)
rspec-support (~> 3.11)
rspec-support (3.12.0)
+ rubocop (1.48.1)
+ json (~> 2.3)
+ parallel (~> 1.10)
+ parser (>= 3.2.0.0)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.26.0, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.28.0)
+ parser (>= 3.2.1.0)
+ ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rufus-scheduler (3.8.2)
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)
connection_pool (>= 2.2.2)
rack (~> 2.0)
@@ -263,6 +297,21 @@ GEM
rufus-scheduler (~> 3.2)
sidekiq (>= 4, < 7)
tilt (>= 1.4.0)
+ solargraph (0.48.0)
+ backport (~> 1.2)
+ benchmark
+ bundler (>= 1.17.2)
+ diff-lcs (~> 1.4)
+ e2mmap
+ jaro_winkler (~> 1.5)
+ kramdown (~> 2.3)
+ kramdown-parser-gfm (~> 1.1)
+ parser (~> 3.0)
+ reverse_markdown (>= 1.0.5, < 3)
+ rubocop (>= 0.52)
+ thor (~> 1.0)
+ tilt (~> 2.0)
+ yard (~> 0.9, >= 0.9.24)
sprockets (4.1.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@@ -284,6 +333,7 @@ GEM
railties (>= 6.0.0)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
+ unicode-display_width (2.4.2)
view_component (2.78.0)
activesupport (>= 5.0.0, < 8.0)
concurrent-ruby (~> 1.0)
@@ -299,11 +349,14 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
+ webrick (1.7.0)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
+ yard (0.9.28)
+ webrick (~> 1.7.0)
zeitwerk (2.6.6)
PLATFORMS
@@ -335,8 +388,11 @@ DEPENDENCIES
rails-settings-cached (~> 2.8.3)
rqrcode (~> 2.0)
rspec-rails
+ sentry-rails
+ sentry-ruby
sidekiq (< 7)
sidekiq-scheduler
+ solargraph
sprockets-rails
sqlite3 (~> 1.4)
stimulus-rails
diff --git a/README.md b/README.md
index b2e7205..90fec7f 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
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
in the log output
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
@@ -23,9 +23,8 @@ so:
After these steps, you should have a working Rails app with a handful of test
users running on [http://localhost:3000](http://localhost:3000).
-
Log in with username "admin" and password "admin is admin". All users listed on
-[http://localhost:3000/admin/ldap_users](http://localhost:3000/admin/ldap_users)
+[http://localhost:3000/admin/users](http://localhost:3000/admin/users)
have the password "user is user".
### Rails app
@@ -79,6 +78,15 @@ The setup task will first delete any existing entries in the directory tree
Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
with a fresh installation, delete both that directory as well as the container.
+### Solargraph
+
+[Solargraph](https://solargraph.org/) is a Ruby language server, which you may
+use with your editor to add features like auto-completion and syntax
+validation. You can add inline documentation for bundled gems with this
+command:
+
+ bundle exec yard gems
+
## Documentation
### Rails
diff --git a/app/assets/stylesheets/components/links.css b/app/assets/stylesheets/components/links.css
index c46f68b..92c9c75 100644
--- a/app/assets/stylesheets/components/links.css
+++ b/app/assets/stylesheets/components/links.css
@@ -5,10 +5,4 @@
&:visited { @apply text-indigo-600; }
&:active { @apply text-red-600; }
}
-
- .devise-links {
- a {
- @apply ks-text-link;
- }
- }
}
diff --git a/app/components/form_elements/fieldset_component.html.erb b/app/components/form_elements/fieldset_component.html.erb
index 4d82a56..2bde8ce 100644
--- a/app/components/form_elements/fieldset_component.html.erb
+++ b/app/components/form_elements/fieldset_component.html.erb
@@ -1,4 +1,5 @@
<%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
+ <% if @positioning == :vertical %>
">
<%= @title %>
@@ -10,4 +11,19 @@
<% end %>
<%= content %>
+ <% elsif @positioning == :horizontal %>
+
+
+
<%= @title %>
+ <% if @descripton.present? %>
+
<%= @descripton %>
+ <% end %>
+
+
+ <%= content %>
+
+
+ <% else %>
+
Invalid positioning argument for FieldsetComponent
.
+ <% end %>
<% end %>
diff --git a/app/components/form_elements/fieldset_component.rb b/app/components/form_elements/fieldset_component.rb
index 8896137..23fad5b 100644
--- a/app/components/form_elements/fieldset_component.rb
+++ b/app/components/form_elements/fieldset_component.rb
@@ -2,10 +2,11 @@
module FormElements
class FieldsetComponent < ViewComponent::Base
- def initialize(tag: "li", title:, description: nil)
- @tag = tag
- @title = title
- @descripton = description
+ def initialize(tag: "li", positioning: :vertical, title:, description: nil)
+ @tag = tag
+ @positioning = positioning
+ @title = title
+ @descripton = description
end
end
end
diff --git a/app/components/form_elements/fieldset_toggle_component.html.erb b/app/components/form_elements/fieldset_toggle_component.html.erb
index 504a5b5..f4acd3d 100644
--- a/app/components/form_elements/fieldset_toggle_component.html.erb
+++ b/app/components/form_elements/fieldset_toggle_component.html.erb
@@ -1,5 +1,5 @@
<%= tag.public_send @tag, class: "flex items-center justify-between mb-6 last:mb-0",
- data: @form.present? ? {
+ data: @form_enabled ? {
controller: "settings--toggle",
:'settings--toggle-switch-enabled-value' => @enabled.to_s
} : nil do %>
@@ -11,16 +11,23 @@
<%= render FormElements::ToggleComponent.new(
enabled: @enabled,
input_enabled: @input_enabled,
- class_names: @form.present? ? "hidden" : nil,
+ class_names: @form_enabled ? "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" %>
+ <% if @form_enabled %>
+ <% if @attribute.present? %>
+ <%= @form.check_box @attribute, {
+ checked: @enabled,
+ data: { :'settings--toggle-target' => "checkbox" }
+ }, "true", "false" %>
+ <% else %>
+
+ <%= check_box_tag @field_name, "true", @enabled, {
+ data: { :'settings--toggle-target' => "checkbox" }
+ } %>
+ <% end %>
<% end %>
<% end %>
diff --git a/app/components/form_elements/fieldset_toggle_component.rb b/app/components/form_elements/fieldset_toggle_component.rb
index b38bee7..686f5f1 100644
--- a/app/components/form_elements/fieldset_toggle_component.rb
+++ b/app/components/form_elements/fieldset_toggle_component.rb
@@ -2,11 +2,13 @@
module FormElements
class FieldsetToggleComponent < ViewComponent::Base
- def initialize(form: nil, attribute: nil, tag: "li", enabled: false,
- input_enabled: true, title:, description:)
+ def initialize(tag: "li", form: nil, attribute: nil, field_name: nil,
+ enabled: false, input_enabled: true, title:, description:)
+ @tag = tag
@form = form
@attribute = attribute
- @tag = tag
+ @field_name = field_name
+ @form_enabled = @form.present? || @field_name.present?
@enabled = enabled
@input_enabled = input_enabled
@title = title
diff --git a/app/components/form_elements/toggle_component.html.erb b/app/components/form_elements/toggle_component.html.erb
index 41a6708..8d6faf9 100644
--- a/app/components/form_elements/toggle_component.html.erb
+++ b/app/components/form_elements/toggle_component.html.erb
@@ -1,6 +1,6 @@
<%= button_tag type: "button", name: "toggle", data: @data,
role: "switch", aria: { checked: @enabled.to_s },
- disabled: !@input_enabled,
+ 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
diff --git a/app/components/form_elements/toggle_component.rb b/app/components/form_elements/toggle_component.rb
index d2f5eae..8bb7181 100644
--- a/app/components/form_elements/toggle_component.rb
+++ b/app/components/form_elements/toggle_component.rb
@@ -2,11 +2,12 @@
module FormElements
class ToggleComponent < ViewComponent::Base
- def initialize(enabled:, input_enabled: true, data: nil, class_names: nil)
+ 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
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index c672886..ee049bc 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -3,6 +3,18 @@ class ApplicationController < ActionController::Base
render :text => exception, :status => 500
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
unless user_signed_in?
redirect_to welcome_path and return
diff --git a/app/controllers/settings/account_controller.rb b/app/controllers/settings/account_controller.rb
deleted file mode 100644
index 385a9ff..0000000
--- a/app/controllers/settings/account_controller.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class Settings::AccountController < SettingsController
-
- def index
- end
-
- def reset_password
- current_user.send_reset_password_instructions
- sign_out current_user
- msg = "We have sent you an email with a link to reset your password."
- redirect_to check_your_email_path, notice: msg
- end
-
-end
diff --git a/app/controllers/settings/profile_controller.rb b/app/controllers/settings/profile_controller.rb
deleted file mode 100644
index 645bd20..0000000
--- a/app/controllers/settings/profile_controller.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class Settings::ProfileController < SettingsController
-
- def index
- @user = current_user
- end
-
- def update
-
- end
-
-end
diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb
index d2fc3e5..c5bd8f8 100644
--- a/app/controllers/settings_controller.rb
+++ b/app/controllers/settings_controller.rb
@@ -1,13 +1,52 @@
class SettingsController < ApplicationController
- before_action :require_user_signed_in
- before_action :set_current_section
+ before_action :authenticate_user!
+ before_action :set_main_nav_section
+ before_action :set_settings_section, only: ['show', 'update']
def index
+ redirect_to setting_path(:profile)
+ end
+
+ def show
+ @user = current_user
+ end
+
+ def update
+ @user = current_user
+ @user.preferences.merge! user_params[:preferences]
+ @user.save!
+
+ redirect_to setting_path(@settings_section), flash: {
+ success: 'Settings saved.'
+ }
+ end
+
+ def reset_password
+ current_user.send_reset_password_instructions
+ sign_out current_user
+ msg = "We have sent you an email with a link to reset your password."
+ redirect_to check_your_email_path, notice: msg
end
private
- def set_current_section
+ def set_main_nav_section
@current_section = :settings
end
+
+ def set_settings_section
+ @settings_section = params[:section]
+ allowed_sections = [:profile, :account, :lightning, :xmpp]
+
+ unless allowed_sections.include?(@settings_section.to_sym)
+ redirect_to setting_path(:profile)
+ end
+ end
+
+ def user_params
+ params.require(:user).permit(preferences: [
+ :lightning_notify_sats_received,
+ :xmpp_exchange_contacts_with_invitees
+ ])
+ end
end
diff --git a/app/controllers/webfinger_controller.rb b/app/controllers/webfinger_controller.rb
new file mode 100644
index 0000000..5cf4012
--- /dev/null
+++ b/app/controllers/webfinger_controller.rb
@@ -0,0 +1,57 @@
+class WebfingerController < ApplicationController
+ before_action :allow_cross_origin_requests, only: [:show]
+
+ layout false
+
+ def show
+ resource = params[:resource]
+
+ if resource && resource.match(/acct:\w+/)
+ useraddress = resource.split(":").last
+ username, org = useraddress.split("@")
+ username.downcase!
+ unless User.where(cn: username, ou: org).any?
+ head 404 and return
+ end
+
+ render json: webfinger(useraddress).to_json,
+ content_type: "application/jrd+json"
+ else
+ head 422 and return
+ end
+ end
+
+ private
+
+ def webfinger(useraddress)
+ links = [];
+
+ links << remotestorage_link(useraddress) if Setting.remotestorage_enabled
+
+ { "links" => links }
+ end
+
+ def remotestorage_link(useraddress)
+ # TODO use when OAuth routes are available
+ # auth_url = new_rs_oauth_url(useraddress)
+ auth_url = "https://example.com/rs/oauth"
+ storage_url = "#{Setting.rs_storage_url}/#{useraddress}"
+
+ {
+ "rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage",
+ "href" => storage_url,
+ "properties" => {
+ "http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13",
+ "http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url,
+ "http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter
+ "http://tools.ietf.org/html/rfc7233": "GET", # content range requests
+ "http://remotestorage.io/spec/web-authoring": nil
+ }
+ }
+ end
+
+ def allow_cross_origin_requests
+ headers['Access-Control-Allow-Origin'] = '*'
+ headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
+ end
+end
diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb
index 74f880a..7025580 100644
--- a/app/controllers/webhooks_controller.rb
+++ b/app/controllers/webhooks_controller.rb
@@ -12,22 +12,28 @@ class WebhooksController < ApplicationController
end
user = User.find_by!(ln_account: payload[:user_login])
-
- # TODO make configurable
- notify_xmpp(user.address, payload[:amount], payload[:memo])
+ notify = user.preferences[:lightning_notify_sats_received]
+ case notify
+ when "xmpp"
+ notify_xmpp(user.address, payload[:amount], payload[:memo])
+ when "email"
+ NotificationMailer.with(user: user, amount_sats: payload[:amount])
+ .lightning_sats_received.deliver_later
+ end
head :ok
end
private
+ # TODO refactor into mailer-like generic class/service
def notify_xmpp(address, amt_sats, memo)
payload = {
type: "normal",
from: "kosmos.org", # TODO domain config
to: address,
subject: "Sats received!",
- body: "#{amt_sats} sats received in your Lightning wallet:\n> #{memo}"
+ body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"
}
XmppSendMessageJob.perform_later(payload)
end
diff --git a/app/javascript/controllers/notification_controller.js b/app/javascript/controllers/notification_controller.js
index e0291ce..9e09dc4 100644
--- a/app/javascript/controllers/notification_controller.js
+++ b/app/javascript/controllers/notification_controller.js
@@ -4,6 +4,10 @@ export default class extends Controller {
static targets = ["buttons", "countdown"]
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"));
setTimeout(() => {
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
new file mode 100644
index 0000000..84f7dd5
--- /dev/null
+++ b/app/mailers/notification_mailer.rb
@@ -0,0 +1,8 @@
+class NotificationMailer < ApplicationMailer
+ def lightning_sats_received
+ @user = params[:user]
+ @amount_sats = params[:amount_sats]
+ @subject = "Sats received"
+ mail to: @user.email, subject: @subject
+ end
+end
diff --git a/app/models/setting.rb b/app/models/setting.rb
index 3a7e8c5..b1364a5 100644
--- a/app/models/setting.rb
+++ b/app/models/setting.rb
@@ -2,6 +2,13 @@
class Setting < RailsSettings::Base
cache_prefix { "v1" }
+ #
+ # Internal services
+ #
+
+ field :redis_url, type: :string, readonly: true,
+ default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
+
#
# Registrations
#
@@ -10,6 +17,13 @@ class Setting < RailsSettings::Base
account accounts donations mail webmaster support
]
+ #
+ # Sentry
+ #
+
+ field :sentry_enabled, type: :boolean, readonly: true,
+ default: (ENV["SENTRY_DSN"].present?.to_s || false)
+
#
# Discourse
#
diff --git a/app/models/user.rb b/app/models/user.rb
index ecc50f5..380d4e7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,6 +1,8 @@
class User < ApplicationRecord
include EmailValidatable
+ serialize :preferences, UserPreferences
+
# Relations
has_many :invitations, dependent: :destroy
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
@@ -24,6 +26,7 @@ class User < ApplicationRecord
validates_format_of :cn, without: /\A-/,
if: Proc.new{ |u| u.cn.present? },
message: "is invalid. Usernames need to start with a letter."
+ # FIXME This needs a server restart to apply values
validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i,
message: "has already been taken"
@@ -40,7 +43,9 @@ class User < ApplicationRecord
devise :ldap_authenticatable,
:confirmable,
:recoverable,
- :validatable
+ :validatable,
+ :timeoutable,
+ :rememberable
def ldap_before_save
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
@@ -55,16 +60,23 @@ class User < ApplicationRecord
end
def devise_after_confirmation
- enable_service %w[ discourse ejabberd gitea mediawiki ]
+ enable_service %w[ discourse gitea mediawiki xmpp ]
#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?
+ if Setting.ejabberd_enabled? &&
+ inviter.preferences[:xmpp_exchange_contacts_with_invitees]
+ exchange_xmpp_contact_with_inviter
+ end
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)
self.password = new_password
self.password_confirmation = new_password_confirmation
@@ -130,8 +142,8 @@ class User < ApplicationRecord
end
def exchange_xmpp_contact_with_inviter
- return unless inviter.services_enabled.include?("ejabberd") &&
- services_enabled.include?("ejabberd")
+ return unless inviter.services_enabled.include?("xmpp") &&
+ services_enabled.include?("xmpp")
XmppExchangeContactsJob.perform_later(inviter, self.cn, self.ou)
end
diff --git a/app/models/user_preferences.rb b/app/models/user_preferences.rb
new file mode 100644
index 0000000..ffdf7f6
--- /dev/null
+++ b/app/models/user_preferences.rb
@@ -0,0 +1,29 @@
+DEFAULT_PREFS = YAML.load_file("#{Rails.root}/config/default_preferences.yml")
+
+class UserPreferences
+ def self.dump(value)
+ process(value).to_yaml
+ end
+
+ def self.load(string)
+ stored_prefs = YAML.load(string || "{}")
+ DEFAULT_PREFS.merge(stored_prefs).with_indifferent_access
+ end
+
+ def self.is_integer?(value)
+ value.to_i.to_s == value
+ end
+
+ def self.process(hash)
+ hash.each do |key, value|
+ if value == "true"
+ hash[key] = true
+ elsif value == "false"
+ hash[key] = false
+ elsif value.is_a?(String) && is_integer?(value)
+ hash[key] = value.to_i
+ end
+ end
+ hash.stringify_keys!.to_h
+ end
+end
diff --git a/app/services/ejabberd_api_client.rb b/app/services/ejabberd_api_client.rb
index 7930aa1..bac501e 100644
--- a/app/services/ejabberd_api_client.rb
+++ b/app/services/ejabberd_api_client.rb
@@ -1,6 +1,6 @@
class EjabberdApiClient
def initialize
- @base_url = ENV["EJABBERD_API_URL"]
+ @base_url = Setting.ejabberd_api_url
end
def post(endpoint, payload)
diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb
index 5d89827..42c7963 100644
--- a/app/views/admin/users/show.html.erb
+++ b/app/views/admin/users/show.html.erb
@@ -135,7 +135,7 @@
XMPP (ejabberd)
<%= render FormElements::ToggleComponent.new(
- enabled: @services_enabled.include?("ejabberd"),
+ enabled: @services_enabled.include?("xmpp"),
input_enabled: false
) %>
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb
index 687d90a..f7c0a70 100644
--- a/app/views/devise/sessions/new.html.erb
+++ b/app/views/devise/sessions/new.html.erb
@@ -7,19 +7,43 @@
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
- required: true, class: "relative grow"%>
+ required: true, class: "relative grow", tabindex: "1" %>
@ kosmos.org
-
+
<%= f.label :password, class: 'block mb-2 font-bold' %>
<%= f.password_field :password, autocomplete: "current-password",
- required: true, class: "w-full"%>
+ required: true, class: "w-full", tabindex: "2" %>
-
- <%= f.submit "Log in", class: 'btn-md btn-blue w-full' %>
+
+ <%= tag.div class: "flex items-center mb-8 gap-x-3", data: {
+ controller: "settings--toggle",
+ :'settings--toggle-switch-enabled-value' => "false"
+ } do %>
+
+ <%= 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" %>
+
+ <%= f.label :remember_me,
+ class: "text-gray-500 flex flex-col",
+ data: { action: "click->settings--toggle#toggleSwitch" } %>
+
+ <%= link_to "Forgot your password?", new_password_path(resource_name),
+ class: "text-gray-500 underline" %>
+
+ <% end %>
+
+
+ <%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %>
<% end %>
-
- <%= render "devise/shared/links" %>
<% end %>
diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb
index b1d1ccd..2499950 100644
--- a/app/views/devise/shared/_links.html.erb
+++ b/app/views/devise/shared/_links.html.erb
@@ -1,25 +1,29 @@
<%- if controller_name != 'sessions' %>
-
- <%= link_to "Log in", new_session_path(resource_name) %>
+
+ <%= link_to "Log in", new_session_path(resource_name),
+ class: "text-gray-500 underline" %>
<% end %>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
-
- <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+
+ <%= link_to "Forgot your password?", new_password_path(resource_name),
+ class: "text-gray-500 underline" %>
<% end %>
- <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
-
- <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+ <%- if devise_mapping.confirmable? && !controller_name.match(/^(confirmations|sessions)$/) %>
+
+ <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name),
+ class: "text-gray-500 underline" %>
<% end %>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
-
- <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+
+ <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name),
+ class: "text-gray-500 underline" %>
<% end %>
diff --git a/app/views/icons/_bell.html.erb b/app/views/icons/_bell.html.erb
index bba561c..3bb750f 100644
--- a/app/views/icons/_bell.html.erb
+++ b/app/views/icons/_bell.html.erb
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/app/views/icons/_message-circle.html.erb b/app/views/icons/_message-circle.html.erb
index 4b21b32..5ff6406 100644
--- a/app/views/icons/_message-circle.html.erb
+++ b/app/views/icons/_message-circle.html.erb
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/app/views/notification_mailer/lightning_sats_received.text.erb b/app/views/notification_mailer/lightning_sats_received.text.erb
new file mode 100644
index 0000000..1e122d4
--- /dev/null
+++ b/app/views/notification_mailer/lightning_sats_received.text.erb
@@ -0,0 +1,3 @@
+You just received <%= number_with_delimiter @amount_sats %> sats in your Lightning account (<%= @user.address %>). Check your wallet app, or open the account page for details:
+
+<%= wallet_transactions_url %>
diff --git a/app/views/settings/_account.html.erb b/app/views/settings/_account.html.erb
new file mode 100644
index 0000000..df0a2ad
--- /dev/null
+++ b/app/views/settings/_account.html.erb
@@ -0,0 +1,19 @@
+
+
+ Password
+ Use the following button to request an email with a password reset link:
+ <%= form_with(url: reset_password_settings_path, method: :post) do %>
+
+ <%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
+
+ <% end %>
+
diff --git a/app/views/settings/_lightning.html.erb b/app/views/settings/_lightning.html.erb
new file mode 100644
index 0000000..6e75343
--- /dev/null
+++ b/app/views/settings/_lightning.html.erb
@@ -0,0 +1,25 @@
+<%= form_for @user, url: setting_path(:lightning), html: { :method => :put } do |f| %>
+
+ Notifications
+
+ <%= render FormElements::FieldsetComponent.new(
+ positioning: :horizontal,
+ title: "Sats received",
+ description: "Notify me when sats are sent to my Lightning Address"
+ ) do %>
+ <% f.fields_for :preferences do |p| %>
+ <%= p.select :lightning_notify_sats_received, options_for_select([
+ ["off", "disabled"],
+ ["Chat (Jabber)", "xmpp"],
+ ["E-Mail", "email"]
+ ], selected: @user.preferences[:lightning_notify_sats_received]) %>
+ <% end %>
+ <% end %>
+
+
+
+
+ <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
+
+
+<% end %>
diff --git a/app/views/settings/_notifications.html.erb b/app/views/settings/_notifications.html.erb
new file mode 100644
index 0000000..ff879d3
--- /dev/null
+++ b/app/views/settings/_notifications.html.erb
@@ -0,0 +1,16 @@
+
+ Lightning Wallet
+
+
+ <%= render FormElements::FieldsetComponent.new(
+ positioning: :horizontal,
+ title: "Sats received",
+ description: "Notify when sats are sent to my Lightning Address"
+ ) do %>
+ <%= select_tag :sats_received, options_for_select([
+ ["off", "off"],
+ ["Chat (Jabber)", "xmpp"]
+ ]) %>
+ <% end %>
+
+
diff --git a/app/views/settings/_profile.html.erb b/app/views/settings/_profile.html.erb
new file mode 100644
index 0000000..f1d14ae
--- /dev/null
+++ b/app/views/settings/_profile.html.erb
@@ -0,0 +1,30 @@
+
+ Profile
+
+ <%= label :user_address, 'User address', class: 'font-bold' %>
+
+
+ disabled="disabled"
+ data-clipboard-target="source" />
+
+
+ <%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
+
+
+ <%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
+
+
+
+
+ Your user address for Chat and Lightning Network.
+
+
+ <%# <%= form_for(@user, as: "profile", url: settings_profile_path) do |f| %>
+ <%#
+ <%# <%= f.submit "Save changes", class: 'btn-md btn-blue w-full sm:w-auto' %>
+ <%#
+ <%# <% end %>
+
diff --git a/app/views/settings/_xmpp.html.erb b/app/views/settings/_xmpp.html.erb
new file mode 100644
index 0000000..a13ce70
--- /dev/null
+++ b/app/views/settings/_xmpp.html.erb
@@ -0,0 +1,18 @@
+<%= form_for @user, url: setting_path(:xmpp), html: { :method => :put } do |f| %>
+
+ Contacts
+
+ <%= render FormElements::FieldsetToggleComponent.new(
+ field_name: "user[preferences][xmpp_exchange_contacts_with_invitees]",
+ enabled: @user.preferences[:xmpp_exchange_contacts_with_invitees],
+ title: "Exchange contacts when invited user signs up",
+ description: "Add each others contacts, so you can chat with them immediately"
+ ) %>
+
+
+
+
+ <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
+
+
+<% end %>
diff --git a/app/views/settings/account/index.html.erb b/app/views/settings/account/index.html.erb
deleted file mode 100644
index effe9c2..0000000
--- a/app/views/settings/account/index.html.erb
+++ /dev/null
@@ -1,23 +0,0 @@
-<%= render HeaderComponent.new(title: "Settings") %>
-
-<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %>
-
-
- Password
- Use the following button to request an email with a password reset link:
- <%= form_with(url: settings_reset_password_path, method: :post) do %>
-
- <%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
-
- <% end %>
-
-<% end %>
diff --git a/app/views/settings/profile/index.html.erb b/app/views/settings/profile/index.html.erb
deleted file mode 100644
index 3e91709..0000000
--- a/app/views/settings/profile/index.html.erb
+++ /dev/null
@@ -1,34 +0,0 @@
-<%= render HeaderComponent.new(title: "Settings") %>
-
-<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %>
-
- Profile
-
- <%= label :user_address, 'User address', class: 'font-bold' %>
-
-
- disabled="disabled"
- data-clipboard-target="source" />
-
-
- <%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
-
-
- <%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
-
-
-
-
- Your user address for Chat and Lightning Network.
-
-
- <%# <%= form_for(@user, as: "profile", url: settings_profile_path) do |f| %>
- <%#
- <%# <%= f.submit "Save changes", class: 'btn-md btn-blue w-full sm:w-auto' %>
- <%#
- <%# <% end %>
-
-<% end %>
diff --git a/app/views/settings/show.html.erb b/app/views/settings/show.html.erb
new file mode 100644
index 0000000..38ddcb5
--- /dev/null
+++ b/app/views/settings/show.html.erb
@@ -0,0 +1,5 @@
+<%= render HeaderComponent.new(title: "Settings") %>
+
+<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %>
+ <%= render partial: @settings_section %>
+<% end %>
diff --git a/app/views/shared/_main_nav.html.erb b/app/views/shared/_main_nav.html.erb
index de5b8eb..7f5ee02 100644
--- a/app/views/shared/_main_nav.html.erb
+++ b/app/views/shared/_main_nav.html.erb
@@ -6,5 +6,5 @@
class: main_nav_class(@current_section, :invitations) %>
<%= link_to "Wallet", wallet_path,
class: main_nav_class(@current_section, :wallet) %>
-<%= link_to "Settings", settings_profile_path,
+<%= link_to "Settings", settings_path,
class: main_nav_class(@current_section, :settings) %>
diff --git a/app/views/shared/_sidenav_settings.html.erb b/app/views/shared/_sidenav_settings.html.erb
index 25d345a..3d6c17e 100644
--- a/app/views/shared/_sidenav_settings.html.erb
+++ b/app/views/shared/_sidenav_settings.html.erb
@@ -1,11 +1,20 @@
<%= render SidenavLinkComponent.new(
- name: "Profile", path: settings_profile_path, icon: "user",
- active: current_page?(settings_profile_path)
+ name: "Profile", path: setting_path(:profile), icon: "user",
+ active: current_page?(setting_path(:profile))
) %>
<%= render SidenavLinkComponent.new(
- name: "Account", path: settings_account_path, icon: "key",
- active: current_page?(settings_account_path)
+ name: "Account", path: setting_path(:account), icon: "key",
+ active: current_page?(setting_path(:account))
) %>
+<% if Setting.ejabberd_enabled %>
<%= render SidenavLinkComponent.new(
- name: "Security", path: "#", icon: "shield", disabled: true
+ name: "Chat", path: setting_path(:xmpp), icon: "message-circle",
+ active: current_page?(setting_path(:xmpp))
) %>
+<% end %>
+<% if Setting.lndhub_enabled %>
+<%= render SidenavLinkComponent.new(
+ name: "Wallet", path: setting_path(:lightning), icon: "zap",
+ active: current_page?(setting_path(:lightning))
+) %>
+<% end %>
diff --git a/config/default_preferences.yml b/config/default_preferences.yml
new file mode 100644
index 0000000..ff7f051
--- /dev/null
+++ b/config/default_preferences.yml
@@ -0,0 +1,2 @@
+lightning_notify_sats_received: disabled # or xmpp, email
+xmpp_exchange_contacts_with_invitees: true
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 1f22161..c51028f 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -62,6 +62,11 @@ Rails.application.configure do
outgoing_email_address = ENV.fetch('SMTP_FROM_ADDRESS', 'accounts@localhost')
outgoing_email_domain = Mail::Address.new(outgoing_email_address).domain
+ config.action_mailer.default_url_options = {
+ host: ENV['AKKOUNTS_DOMAIN'],
+ protocol: "https",
+ }
+
config.action_mailer.default_options = {
from: outgoing_email_address,
message_id: -> { "<#{Mail.random_tag}@#{outgoing_email_domain}>" },
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 3b0ffeb..bb264bf 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -186,13 +186,13 @@ Devise.setup do |config|
# ==> Configuration for :rememberable
# 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.
config.expire_all_remember_me_on_sign_out = true
# 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
# secure: true in order to force SSL only cookies.
@@ -210,7 +210,7 @@ Devise.setup do |config|
# ==> Configuration for :timeoutable
# 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.
- # config.timeout_in = 30.minutes
+ config.timeout_in = 30.minutes
# ==> Configuration for :lockable
# Defines which strategy will be used to lock an account.
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
new file mode 100644
index 0000000..202fe75
--- /dev/null
+++ b/config/initializers/sentry.rb
@@ -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
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
new file mode 100644
index 0000000..044cec0
--- /dev/null
+++ b/config/initializers/sidekiq.rb
@@ -0,0 +1,5 @@
+require_relative "../../app/models/setting"
+
+Sidekiq.configure_server do |config|
+ config.redis = { url: Setting.redis_url }
+end
diff --git a/config/routes.rb b/config/routes.rb
index b83d631..743eda1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,7 +1,7 @@
require 'sidekiq/web'
Rails.application.routes.draw do
- devise_for :users, :controllers => { :confirmations => "users/confirmations" }
+ devise_for :users, controllers: { confirmations: "users/confirmations" }
get 'welcome', to: 'welcome#index'
get 'check_your_email', to: 'welcome#check_your_email'
@@ -10,13 +10,6 @@ Rails.application.routes.draw do
match 'signup/:step', to: 'signup#steps', as: :signup_steps, via: [:get, :post]
post 'signup_validate', to: 'signup#validate'
- namespace :settings do
- get 'profile', to: 'profile#index'
- post 'profile', to: 'profile#update'
- get 'account', to: 'account#index'
- post 'reset_password', to: 'account#reset_password'
- end
-
namespace :contributions do
root to: 'donations#index'
get 'projects', to: 'projects#index'
@@ -28,6 +21,12 @@ Rails.application.routes.draw do
get 'wallet', to: 'wallet#index'
get 'wallet/transactions', to: 'wallet#transactions'
+ resources :settings, param: 'section', only: ['index', 'show', 'update'] do
+ collection do
+ post 'reset_password'
+ end
+ end
+
get 'lnurlpay/:address', to: 'lnurlpay#index',
as: 'lightning_address', constraints: { address: /[^\/]+/}
get 'lnurlpay/:address/invoice', to: 'lnurlpay#invoice',
@@ -60,6 +59,9 @@ Rails.application.routes.draw do
get 'oauth/token/:id/launch_app' => 'oauth#launch_app', as: :launch_app
end
+ get ".well-known/webfinger" => "webfinger#show"
+
+
authenticate :user, ->(user) { user.is_admin? } do
mount Sidekiq::Web => '/sidekiq'
end
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index 615bb16..adc65b2 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -1,3 +1,4 @@
:concurrency: 2
:queues:
- default
+ - mailers
diff --git a/db/migrate/20230319101128_add_remember_created_at_to_users.rb b/db/migrate/20230319101128_add_remember_created_at_to_users.rb
new file mode 100644
index 0000000..6457dab
--- /dev/null
+++ b/db/migrate/20230319101128_add_remember_created_at_to_users.rb
@@ -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
diff --git a/db/migrate/20230403135149_add_preferences_to_users.rb b/db/migrate/20230403135149_add_preferences_to_users.rb
new file mode 100644
index 0000000..2defcb3
--- /dev/null
+++ b/db/migrate/20230403135149_add_preferences_to_users.rb
@@ -0,0 +1,5 @@
+class AddPreferencesToUsers < ActiveRecord::Migration[7.0]
+ def change
+ add_column :users, :preferences, :text
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 18a0b86..8947149 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_03_12_212030) do
+ActiveRecord::Schema[7.0].define(version: 2023_04_03_150200) do
create_table "donations", force: :cascade do |t|
t.integer "user_id"
t.integer "amount_sats"
@@ -71,6 +71,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_12_212030) do
t.text "ln_login_ciphertext"
t.text "ln_password_ciphertext"
t.string "ln_account"
+ t.string "nostr_pubkey"
+ t.datetime "remember_created_at"
+ t.string "remember_token"
+ t.text "preferences"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
diff --git a/docker-compose.yml b/docker-compose.yml
index a4d5a29..8a0de47 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,11 +3,67 @@ services:
image: 4teamwork/389ds:latest
volumes:
- ./tmp/389ds:/data
+ networks:
+ - external_network
+ - internal_network
ports:
- "389:3389"
environment:
DS_DM_PASSWORD: passthebutter
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:
# image: osixia/phpldapadmin:0.9.0
# ports:
@@ -16,19 +72,8 @@ services:
# PHPLDAPADMIN_HTTPS: false
# PHPLDAPADMIN_LDAP_HOSTS: "#PYTHON2BASH:[{'ldap': [{'server': [{'tls': False}, {'port': 3389}]}, {'login': [{'bind_id': 'cn=Directory Manager'}, {'bind_pass': 'passthebutter'}]}]}]"
# PHPLDAPADMIN_LDAP_CLIENT_TLS: false
- # web:
- # build: .
- # tty: true
- # command: bash -c "sleep 5 && rm -f tmp/pids/server.pid && bin/dev"
- # volumes:
- # - .:/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
+
+networks:
+ external_network:
+ internal_network:
+ internal: true
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
deleted file mode 100644
index 3af18f7..0000000
--- a/docker/entrypoint.sh
+++ /dev/null
@@ -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 "$@"
diff --git a/package.json b/package.json
index 066231b..60a5ede 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"postcss-preset-env": "^7.8.3",
"tailwindcss": "^3.2.4"
},
- "version": "0.4.0",
+ "version": "0.5.0",
"scripts": {
"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"
diff --git a/spec/features/admin/settings_spec.rb b/spec/features/admin/settings_spec.rb
index 7a394ba..a522d31 100644
--- a/spec/features/admin/settings_spec.rb
+++ b/spec/features/admin/settings_spec.rb
@@ -46,5 +46,26 @@ RSpec.describe 'Admin/global settings', type: :feature do
expect(page).to_not have_checked_field("setting[ejabberd_enabled]")
expect(page).to_not have_field("API URL", disabled: true)
end
+
+ scenario "View remoteStorage settings" do
+ visit admin_settings_services_path(params: { s: "remotestorage" })
+
+ expect(page).to have_content("Enable RemoteStorage integration")
+ expect(page).to have_field("Storage URL",
+ with: "https://storage.kosmos.org",
+ disabled: true)
+ end
+
+ scenario "Disable remoteStorage integration" do
+ visit admin_settings_services_path(params: { s: "remotestorage" })
+ expect(page).to have_checked_field("setting[remotestorage_enabled]")
+
+ uncheck "setting[remotestorage_enabled]"
+ click_button "Save"
+
+ expect(current_url).to eq(admin_settings_services_url(params: { s: "remotestorage" }))
+ expect(page).to_not have_checked_field("setting[remotestorage_enabled]")
+ expect(page).to_not have_field("Storage URL", disabled: true)
+ end
end
end
diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb
index 9959661..30511a7 100644
--- a/spec/features/signup_spec.rb
+++ b/spec/features/signup_spec.rb
@@ -53,11 +53,11 @@ RSpec.describe "Signup", type: :feature do
expect(page).to have_content("Choose a password")
expect(CreateAccount).to receive(:call)
- .with(
+ .with({
username: "tony", domain: "kosmos.org",
email: "tony@example.com", password: "a-valid-password",
invitation: Invitation.last
- ).and_return(true)
+ }).and_return(true)
fill_in "user_password", with: "a-valid-password"
click_button "Create account"
@@ -97,11 +97,11 @@ RSpec.describe "Signup", type: :feature do
expect(page).to have_content("Password is too short")
expect(CreateAccount).to receive(:call)
- .with(
+ .with({
username: "tony", domain: "kosmos.org",
email: "tony@example.com", password: "a-valid-password",
invitation: Invitation.last
- ).and_return(true)
+ }).and_return(true)
fill_in "user_password", with: "a-valid-password"
click_button "Create account"
diff --git a/spec/models/user_preferences_spec.rb b/spec/models/user_preferences_spec.rb
new file mode 100644
index 0000000..4bb7b42
--- /dev/null
+++ b/spec/models/user_preferences_spec.rb
@@ -0,0 +1,41 @@
+require 'rails_helper'
+
+RSpec.describe UserPreferences, type: :model do
+ let(:default_prefs) { YAML.load_file("#{Rails.root}/config/default_preferences.yml") }
+
+ describe ".load" do
+ it "provides default values when no preferences are stored yet" do
+ expect(UserPreferences.load(nil)).to eq(default_prefs)
+ end
+
+ it "provides default values for unset preferences" do
+ prefs = UserPreferences.load("lightning_notify_sats_received: xmpp")
+ expect(prefs[:lightning_notify_sats_received]).to eq("xmpp")
+ expect(prefs[:xmpp_exchange_contacts_with_invitees]).to eq(true)
+ end
+ end
+
+ describe ".process" do
+ it "turns all keys into strings" do
+ res = UserPreferences.process({ foo: "bar" })
+ expect(res[:foo]).to be(nil)
+ expect(res['foo']).to eq("bar")
+ end
+
+ it "converts value 'true' to boolean" do
+ res = UserPreferences.process({ lightning_notify_sats_received: "true" })
+ expect(res['lightning_notify_sats_received']).to be(true)
+ end
+
+ it "converts value 'false' to boolean" do
+ res = UserPreferences.process({ lightning_notify_sats_received: "false" })
+ expect(res['lightning_notify_sats_received']).to be(false)
+ end
+
+ it "converts value string with integer into integer" do
+ res = UserPreferences.process({ lightning_notify_sats_received_threshold: 1000 })
+ expect(res['lightning_notify_sats_received_threshold']).to be_a(Integer)
+ expect(res['lightning_notify_sats_received_threshold']).to eq(1000)
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 1dfc1ac..c1105c9 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -109,7 +109,7 @@ RSpec.describe User, type: :model do
before do
Invitation.create! user: user, invited_user_id: guest.id, used_at: DateTime.now
- allow_any_instance_of(User).to receive(:services_enabled).and_return(%w[ ejabberd ])
+ allow_any_instance_of(User).to receive(:services_enabled).and_return(%w[ xmpp ])
end
it "enqueues a job to exchange XMPP contacts between inviter and invitee" do
@@ -131,14 +131,16 @@ RSpec.describe User, type: :model do
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
it "enables default services" do
- expect(user).to receive(:enable_service).with(%w[ discourse ejabberd gitea mediawiki ])
+ expect(user).to receive(:enable_service).with(%w[ discourse gitea mediawiki xmpp ])
user.send(:devise_after_confirmation)
end
- context "for invited user with ejabberd enabled" do
+ context "for invited user with xmpp enabled" do
let(:guest) { create :user, id: 2, cn: "isaacnewton", ou: "kosmos.org", email: "newt@example.com" }
before do
+ # TODO remove when defaults are implemented
+ user.update! preferences: { xmpp_exchange_contacts_with_invitees: true }
Invitation.create! user: user, invited_user_id: guest.id, used_at: DateTime.now
allow_any_instance_of(User).to receive(:enable_service).and_return(true)
end
@@ -147,6 +149,17 @@ RSpec.describe User, type: :model do
expect(guest).to receive(:exchange_xmpp_contact_with_inviter)
guest.send(:devise_after_confirmation)
end
+
+ context "automatic contact exchange disabled" do
+ before do
+ user.update! preferences: { xmpp_exchange_contacts_with_invitees: false }
+ end
+
+ it "does not exchange XMPP contacts with the inviter" do
+ expect(guest).to_not receive(:exchange_xmpp_contact_with_inviter)
+ guest.send(:devise_after_confirmation)
+ end
+ end
end
end
end
diff --git a/spec/requests/webfinger_spec.rb b/spec/requests/webfinger_spec.rb
new file mode 100644
index 0000000..f944a7a
--- /dev/null
+++ b/spec/requests/webfinger_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+RSpec.describe "WebFinger", type: :request do
+ describe "remoteStorage link relation" do
+ context "user exists" do
+ before do
+ create :user, cn: 'tony', ou: 'kosmos.org'
+ end
+
+ context "remoteStorage enabled globally" do
+ it "includes the remoteStorage link for the user" do
+ get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
+ expect(response).to have_http_status(:ok)
+
+ res = JSON.parse(response.body)
+ rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
+
+ expect(rs_link["href"]).to eql("https://storage.kosmos.org/tony@kosmos.org")
+
+ oauth_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-4.2"]
+ expect(oauth_url).to eql("https://example.com/rs/oauth")
+ end
+ end
+
+ context "remoteStorage not available" do
+ before do
+ Setting.remotestorage_enabled = false
+ end
+
+ it "does not include the remoteStorage link" do
+ get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
+ expect(response).to have_http_status(:ok)
+
+ res = JSON.parse(response.body)
+ rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
+
+ expect(rs_link).to be_nil
+ end
+ end
+ end
+
+ context "user does not exist" do
+ it "does return a 404 status" do
+ get "/.well-known/webfinger?resource=acct%3Ajane.doe%40kosmos.org"
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/requests/webhooks_spec.rb b/spec/requests/webhooks_spec.rb
index 06c834e..96a3b8f 100644
--- a/spec/requests/webhooks_spec.rb
+++ b/spec/requests/webhooks_spec.rb
@@ -55,22 +55,51 @@ RSpec.describe "Webhooks", type: :request do
before do
user.save! #FIXME this should not be necessary
- post "/webhooks/lndhub", params: payload.to_json
end
it "returns a 200 status" do
+ post "/webhooks/lndhub", params: payload.to_json
expect(response).to have_http_status(:ok)
end
- it "sends an XMPP message to the account owner's JID" do
- expect(enqueued_jobs.size).to eq(1)
+ it "does not send notifications by default" do
+ expect(enqueued_jobs.size).to eq(0)
+ end
- msg = enqueued_jobs.first['arguments'].first
- expect(msg["type"]).to eq('normal')
- expect(msg["from"]).to eq('kosmos.org')
- expect(msg["to"]).to eq(user.address)
- expect(msg["subject"]).to eq('Sats received!')
- expect(msg["body"]).to match(/^12300 sats received/)
+ context "notification preference set to 'xmpp'" do
+ before do
+ user.update! preferences: { lightning_notify_sats_received: "xmpp" }
+ post "/webhooks/lndhub", params: payload.to_json
+ end
+
+ it "sends an XMPP message to the account owner's JID" do
+ expect(enqueued_jobs.size).to eq(1)
+ expect(enqueued_jobs.first["job_class"]).to eq("XmppSendMessageJob")
+
+ msg = enqueued_jobs.first["arguments"].first
+ expect(msg["type"]).to eq("normal")
+ expect(msg["from"]).to eq("kosmos.org")
+ expect(msg["to"]).to eq(user.address)
+ expect(msg["subject"]).to eq("Sats received!")
+ expect(msg["body"]).to match(/^12,300 sats received/)
+ end
+ end
+
+ context "notification preference set to 'email'" do
+ before do
+ user.update! preferences: { lightning_notify_sats_received: "email" }
+ post "/webhooks/lndhub", params: payload.to_json
+ end
+
+ it "sends an email notification to the account owner" do
+ expect(enqueued_jobs.size).to eq(1)
+ expect(enqueued_jobs.first["job_class"]).to eq("ActionMailer::MailDeliveryJob")
+ args = enqueued_jobs.first['arguments']
+ expect(args[0]).to eq("NotificationMailer")
+ expect(args[1]).to eq("lightning_sats_received")
+ expect(args[3]["params"]["user"]["_aj_globalid"]).to eq("gid://akkounts/User/1")
+ expect(args[3]["params"]["amount_sats"]).to eq(12300)
+ end
end
end
end