Compare commits
26 Commits
v0.9.0
...
feature/ld
| Author | SHA1 | Date | |
|---|---|---|---|
|
0bd77bc37a
|
|||
|
02af69b055
|
|||
|
5d459e7e7d
|
|||
| 51a3cb60ec | |||
| 43c57c128f | |||
|
5a3adba603
|
|||
|
3715cb518b
|
|||
|
2c9ecc1fef
|
|||
|
095747e89b
|
|||
|
2130369604
|
|||
|
c996351930
|
|||
| 8b897168cc | |||
|
4217ba52e0
|
|||
|
de20931d30
|
|||
|
8de0a2e26e
|
|||
|
06521d1c34
|
|||
|
38b3d68fd5
|
|||
|
7f2df3b025
|
|||
|
da22a9d448
|
|||
|
e3b96d5cff
|
|||
|
c36cf5eee6
|
|||
|
54220019bb
|
|||
|
079ee8833c
|
|||
|
26d613bdca
|
|||
|
69b3afb8f7
|
|||
|
fee951c05c
|
@@ -31,6 +31,7 @@
|
||||
# Service Integrations
|
||||
#
|
||||
|
||||
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||
# BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||
# BTCPAY_STORE_ID=''
|
||||
# BTCPAY_AUTH_TOKEN=''
|
||||
@@ -57,6 +58,7 @@
|
||||
# LNDHUB_PG_PASSWORD=''
|
||||
|
||||
# MASTODON_PUBLIC_URL='https://kosmos.social'
|
||||
# MASTODON_ADDRESS_DOMAIN='https://kosmos.org'
|
||||
|
||||
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ PRIMARY_DOMAIN=kosmos.org
|
||||
|
||||
REDIS_URL='redis://localhost:6379/0'
|
||||
|
||||
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
||||
BTCPAY_STORE_ID='123456'
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
focus:ring-blue-400 focus:ring-opacity-75;
|
||||
}
|
||||
|
||||
.btn-emerald {
|
||||
@apply bg-emerald-500 hover:bg-emerald-600 text-white
|
||||
focus:ring-emerald-400 focus:ring-opacity-75;
|
||||
}
|
||||
|
||||
.btn-red {
|
||||
@apply bg-red-600 hover:bg-red-700 text-white
|
||||
focus:ring-red-500 focus:ring-opacity-75;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="md:min-h-[50vh] bg-white rounded-lg shadow">
|
||||
<div class="px-6 sm:px-12 pt-2 sm:pt-4">
|
||||
<%= render partial: @tabnav_partial %>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<!-- Modal Container -->
|
||||
<div data-modal-target="container"
|
||||
class="max-h-screen w-auto max-w-lg relative
|
||||
class="relative m-4 max-h-screen w-auto max-w-full
|
||||
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
|
||||
<!-- Modal Card -->
|
||||
<div class="m-1 bg-white rounded shadow">
|
||||
|
||||
@@ -34,6 +34,8 @@ class NotificationComponent < ViewComponent::Base
|
||||
'alert-octagon'
|
||||
when 'alert'
|
||||
'alert-octagon'
|
||||
when 'warning'
|
||||
'alert-octagon'
|
||||
else
|
||||
'info'
|
||||
end
|
||||
|
||||
@@ -3,18 +3,16 @@ class Admin::DonationsController < Admin::BaseController
|
||||
before_action :set_current_section, only: [:index, :show, :new, :edit]
|
||||
|
||||
# GET /donations
|
||||
# GET /donations.json
|
||||
def index
|
||||
@pagy, @donations = pagy(Donation.all.order('created_at desc'))
|
||||
@pagy, @donations = pagy(Donation.completed.order('paid_at desc'))
|
||||
|
||||
@stats = {
|
||||
overall_sats: @donations.all.sum("amount_sats"),
|
||||
donor_count: Donation.distinct.count(:user_id)
|
||||
overall_sats: @donations.sum("amount_sats"),
|
||||
donor_count: Donation.completed.count(:user_id)
|
||||
}
|
||||
end
|
||||
|
||||
# GET /donations/1
|
||||
# GET /donations/1.json
|
||||
def show
|
||||
end
|
||||
|
||||
@@ -28,54 +26,41 @@ class Admin::DonationsController < Admin::BaseController
|
||||
end
|
||||
|
||||
# POST /donations
|
||||
# POST /donations.json
|
||||
def create
|
||||
@donation = Donation.new(donation_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @donation.save
|
||||
format.html do
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully created.'
|
||||
}
|
||||
end
|
||||
format.json { render :show, status: :created, location: @donation }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_entity }
|
||||
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
||||
end
|
||||
if @donation.paid_at == nil
|
||||
@donation.errors.add(:paid_at, message: "is required")
|
||||
render :new, status: :unprocessable_entity and return
|
||||
end
|
||||
|
||||
if @donation.save
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully created.'
|
||||
}
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /donations/1
|
||||
# PATCH/PUT /donations/1.json
|
||||
# PUT /donations/1
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @donation.update(donation_params)
|
||||
format.html do
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully updated.'
|
||||
}
|
||||
end
|
||||
format.json { render :show, status: :ok, location: @donation }
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_entity }
|
||||
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
||||
end
|
||||
if @donation.update(donation_params)
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully updated.'
|
||||
}
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /donations/1
|
||||
# DELETE /donations/1.json
|
||||
def destroy
|
||||
@donation.destroy
|
||||
respond_to do |format|
|
||||
format.html do redirect_to admin_donations_url, flash: {
|
||||
success: 'Donation was successfully destroyed.'
|
||||
}
|
||||
end
|
||||
format.json { head :no_content }
|
||||
end
|
||||
|
||||
redirect_to admin_donations_url, flash: {
|
||||
success: 'Donation was successfully destroyed.'
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
@@ -86,7 +71,10 @@ class Admin::DonationsController < Admin::BaseController
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def donation_params
|
||||
params.require(:donation).permit(:user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :paid_at)
|
||||
params.require(:donation).permit(
|
||||
:user_id, :donation_method,
|
||||
:amount_sats, :fiat_amount, :fiat_currency,
|
||||
:public_name, :paid_at)
|
||||
end
|
||||
|
||||
def set_current_section
|
||||
|
||||
@@ -41,4 +41,26 @@ class ApplicationController < ActionController::Base
|
||||
def after_sign_in_path_for(user)
|
||||
session[:user_return_to] || root_path
|
||||
end
|
||||
|
||||
def lndhub_authenticate(options={})
|
||||
if session[:ln_auth_token].present? && !options[:force_reauth]
|
||||
@ln_auth_token = session[:ln_auth_token]
|
||||
else
|
||||
lndhub = Lndhub.new
|
||||
auth_token = lndhub.authenticate(current_user)
|
||||
session[:ln_auth_token] = auth_token
|
||||
@ln_auth_token = auth_token
|
||||
end
|
||||
rescue => e
|
||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||
end
|
||||
|
||||
def lndhub_fetch_balance
|
||||
@balance = LndhubManager::FetchUserBalance.call(auth_token: @ln_auth_token)
|
||||
rescue AuthError
|
||||
lndhub_authenticate(force_reauth: true)
|
||||
raise if @fetch_balance_retried
|
||||
@fetch_balance_retried = true
|
||||
lndhub_fetch_balance
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,129 @@
|
||||
class Contributions::DonationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
include BtcpayHelper
|
||||
|
||||
# GET /donations
|
||||
# GET /donations.json
|
||||
before_action :authenticate_user!
|
||||
before_action :set_donation_methods, only: [:index, :create]
|
||||
before_action :require_donation_method_enabled, only: [:create]
|
||||
before_action :validate_donation_params, only: [:create]
|
||||
before_action :set_donation, only: [:confirm_btcpay]
|
||||
|
||||
# GET /contributions/donations
|
||||
def index
|
||||
@donations = current_user.donations.completed
|
||||
@current_section = :contributions
|
||||
@donations_completed = current_user.donations.completed.order('paid_at desc')
|
||||
@donations_pending = current_user.donations.processing.order('created_at desc')
|
||||
|
||||
if Setting.lndhub_enabled?
|
||||
begin
|
||||
lndhub_authenticate
|
||||
lndhub_fetch_balance
|
||||
rescue
|
||||
@balance = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# POST /contributions/donations
|
||||
def create
|
||||
if params[:currency] == "sats"
|
||||
fiat_amount = nil
|
||||
fiat_currency = nil
|
||||
amount_sats = params[:amount]
|
||||
else
|
||||
fiat_amount = params[:amount].to_i
|
||||
fiat_currency = params[:currency]
|
||||
amount_sats = nil
|
||||
end
|
||||
|
||||
@donation = current_user.donations.create!(
|
||||
donation_method: params[:donation_method],
|
||||
payment_method: nil,
|
||||
paid_at: nil,
|
||||
amount_sats: amount_sats,
|
||||
fiat_amount: (fiat_amount.nil? ? nil : fiat_amount * 100), # store in cents
|
||||
fiat_currency: fiat_currency,
|
||||
public_name: params[:public_name]
|
||||
)
|
||||
|
||||
case params[:donation_method]
|
||||
when "btcpay"
|
||||
res = BtcpayManager::CreateInvoice.call(
|
||||
amount: fiat_amount || (amount_sats.to_f / 100000000),
|
||||
currency: fiat_currency || "BTC",
|
||||
redirect_url: confirm_btcpay_contributions_donation_url(@donation)
|
||||
)
|
||||
|
||||
@donation.update! btcpay_invoice_id: res["id"]
|
||||
|
||||
redirect_to btcpay_checkout_url(res["id"]), allow_other_host: true
|
||||
else
|
||||
redirect_to contributions_donations_url, flash: {
|
||||
error: "Donation method currently not available"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_btcpay
|
||||
redirect_to contributions_donations_url and return if @donation.completed?
|
||||
|
||||
invoice = BtcpayManager::FetchInvoice.call(invoice_id: @donation.btcpay_invoice_id)
|
||||
|
||||
if @donation.amount_sats.present?
|
||||
# TODO make default fiat currency configurable and/or determine from user's
|
||||
# i18n browser settings
|
||||
@donation.fiat_currency = "EUR"
|
||||
exchange_rate = BtcpayManager::FetchExchangeRate.call(fiat_currency: @donation.fiat_currency)
|
||||
@donation.fiat_amount = (((@donation.amount_sats.to_f / 100000000) * exchange_rate) * 100).to_i
|
||||
else
|
||||
amt_str = invoice["paymentMethods"].first["amount"]
|
||||
@donation.amount_sats = amt_str.tr(".","").sub(/0*$/, "").to_i
|
||||
end
|
||||
|
||||
case invoice["status"]
|
||||
when "Settled"
|
||||
@donation.paid_at = DateTime.now
|
||||
@donation.payment_status = "settled"
|
||||
@donation.save!
|
||||
flash_message = { success: "Thank you!" }
|
||||
when "Processing"
|
||||
unless @donation.processing?
|
||||
@donation.payment_status = "processing"
|
||||
@donation.save!
|
||||
flash_message = { success: "Thank you! We will send you an email when the payment is confirmed." }
|
||||
BtcpayCheckDonationJob.set(wait: 20.seconds).perform_later(@donation)
|
||||
end
|
||||
when "Expired"
|
||||
flash_message = { warning: "The payment request for this donation has expired" }
|
||||
else
|
||||
flash_message = { warning: "Could not determine status of payment" }
|
||||
end
|
||||
|
||||
redirect_to contributions_donations_url, flash: flash_message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_donation
|
||||
@donation = current_user.donations.find_by(id: params[:id])
|
||||
http_status :not_found unless @donation.present?
|
||||
end
|
||||
|
||||
def set_donation_methods
|
||||
@donation_methods = []
|
||||
@donation_methods.push :btcpay if Setting.btcpay_enabled?
|
||||
@donation_methods.push :lndhub if Setting.lndhub_enabled?
|
||||
@donation_methods.push :opencollective if Setting.opencollective_enabled?
|
||||
end
|
||||
|
||||
def require_donation_method_enabled
|
||||
http_status :forbidden unless @donation_methods.include?(
|
||||
params[:donation_method].to_sym
|
||||
)
|
||||
end
|
||||
|
||||
def validate_donation_params
|
||||
if !%w[EUR USD sats].include?(params[:currency]) || (params[:amount].to_i <= 0)
|
||||
http_status :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,10 +2,11 @@ require "rqrcode"
|
||||
require "lnurl"
|
||||
|
||||
class Services::LightningController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :authenticate_with_lndhub
|
||||
before_action :set_current_section
|
||||
before_action :fetch_balance
|
||||
before_action :require_service_available
|
||||
before_action :authenticate_user!
|
||||
before_action :lndhub_authenticate
|
||||
before_action :lndhub_fetch_balance
|
||||
|
||||
def index
|
||||
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
||||
@@ -55,32 +56,12 @@ class Services::LightningController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def authenticate_with_lndhub(options={})
|
||||
if session[:ln_auth_token].present? && !options[:force_reauth]
|
||||
@ln_auth_token = session[:ln_auth_token]
|
||||
else
|
||||
lndhub = Lndhub.new
|
||||
auth_token = lndhub.authenticate(current_user)
|
||||
session[:ln_auth_token] = auth_token
|
||||
@ln_auth_token = auth_token
|
||||
end
|
||||
rescue => e
|
||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||
end
|
||||
|
||||
def set_current_section
|
||||
@current_section = :services
|
||||
end
|
||||
|
||||
def fetch_balance
|
||||
lndhub = Lndhub.new
|
||||
data = lndhub.balance @ln_auth_token
|
||||
@balance = data["BTC"]["AvailableBalance"] rescue nil
|
||||
rescue AuthError
|
||||
authenticate_with_lndhub(force_reauth: true)
|
||||
raise if @fetch_balance_retried
|
||||
@fetch_balance_retried = true
|
||||
fetch_balance
|
||||
def require_service_available
|
||||
http_status :not_found unless Setting.lndhub_enabled?
|
||||
end
|
||||
|
||||
def fetch_transactions
|
||||
|
||||
@@ -12,7 +12,7 @@ class SettingsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
if @settings_section == "experiments"
|
||||
if @settings_section == "nostr"
|
||||
session[:shared_secret] ||= SecureRandom.base64(12)
|
||||
end
|
||||
end
|
||||
@@ -88,6 +88,7 @@ class SettingsController < ApplicationController
|
||||
|
||||
def set_nostr_pubkey
|
||||
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
|
||||
|
||||
is_valid_id = NostrManager::ValidateId.call(event: signed_event)
|
||||
is_valid_sig = NostrManager::VerifySignature.call(event: signed_event)
|
||||
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
|
||||
@@ -97,30 +98,26 @@ class SettingsController < ApplicationController
|
||||
http_status :unprocessable_entity and return
|
||||
end
|
||||
|
||||
pubkey_taken = User.all_except(current_user).where(
|
||||
ou: current_user.ou, nostr_pubkey: signed_event[:pubkey]
|
||||
).any?
|
||||
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event[:pubkey])
|
||||
|
||||
if pubkey_taken
|
||||
if user_with_pubkey.present? && (user_with_pubkey != current_user)
|
||||
flash[:alert] = "Public key already in use for a different account"
|
||||
http_status :unprocessable_entity and return
|
||||
end
|
||||
|
||||
current_user.update! nostr_pubkey: signed_event[:pubkey]
|
||||
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event[:pubkey])
|
||||
session[:shared_secret] = nil
|
||||
|
||||
flash[:success] = "Public key verification successful"
|
||||
http_status :ok
|
||||
rescue
|
||||
flash[:alert] = "Public key could not be verified"
|
||||
http_status :unprocessable_entity and return
|
||||
end
|
||||
|
||||
# DELETE /settings/nostr_pubkey
|
||||
def remove_nostr_pubkey
|
||||
current_user.update! nostr_pubkey: nil
|
||||
# TODO require current pubkey or password to delete
|
||||
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: nil)
|
||||
|
||||
redirect_to setting_path(:experiments), flash: {
|
||||
redirect_to setting_path(:nostr), flash: {
|
||||
success: 'Public key removed from account'
|
||||
}
|
||||
end
|
||||
@@ -134,8 +131,8 @@ class SettingsController < ApplicationController
|
||||
def set_settings_section
|
||||
@settings_section = params[:section]
|
||||
allowed_sections = [
|
||||
:profile, :account, :xmpp, :email, :lightning, :remotestorage,
|
||||
:experiments
|
||||
:profile, :account, :xmpp, :email,
|
||||
:lightning, :remotestorage, :nostr
|
||||
]
|
||||
|
||||
unless allowed_sections.include?(@settings_section.to_sym)
|
||||
@@ -165,7 +162,7 @@ class SettingsController < ApplicationController
|
||||
|
||||
def nostr_event_params
|
||||
params.permit(signed_event: [
|
||||
:id, :pubkey, :created_at, :kind, :tags, :content, :sig
|
||||
:id, :pubkey, :created_at, :kind, :content, :sig, tags: []
|
||||
])
|
||||
end
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
module ApplicationHelper
|
||||
include Pagy::Frontend
|
||||
|
||||
def sats_to_btc(sats)
|
||||
sats.to_f / 100000000
|
||||
end
|
||||
|
||||
def main_nav_class(current_section, link_to_section)
|
||||
if current_section == link_to_section
|
||||
"bg-gray-900/50 text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
|
||||
|
||||
7
app/helpers/btcpay_helper.rb
Normal file
7
app/helpers/btcpay_helper.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
module BtcpayHelper
|
||||
|
||||
def btcpay_checkout_url(invoice_id)
|
||||
"#{Setting.btcpay_public_url}/i/#{invoice_id}"
|
||||
end
|
||||
|
||||
end
|
||||
28
app/jobs/btcpay_check_donation_job.rb
Normal file
28
app/jobs/btcpay_check_donation_job.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
class BtcpayCheckDonationJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(donation)
|
||||
return if donation.completed?
|
||||
|
||||
invoice = BtcpayManager::FetchInvoice.call(
|
||||
invoice_id: donation.btcpay_invoice_id
|
||||
)
|
||||
|
||||
case invoice["status"]
|
||||
when "Settled"
|
||||
donation.paid_at = DateTime.now
|
||||
donation.payment_status = "settled"
|
||||
donation.save!
|
||||
|
||||
NotificationMailer.with(user: donation.user)
|
||||
.bitcoin_donation_confirmed
|
||||
.deliver_later
|
||||
when "Processing"
|
||||
re_enqueue_job(donation)
|
||||
end
|
||||
end
|
||||
|
||||
def re_enqueue_job(donation)
|
||||
self.class.set(wait: 20.seconds).perform_later(donation)
|
||||
end
|
||||
end
|
||||
@@ -23,4 +23,11 @@ class NotificationMailer < ApplicationMailer
|
||||
@subject = "New invitations added to your account"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
|
||||
def bitcoin_donation_confirmed
|
||||
@user = params[:user]
|
||||
@donation = params[:donation]
|
||||
@subject = "Donation confirmed"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,12 +4,25 @@ class Donation < ApplicationRecord
|
||||
|
||||
# Validations
|
||||
validates_presence_of :user
|
||||
validates_presence_of :amount_sats
|
||||
validates_presence_of :paid_at
|
||||
|
||||
# Hooks
|
||||
# TODO before_create :store_fiat_value
|
||||
validates_presence_of :donation_method,
|
||||
inclusion: { in: %w[ custom btcpay lndhub ] }
|
||||
validates_presence_of :payment_status, allow_nil: true,
|
||||
inclusion: { in: %w[ processing settled ] }
|
||||
validates_presence_of :paid_at, allow_nil: true
|
||||
validates_presence_of :amount_sats, allow_nil: true
|
||||
validates_presence_of :fiat_amount, allow_nil: true
|
||||
validates_presence_of :fiat_currency, allow_nil: true,
|
||||
inclusion: { in: %w[ EUR USD ] }
|
||||
|
||||
#Scopes
|
||||
scope :completed, -> { where.not(paid_at: nil) }
|
||||
scope :processing, -> { where(payment_status: "processing") }
|
||||
scope :completed, -> { where(payment_status: "settled") }
|
||||
|
||||
def processing?
|
||||
payment_status == "processing"
|
||||
end
|
||||
|
||||
def completed?
|
||||
payment_status == "settled"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,6 +51,9 @@ class Setting < RailsSettings::Base
|
||||
field :btcpay_enabled, type: :boolean,
|
||||
default: ENV["BTCPAY_API_URL"].present?
|
||||
|
||||
field :btcpay_public_url, type: :string,
|
||||
default: ENV["BTCPAY_PUBLIC_URL"].presence
|
||||
|
||||
field :btcpay_store_id, type: :string,
|
||||
default: ENV["BTCPAY_STORE_ID"].presence
|
||||
|
||||
@@ -157,7 +160,13 @@ class Setting < RailsSettings::Base
|
||||
# Nostr
|
||||
#
|
||||
|
||||
field :nostr_enabled, type: :boolean, default: true
|
||||
field :nostr_enabled, type: :boolean, default: false
|
||||
|
||||
#
|
||||
# OpenCollective
|
||||
#
|
||||
|
||||
field :opencollective_enabled, type: :boolean, default: true
|
||||
|
||||
#
|
||||
# RemoteStorage
|
||||
|
||||
@@ -50,8 +50,6 @@ class User < ApplicationRecord
|
||||
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
|
||||
if: -> { defined?(@display_name) }
|
||||
|
||||
validates_uniqueness_of :nostr_pubkey, allow_blank: true
|
||||
|
||||
validate :acceptable_avatar
|
||||
|
||||
#
|
||||
@@ -163,37 +161,41 @@ class User < ApplicationRecord
|
||||
@display_name ||= ldap_entry[:display_name]
|
||||
end
|
||||
|
||||
def nostr_pubkey
|
||||
@nostr_pubkey ||= ldap_entry[:nostr_key]
|
||||
end
|
||||
|
||||
def nostr_pubkey_bech32
|
||||
return nil unless nostr_pubkey.present?
|
||||
Nostr::PublicKey.new(nostr_pubkey).to_bech32
|
||||
end
|
||||
|
||||
def avatar
|
||||
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
|
||||
end
|
||||
|
||||
def services_enabled
|
||||
ldap_entry[:service] || []
|
||||
ldap_entry[:services_enabled] || []
|
||||
end
|
||||
|
||||
def enable_service(service)
|
||||
current_services = services_enabled
|
||||
new_services = Array(service).map(&:to_s)
|
||||
services = (current_services + new_services).uniq
|
||||
ldap.replace_attribute(dn, :service, services)
|
||||
ldap.replace_attribute(dn, :serviceEnabled, services)
|
||||
end
|
||||
|
||||
def disable_service(service)
|
||||
current_services = services_enabled
|
||||
disabled_services = Array(service).map(&:to_s)
|
||||
services = (current_services - disabled_services).uniq
|
||||
ldap.replace_attribute(dn, :service, services)
|
||||
ldap.replace_attribute(dn, :serviceEnabled, services)
|
||||
end
|
||||
|
||||
def disable_all_services
|
||||
ldap.delete_attribute(dn,:service)
|
||||
end
|
||||
|
||||
def nostr_pubkey_bech32
|
||||
return nil unless nostr_pubkey.present?
|
||||
Nostr::PublicKey.new(nostr_pubkey).to_bech32
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ldap
|
||||
|
||||
21
app/services/btcpay_manager/create_invoice.rb
Normal file
21
app/services/btcpay_manager/create_invoice.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module BtcpayManager
|
||||
class CreateInvoice < BtcpayManagerService
|
||||
def initialize(amount:, currency:, redirect_url:)
|
||||
@amount = amount
|
||||
@currency = currency
|
||||
@redirect_url = redirect_url
|
||||
end
|
||||
|
||||
def call
|
||||
post "/invoices", {
|
||||
amount: @amount.to_s,
|
||||
currency: @currency,
|
||||
checkout: {
|
||||
redirectURL: @redirect_url,
|
||||
redirectAutomatically: true,
|
||||
requiresRefundEmail: false
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
14
app/services/btcpay_manager/fetch_exchange_rate.rb
Normal file
14
app/services/btcpay_manager/fetch_exchange_rate.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module BtcpayManager
|
||||
class FetchExchangeRate < BtcpayManagerService
|
||||
def initialize(fiat_currency:)
|
||||
@fiat_currency = fiat_currency
|
||||
end
|
||||
|
||||
def call
|
||||
pair_str = "BTC_#{@fiat_currency}"
|
||||
res = get "rates", { currencyPair: pair_str }
|
||||
pair = res.find{|p| p["currencyPair"] == pair_str }
|
||||
rate = pair["rate"].to_f
|
||||
end
|
||||
end
|
||||
end
|
||||
14
app/services/btcpay_manager/fetch_invoice.rb
Normal file
14
app/services/btcpay_manager/fetch_invoice.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module BtcpayManager
|
||||
class FetchInvoice < BtcpayManagerService
|
||||
def initialize(invoice_id:)
|
||||
@invoice_id = invoice_id
|
||||
end
|
||||
|
||||
def call
|
||||
invoice = get "/invoices/#{@invoice_id}"
|
||||
payment_methods = get "/invoices/#{@invoice_id}/payment-methods"
|
||||
invoice["paymentMethods"] = payment_methods
|
||||
invoice
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
module BtcpayManager
|
||||
class FetchLightningWalletBalance < BtcpayManagerService
|
||||
def call
|
||||
res = get "stores/#{store_id}/lightning/BTC/balance"
|
||||
res = get "/lightning/BTC/balance"
|
||||
|
||||
{
|
||||
confirmed_balance: res["offchain"]["local"].to_i / 1000 # msats to sats
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module BtcpayManager
|
||||
class FetchOnchainWalletBalance < BtcpayManagerService
|
||||
def call
|
||||
res = get "stores/#{store_id}/payment-methods/onchain/BTC/wallet"
|
||||
res = get "/payment-methods/onchain/BTC/wallet"
|
||||
|
||||
{
|
||||
balance: (res["balance"].to_f * 100000000).to_i, # BTC to sats
|
||||
|
||||
@@ -2,23 +2,35 @@
|
||||
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
|
||||
#
|
||||
class BtcpayManagerService < ApplicationService
|
||||
attr_reader :base_url, :store_id, :auth_token
|
||||
|
||||
def initialize
|
||||
@base_url = Setting.btcpay_api_url
|
||||
@store_id = Setting.btcpay_store_id
|
||||
@auth_token = Setting.btcpay_auth_token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get(endpoint)
|
||||
res = Faraday.get("#{base_url}/#{endpoint}", {}, {
|
||||
def base_url
|
||||
@base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
|
||||
end
|
||||
|
||||
def auth_token
|
||||
@auth_token ||= Setting.btcpay_auth_token
|
||||
end
|
||||
|
||||
def headers
|
||||
{
|
||||
"Content-Type" => "application/json",
|
||||
"Accept" => "application/json",
|
||||
"Authorization" => "token #{auth_token}"
|
||||
})
|
||||
}
|
||||
end
|
||||
|
||||
def endpoint_url(path)
|
||||
"#{base_url}/#{path.gsub(/^\//, '')}"
|
||||
end
|
||||
|
||||
def get(path, params = {})
|
||||
res = Faraday.get endpoint_url(path), params, headers
|
||||
JSON.parse(res.body)
|
||||
end
|
||||
|
||||
def post(path, payload)
|
||||
res = Faraday.post endpoint_url(path), payload.to_json, headers
|
||||
JSON.parse(res.body)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ module LdapManager
|
||||
attributes = %w{ jpegPhoto }
|
||||
filter = Net::LDAP::Filter.eq("cn", @cn)
|
||||
|
||||
entry = ldap_client.search(base: treebase, filter: filter, attributes: attributes).first
|
||||
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
|
||||
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
|
||||
end
|
||||
end
|
||||
|
||||
18
app/services/ldap_manager/fetch_user_by_nostr_key.rb
Normal file
18
app/services/ldap_manager/fetch_user_by_nostr_key.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
module LdapManager
|
||||
class FetchUserByNostrKey < LdapManagerService
|
||||
def initialize(pubkey:)
|
||||
@ou = Setting.primary_domain
|
||||
@pubkey = pubkey
|
||||
end
|
||||
|
||||
def call
|
||||
treebase = "ou=#{@ou},cn=users,#{ldap_suffix}"
|
||||
attributes = %w{ cn }
|
||||
filter = Net::LDAP::Filter.eq("nostrKey", @pubkey)
|
||||
|
||||
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
|
||||
|
||||
User.find_by cn: entry.cn, ou: @ou unless entry.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
16
app/services/ldap_manager/update_nostr_key.rb
Normal file
16
app/services/ldap_manager/update_nostr_key.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module LdapManager
|
||||
class UpdateNostrKey < LdapManagerService
|
||||
def initialize(dn:, pubkey:)
|
||||
@dn = dn
|
||||
@pubkey = pubkey
|
||||
end
|
||||
|
||||
def call
|
||||
if @pubkey.present?
|
||||
replace_attribute @dn, :nostrKey, @pubkey
|
||||
else
|
||||
delete_attribute @dn, :nostrKey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,2 @@
|
||||
class LdapManagerService < LdapService
|
||||
def suffix
|
||||
@suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
class LdapService < ApplicationService
|
||||
def initialize
|
||||
@suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
||||
def modify(dn, operations=[])
|
||||
client.modify dn: dn, operations: operations
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def add_attribute(dn, attr, values)
|
||||
ldap_client.add_attribute dn, attr, values
|
||||
client.add_attribute dn, attr, values
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def replace_attribute(dn, attr, values)
|
||||
ldap_client.replace_attribute dn, attr, values
|
||||
client.replace_attribute dn, attr, values
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def delete_attribute(dn, attr)
|
||||
ldap_client.delete_attribute dn, attr
|
||||
client.delete_attribute dn, attr
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def add_entry(dn, attrs, interactive=false)
|
||||
puts "Adding entry: #{dn}" if interactive
|
||||
res = ldap_client.add dn: dn, attributes: attrs
|
||||
puts res.inspect if interactive && !res
|
||||
res
|
||||
puts "Add entry: #{dn}" if interactive
|
||||
client.add dn: dn, attributes: attrs
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def delete_entry(dn, interactive=false)
|
||||
puts "Deleting entry: #{dn}" if interactive
|
||||
res = ldap_client.delete dn: dn
|
||||
puts res.inspect if interactive && !res
|
||||
res
|
||||
puts "Delete entry: #{dn}" if interactive
|
||||
client.delete dn: dn
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def delete_all_entries!
|
||||
def delete_all_users!
|
||||
delete_all_entries!(objectclass: "person")
|
||||
end
|
||||
|
||||
def delete_all_entries!(objectclass: "*")
|
||||
if Rails.env.production?
|
||||
raise "Mass deletion of entries not allowed in production"
|
||||
end
|
||||
|
||||
filter = Net::LDAP::Filter.eq("objectClass", "*")
|
||||
entries = ldap_client.search(base: @suffix, filter: filter, attributes: %w{dn})
|
||||
filter = Net::LDAP::Filter.eq("objectClass", objectclass)
|
||||
entries = client.search(base: ldap_suffix, filter: filter, attributes: %w{dn})
|
||||
entries.sort_by!{ |e| e.dn.length }.reverse!
|
||||
|
||||
entries.each do |e|
|
||||
@@ -45,18 +51,18 @@ class LdapService < ApplicationService
|
||||
|
||||
def fetch_users(args={})
|
||||
if args[:ou]
|
||||
treebase = "ou=#{args[:ou]},cn=users,#{@suffix}"
|
||||
treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}"
|
||||
else
|
||||
treebase = ldap_config["base"]
|
||||
end
|
||||
|
||||
attributes = %w[
|
||||
dn cn uid mail displayName admin service
|
||||
mailRoutingAddress mailpassword
|
||||
dn cn uid mail displayName admin serviceEnabled
|
||||
mailRoutingAddress mailpassword nostrKey
|
||||
]
|
||||
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
|
||||
|
||||
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
||||
entries = client.search(base: treebase, filter: filter, attributes: attributes)
|
||||
entries.sort_by! { |e| e.cn[0] }
|
||||
entries = entries.collect do |e|
|
||||
{
|
||||
@@ -64,9 +70,10 @@ class LdapService < ApplicationService
|
||||
mail: e.try(:mail) ? e.mail.first : nil,
|
||||
display_name: e.try(:displayName) ? e.displayName.first : nil,
|
||||
admin: e.try(:admin) ? 'admin' : nil,
|
||||
service: e.try(:service),
|
||||
services_enabled: e.try(:serviceEnabled),
|
||||
email_maildrop: e.try(:mailRoutingAddress),
|
||||
email_password: e.try(:mailpassword)
|
||||
email_password: e.try(:mailpassword),
|
||||
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -75,9 +82,9 @@ class LdapService < ApplicationService
|
||||
attributes = %w{dn ou description}
|
||||
filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit")
|
||||
# filter = Net::LDAP::Filter.eq("objectClass", "*")
|
||||
treebase = "cn=users,#{@suffix}"
|
||||
treebase = "cn=users,#{ldap_suffix}"
|
||||
|
||||
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
||||
entries = client.search(base: treebase, filter: filter, attributes: attributes)
|
||||
|
||||
entries.sort_by! { |e| e.ou[0] }
|
||||
|
||||
@@ -91,10 +98,10 @@ class LdapService < ApplicationService
|
||||
end
|
||||
|
||||
def add_organization(ou, description, interactive=false)
|
||||
dn = "ou=#{ou},cn=users,#{@suffix}"
|
||||
dn = "ou=#{ou},cn=users,#{ldap_suffix}"
|
||||
|
||||
aci = <<-EOS
|
||||
(target="ldap:///cn=*,ou=#{ou},cn=users,#{@suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{@suffix}";)
|
||||
(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
|
||||
EOS
|
||||
|
||||
attrs = {
|
||||
@@ -115,22 +122,22 @@ class LdapService < ApplicationService
|
||||
delete_all_entries!
|
||||
|
||||
user_read_aci = <<-EOS
|
||||
(target="ldap:///#{@suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";)
|
||||
(target="ldap:///#{ldap_suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";)
|
||||
EOS
|
||||
|
||||
add_entry @suffix, {
|
||||
add_entry ldap_suffix, {
|
||||
dc: "kosmos", objectClass: ["top", "domain"], aci: user_read_aci
|
||||
}, true
|
||||
|
||||
add_entry "cn=users,#{@suffix}", {
|
||||
add_entry "cn=users,#{ldap_suffix}", {
|
||||
cn: "users", objectClass: ["top", "organizationalRole"]
|
||||
}, true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ldap_client
|
||||
ldap_client ||= Net::LDAP.new host: ldap_config['host'],
|
||||
def client
|
||||
client ||= Net::LDAP.new host: ldap_config['host'],
|
||||
port: ldap_config['port'],
|
||||
# TODO has to be :simple_tls if TLS is enabled
|
||||
# encryption: ldap_config['ssl'],
|
||||
@@ -144,4 +151,8 @@ class LdapService < ApplicationService
|
||||
def ldap_config
|
||||
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
|
||||
end
|
||||
|
||||
def ldap_suffix
|
||||
@ldap_suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
class Lndhub
|
||||
class Lndhub < ApplicationService
|
||||
attr_accessor :auth_token
|
||||
|
||||
def initialize
|
||||
@base_url = ENV["LNDHUB_API_URL"]
|
||||
end
|
||||
|
||||
def post(endpoint, payload)
|
||||
def post(path, payload)
|
||||
headers = { "Content-Type" => "application/json" }
|
||||
if auth_token
|
||||
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||
end
|
||||
|
||||
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
||||
res = Faraday.post endpoint_url(path), payload.to_json, headers
|
||||
log_error(res) if res.status != 200
|
||||
|
||||
JSON.parse(res.body)
|
||||
end
|
||||
|
||||
def get(endpoint, auth_token)
|
||||
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
|
||||
def get(path, auth_token)
|
||||
res = Faraday.get(endpoint_url(path), {}, {
|
||||
"Content-Type" => "application/json",
|
||||
"Accept" => "application/json",
|
||||
"Authorization" => "Bearer #{auth_token}"
|
||||
@@ -42,7 +38,7 @@ class Lndhub
|
||||
self.auth_token
|
||||
end
|
||||
|
||||
def balance(user_token=nil)
|
||||
def fetch_balance(user_token=nil)
|
||||
get "balance", user_token || auth_token
|
||||
end
|
||||
|
||||
@@ -72,4 +68,14 @@ class Lndhub
|
||||
Sentry.capture_message("Lndhub API request failed: #{res.body}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_url
|
||||
@base_url ||= Setting.lndhub_api_url
|
||||
end
|
||||
|
||||
def endpoint_url(path)
|
||||
"#{base_url}/#{path.gsub(/^\//, '')}"
|
||||
end
|
||||
end
|
||||
|
||||
12
app/services/lndhub_manager/fetch_user_balance.rb
Normal file
12
app/services/lndhub_manager/fetch_user_balance.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module LndhubManager
|
||||
class FetchUserBalance < Lndhub
|
||||
def initialize(auth_token:)
|
||||
@auth_token = auth_token
|
||||
end
|
||||
|
||||
def call
|
||||
data = fetch_balance(auth_token)
|
||||
data["BTC"]["AvailableBalance"] rescue nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,13 +1,13 @@
|
||||
class LndhubV2 < Lndhub
|
||||
|
||||
def post(endpoint, payload, options={})
|
||||
def post(path, payload, options={})
|
||||
headers = { "Content-Type" => "application/json" }
|
||||
if auth_token
|
||||
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||
elsif options[:admin_token]
|
||||
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
|
||||
end
|
||||
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
||||
res = Faraday.post endpoint_url(path), payload.to_json, headers
|
||||
log_error(res) if res.status != 200
|
||||
|
||||
JSON.parse(res.body)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
json.extract! donation, :id, :user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :created_at, :updated_at
|
||||
json.url donation_url(donation, format: :json)
|
||||
@@ -14,14 +14,24 @@
|
||||
<%= form.label :user_id %>
|
||||
<%= form.collection_select :user_id, User.where(ou: Setting.primary_domain).order(:cn), :id, :cn, {} %>
|
||||
|
||||
<%= form.label :donation_method, "Donation method" %>
|
||||
<%= form.select :donation_method, options_for_select([
|
||||
["Custom (manual)", "custom"],
|
||||
["BTCPay", "btcpay"],
|
||||
["LndHub account", "lndhub"],
|
||||
["OpenCollective", "opencollective"]
|
||||
], selected: (donation.donation_method || "custom")) %>
|
||||
|
||||
<%= form.label :amount_sats, "Amount BTC (sats)" %>
|
||||
<%= form.number_field :amount_sats %>
|
||||
|
||||
<%= form.label :amount_eur, "Amount EUR (cents)" %>
|
||||
<%= form.number_field :amount_eur %>
|
||||
<%= form.label :fiat_amount, "Fiat Amount (cents)" %>
|
||||
<%= form.number_field :fiat_amount %>
|
||||
|
||||
<%= form.label :amount_usd, "Amount USD (cents)"%>
|
||||
<%= form.number_field :amount_usd %>
|
||||
<%= form.label :fiat_currency, "Fiat Currency" %>
|
||||
<%= form.select :fiat_currency, options_for_select([
|
||||
["EUR", "EUR"], ["USD", "USD"], ["sats", "sats"]
|
||||
], selected: donation.fiat_currency) %>
|
||||
|
||||
<%= form.label :public_name %>
|
||||
<%= form.text_field :public_name %>
|
||||
|
||||
@@ -25,9 +25,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th class="text-right">Amount BTC</th>
|
||||
<th class="text-right">in EUR</th>
|
||||
<th class="text-right">in USD</th>
|
||||
<th class="text-right">Sats</th>
|
||||
<th class="text-right">Fiat Amount</th>
|
||||
<th class="pl-2">Public name</th>
|
||||
<th>Date</th>
|
||||
<th></th>
|
||||
@@ -36,10 +35,9 @@
|
||||
<tbody>
|
||||
<% @donations.each do |donation| %>
|
||||
<tr>
|
||||
<td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td>
|
||||
<td class="text-right"><%= sats_to_btc donation.amount_sats %></td>
|
||||
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
|
||||
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
|
||||
<td><%= link_to donation.user.cn, admin_user_path(donation.user.cn), class: 'ks-text-link' %></td>
|
||||
<td class="text-right"><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></td>
|
||||
<td class="text-right"><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %></td>
|
||||
<td class="pl-2"><%= donation.public_name %></td>
|
||||
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td>
|
||||
<td class="text-right">
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
json.array! @donations, partial: "donations/donation", as: :donation
|
||||
@@ -6,19 +6,19 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<td><%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td>
|
||||
<td><%= link_to @donation.user.cn, admin_user_path(@donation.user.cn), class: 'ks-text-link' %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Donation Method</th>
|
||||
<td><%= @donation.donation_method %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Amount sats</th>
|
||||
<td><%= @donation.amount_sats %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Amount EUR</th>
|
||||
<td><%= @donation.amount_eur %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Amount USD</th>
|
||||
<td><%= @donation.amount_usd %></td>
|
||||
<th>Fiat amount</th>
|
||||
<td><% if @donation.fiat_amount.present? %><%= number_to_currency @donation.fiat_amount.to_f / 100, unit: "" %> <%= @donation.fiat_currency %><% end %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Public name</th>
|
||||
@@ -26,7 +26,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<td><%= @donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
||||
<td><%= @donation.paid_at&.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
json.partial! "donations/donation", donation: @donation
|
||||
36
app/views/contributions/donations/_bitcoin.html.erb
Normal file
36
app/views/contributions/donations/_bitcoin.html.erb
Normal file
@@ -0,0 +1,36 @@
|
||||
<div class="rounded-lg p-6 bg-emerald-50 hover:bg-emerald-100 transition-colors">
|
||||
<h3 class="mb-4 text-lg font-bold">Donate directly with Bitcoin</h3>
|
||||
<p class="mb-6">
|
||||
Open-source money for open-source services.
|
||||
</p>
|
||||
<div data-controller="modal" data-action="keydown.esc->modal#close">
|
||||
<button class="btn-md btn-emerald w-full lg:w-1/2" data-action="click->modal#open">
|
||||
Donate
|
||||
</button>
|
||||
<%= render ModalComponent.new(show_close_button: false) do %>
|
||||
<div>
|
||||
<h3>Your contribution</h3>
|
||||
|
||||
<%= form_with(url: contributions_donations_url, method: :post) do |f| %>
|
||||
<%= f.hidden_field :donation_method, value: "btcpay" %>
|
||||
|
||||
<div class="mb-6 flex gap-2">
|
||||
<%= f.number_field :amount, required: true %>
|
||||
<%= f.select :currency, options_for_select([
|
||||
["EUR", "EUR"], ["USD", "USD"], ["sats", "sats"]
|
||||
], selected: "EUR"), class: "flex-none" %>
|
||||
</div>
|
||||
|
||||
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Public name") do %>
|
||||
<%= f.text_field :public_name, class: "w-full", placeholder: "Anonymous" %>
|
||||
<% end %>
|
||||
|
||||
<p class="mt-12">
|
||||
<%= f.submit 'Continue', data: { turbo: false },
|
||||
class: "btn-md btn-blue w-full" %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
37
app/views/contributions/donations/_list.html.erb
Normal file
37
app/views/contributions/donations/_list.html.erb
Normal file
@@ -0,0 +1,37 @@
|
||||
<ul class="list-none">
|
||||
<% donations.each do |donation| %>
|
||||
<li class="mb-8 grid gap-y-2 grid-cols-2 items-center">
|
||||
<h3 class="mb-0">
|
||||
<% if donation.completed? %>
|
||||
<%= donation.paid_at.strftime("%B %d, %Y") %>
|
||||
<% else %>
|
||||
<%= donation.created_at.strftime("%B %d, %Y") %>
|
||||
<% end %>
|
||||
</h3>
|
||||
<p class="row-span-2 font-mono text-right mb-0">
|
||||
<span class="text-xl">
|
||||
<%= number_with_delimiter donation.amount_sats %> sats
|
||||
</span>
|
||||
<br>
|
||||
<span class="text-sm text-gray-500">
|
||||
(~ <%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %>)
|
||||
</span>
|
||||
</p>
|
||||
<p class="mb-0 text-gray-500">
|
||||
<% if donation.processing? %>
|
||||
Waiting for confirmations
|
||||
<% if donation.donation_method == "btcpay" %>
|
||||
<%= link_to "check status", btcpay_checkout_url(donation.btcpay_invoice_id),
|
||||
class: "ml-2 btn-sm btn-gray" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% if donation.public_name.present? %>
|
||||
As: <%= donation.public_name %>
|
||||
<% else %>
|
||||
Anonymous
|
||||
<% end %>
|
||||
<% end %>
|
||||
</p>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="rounded-lg p-6 bg-zinc-100 hover:bg-zinc-200 transition-colors">
|
||||
<h3 class="mb-4 text-lg font-bold text-gray-500">Donate via OpenCollective</h3>
|
||||
<p class="text-gray-600 text-gray-500">
|
||||
Coming soon.
|
||||
</p>
|
||||
</div>
|
||||
@@ -2,50 +2,39 @@
|
||||
|
||||
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
|
||||
<section>
|
||||
<% if @donations.any? %>
|
||||
<p class="mb-12">
|
||||
Your financial contributions to the development and upkeep of Kosmos
|
||||
software and services.
|
||||
</p>
|
||||
<ul class="list-none">
|
||||
<% @donations.each do |donation| %>
|
||||
<li class="mb-8 grid gap-y-2 gap-x-8 grid-cols-2 items-center">
|
||||
<h3 class="mb-0">
|
||||
<%= donation.paid_at.strftime("%B %d, %Y") %>
|
||||
</h3>
|
||||
<p class="row-span-2 font-mono text-right mb-0">
|
||||
<span class="text-xl">
|
||||
<%= number_with_delimiter donation.amount_sats %> sats
|
||||
</span>
|
||||
<br>
|
||||
<span class="text-sm text-gray-500">
|
||||
(~ <%= number_to_currency donation.amount_eur / 100, unit: "" %> EUR)
|
||||
</span>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<% if donation.public_name.present? %>
|
||||
Public name: <%= donation.public_name %>
|
||||
<% else %>
|
||||
Anonymous
|
||||
<% end %>
|
||||
</p>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<div class="text-center">
|
||||
<p class="mt-8 mb-12 inline-flex align-center items-center">
|
||||
<%= image_tag("/img/illustrations/undraw_savings_re_eq4w.svg", class: 'h-48') %>
|
||||
</p>
|
||||
<h3>
|
||||
No donations yet
|
||||
</h3>
|
||||
<p class="text-gray-500">
|
||||
The donation process is not automated yet.<br>Please
|
||||
<a href="https://wiki.kosmos.org/Main_Page#Community_.2F_Getting_in_touch_.2F_Getting_involved" class="ks-text-link" target="_blank">contact us</a>
|
||||
if you'd like to contribute this way right now.
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<p class="mb-12">
|
||||
Your financial contributions to the development and upkeep of Kosmos
|
||||
software and services.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="donation-methods">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||
<% if @donation_methods.include?(:btcpay) ||
|
||||
@donation_methods.include?(:lndhub) %>
|
||||
<%= render partial: "contributions/donations/bitcoin", locals: {
|
||||
donation_methods: @donation_methods, lndhub_balance: @balance
|
||||
} %>
|
||||
<% end %>
|
||||
<% if @donation_methods.include?(:opencollective) %>
|
||||
<%= render partial: "contributions/donations/opencollective" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<% if @donations_pending.any? %>
|
||||
<section class="donation-list">
|
||||
<h2>Pending</h2>
|
||||
<%= render partial: "contributions/donations/list",
|
||||
locals: { donations: @donations_pending } %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<% if @donations_completed.any? %>
|
||||
<section class="donation-list">
|
||||
<h2>Past contributions</h2>
|
||||
<%= render partial: "contributions/donations/list",
|
||||
locals: { donations: @donations_completed } %>
|
||||
</section>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
6
app/views/icons/_nostrich-head.html.erb
Normal file
6
app/views/icons/_nostrich-head.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" class="icon-nostrich-head <%= custom_class %>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.03377 4.84648C2.38935 5.60878 1.88639 6.49681 1.5799 7.4713C3.32454 7.07836 5.64286 6.98406 6.95527 6.88189C7.36392 5.20013 8.52701 3.91915 10.476 4.0056C11.3169 4.04489 12.0556 4.58714 12.5664 5.42017C12.9436 5.01937 13.4466 4.75218 14.1146 4.65787C14.1617 4.65787 14.2639 4.65001 14.3425 4.65001C12.9593 3.14114 10.9868 2.18237 8.77849 2.18237C8.3777 2.18237 7.98476 2.22167 7.59183 2.28454C7.51324 2.28454 7.41108 2.30026 7.27748 2.33169C7.26962 2.33169 7.2539 2.33169 7.24604 2.33169C7.23818 2.33169 7.23032 2.33169 7.21461 2.33169C5.69001 2.70105 4.54264 2.40242 3.89037 1.51438C3.81964 1.42008 3.54458 1.00357 3.45814 0.272705C2.97876 0.767805 2.66441 1.58511 2.9316 2.45743C3.14379 3.149 3.54458 3.51836 3.97681 3.73054C3.31668 3.76984 2.76657 3.6441 2.21646 3.22759C1.89425 2.98396 1.68992 2.71677 1.352 2.01734C1.03765 2.51244 1.06909 3.06255 1.13195 3.34547C1.21054 3.72268 1.40701 4.14706 1.65849 4.39068C2.04357 4.76789 2.59368 4.85434 3.04162 4.84648H3.03377Z" fill="currentColor"/>
|
||||
<path d="M10.4837 11.3458C11.4602 11.3458 12.2519 9.99116 12.2519 8.32016C12.2519 6.64917 11.4602 5.29456 10.4837 5.29456C9.50711 5.29456 8.71545 6.64917 8.71545 8.32016C8.71545 9.99116 9.50711 11.3458 10.4837 11.3458Z" fill="currentColor"/>
|
||||
<path d="M14.3737 10.615C15.1376 10.615 15.7569 9.53831 15.7569 8.21019C15.7569 6.88207 15.1376 5.80542 14.3737 5.80542C13.6099 5.80542 12.9906 6.88207 12.9906 8.21019C12.9906 9.53831 13.6099 10.615 14.3737 10.615Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.52542 23.9833C7.53337 23.6314 7.66454 22.5232 8.7864 20.3047C9.2815 19.3381 10.4053 18.0021 11.2462 17.2791C11.6941 16.8862 12.1421 16.5561 12.5822 16.2496C12.8101 16.116 13.0222 15.9745 13.2266 15.8252C16.9076 13.5684 20.157 14.0396 22.8528 14.4306L22.9321 14.4421C22.9321 14.4421 23.5765 12.5246 20.9203 11.5344C19.4743 11 17.7689 10.5677 16.3465 10.2691C16.1422 10.6385 15.8828 10.9528 15.5763 11.1886C15.5721 11.1917 15.5678 11.195 15.5634 11.1983C15.3354 11.3696 14.795 11.7757 13.816 11.6601C13.313 11.5972 12.9279 11.3929 12.6215 11.0943C12.1028 11.9509 11.3562 12.5088 10.4917 12.5874C8.09483 12.7918 6.88458 10.7799 6.806 8.55591C5.00635 8.7288 2.55443 9.83688 1.24988 10.4813L1.25662 22.0396C2.92115 22.6846 5.41819 23.4807 7.52542 23.9833Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
3
app/views/icons/_nostrich-n.html.erb
Normal file
3
app/views/icons/_nostrich-n.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" class="icon-nostrich-n <%= custom_class %>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 10.4604V23.135C24 23.6117 23.6161 23.9985 23.1429 23.9985H12.8578C12.3847 23.9985 12.0008 23.6117 12.0008 23.135V20.7746C12.0476 17.8812 12.3515 15.1096 12.9894 13.8487C13.3718 13.0904 14.0021 12.6777 14.7262 12.4569C16.0942 12.0426 18.4947 12.3259 19.5135 12.2772C19.5135 12.2772 22.5912 12.4005 22.5912 10.6447C22.5912 9.23147 21.2156 9.34264 21.2156 9.34264C19.6994 9.38223 18.5446 9.27868 17.7963 8.98173C16.5432 8.48528 16.5009 7.57462 16.4963 7.27005C16.4343 3.75228 11.2858 3.33046 6.74939 4.20305C1.78976 5.1533 6.80381 12.3152 6.80381 21.8756V23.1518C6.79474 23.6208 6.41834 24 5.94974 24H0.857089C0.383951 24 0 23.6132 0 23.1365V1.21523C0 0.738579 0.383951 0.351777 0.857089 0.351777H5.64439C6.11753 0.351777 6.50148 0.738579 6.50148 1.21523C6.50148 1.92335 7.29206 2.31777 7.86345 1.90508C9.58519 0.662437 11.7952 0 14.2682 0C19.8083 0 23.997 3.25279 23.997 10.4604H24ZM14.8033 7.88832C14.8033 6.86802 13.9825 6.04112 12.9697 6.04112C11.9569 6.04112 11.1361 6.86802 11.1361 7.88832C11.1361 8.90863 11.9569 9.73553 12.9697 9.73553C13.9825 9.73553 14.8033 8.90863 14.8033 7.88832Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
app/views/icons/_nostrich.html.erb
Normal file
3
app/views/icons/_nostrich.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" class="icon-nostrich <%= custom_class %>" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.7084 10.1607C18.1683 13.3466 14.8705 14.0207 12.9733 13.9618C12.8515 13.958 12.7366 14.0173 12.6647 14.1157C12.4684 14.384 12.1547 14.7309 11.9125 14.7309C11.6405 14.7309 11.3957 15.254 11.284 15.5795C11.2723 15.6137 11.3059 15.6452 11.3403 15.634C14.345 14.6584 15.5241 14.3238 16.032 14.4178C16.4421 14.4937 17.209 15.8665 17.5413 16.5434C16.7155 16.5909 16.4402 15.8507 16.2503 15.7178C16.0985 15.6116 16.0415 16.0974 16.032 16.3536C15.8517 16.2587 15.6239 16.1259 15.6049 15.7178C15.5859 15.3098 15.3771 15.4142 15.2157 15.4332C15.0544 15.4521 12.5769 16.2493 12.2067 16.3536C11.8366 16.458 11.4094 16.6004 11.0582 16.8471C10.4697 17.1318 10.09 16.9325 9.98561 16.4485C9.90208 16.0614 10.4444 14.8701 10.726 14.3229C10.3779 14.4526 9.65529 14.7158 9.54898 14.7309C9.44588 14.7457 8.13815 15.7552 7.43879 16.3038C7.398 16.3358 7.37174 16.3827 7.36236 16.4336C7.25047 17.0416 6.89335 17.2118 6.27423 17.5303C5.77602 17.7867 4.036 20.4606 3.14127 21.9041C3.0794 22.0039 2.9886 22.0806 2.8911 22.1461C2.32279 22.5276 1.74399 23.4985 1.50923 23.9737C1.17511 23.0095 1.61048 22.1802 1.86993 21.886C1.75602 21.7873 1.49341 21.8449 1.37634 21.886C1.69907 20.7757 2.82862 20.7757 2.79066 20.7757C2.99948 20.5954 5.44842 17.0938 5.50538 16.9325C5.56187 16.7725 5.46892 16.0242 6.69975 15.6139C6.7193 15.6073 6.73868 15.5984 6.75601 15.5873C7.71493 14.971 8.43427 13.9774 8.67571 13.5542C7.39547 13.4662 5.92943 12.7525 5.16289 12.294C4.99765 12.1952 4.8224 12.1092 4.63108 12.0875C3.58154 11.9687 2.53067 12.6401 2.10723 13.0228C1.93258 12.7799 2.12938 12.0739 2.24961 11.7513C1.82437 11.6905 1.19916 12.308 0.939711 12.6243C0.658747 12.184 0.904907 11.397 1.06311 11.0585C0.501179 11.0737 0.120232 11.3306 0 11.4571C0.465109 7.99343 4.02275 9.00076 4.06259 9.04675C3.87275 8.84937 3.88857 8.59126 3.92021 8.48688C6.0749 8.54381 7.08105 8.18321 7.71702 7.81313C12.7288 5.01374 14.8882 6.73133 15.6856 7.1631C16.4829 7.59487 17.9304 7.77042 18.9318 7.37187C20.1278 6.83097 19.9478 5.43673 19.7054 4.90461C19.4397 4.32101 17.9399 3.51438 17.4084 2.49428C16.8768 1.47418 17.34 0.233672 17.9558 0.0607684C18.5425 -0.103972 18.9615 0.0876835 19.2831 0.378128C19.4974 0.571763 20.0994 0.710259 20.3509 0.800409C20.6024 0.890558 21.0201 1.00918 20.9964 1.08035C20.9726 1.15152 20.5699 1.14202 20.5075 1.14202C20.3794 1.14202 20.2275 1.161 20.3794 1.23217C20.5575 1.30439 20.8263 1.40936 20.955 1.47846C20.9717 1.48744 20.9683 1.51084 20.95 1.51577C20.0765 1.75085 19.2966 1.26578 18.7183 1.82526C18.1298 2.39463 19.3827 2.83114 20.0282 3.51438C20.6736 4.19762 21.3381 5.01372 20.8065 6.87365C20.395 8.31355 18.6703 9.53781 17.7795 10.0167C17.7282 10.0442 17.7001 10.1031 17.7084 10.1607Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,11 @@
|
||||
Hi <%= @user.display_name.presence || @user.cn %>,
|
||||
|
||||
Your bitcoin donation has been confirmed successfully. <3
|
||||
|
||||
Thank you so much for helping us with keeping the lights on, as well as with continually improving our services for you!
|
||||
|
||||
You can find all of your past financial contributions on this page:
|
||||
|
||||
<%= contributions_donations_url %>
|
||||
|
||||
Have a nice day!
|
||||
@@ -34,7 +34,7 @@
|
||||
<% end %>
|
||||
<% if Setting.nostr_enabled %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
name: "Experiments", path: setting_path(:experiments), icon: "science",
|
||||
active: @settings_section.to_s == "experiments"
|
||||
name: "Nostr", path: setting_path(:nostr), icon: "nostrich-head",
|
||||
active: @settings_section.to_s == "nostr"
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
6
app/views/shared/status_unprocessable_entity.html.erb
Normal file
6
app/views/shared/status_unprocessable_entity.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<%= render HeaderCompactComponent.new(title: "422") %>
|
||||
|
||||
<%= render MainCompactComponent.new do %>
|
||||
<h2>Unprocessable content</h2>
|
||||
<p>The data provided was malformed. Please go back and try again.</p>
|
||||
<% end %>
|
||||
@@ -27,7 +27,6 @@ Devise.setup do |config|
|
||||
config.ldap_auth_password_builder = Proc.new() { |new_password|
|
||||
salt = SecureRandom.hex(32)
|
||||
hashed_pw = Base64.strict_encode64(Digest::SHA512.digest(new_password + salt) + salt)
|
||||
puts '{SSHA512}' + hashed_pw
|
||||
'{SSHA512}' + hashed_pw
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,12 @@ Rails.application.routes.draw do
|
||||
|
||||
namespace :contributions do
|
||||
root to: 'donations#index'
|
||||
resources :donations, only: ['index', 'create'] do
|
||||
member do
|
||||
get 'confirm_btcpay'
|
||||
end
|
||||
end
|
||||
get 'projects', to: 'projects#index'
|
||||
resources :donations, only: ['index']
|
||||
end
|
||||
|
||||
resources :invitations, only: ['index', 'show', 'create', 'destroy']
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class ChangeDonationAmountsAndCurrency < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
rename_column :donations, :amount_usd, :fiat_amount
|
||||
add_column :donations, :fiat_currency, :string, default: "USD"
|
||||
remove_column :donations, :amount_eur, :integer
|
||||
|
||||
Donation.update_all(fiat_currency: 'USD')
|
||||
end
|
||||
end
|
||||
7
db/migrate/20240214121049_add_new_donation_fields.rb
Normal file
7
db/migrate/20240214121049_add_new_donation_fields.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class AddNewDonationFields < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :donations, :donation_method, :string
|
||||
add_column :donations, :payment_method, :string, default: nil
|
||||
add_column :donations, :btcpay_invoice_id, :string, default: nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
class AddPaymentStatusToDonations < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :donations, :payment_status, :string, default: nil
|
||||
add_index :donations, :payment_status
|
||||
|
||||
Donation.completed.update_all payment_status: "settled"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class RemoveNostrPubkeyFromUsers < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
remove_column :users, :nostr_pubkey, :string
|
||||
end
|
||||
end
|
||||
@@ -10,12 +10,12 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_02_16_124640) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_03_16_153558) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
t.integer "record_id", null: false
|
||||
t.integer "blob_id", null: false
|
||||
t.bigint "record_id", null: false
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
|
||||
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
|
||||
@@ -34,7 +34,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_16_124640) do
|
||||
end
|
||||
|
||||
create_table "active_storage_variant_records", force: :cascade do |t|
|
||||
t.integer "blob_id", null: false
|
||||
t.bigint "blob_id", null: false
|
||||
t.string "variation_digest", null: false
|
||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
end
|
||||
@@ -129,7 +129,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_16_124640) do
|
||||
t.string "unconfirmed_email"
|
||||
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"
|
||||
|
||||
@@ -3,6 +3,10 @@ require 'sidekiq/testing'
|
||||
ldap = LdapService.new
|
||||
|
||||
Sidekiq::Testing.inline! do
|
||||
ldap.delete_all_users!
|
||||
|
||||
puts "Create user: admin"
|
||||
|
||||
CreateAccount.call(account: {
|
||||
username: "admin", domain: "kosmos.org", email: "admin@example.com",
|
||||
password: "admin is admin", confirmed: true
|
||||
@@ -10,6 +14,7 @@ Sidekiq::Testing.inline! do
|
||||
|
||||
ldap.add_attribute "cn=admin,ou=kosmos.org,cn=users,dc=kosmos,dc=org", :admin, "true"
|
||||
|
||||
puts "Create 35 random users"
|
||||
35.times do |n|
|
||||
username = Faker::Name.unique.first_name.downcase
|
||||
email = Faker::Internet.unique.email
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace :ldap do
|
||||
desc "Reset the LDAP directory and set up base entries and default org"
|
||||
task setup: :environment do |t, args|
|
||||
task setup: [:environment, :add_custom_attributes] do |t, args|
|
||||
ldap = LdapService.new
|
||||
|
||||
ldap.delete_entry "cn=admin_role,ou=kosmos.org,cn=users,dc=kosmos,dc=org", true
|
||||
@@ -19,6 +19,54 @@ namespace :ldap do
|
||||
}, true
|
||||
end
|
||||
|
||||
# TODO
|
||||
desc "Add application account to directory"
|
||||
task add_application_account: :environment do |t, args|
|
||||
# Add uid=service,ou=kosmos.org,cn=applications,dc=kosmos,dc=org with userPassword
|
||||
end
|
||||
|
||||
# TODO
|
||||
desc "Add application ACI/permissions for OU, i.e. read/search users"
|
||||
task add_application_account: :environment do |t, args|
|
||||
# (target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
|
||||
end
|
||||
|
||||
desc "Add custom attributes to schema"
|
||||
task add_custom_attributes: :environment do |t, args|
|
||||
%w[ admin service_enabled nostr_key ].each do |name|
|
||||
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "add")
|
||||
Rake::Task['ldap:modify_ldap_schema'].reenable
|
||||
end
|
||||
end
|
||||
|
||||
desc "Delete custom attributes from schema"
|
||||
task delete_custom_attributes: :environment do |t, args|
|
||||
%w[ admin service_enabled nostr_key ].each do |name|
|
||||
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "delete")
|
||||
Rake::Task['ldap:modify_ldap_schema'].reenable
|
||||
end
|
||||
end
|
||||
|
||||
desc "Modify LDAP schema"
|
||||
task :modify_ldap_schema, [:name, :operation] => [:environment] do |t, args|
|
||||
puts "Modify schema: #{args[:operation]} #{args[:name]}"
|
||||
|
||||
filename = "#{Rails.root}/schemas/ldap/#{args[:name]}.ldif"
|
||||
ldif = YAML.safe_load(File.read(filename))
|
||||
dn = ldif["dn"]
|
||||
attribute = ldif["add"]
|
||||
value = ldif[attribute]
|
||||
operation = [ args[:operation].to_sym, attribute.to_sym, value ]
|
||||
|
||||
ldap = LdapService.new
|
||||
res = ldap.modify dn, [ operation ]
|
||||
|
||||
if res != 0
|
||||
puts "Result code: #{res}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
desc "List user domains/organizations"
|
||||
task list_organizations: :environment do |t, args|
|
||||
ldap = LdapService.new
|
||||
|
||||
4
schemas/ldap/aci.ldif
Normal file
4
schemas/ldap/aci.ldif
Normal file
@@ -0,0 +1,4 @@
|
||||
dn: ou=kosmos.org,cn=users,dc=kosmos,dc=org
|
||||
changetype: modify
|
||||
add: aci
|
||||
aci: (target="ldap:///cn=*,ou=kosmos.org,cn=users,dc=kosmos,dc=org")(targetattr="cn || sn || uid || mail || userPassword || serviceEnabled || displayName || jpegPhoto || nsRole || objectClass") (version 3.0; acl "service-kosmos-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=kosmos.org,cn=applications,dc=kosmos,dc=org";)
|
||||
9
schemas/ldap/admin.ldif
Normal file
9
schemas/ldap/admin.ldif
Normal file
@@ -0,0 +1,9 @@
|
||||
dn: cn=schema
|
||||
changetype: modify
|
||||
add: attributeTypes
|
||||
attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.1
|
||||
NAME 'admin'
|
||||
DESC 'Admin flag'
|
||||
EQUALITY booleanMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
|
||||
SINGLE-VALUE )
|
||||
4
schemas/ldap/delete-aci.ldif
Normal file
4
schemas/ldap/delete-aci.ldif
Normal file
@@ -0,0 +1,4 @@
|
||||
dn: ou=kosmos.org,cn=users,dc=kosmos,dc=org
|
||||
changetype: modify
|
||||
delete: aci
|
||||
aci: (target="ldap:///cn=*,ou=kosmos.org,cn=users,dc=kosmos,dc=org")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-kosmos-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=kosmos.org,cn=applications,dc=kosmos,dc=org";)
|
||||
9
schemas/ldap/nostr_key.ldif
Normal file
9
schemas/ldap/nostr_key.ldif
Normal file
@@ -0,0 +1,9 @@
|
||||
dn: cn=schema
|
||||
changetype: modify
|
||||
add: attributeTypes
|
||||
attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.21
|
||||
NAME 'nostrKey'
|
||||
DESC 'Nostr public key'
|
||||
EQUALITY caseIgnoreMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
SINGLE-VALUE )
|
||||
8
schemas/ldap/service_enabled.ldif
Normal file
8
schemas/ldap/service_enabled.ldif
Normal file
@@ -0,0 +1,8 @@
|
||||
dn: cn=schema
|
||||
changetype: modify
|
||||
add: attributeTypes
|
||||
attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.2
|
||||
NAME 'serviceEnabled'
|
||||
DESC 'Services enabled for account'
|
||||
EQUALITY caseExactMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
|
||||
35
spec/features/contributions/donations_spec.rb
Normal file
35
spec/features/contributions/donations_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Donations page', type: :feature do
|
||||
let(:user) { create :user }
|
||||
|
||||
before do
|
||||
login_as user, :scope => :user
|
||||
end
|
||||
|
||||
describe "Donation methods" do
|
||||
scenario "Only BTCPay enabled" do
|
||||
Setting.btcpay_enabled = true
|
||||
Setting.lndhub_enabled = false
|
||||
Setting.opencollective_enabled = false
|
||||
visit contributions_donations_url
|
||||
|
||||
within ".donation-methods" do
|
||||
expect(page).to have_content("Bitcoin")
|
||||
expect(page).not_to have_content("OpenCollective")
|
||||
end
|
||||
end
|
||||
|
||||
scenario "Only OpenCollective enabled" do
|
||||
Setting.btcpay_enabled = false
|
||||
Setting.lndhub_enabled = false
|
||||
Setting.opencollective_enabled = true
|
||||
visit contributions_donations_url
|
||||
|
||||
within ".donation-methods" do
|
||||
expect(page).not_to have_content("Bitcoin")
|
||||
expect(page).to have_content("OpenCollective")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,15 +1,19 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Experimental Settings', type: :feature do
|
||||
RSpec.describe 'Nostr Settings', type: :feature do
|
||||
let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
|
||||
|
||||
before do
|
||||
login_as user, scope: :user
|
||||
|
||||
allow_any_instance_of(User).to receive(:dn)
|
||||
.and_return("cn=#{user.cn},ou=kosmos.org,cn=users,dc=kosmos,dc=org")
|
||||
allow_any_instance_of(User).to receive(:nostr_pubkey).and_return(nil)
|
||||
end
|
||||
|
||||
describe 'Adding a nostr pubkey' do
|
||||
scenario 'Without nostr browser extension available' do
|
||||
visit setting_path(:experiments)
|
||||
visit setting_path(:nostr)
|
||||
expect(page).to have_content("No browser extension found")
|
||||
expect(page).to have_css('button[data-settings--nostr-pubkey-target=setPubkey]:disabled')
|
||||
end
|
||||
@@ -22,19 +26,22 @@ RSpec.describe 'Experimental Settings', type: :feature do
|
||||
|
||||
context "With pubkey configured" do
|
||||
before do
|
||||
user.update! nostr_pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||
allow_any_instance_of(User).to receive(:nostr_pubkey)
|
||||
.and_return("ce273cbfb0d4e3e06930773a337c1459e4849efd4cb4c751b906a561c98a6d09")
|
||||
end
|
||||
|
||||
scenario 'Remove nostr pubkey from account' do
|
||||
visit setting_path(:experiments)
|
||||
visit setting_path(:nostr)
|
||||
expect(page).to have_field("nostr_public_key",
|
||||
with: "npub1qlsc3g0lsl8pw8230w8d9wm6xxcax3f6pkemz5measrmwfxjxteslf2hac",
|
||||
with: "npub1ecnne0as6n37q6fswuarxlq5t8jgf8hafj6vw5deq6jkrjv2d5ysnehu73",
|
||||
disabled: true)
|
||||
|
||||
expect(LdapManager::UpdateNostrKey).to receive(:call).with(
|
||||
dn: "cn=jimmy,ou=kosmos.org,cn=users,dc=kosmos,dc=org",
|
||||
pubkey: nil
|
||||
)
|
||||
|
||||
click_link "Remove"
|
||||
expect(page).to_not have_field("nostr_public_key")
|
||||
expect(page).to have_content("verify your public key")
|
||||
expect(user.reload.nostr_pubkey).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
32
spec/fixtures/btcpay/create_invoice.rb
vendored
Normal file
32
spec/fixtures/btcpay/create_invoice.rb
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"id" => "Q9GBe143MXHkdpZeH4Ftx5",
|
||||
"storeId" => "AxQQ6oH4YX7n5pH1JPBLu97QD6RTybtj8m2W8YzhYr6T",
|
||||
"amount" => "1",
|
||||
"checkoutLink" => "http://10.1.1.163:23001/i/Q9GBe143MXHkdpZeH4Ftx5",
|
||||
"status" => "New",
|
||||
"additionalStatus" => "None",
|
||||
"monitoringExpiration" => 1707995026,
|
||||
"expirationTime" => 1707908626,
|
||||
"createdTime" => 1707907726,
|
||||
"availableStatusesForManualMarking" =>["Settled", "Invalid"],
|
||||
"archived" => false,
|
||||
"type" => "Standard",
|
||||
"currency" => "EUR",
|
||||
"metadata" => {},
|
||||
"checkout" => {
|
||||
"speedPolicy" => "MediumSpeed",
|
||||
"paymentMethods" => ["BTC", "BTC-LightningNetwork"],
|
||||
"defaultPaymentMethod" => "BTC-LightningNetwork",
|
||||
"expirationMinutes" => 15,
|
||||
"monitoringMinutes" => 1440,
|
||||
"paymentTolerance" => 0.0,
|
||||
"redirectURL" => "http://localhost:3000/contributions/donations",
|
||||
"redirectAutomatically" => false,
|
||||
"requiresRefundEmail" => false,
|
||||
"defaultLanguage" => nil,
|
||||
"checkoutType" => nil,
|
||||
"lazyPaymentMethods" => nil},
|
||||
"receipt" => {
|
||||
"enabled" => nil, "showQR" => nil, "showPayments" => nil
|
||||
}
|
||||
}
|
||||
41
spec/fixtures/btcpay/lightning_eur_settled_invoice.json
vendored
Normal file
41
spec/fixtures/btcpay/lightning_eur_settled_invoice.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "MCkDbf2cUgBuuisUCgnRnb",
|
||||
"storeId": "AxQQ6oH4YX7n5pH1JPBLu97QD6RTybtj8m2W8YzhYr6T",
|
||||
"amount": "1",
|
||||
"checkoutLink": "http://10.1.1.163:23001/i/MCkDbf2cUgBuuisUCgnRnb",
|
||||
"status": "Settled",
|
||||
"additionalStatus": "None",
|
||||
"monitoringExpiration": 1708169508,
|
||||
"expirationTime": 1708083108,
|
||||
"createdTime": 1708082208,
|
||||
"availableStatusesForManualMarking": [
|
||||
|
||||
],
|
||||
"archived": false,
|
||||
"type": "Standard",
|
||||
"currency": "EUR",
|
||||
"metadata": {
|
||||
},
|
||||
"checkout": {
|
||||
"speedPolicy": "MediumSpeed",
|
||||
"paymentMethods": [
|
||||
"BTC",
|
||||
"BTC-LightningNetwork"
|
||||
],
|
||||
"defaultPaymentMethod": "BTC-LightningNetwork",
|
||||
"expirationMinutes": 15,
|
||||
"monitoringMinutes": 1440,
|
||||
"paymentTolerance": 0.0,
|
||||
"redirectURL": "http://localhost:3000/contributions/donations/27/confirm_btcpay",
|
||||
"redirectAutomatically": true,
|
||||
"requiresRefundEmail": false,
|
||||
"defaultLanguage": null,
|
||||
"checkoutType": null,
|
||||
"lazyPaymentMethods": null
|
||||
},
|
||||
"receipt": {
|
||||
"enabled": null,
|
||||
"showQR": null,
|
||||
"showPayments": null
|
||||
}
|
||||
}
|
||||
46
spec/fixtures/btcpay/lightning_eur_settled_payments.json
vendored
Normal file
46
spec/fixtures/btcpay/lightning_eur_settled_payments.json
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "bc1qtvwjguv679lcch9a9zxzxcengq3t3zgd5zm0pd",
|
||||
"paymentLink": "bitcoin:bc1qtvwjguv679lcch9a9zxzxcengq3t3zgd5zm0pd",
|
||||
"rate": "48532.8",
|
||||
"paymentMethodPaid": "0",
|
||||
"totalPaid": "0.00002061",
|
||||
"due": "0",
|
||||
"amount": "0.00002061",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
|
||||
],
|
||||
"paymentMethod": "BTC",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "lnbc20610n1pju73pqpp5rrvhc34tzt3vz70r33c2n2qqtm6hxau2hyl9w23kvrx56vhsfh5sdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp56grmnp2ezj4n323squu2p2e09g5lgncsr7f34084p5dyjgmwcssq9qyyssqfrc9x37qcvvpsx8m4zvu9glvcfcmqzs9ttfsg30g2gjxfkylvp8rdud2yx8gshs2jv0rea0etjrcygrc0hp4vckgsfs4grsnl854ajgpurzzp4",
|
||||
"paymentLink": "lightning:lnbc20610n1pju73pqpp5rrvhc34tzt3vz70r33c2n2qqtm6hxau2hyl9w23kvrx56vhsfh5sdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp56grmnp2ezj4n323squu2p2e09g5lgncsr7f34084p5dyjgmwcssq9qyyssqfrc9x37qcvvpsx8m4zvu9glvcfcmqzs9ttfsg30g2gjxfkylvp8rdud2yx8gshs2jv0rea0etjrcygrc0hp4vckgsfs4grsnl854ajgpurzzp4",
|
||||
"rate": "48532.8",
|
||||
"paymentMethodPaid": "0.00002061",
|
||||
"totalPaid": "0.00002061",
|
||||
"due": "0",
|
||||
"amount": "0.00002061",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
{
|
||||
"id": "18d97c46ab12e2c179e38c70a9a8005ef573778ab93e572a3660cd4d32f04de9",
|
||||
"receivedDate": 1708082214,
|
||||
"value": "0.00002061",
|
||||
"fee": "0.0",
|
||||
"status": "Settled",
|
||||
"destination": "lnbc20610n1pju73pqpp5rrvhc34tzt3vz70r33c2n2qqtm6hxau2hyl9w23kvrx56vhsfh5sdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp56grmnp2ezj4n323squu2p2e09g5lgncsr7f34084p5dyjgmwcssq9qyyssqfrc9x37qcvvpsx8m4zvu9glvcfcmqzs9ttfsg30g2gjxfkylvp8rdud2yx8gshs2jv0rea0etjrcygrc0hp4vckgsfs4grsnl854ajgpurzzp4"
|
||||
}
|
||||
],
|
||||
"paymentMethod": "BTC-LightningNetwork",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
"paymentHash": "18d97c46ab12e2c179e38c70a9a8005ef573778ab93e572a3660cd4d32f04de9"
|
||||
}
|
||||
}
|
||||
]
|
||||
41
spec/fixtures/btcpay/lightning_sats_settled_invoice.json
vendored
Normal file
41
spec/fixtures/btcpay/lightning_sats_settled_invoice.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "JxjfeJi1TtX8FcWSjEvGxg",
|
||||
"storeId": "AxQQ6oH4YX7n5pH1JPBLu97QD6RTybtj8m2W8YzhYr6T",
|
||||
"amount": "0.0001",
|
||||
"checkoutLink": "http://10.1.1.163:23001/i/JxjfeJi1TtX8FcWSjEvGxg",
|
||||
"status": "Settled",
|
||||
"additionalStatus": "None",
|
||||
"monitoringExpiration": 1708180292,
|
||||
"expirationTime": 1708093892,
|
||||
"createdTime": 1708092992,
|
||||
"availableStatusesForManualMarking": [
|
||||
|
||||
],
|
||||
"archived": false,
|
||||
"type": "Standard",
|
||||
"currency": "BTC",
|
||||
"metadata": {
|
||||
},
|
||||
"checkout": {
|
||||
"speedPolicy": "MediumSpeed",
|
||||
"paymentMethods": [
|
||||
"BTC",
|
||||
"BTC-LightningNetwork"
|
||||
],
|
||||
"defaultPaymentMethod": "BTC-LightningNetwork",
|
||||
"expirationMinutes": 15,
|
||||
"monitoringMinutes": 1440,
|
||||
"paymentTolerance": 0.0,
|
||||
"redirectURL": "http://localhost:3000/contributions/donations/32/confirm_btcpay",
|
||||
"redirectAutomatically": true,
|
||||
"requiresRefundEmail": false,
|
||||
"defaultLanguage": null,
|
||||
"checkoutType": null,
|
||||
"lazyPaymentMethods": null
|
||||
},
|
||||
"receipt": {
|
||||
"enabled": null,
|
||||
"showQR": null,
|
||||
"showPayments": null
|
||||
}
|
||||
}
|
||||
46
spec/fixtures/btcpay/lightning_sats_settled_payments.json
vendored
Normal file
46
spec/fixtures/btcpay/lightning_sats_settled_payments.json
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "bc1q9fay59qdmtv46d5hpf62vt5eyd7ag98t4h0s3g",
|
||||
"paymentLink": "bitcoin:bc1q9fay59qdmtv46d5hpf62vt5eyd7ag98t4h0s3g",
|
||||
"rate": "1.0",
|
||||
"paymentMethodPaid": "0",
|
||||
"totalPaid": "0.0001",
|
||||
"due": "0",
|
||||
"amount": "0.0001",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
|
||||
],
|
||||
"paymentMethod": "BTC",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "lnbc100u1pju7mjqpp54yt6z4g4j294vta90yn35pwch76a4h47txx4m4njfdqmcsa4w50qdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5gx3wee4c2gsl7lnmx4qkrha3mkxh926j5qpdqz7wyyya04wks7cq9qyyssq4ft7a69c93kr04hamp5ah958ay222dvdrzr3nl599nx0l3ejpqe4ktarkxdymsxgg6v3evat9e9u0fp2vg2r2z860fn0h04znq9c6psqh8s53q",
|
||||
"paymentLink": "lightning:lnbc100u1pju7mjqpp54yt6z4g4j294vta90yn35pwch76a4h47txx4m4njfdqmcsa4w50qdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5gx3wee4c2gsl7lnmx4qkrha3mkxh926j5qpdqz7wyyya04wks7cq9qyyssq4ft7a69c93kr04hamp5ah958ay222dvdrzr3nl599nx0l3ejpqe4ktarkxdymsxgg6v3evat9e9u0fp2vg2r2z860fn0h04znq9c6psqh8s53q",
|
||||
"rate": "1.0",
|
||||
"paymentMethodPaid": "0.0001",
|
||||
"totalPaid": "0.0001",
|
||||
"due": "0",
|
||||
"amount": "0.0001",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
{
|
||||
"id": "a917a15515928b562fa579271a05d8bfb5dadebe598d5dd6724b41bc43b5751e",
|
||||
"receivedDate": 1708093015,
|
||||
"value": "0.0001",
|
||||
"fee": "0.0",
|
||||
"status": "Settled",
|
||||
"destination": "lnbc100u1pju7mjqpp54yt6z4g4j294vta90yn35pwch76a4h47txx4m4njfdqmcsa4w50qdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5gx3wee4c2gsl7lnmx4qkrha3mkxh926j5qpdqz7wyyya04wks7cq9qyyssq4ft7a69c93kr04hamp5ah958ay222dvdrzr3nl599nx0l3ejpqe4ktarkxdymsxgg6v3evat9e9u0fp2vg2r2z860fn0h04znq9c6psqh8s53q"
|
||||
}
|
||||
],
|
||||
"paymentMethod": "BTC-LightningNetwork",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
"paymentHash": "a917a15515928b562fa579271a05d8bfb5dadebe598d5dd6724b41bc43b5751e"
|
||||
}
|
||||
}
|
||||
]
|
||||
42
spec/fixtures/btcpay/onchain_eur_processing_invoice.json
vendored
Normal file
42
spec/fixtures/btcpay/onchain_eur_processing_invoice.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "K4e31MhbLKmr3D7qoNYRd3",
|
||||
"storeId": "AxQQ6oH4YX7n5pH1JPBLu97QD6RTybtj8m2W8YzhYr6T",
|
||||
"amount": "100",
|
||||
"checkoutLink": "http://10.1.1.163:23001/i/K4e31MhbLKmr3D7qoNYRd3",
|
||||
"status": "Processing",
|
||||
"additionalStatus": "None",
|
||||
"monitoringExpiration": 1708173683,
|
||||
"expirationTime": 1708087283,
|
||||
"createdTime": 1708086383,
|
||||
"availableStatusesForManualMarking": [
|
||||
"Settled",
|
||||
"Invalid"
|
||||
],
|
||||
"archived": false,
|
||||
"type": "Standard",
|
||||
"currency": "USD",
|
||||
"metadata": {
|
||||
},
|
||||
"checkout": {
|
||||
"speedPolicy": "MediumSpeed",
|
||||
"paymentMethods": [
|
||||
"BTC",
|
||||
"BTC-LightningNetwork"
|
||||
],
|
||||
"defaultPaymentMethod": "BTC-LightningNetwork",
|
||||
"expirationMinutes": 15,
|
||||
"monitoringMinutes": 1440,
|
||||
"paymentTolerance": 0.0,
|
||||
"redirectURL": "http://localhost:3000/contributions/donations/28/confirm_btcpay",
|
||||
"redirectAutomatically": true,
|
||||
"requiresRefundEmail": false,
|
||||
"defaultLanguage": null,
|
||||
"checkoutType": null,
|
||||
"lazyPaymentMethods": null
|
||||
},
|
||||
"receipt": {
|
||||
"enabled": null,
|
||||
"showQR": null,
|
||||
"showPayments": null
|
||||
}
|
||||
}
|
||||
46
spec/fixtures/btcpay/onchain_eur_processing_payments.json
vendored
Normal file
46
spec/fixtures/btcpay/onchain_eur_processing_payments.json
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh",
|
||||
"paymentLink": "bitcoin:bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh",
|
||||
"rate": "52259.2",
|
||||
"paymentMethodPaid": "0.00191354",
|
||||
"totalPaid": "0.00191354",
|
||||
"due": "0",
|
||||
"amount": "0.00191354",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
{
|
||||
"id": "21da85563274d0c3975273c1a2a8551bddeebb68b8f8a3242f63dd4cc238b480-1",
|
||||
"receivedDate": 1708086448,
|
||||
"value": "0.00191354",
|
||||
"fee": "0.0",
|
||||
"status": "Processing",
|
||||
"destination": "bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh"
|
||||
}
|
||||
],
|
||||
"paymentMethod": "BTC",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "lnbc1913540n1pju74r0pp5vpnw6l84ytu5u5evehn00xwsrppg7w45cj4mrwjwng474w7x3ugqdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5sscc8ufdw29lpv9z09c6edhzc2njg0lmspsk8sdunek7yrkeu3mq9qyyssqnzz9xrhpy7sej5k62vcjju253kxx87jveq7vusl2sgaeuyh48ph9ecuud6f329syuut3z8w544c6ynhtx4ratundzmp7fs6sdll8g0spurjhnx",
|
||||
"paymentLink": "lightning:lnbc1913540n1pju74r0pp5vpnw6l84ytu5u5evehn00xwsrppg7w45cj4mrwjwng474w7x3ugqdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5sscc8ufdw29lpv9z09c6edhzc2njg0lmspsk8sdunek7yrkeu3mq9qyyssqnzz9xrhpy7sej5k62vcjju253kxx87jveq7vusl2sgaeuyh48ph9ecuud6f329syuut3z8w544c6ynhtx4ratundzmp7fs6sdll8g0spurjhnx",
|
||||
"rate": "52259.2",
|
||||
"paymentMethodPaid": "0",
|
||||
"totalPaid": "0.00191354",
|
||||
"due": "0",
|
||||
"amount": "0.00191354",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
|
||||
],
|
||||
"paymentMethod": "BTC-LightningNetwork",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
"paymentHash": "6066ed7cf522f94e532ccde6f799d018428f3ab4c4abb1ba4e9a2beabbc68f10"
|
||||
}
|
||||
}
|
||||
]
|
||||
41
spec/fixtures/btcpay/onchain_eur_settled_invoice.json
vendored
Normal file
41
spec/fixtures/btcpay/onchain_eur_settled_invoice.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "K4e31MhbLKmr3D7qoNYRd3",
|
||||
"storeId": "AxQQ6oH4YX7n5pH1JPBLu97QD6RTybtj8m2W8YzhYr6T",
|
||||
"amount": "100",
|
||||
"checkoutLink": "http://10.1.1.163:23001/i/K4e31MhbLKmr3D7qoNYRd3",
|
||||
"status": "Settled",
|
||||
"additionalStatus": "None",
|
||||
"monitoringExpiration": 1708173683,
|
||||
"expirationTime": 1708087283,
|
||||
"createdTime": 1708086383,
|
||||
"availableStatusesForManualMarking": [
|
||||
|
||||
],
|
||||
"archived": false,
|
||||
"type": "Standard",
|
||||
"currency": "USD",
|
||||
"metadata": {
|
||||
},
|
||||
"checkout": {
|
||||
"speedPolicy": "MediumSpeed",
|
||||
"paymentMethods": [
|
||||
"BTC",
|
||||
"BTC-LightningNetwork"
|
||||
],
|
||||
"defaultPaymentMethod": "BTC-LightningNetwork",
|
||||
"expirationMinutes": 15,
|
||||
"monitoringMinutes": 1440,
|
||||
"paymentTolerance": 0.0,
|
||||
"redirectURL": "http://localhost:3000/contributions/donations/28/confirm_btcpay",
|
||||
"redirectAutomatically": true,
|
||||
"requiresRefundEmail": false,
|
||||
"defaultLanguage": null,
|
||||
"checkoutType": null,
|
||||
"lazyPaymentMethods": null
|
||||
},
|
||||
"receipt": {
|
||||
"enabled": null,
|
||||
"showQR": null,
|
||||
"showPayments": null
|
||||
}
|
||||
}
|
||||
46
spec/fixtures/btcpay/onchain_eur_settled_payments.json
vendored
Normal file
46
spec/fixtures/btcpay/onchain_eur_settled_payments.json
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh",
|
||||
"paymentLink": "bitcoin:bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh",
|
||||
"rate": "52259.2",
|
||||
"paymentMethodPaid": "0.00191354",
|
||||
"totalPaid": "0.00191354",
|
||||
"due": "0",
|
||||
"amount": "0.00191354",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
{
|
||||
"id": "218652f351508c46cfd99de1c6cdc0dcb66bc1bbfaf38578235d080046a96305-1",
|
||||
"receivedDate": 1708106396,
|
||||
"value": "0.00191354",
|
||||
"fee": "0.0",
|
||||
"status": "Settled",
|
||||
"destination": "bc1qqxm55h6yzych9kg6kquak4c73nyv352070tsmh"
|
||||
}
|
||||
],
|
||||
"paymentMethod": "BTC",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"activated": true,
|
||||
"destination": "lnbc1913540n1pju74r0pp5vpnw6l84ytu5u5evehn00xwsrppg7w45cj4mrwjwng474w7x3ugqdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5sscc8ufdw29lpv9z09c6edhzc2njg0lmspsk8sdunek7yrkeu3mq9qyyssqnzz9xrhpy7sej5k62vcjju253kxx87jveq7vusl2sgaeuyh48ph9ecuud6f329syuut3z8w544c6ynhtx4ratundzmp7fs6sdll8g0spurjhnx",
|
||||
"paymentLink": "lightning:lnbc1913540n1pju74r0pp5vpnw6l84ytu5u5evehn00xwsrppg7w45cj4mrwjwng474w7x3ugqdphg3hkuct5d9hkugr5dus8g6r9yp9k7umddaejqcm0dacx2unpw35hvegcqzzsxqyz5vqsp5sscc8ufdw29lpv9z09c6edhzc2njg0lmspsk8sdunek7yrkeu3mq9qyyssqnzz9xrhpy7sej5k62vcjju253kxx87jveq7vusl2sgaeuyh48ph9ecuud6f329syuut3z8w544c6ynhtx4ratundzmp7fs6sdll8g0spurjhnx",
|
||||
"rate": "52259.2",
|
||||
"paymentMethodPaid": "0",
|
||||
"totalPaid": "0.00191354",
|
||||
"due": "0",
|
||||
"amount": "0.00191354",
|
||||
"networkFee": "0",
|
||||
"payments": [
|
||||
|
||||
],
|
||||
"paymentMethod": "BTC-LightningNetwork",
|
||||
"cryptoCode": "BTC",
|
||||
"additionalData": {
|
||||
"paymentHash": "6066ed7cf522f94e532ccde6f799d018428f3ab4c4abb1ba4e9a2beabbc68f10"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,9 +1,4 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ApplicationHelper do
|
||||
describe "sats_to_btc" do
|
||||
it "converts satoshis to BTC" do
|
||||
expect(helper.sats_to_btc(120000000)).to eq(1.2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
79
spec/jobs/btcpay_check_donation_job_spec.rb
Normal file
79
spec/jobs/btcpay_check_donation_job_spec.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
require 'rails_helper'
|
||||
require 'webmock/rspec'
|
||||
|
||||
RSpec.describe BtcpayCheckDonationJob, type: :job do
|
||||
let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
|
||||
|
||||
let(:donation) do
|
||||
user.donations.create!(
|
||||
donation_method: "btcpay",
|
||||
btcpay_invoice_id: "K4e31MhbLKmr3D7qoNYRd3",
|
||||
paid_at: nil, payment_status: "processing",
|
||||
fiat_amount: 120, fiat_currency: "USD"
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
||||
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
||||
display_name: nil
|
||||
})
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
clear_enqueued_jobs
|
||||
clear_performed_jobs
|
||||
end
|
||||
|
||||
describe "invoice still processing" do
|
||||
subject(:job) { described_class.perform_later(donation) }
|
||||
|
||||
before do
|
||||
invoice = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_processing_invoice.json")
|
||||
payments = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_processing_payments.json")
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3")
|
||||
.to_return(status: 200, headers: {}, body: invoice)
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3/payment-methods")
|
||||
.to_return(status: 200, headers: {}, body: payments)
|
||||
end
|
||||
|
||||
it "enqueues itself to check again later" do
|
||||
expect_any_instance_of(described_class).to receive(:re_enqueue_job).once
|
||||
perform_enqueued_jobs { job }
|
||||
end
|
||||
end
|
||||
|
||||
describe "invoice settled" do
|
||||
subject(:job) { described_class.perform_later(donation) }
|
||||
|
||||
before do
|
||||
invoice = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_settled_invoice.json")
|
||||
payments = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_settled_payments.json")
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3")
|
||||
.to_return(status: 200, headers: {}, body: invoice)
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3/payment-methods")
|
||||
.to_return(status: 200, headers: {}, body: payments)
|
||||
end
|
||||
|
||||
it "updates the donation record" do
|
||||
perform_enqueued_jobs(only: described_class) { job }
|
||||
donation.reload
|
||||
expect(donation.paid_at).not_to be_nil
|
||||
expect(donation.payment_status).to eq("settled")
|
||||
end
|
||||
|
||||
it "notifies the user via email" do
|
||||
perform_enqueued_jobs(only: described_class) { job }
|
||||
expect(enqueued_jobs.size).to eq(1)
|
||||
job = enqueued_jobs.select{|j| j['job_class'] == "ActionMailer::MailDeliveryJob"}.first
|
||||
expect(job['arguments'][0]).to eq('NotificationMailer')
|
||||
expect(job['arguments'][1]).to eq('bitcoin_donation_confirmed')
|
||||
expect(job['arguments'][3]['params']['user']['_aj_globalid']).to eq('gid://akkounts/User/1')
|
||||
end
|
||||
|
||||
it "does not enqueue itself again" do
|
||||
expect_any_instance_of(described_class).not_to receive(:re_enqueue_job)
|
||||
perform_enqueued_jobs(only: described_class) { job }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -66,7 +66,7 @@ RSpec.describe User, type: :model do
|
||||
it "returns the entries from the LDAP service attribute" do
|
||||
expect(user).to receive(:ldap_entry).and_return({
|
||||
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
||||
service: ["discourse", "email", "gitea", "wiki", "xmpp"]
|
||||
services_enabled: ["discourse", "email", "gitea", "wiki", "xmpp"]
|
||||
})
|
||||
expect(user.services_enabled).to eq(["discourse", "email", "gitea", "wiki", "xmpp"])
|
||||
end
|
||||
@@ -76,21 +76,21 @@ RSpec.describe User, type: :model do
|
||||
before do
|
||||
allow(user).to receive(:ldap_entry).and_return({
|
||||
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
||||
service: ["discourse", "gitea"]
|
||||
services_enabled: ["discourse", "gitea"]
|
||||
})
|
||||
allow(user).to receive(:dn).and_return(dn)
|
||||
end
|
||||
|
||||
it "adds the service to the LDAP entry" do
|
||||
expect_any_instance_of(LdapService).to receive(:replace_attribute)
|
||||
.with(dn, :service, ["discourse", "gitea", "wiki"]).and_return(true)
|
||||
.with(dn, :serviceEnabled, ["discourse", "gitea", "wiki"]).and_return(true)
|
||||
|
||||
user.enable_service(:wiki)
|
||||
end
|
||||
|
||||
it "adds multiple service to the LDAP entry" do
|
||||
expect_any_instance_of(LdapService).to receive(:replace_attribute)
|
||||
.with(dn, :service, ["discourse", "gitea", "wiki", "xmpp"]).and_return(true)
|
||||
.with(dn, :serviceEnabled, ["discourse", "gitea", "wiki", "xmpp"]).and_return(true)
|
||||
|
||||
user.enable_service([:wiki, :xmpp])
|
||||
end
|
||||
@@ -100,21 +100,21 @@ RSpec.describe User, type: :model do
|
||||
before do
|
||||
allow(user).to receive(:ldap_entry).and_return({
|
||||
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
||||
service: ["discourse", "gitea", "xmpp"]
|
||||
services_enabled: ["discourse", "gitea", "xmpp"]
|
||||
})
|
||||
allow(user).to receive(:dn).and_return(dn)
|
||||
end
|
||||
|
||||
it "removes the service from the LDAP entry" do
|
||||
expect_any_instance_of(LdapService).to receive(:replace_attribute)
|
||||
.with(dn, :service, ["discourse", "gitea"]).and_return(true)
|
||||
.with(dn, :serviceEnabled, ["discourse", "gitea"]).and_return(true)
|
||||
|
||||
user.disable_service(:xmpp)
|
||||
end
|
||||
|
||||
it "removes multiple services from the LDAP entry" do
|
||||
expect_any_instance_of(LdapService).to receive(:replace_attribute)
|
||||
.with(dn, :service, ["discourse"]).and_return(true)
|
||||
.with(dn, :serviceEnabled, ["discourse"]).and_return(true)
|
||||
|
||||
user.disable_service([:xmpp, "gitea"])
|
||||
end
|
||||
@@ -206,9 +206,21 @@ RSpec.describe User, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#nostr_pubkey" do
|
||||
before do
|
||||
allow_any_instance_of(User).to receive(:ldap_entry)
|
||||
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
|
||||
end
|
||||
|
||||
it "returns the raw pubkey from LDAP" do
|
||||
expect(user.nostr_pubkey).to eq("07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#nostr_pubkey_bech32" do
|
||||
before do
|
||||
user.update! nostr_pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||
allow_any_instance_of(User).to receive(:ldap_entry)
|
||||
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
|
||||
end
|
||||
|
||||
it "encodes the hexadecimal pubkey to bech32" do
|
||||
|
||||
270
spec/requests/contributions/donations_spec.rb
Normal file
270
spec/requests/contributions/donations_spec.rb
Normal file
@@ -0,0 +1,270 @@
|
||||
require 'rails_helper'
|
||||
require 'webmock/rspec'
|
||||
|
||||
RSpec.describe "Donations", type: :request do
|
||||
let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
|
||||
|
||||
before do
|
||||
Warden.test_mode!
|
||||
login_as user, scope: :user
|
||||
end
|
||||
|
||||
after { Warden.test_reset! }
|
||||
|
||||
describe "#create" do
|
||||
describe "with disabled methods" do
|
||||
before do
|
||||
Setting.btcpay_enabled = false
|
||||
end
|
||||
|
||||
it "returns a 403" do
|
||||
post "/contributions/donations", params: { donation_method: "btcpay" }
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with fake methods" do
|
||||
it "returns a 403" do
|
||||
post "/contributions/donations", params: { donation_method: "remotestorage" }
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with invalid fiat currency" do
|
||||
it "returns a 422" do
|
||||
post "/contributions/donations", params: {
|
||||
donation_method: "btcpay", amount: "10", currency: "GBP"
|
||||
}
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with bad amount" do
|
||||
it "returns a 422" do
|
||||
post "/contributions/donations", params: {
|
||||
donation_method: "btcpay", amount: ""
|
||||
}
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with BTCPay" do
|
||||
before { Setting.btcpay_enabled = true }
|
||||
|
||||
describe "amount in EUR" do
|
||||
before do
|
||||
expect(BtcpayManager::CreateInvoice).to receive(:call)
|
||||
.with(amount: 25, currency: "EUR", redirect_url: "http://www.example.com/contributions/donations/1/confirm_btcpay")
|
||||
.and_return({
|
||||
"id" => "Q9GBe143HJIkdpZeH4Ftx5",
|
||||
"amount" => "25",
|
||||
"currency" => "EUR",
|
||||
"checkoutLink" => "#{Setting.btcpay_api_url}/i/Q9GBe143HJIkdpZeH4Ftx5",
|
||||
"expirationTime" => 1707908626,
|
||||
"checkout" => { "redirectURL" => "http://www.example.com/contributions/donations/1/confirm_btcpay" }
|
||||
})
|
||||
|
||||
post "/contributions/donations", params: {
|
||||
donation_method: "btcpay", amount: "25", currency: "EUR",
|
||||
public_name: "Mickey"
|
||||
}
|
||||
end
|
||||
|
||||
it "creates a new donation record" do
|
||||
expect(user.donations.count).to eq(1)
|
||||
donation = user.donations.first
|
||||
expect(donation.donation_method).to eq("btcpay")
|
||||
expect(donation.payment_method).to be_nil
|
||||
expect(donation.paid_at).to be_nil
|
||||
expect(donation.public_name).to eq("Mickey")
|
||||
expect(donation.amount_sats).to be_nil
|
||||
expect(donation.fiat_amount).to eq(2500)
|
||||
expect(donation.fiat_currency).to eq("EUR")
|
||||
expect(donation.btcpay_invoice_id).to eq("Q9GBe143HJIkdpZeH4Ftx5")
|
||||
end
|
||||
|
||||
it "redirects to the BTCPay checkout page" do
|
||||
expect(response).to redirect_to("https://btcpay.example.com/i/Q9GBe143HJIkdpZeH4Ftx5")
|
||||
end
|
||||
end
|
||||
|
||||
describe "amount in sats" do
|
||||
before do
|
||||
expect(BtcpayManager::CreateInvoice).to receive(:call)
|
||||
.with(amount: 0.0001, currency: "BTC", redirect_url: "http://www.example.com/contributions/donations/1/confirm_btcpay")
|
||||
.and_return({
|
||||
"id" => "Q9GBe143HJIkdpZeH4Ftx5",
|
||||
"amount" => "0.0001",
|
||||
"currency" => "BTC",
|
||||
"checkoutLink" => "#{Setting.btcpay_api_url}/i/Q9GBe143HJIkdpZeH4Ftx5",
|
||||
"expirationTime" => 1707908626,
|
||||
"checkout" => { "redirectURL" => "http://www.example.com/contributions/donations/1/confirm_btcpay" }
|
||||
})
|
||||
|
||||
post "/contributions/donations", params: {
|
||||
donation_method: "btcpay", amount: "10000", currency: "sats",
|
||||
public_name: "Garret Holmes"
|
||||
}
|
||||
end
|
||||
|
||||
it "creates a new donation record" do
|
||||
expect(user.donations.count).to eq(1)
|
||||
donation = user.donations.first
|
||||
expect(donation.donation_method).to eq("btcpay")
|
||||
expect(donation.payment_method).to be_nil
|
||||
expect(donation.paid_at).to be_nil
|
||||
expect(donation.public_name).to eq("Garret Holmes")
|
||||
expect(donation.amount_sats).to eq(10000)
|
||||
expect(donation.fiat_amount).to be_nil
|
||||
expect(donation.fiat_currency).to be_nil
|
||||
expect(donation.btcpay_invoice_id).to eq("Q9GBe143HJIkdpZeH4Ftx5")
|
||||
end
|
||||
|
||||
it "redirects to the BTCPay checkout page" do
|
||||
expect(response).to redirect_to("https://btcpay.example.com/i/Q9GBe143HJIkdpZeH4Ftx5")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#confirm_btcpay" do
|
||||
before { Setting.btcpay_enabled = true }
|
||||
|
||||
describe "with donation of another user" do
|
||||
let(:other_user) { create :user, id: 3, cn: "carl", ou: 'kosmos.org', email: "carl@example.com" }
|
||||
|
||||
before do
|
||||
@donation = other_user.donations.create!(
|
||||
donation_method: "btcpay", btcpay_invoice_id: "123abc",
|
||||
fiat_amount: 25, fiat_currency: "EUR", paid_at: nil
|
||||
)
|
||||
get confirm_btcpay_contributions_donation_path(@donation.id)
|
||||
end
|
||||
|
||||
it "returns a 404" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
describe "with confirmed donation" do
|
||||
before do
|
||||
@donation = user.donations.create!(
|
||||
donation_method: "btcpay", btcpay_invoice_id: "123abc",
|
||||
fiat_amount: 25, fiat_currency: "EUR",
|
||||
paid_at: "2024-02-16", payment_status: "settled"
|
||||
)
|
||||
get confirm_btcpay_contributions_donation_path(@donation.id)
|
||||
end
|
||||
|
||||
it "redirects to the donations index" do
|
||||
expect(response).to redirect_to(contributions_donations_url)
|
||||
end
|
||||
end
|
||||
|
||||
describe "settled via Lightning" do
|
||||
describe "amount in EUR" do
|
||||
subject do
|
||||
user.donations.create!(
|
||||
donation_method: "btcpay", btcpay_invoice_id: "MCkDbf2cUgBuuisUCgnRnb",
|
||||
fiat_amount: 25, fiat_currency: "EUR", paid_at: nil
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
invoice = File.read("#{Rails.root}/spec/fixtures/btcpay/lightning_eur_settled_invoice.json")
|
||||
payments = File.read("#{Rails.root}/spec/fixtures/btcpay/lightning_eur_settled_payments.json")
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/MCkDbf2cUgBuuisUCgnRnb")
|
||||
.to_return(status: 200, headers: {}, body: invoice)
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/MCkDbf2cUgBuuisUCgnRnb/payment-methods")
|
||||
.to_return(status: 200, headers: {}, body: payments)
|
||||
|
||||
get confirm_btcpay_contributions_donation_path(subject)
|
||||
end
|
||||
|
||||
it "updates the donation record" do
|
||||
subject.reload
|
||||
expect(subject.paid_at).not_to be_nil
|
||||
expect(subject.amount_sats).to eq(2061)
|
||||
end
|
||||
|
||||
it "redirects to the donations index" do
|
||||
expect(response).to redirect_to(contributions_donations_url)
|
||||
end
|
||||
end
|
||||
|
||||
describe "amount in sats" do
|
||||
subject do
|
||||
user.donations.create!(
|
||||
donation_method: "btcpay", btcpay_invoice_id: "JxjfeJi1TtX8FcWSjEvGxg",
|
||||
amount_sats: 10000, fiat_amount: nil, fiat_currency: nil, paid_at: nil
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
invoice = File.read("#{Rails.root}/spec/fixtures/btcpay/lightning_sats_settled_invoice.json")
|
||||
payments = File.read("#{Rails.root}/spec/fixtures/btcpay/lightning_sats_settled_payments.json")
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/JxjfeJi1TtX8FcWSjEvGxg")
|
||||
.to_return(status: 200, headers: {}, body: invoice)
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/JxjfeJi1TtX8FcWSjEvGxg/payment-methods")
|
||||
.to_return(status: 200, headers: {}, body: payments)
|
||||
|
||||
expect(BtcpayManager::FetchExchangeRate).to receive(:call)
|
||||
.with(fiat_currency: "EUR").and_return(48532.00)
|
||||
|
||||
get confirm_btcpay_contributions_donation_path(subject)
|
||||
end
|
||||
|
||||
it "updates the donation record" do
|
||||
subject.reload
|
||||
expect(subject.paid_at).not_to be_nil
|
||||
expect(subject.amount_sats).to eq(10000)
|
||||
expect(subject.fiat_amount).to eq(485)
|
||||
expect(subject.fiat_currency).to eq("EUR")
|
||||
end
|
||||
|
||||
it "redirects to the donations index" do
|
||||
expect(response).to redirect_to(contributions_donations_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "on-chain" do
|
||||
describe "waiting for confirmations" do
|
||||
subject do
|
||||
user.donations.create!(
|
||||
donation_method: "btcpay", btcpay_invoice_id: "K4e31MhbLKmr3D7qoNYRd3",
|
||||
fiat_amount: 120, fiat_currency: "USD", paid_at: nil
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
invoice = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_processing_invoice.json")
|
||||
payments = File.read("#{Rails.root}/spec/fixtures/btcpay/onchain_eur_processing_payments.json")
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3")
|
||||
.to_return(status: 200, headers: {}, body: invoice)
|
||||
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/K4e31MhbLKmr3D7qoNYRd3/payment-methods")
|
||||
.to_return(status: 200, headers: {}, body: payments)
|
||||
|
||||
get confirm_btcpay_contributions_donation_path(subject)
|
||||
end
|
||||
|
||||
it "updates the donation record" do
|
||||
subject.reload
|
||||
expect(subject.paid_at).to be_nil
|
||||
expect(subject.amount_sats).to eq(191354)
|
||||
expect(subject.payment_status).to eq("processing")
|
||||
end
|
||||
|
||||
it "enqueues a job to periodically check the invoice status" do
|
||||
expect(enqueued_jobs.size).to eq(1)
|
||||
expect(enqueued_jobs.first["job_class"]).to eq("BtcpayCheckDonationJob")
|
||||
expect(enqueued_jobs.first['arguments'][0]["_aj_globalid"]).to eq("gid://akkounts/Donation/#{subject.id}")
|
||||
end
|
||||
|
||||
it "redirects to the donations index" do
|
||||
expect(response).to redirect_to(contributions_donations_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,14 +2,23 @@ require 'rails_helper'
|
||||
|
||||
RSpec.describe "Settings", type: :request do
|
||||
let(:user) { create :user, cn: 'mark', ou: 'kosmos.org' }
|
||||
let(:other_user) { create :user, id: 2, cn: 'markymark', ou: 'kosmos.org', email: 'markymark@interscope.com' }
|
||||
|
||||
before do
|
||||
login_as user, :scope => :user
|
||||
|
||||
allow_any_instance_of(User).to receive(:dn)
|
||||
.and_return("cn=#{user.cn},ou=kosmos.org,cn=users,dc=kosmos,dc=org")
|
||||
allow_any_instance_of(User).to receive(:nostr_pubkey).and_return(nil)
|
||||
|
||||
allow(LdapManager::FetchUserByNostrKey).to receive(:call).with(
|
||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||
).and_return(nil)
|
||||
end
|
||||
|
||||
describe "GET /settings/experiments" do
|
||||
describe "GET /settings/nostr" do
|
||||
it "works" do
|
||||
get setting_path(:experiments)
|
||||
get setting_path(:nostr)
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
@@ -22,6 +31,11 @@ RSpec.describe "Settings", type: :request do
|
||||
|
||||
context "With valid data" do
|
||||
before do
|
||||
expect(LdapManager::UpdateNostrKey).to receive(:call).with(
|
||||
dn: "cn=mark,ou=kosmos.org,cn=users,dc=kosmos,dc=org",
|
||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||
).and_return(0)
|
||||
|
||||
post set_nostr_pubkey_settings_path, params: {
|
||||
signed_event: {
|
||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
||||
@@ -41,41 +55,17 @@ RSpec.describe "Settings", type: :request do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it "saves the pubkey" do
|
||||
expect(user.nostr_pubkey).to eq("07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3")
|
||||
it "informs the user about the success" do
|
||||
expect(flash[:success]).to eq("Public key verification successful")
|
||||
end
|
||||
end
|
||||
|
||||
context "With wrong username" do
|
||||
context "With key already in use by someone else" do
|
||||
before do
|
||||
post set_nostr_pubkey_settings_path, params: {
|
||||
signed_event: {
|
||||
id: "2e1e20ee762d6a5b5b30835eda9ca03146e4baf82490e53fd75794c08de08ac0",
|
||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
||||
created_at: 1678255391,
|
||||
kind: 1,
|
||||
content: "Connect my public key to admin@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
||||
sig: "2ace19c9db892ac6383848721a3e08b13d90d689fdeac60d9633a623d3f08eb7e0d468f1b3e928d1ea979477c2ec46ee6cdb2d053ef2e4ed3c0630a51d249029"
|
||||
}
|
||||
}.to_json, headers: {
|
||||
"CONTENT_TYPE" => "application/json",
|
||||
"HTTP_ACCEPT" => "application/json"
|
||||
}
|
||||
end
|
||||
|
||||
it "returns a 422 status" do
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
|
||||
it "does not save the pubkey" do
|
||||
expect(user.nostr_pubkey).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "With wrong shared secret" do
|
||||
before do
|
||||
session_stub = { shared_secret: "ho-chi-minh" }
|
||||
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
|
||||
expect(LdapManager::FetchUserByNostrKey).to receive(:call).with(
|
||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
|
||||
).and_return(other_user)
|
||||
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
||||
|
||||
post set_nostr_pubkey_settings_path, params: {
|
||||
signed_event: {
|
||||
@@ -96,8 +86,67 @@ RSpec.describe "Settings", type: :request do
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
|
||||
it "does not save the pubkey" do
|
||||
expect(user.nostr_pubkey).to be_nil
|
||||
it "informs the user about the failure" do
|
||||
expect(flash[:alert]).to eq("Public key already in use for a different account")
|
||||
end
|
||||
end
|
||||
|
||||
context "With wrong username" do
|
||||
before do
|
||||
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
||||
|
||||
post set_nostr_pubkey_settings_path, params: {
|
||||
signed_event: {
|
||||
id: "2e1e20ee762d6a5b5b30835eda9ca03146e4baf82490e53fd75794c08de08ac0",
|
||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
||||
created_at: 1678255391,
|
||||
kind: 1,
|
||||
content: "Connect my public key to admin@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
||||
sig: "2ace19c9db892ac6383848721a3e08b13d90d689fdeac60d9633a623d3f08eb7e0d468f1b3e928d1ea979477c2ec46ee6cdb2d053ef2e4ed3c0630a51d249029"
|
||||
}
|
||||
}.to_json, headers: {
|
||||
"CONTENT_TYPE" => "application/json",
|
||||
"HTTP_ACCEPT" => "application/json"
|
||||
}
|
||||
end
|
||||
|
||||
it "returns a 422 status" do
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
|
||||
it "informs the user about the failure" do
|
||||
expect(flash[:alert]).to eq("Public key could not be verified")
|
||||
end
|
||||
end
|
||||
|
||||
context "With wrong shared secret" do
|
||||
before do
|
||||
session_stub = { shared_secret: "ho-chi-minh" }
|
||||
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
|
||||
|
||||
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
|
||||
|
||||
post set_nostr_pubkey_settings_path, params: {
|
||||
signed_event: {
|
||||
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
|
||||
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
|
||||
created_at: 1678254161,
|
||||
kind: 1,
|
||||
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
|
||||
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
|
||||
}
|
||||
}.to_json, headers: {
|
||||
"CONTENT_TYPE" => "application/json",
|
||||
"HTTP_ACCEPT" => "application/json"
|
||||
}
|
||||
end
|
||||
|
||||
it "returns a 422 status" do
|
||||
expect(response).to have_http_status(422)
|
||||
end
|
||||
|
||||
it "informs the user about the failure" do
|
||||
expect(flash[:alert]).to eq("Public key could not be verified")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,6 +19,11 @@ RSpec.describe "Well-known URLs", type: :request do
|
||||
context "user does not have a nostr pubkey configured" do
|
||||
let(:user) { create :user, cn: 'spongebob', ou: 'kosmos.org' }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(User).to receive(:ldap_entry)
|
||||
.and_return({ nostr_key: nil })
|
||||
end
|
||||
|
||||
it "returns a 404 status" do
|
||||
get "/.well-known/nostr.json?name=spongebob"
|
||||
expect(response).to have_http_status(:not_found)
|
||||
@@ -26,8 +31,12 @@ RSpec.describe "Well-known URLs", type: :request do
|
||||
end
|
||||
|
||||
context "user with nostr pubkey" do
|
||||
let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org', nostr_pubkey: '438d35a6750d0dd6b75d032af8a768aad76b62f0c70ecb45f9c4d9e63540f7f4' }
|
||||
before { user.save! }
|
||||
let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org' }
|
||||
before do
|
||||
user.save!
|
||||
allow_any_instance_of(User).to receive(:nostr_pubkey)
|
||||
.and_return('438d35a6750d0dd6b75d032af8a768aad76b62f0c70ecb45f9c4d9e63540f7f4')
|
||||
end
|
||||
|
||||
it "returns a NIP-05 response" do
|
||||
get "/.well-known/nostr.json?name=bobdylan"
|
||||
|
||||
Reference in New Issue
Block a user