Merge branch 'master' into feature/default_chatrooms
This commit is contained in:
		
						commit
						cb2197893c
					
				@ -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'
 | 
			
		||||
 | 
			
		||||
@ -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'
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
<%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
 | 
			
		||||
  <% if @positioning == :vertical %>
 | 
			
		||||
  <label class="block">
 | 
			
		||||
    <p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>">
 | 
			
		||||
      <%= @title %>
 | 
			
		||||
@ -10,4 +11,19 @@
 | 
			
		||||
    <% end %>
 | 
			
		||||
    <%= content %>
 | 
			
		||||
  </label>
 | 
			
		||||
  <% elsif @positioning == :horizontal %>
 | 
			
		||||
  <label class="block flex items-center justify-between">
 | 
			
		||||
    <div class="flex flex-col">
 | 
			
		||||
      <label class="font-bold mb-1"><%= @title %></label>
 | 
			
		||||
      <% if @descripton.present? %>
 | 
			
		||||
      <p class="text-gray-500"><%= @descripton %></p>
 | 
			
		||||
      <% end %>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="relative ml-4 inline-flex flex-shrink-0">
 | 
			
		||||
      <%= content %>
 | 
			
		||||
    </div>
 | 
			
		||||
  </label>
 | 
			
		||||
  <% else %>
 | 
			
		||||
  <p>Invalid <code>positioning<code> argument for <code>FieldsetComponent</code>.</p>
 | 
			
		||||
  <% end %>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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 %>
 | 
			
		||||
        <input name="<%= @field_name %>" type="hidden" value="false" autocomplete="off">
 | 
			
		||||
        <%= check_box_tag @field_name, "true", @enabled, {
 | 
			
		||||
              data: { :'settings--toggle-target' => "checkbox" }
 | 
			
		||||
            } %>
 | 
			
		||||
      <% end %>
 | 
			
		||||
    <% end %>
 | 
			
		||||
  </div>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
class Settings::ProfileController < SettingsController
 | 
			
		||||
 | 
			
		||||
  def index
 | 
			
		||||
    @user = current_user
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update
 | 
			
		||||
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
end
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										57
									
								
								app/controllers/webfinger_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/controllers/webfinger_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								app/mailers/notification_mailer.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/mailers/notification_mailer.rb
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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?
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										29
									
								
								app/models/user_preferences.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								app/models/user_preferences.rb
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								app/views/admin/settings/services/_remotestorage.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/views/admin/settings/services/_remotestorage.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
<h3>RemoteStorage</h3>
 | 
			
		||||
<ul role="list">
 | 
			
		||||
  <%= 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 %>
 | 
			
		||||
</ul>
 | 
			
		||||
@ -135,7 +135,7 @@
 | 
			
		||||
          <td>XMPP (ejabberd)</td>
 | 
			
		||||
          <td>
 | 
			
		||||
            <%= render FormElements::ToggleComponent.new(
 | 
			
		||||
              enabled: @services_enabled.include?("ejabberd"),
 | 
			
		||||
              enabled: @services_enabled.include?("xmpp"),
 | 
			
		||||
              input_enabled: false
 | 
			
		||||
            ) %>
 | 
			
		||||
          </td>
 | 
			
		||||
 | 
			
		||||
@ -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-bell"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></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-bell <%= custom_class %>"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 342 B  | 
@ -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-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></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-message-circle <%= custom_class %>"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 449 B  | 
@ -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 %>
 | 
			
		||||
							
								
								
									
										19
									
								
								app/views/settings/_account.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/views/settings/_account.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
<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>
 | 
			
		||||
  <h3>Password</h3>
 | 
			
		||||
  <p class="mb-8">Use the following button to request an email with a password reset link:</p>
 | 
			
		||||
  <%= form_with(url: reset_password_settings_path, method: :post) do %>
 | 
			
		||||
    <p>
 | 
			
		||||
      <%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
 | 
			
		||||
    </p>
 | 
			
		||||
  <% end %>
 | 
			
		||||
</section>
 | 
			
		||||
							
								
								
									
										25
									
								
								app/views/settings/_lightning.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/views/settings/_lightning.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
<%= form_for @user, url: setting_path(:lightning), html: { :method => :put } do |f| %>
 | 
			
		||||
<section>
 | 
			
		||||
  <h3>Notifications</h3>
 | 
			
		||||
  <ul role="list">
 | 
			
		||||
    <%= 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 %>
 | 
			
		||||
  </ul>
 | 
			
		||||
</section>
 | 
			
		||||
<section>
 | 
			
		||||
  <p class="pt-6 border-t border-gray-200 text-right">
 | 
			
		||||
    <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
 | 
			
		||||
  </p>
 | 
			
		||||
</section>
 | 
			
		||||
<% end %>
 | 
			
		||||
							
								
								
									
										16
									
								
								app/views/settings/_notifications.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/views/settings/_notifications.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
<section>
 | 
			
		||||
  <h3>Lightning Wallet</h3>
 | 
			
		||||
 | 
			
		||||
  <ul role="list">
 | 
			
		||||
    <%= 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 %>
 | 
			
		||||
  </ul>
 | 
			
		||||
</section>
 | 
			
		||||
							
								
								
									
										30
									
								
								app/views/settings/_profile.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/views/settings/_profile.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
<section>
 | 
			
		||||
  <h3>Profile</h3>
 | 
			
		||||
  <p class="mb-2">
 | 
			
		||||
    <%= label :user_address, 'User address', class: 'font-bold' %>
 | 
			
		||||
  </p>
 | 
			
		||||
  <p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
 | 
			
		||||
    <input type="text" id="user_address" class="grow"
 | 
			
		||||
           value=<%= @user.address %> disabled="disabled"
 | 
			
		||||
           data-clipboard-target="source" />
 | 
			
		||||
    <button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0"
 | 
			
		||||
            data-clipboard-target="trigger" data-action="clipboard#copy"
 | 
			
		||||
            title="Copy to clipboard">
 | 
			
		||||
      <span class="content-initial">
 | 
			
		||||
        <%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
 | 
			
		||||
      </span>
 | 
			
		||||
      <span class="content-active hidden">
 | 
			
		||||
        <%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
 | 
			
		||||
      </span>
 | 
			
		||||
    </button>
 | 
			
		||||
  </p>
 | 
			
		||||
  <p class="text-sm text-gray-500">
 | 
			
		||||
    Your user address for Chat and Lightning Network.
 | 
			
		||||
  </p>
 | 
			
		||||
 | 
			
		||||
  <%# <%= form_for(@user, as: "profile", url: settings_profile_path) do |f| %>
 | 
			
		||||
  <%#   <p class="mt-8">
 | 
			
		||||
  <%#     <%= f.submit "Save changes", class: 'btn-md btn-blue w-full sm:w-auto' %>
 | 
			
		||||
  <%#   </p>
 | 
			
		||||
  <%# <% end %>
 | 
			
		||||
</section>
 | 
			
		||||
							
								
								
									
										18
									
								
								app/views/settings/_xmpp.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/views/settings/_xmpp.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
<%= form_for @user, url: setting_path(:xmpp), html: { :method => :put } do |f| %>
 | 
			
		||||
<section>
 | 
			
		||||
  <h3>Contacts</h3>
 | 
			
		||||
  <ul role="list">
 | 
			
		||||
    <%= 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"
 | 
			
		||||
    ) %>
 | 
			
		||||
  </ul>
 | 
			
		||||
</section>
 | 
			
		||||
<section>
 | 
			
		||||
  <p class="pt-6 border-t border-gray-200 text-right">
 | 
			
		||||
    <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
 | 
			
		||||
  </p>
 | 
			
		||||
</section>
 | 
			
		||||
<% end %>
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
<%= render HeaderComponent.new(title: "Settings") %>
 | 
			
		||||
 | 
			
		||||
<%= 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>
 | 
			
		||||
    <h3>Password</h3>
 | 
			
		||||
    <p class="mb-8">Use the following button to request an email with a password reset link:</p>
 | 
			
		||||
    <%= form_with(url: settings_reset_password_path, method: :post) do %>
 | 
			
		||||
      <p>
 | 
			
		||||
        <%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
 | 
			
		||||
      </p>
 | 
			
		||||
    <% end %>
 | 
			
		||||
  </section>
 | 
			
		||||
<% end %>
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
<%= render HeaderComponent.new(title: "Settings") %>
 | 
			
		||||
 | 
			
		||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %>
 | 
			
		||||
  <section>
 | 
			
		||||
    <h3>Profile</h3>
 | 
			
		||||
    <p class="mb-2">
 | 
			
		||||
      <%= label :user_address, 'User address', class: 'font-bold' %>
 | 
			
		||||
    </p>
 | 
			
		||||
    <p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
 | 
			
		||||
      <input type="text" id="user_address" class="grow"
 | 
			
		||||
             value=<%= @user.address %> disabled="disabled"
 | 
			
		||||
             data-clipboard-target="source" />
 | 
			
		||||
      <button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0"
 | 
			
		||||
              data-clipboard-target="trigger" data-action="clipboard#copy"
 | 
			
		||||
              title="Copy to clipboard">
 | 
			
		||||
        <span class="content-initial">
 | 
			
		||||
          <%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
 | 
			
		||||
        </span>
 | 
			
		||||
        <span class="content-active hidden">
 | 
			
		||||
          <%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
 | 
			
		||||
        </span>
 | 
			
		||||
      </button>
 | 
			
		||||
    </p>
 | 
			
		||||
    <p class="text-sm text-gray-500">
 | 
			
		||||
      Your user address for Chat and Lightning Network.
 | 
			
		||||
    </p>
 | 
			
		||||
 | 
			
		||||
    <%# <%= form_for(@user, as: "profile", url: settings_profile_path) do |f| %>
 | 
			
		||||
    <%#   <p class="mt-8">
 | 
			
		||||
    <%#     <%= f.submit "Save changes", class: 'btn-md btn-blue w-full sm:w-auto' %>
 | 
			
		||||
    <%#   </p>
 | 
			
		||||
    <%# <% end %>
 | 
			
		||||
  </section>
 | 
			
		||||
<% end %>
 | 
			
		||||
							
								
								
									
										5
									
								
								app/views/settings/show.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/views/settings/show.html.erb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
<%= render HeaderComponent.new(title: "Settings") %>
 | 
			
		||||
 | 
			
		||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %>
 | 
			
		||||
  <%= render partial: @settings_section %>
 | 
			
		||||
<% end %>
 | 
			
		||||
@ -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" })),
 | 
			
		||||
) %>
 | 
			
		||||
 | 
			
		||||
@ -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) %>
 | 
			
		||||
 | 
			
		||||
@ -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 %>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								config/default_preferences.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								config/default_preferences.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
lightning_notify_sats_received: disabled # or xmpp, email
 | 
			
		||||
xmpp_exchange_contacts_with_invitees: true
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								db/migrate/20230403135149_add_preferences_to_users.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20230403135149_add_preferences_to_users.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
class AddPreferencesToUsers < ActiveRecord::Migration[7.0]
 | 
			
		||||
  def change
 | 
			
		||||
    add_column :users, :preferences, :text
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										41
									
								
								spec/models/user_preferences_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								spec/models/user_preferences_spec.rb
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										49
									
								
								spec/requests/webfinger_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								spec/requests/webfinger_spec.rb
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user