diff --git a/.env.example b/.env.example
index 6cff8b9..155ec8a 100644
--- a/.env.example
+++ b/.env.example
@@ -22,6 +22,7 @@ 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'
+RS_STORAGE_URL='https://storage.kosmos.org'
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
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/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/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/jobs/xmpp_exchange_contacts_job.rb b/app/jobs/xmpp_exchange_contacts_job.rb
index c082e32..f1b20e0 100644
--- a/app/jobs/xmpp_exchange_contacts_job.rb
+++ b/app/jobs/xmpp_exchange_contacts_job.rb
@@ -2,8 +2,9 @@ class XmppExchangeContactsJob < ApplicationJob
queue_as :default
def perform(inviter, invitee)
- return unless inviter.services_enabled.include?("ejabberd") &&
- invitee.services_enabled.include?("ejabberd")
+ return unless inviter.services_enabled.include?("xmpp") &&
+ invitee.services_enabled.include?("xmpp") &&
+ inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new
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 4b02225..2cbc615 100644
--- a/app/models/setting.rb
+++ b/app/models/setting.rb
@@ -111,4 +111,14 @@ class Setting < RailsSettings::Base
#
field :nostr_enabled, type: :boolean, default: true
+
+ #
+ # RemoteStorage
+ #
+
+ field :remotestorage_enabled, type: :boolean,
+ default: (ENV["RS_STORAGE_URL"].present?.to_s || false)
+
+ field :rs_storage_url, type: :string,
+ default: ENV["RS_STORAGE_URL"].presence
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 937bc9c..4945d6e 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'
@@ -56,7 +58,7 @@ 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? || !Setting.ejabberd_enabled?
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 b90b499..a0a8b87 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/settings/services/_remotestorage.html.erb b/app/views/admin/settings/services/_remotestorage.html.erb
new file mode 100644
index 0000000..fdfa3f4
--- /dev/null
+++ b/app/views/admin/settings/services/_remotestorage.html.erb
@@ -0,0 +1,17 @@
+RemoteStorage
+
+ <%= render FormElements::FieldsetToggleComponent.new(
+ form: f,
+ attribute: :remotestorage_enabled,
+ enabled: Setting.remotestorage_enabled?,
+ title: "Enable RemoteStorage integration",
+ description: "RemoteStorage configuration present and features enabled"
+ ) %>
+ <% if Setting.remotestorage_enabled? %>
+ <%= render FormElements::FieldsetComponent.new(title: "Storage URL") do %>
+ <%= f.text_field :rs_storage_url,
+ value: Setting.rs_storage_url,
+ class: "w-full", disabled: true %>
+ <% end %>
+ <% end %>
+
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/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/_admin_sidenav_settings_services.html.erb b/app/views/shared/_admin_sidenav_settings_services.html.erb
index c897b6d..142f6fc 100644
--- a/app/views/shared/_admin_sidenav_settings_services.html.erb
+++ b/app/views/shared/_admin_sidenav_settings_services.html.erb
@@ -47,3 +47,10 @@
icon: Setting.nostr_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "nostr" })),
) %>
+<%= render SidenavLinkComponent.new(
+ level: 2,
+ name: "RemoteStorage",
+ path: admin_settings_services_path(params: { s: "remotestorage" }),
+ icon: Setting.remotestorage_enabled? ? "check" : "x",
+ active: current_page?(admin_settings_services_path(params: { s: "remotestorage" })),
+) %>
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/routes.rb b/config/routes.rb
index e729019..fdc8454 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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',
@@ -54,6 +53,8 @@ Rails.application.routes.draw do
end
end
+ get ".well-known/webfinger" => "webfinger#show"
+
authenticate :user, ->(user) { user.is_admin? } do
mount Sidekiq::Web => '/sidekiq'
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 a292eed..1a85f04 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_19_101128) do
+ActiveRecord::Schema[7.0].define(version: 2023_04_03_135149) do
create_table "donations", force: :cascade do |t|
t.integer "user_id"
t.integer "amount_sats"
@@ -57,8 +57,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_19_101128) 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/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/jobs/xmpp_exchange_contacts_job_spec.rb b/spec/jobs/xmpp_exchange_contacts_job_spec.rb
index 013a80d..c711e87 100644
--- a/spec/jobs/xmpp_exchange_contacts_job_spec.rb
+++ b/spec/jobs/xmpp_exchange_contacts_job_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe XmppExchangeContactsJob, type: :job do
before do
stub_request(:post, "http://xmpp.example.com/api/add_rosteritem")
.to_return(status: 200, body: "", headers: {})
- allow_any_instance_of(User).to receive(:services_enabled).and_return(["ejabberd"])
+ allow_any_instance_of(User).to receive(:services_enabled).and_return(["xmpp"])
end
it "posts add_rosteritem commands to the ejabberd API" do
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 b8f8675..68677b7 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -108,7 +108,7 @@ 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
@@ -120,10 +120,12 @@ RSpec.describe User, type: :model do
expect(job['arguments'][0]['_aj_globalid']).to eq('gid://akkounts/User/1')
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)
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