Compare commits
5 Commits
v0.9.0
...
f57fff0087
| Author | SHA1 | Date | |
|---|---|---|---|
|
f57fff0087
|
|||
|
18ff3d3f0d
|
|||
|
1b3ac90ddd
|
|||
|
5db0ee6658
|
|||
|
da31a027c5
|
79
.env.example
79
.env.example
@@ -1,14 +1,14 @@
|
|||||||
# PRIMARY_DOMAIN=kosmos.org
|
PRIMARY_DOMAIN=kosmos.org
|
||||||
# AKKOUNTS_DOMAIN=accounts.example.com
|
AKKOUNTS_DOMAIN=accounts.example.com
|
||||||
|
|
||||||
# SMTP_SERVER=smtp.example.com
|
SMTP_SERVER=smtp.example.com
|
||||||
# SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
# SMTP_LOGIN=accounts
|
SMTP_LOGIN=accounts
|
||||||
# SMTP_PASSWORD=123abc
|
SMTP_PASSWORD=123abc
|
||||||
# SMTP_FROM_ADDRESS=accounts@example.com
|
SMTP_FROM_ADDRESS=accounts@example.com
|
||||||
# SMTP_DOMAIN=example.com
|
SMTP_DOMAIN=example.com
|
||||||
# SMTP_AUTH_METHOD=plain
|
SMTP_AUTH_METHOD=plain
|
||||||
# SMTP_ENABLE_STARTTLS=auto
|
SMTP_ENABLE_STARTTLS=auto
|
||||||
|
|
||||||
# S3_ENABLED=true
|
# S3_ENABLED=true
|
||||||
# S3_ENDPOINT=https://s3.kosmos.org
|
# S3_ENDPOINT=https://s3.kosmos.org
|
||||||
@@ -18,47 +18,48 @@
|
|||||||
# S3_ACCESS_KEY=123456abcdefg
|
# S3_ACCESS_KEY=123456abcdefg
|
||||||
# S3_SECRET_KEY=123456789123456789123456789
|
# S3_SECRET_KEY=123456789123456789123456789
|
||||||
|
|
||||||
# LDAP_HOST=localhost
|
LDAP_HOST=localhost
|
||||||
# LDAP_PORT=389
|
LDAP_PORT=389
|
||||||
# LDAP_ADMIN_PASSWORD=passthebutter
|
LDAP_ADMIN_PASSWORD=passthebutter
|
||||||
# LDAP_SUFFIX='dc=kosmos,dc=org'
|
LDAP_SUFFIX='dc=kosmos,dc=org'
|
||||||
|
|
||||||
# REDIS_URL='redis://localhost:6379/1'
|
REDIS_URL='redis://localhost:6379/1'
|
||||||
|
|
||||||
# WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||||
|
|
||||||
#
|
#
|
||||||
# Service Integrations
|
# Service Integrations
|
||||||
#
|
#
|
||||||
|
|
||||||
# BTCPAY_API_URL='http://localhost:23001/api/v1'
|
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||||
# BTCPAY_STORE_ID=''
|
BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||||
# BTCPAY_AUTH_TOKEN=''
|
BTCPAY_STORE_ID=''
|
||||||
|
BTCPAY_AUTH_TOKEN=''
|
||||||
|
|
||||||
# DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
||||||
# DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||||
|
|
||||||
# DRONECI_PUBLIC_URL='https://drone.kosmos.org'
|
DRONECI_PUBLIC_URL='https://drone.kosmos.org'
|
||||||
|
|
||||||
# EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
|
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
|
||||||
# EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
||||||
|
|
||||||
# GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
||||||
|
|
||||||
# LNDHUB_API_URL='http://localhost:3023'
|
LNDHUB_API_URL='http://localhost:3023'
|
||||||
# LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||||
# LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||||
# LNDHUB_ADMIN_UI=true
|
LNDHUB_ADMIN_UI=true
|
||||||
# LNDHUB_ADMIN_TOKEN=123456789
|
LNDHUB_ADMIN_TOKEN=123456789
|
||||||
# LNDHUB_PG_HOST=localhost
|
LNDHUB_PG_HOST=localhost
|
||||||
# LNDHUB_PG_PORT=5432
|
LNDHUB_PG_PORT=5432
|
||||||
# LNDHUB_PG_DATABASE=lndhub
|
LNDHUB_PG_DATABASE=lndhub
|
||||||
# LNDHUB_PG_USERNAME=lndhub
|
LNDHUB_PG_USERNAME=lndhub
|
||||||
# LNDHUB_PG_PASSWORD=''
|
LNDHUB_PG_PASSWORD=''
|
||||||
|
|
||||||
# MASTODON_PUBLIC_URL='https://kosmos.social'
|
MASTODON_PUBLIC_URL='https://kosmos.social'
|
||||||
|
|
||||||
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||||
|
|
||||||
# RS_STORAGE_URL='https://storage.kosmos.org'
|
RS_STORAGE_URL='https://storage.kosmos.org'
|
||||||
# RS_REDIS_URL='redis://localhost:6379/2'
|
RS_REDIS_URL='redis://localhost:6379/2'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ PRIMARY_DOMAIN=kosmos.org
|
|||||||
|
|
||||||
REDIS_URL='redis://localhost:6379/0'
|
REDIS_URL='redis://localhost:6379/0'
|
||||||
|
|
||||||
|
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||||
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
||||||
BTCPAY_STORE_ID='123456'
|
BTCPAY_STORE_ID='123456'
|
||||||
|
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -14,10 +14,8 @@ so:
|
|||||||
|
|
||||||
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
|
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
|
||||||
Docker Desktop)
|
Docker Desktop)
|
||||||
3. Run `docker compose up --build` and wait until all services have started
|
3. Run `docker compose up` and wait until 389ds announces its successful start
|
||||||
(389ds might take an extra minute to be ready). This will take a while when
|
in the log output
|
||||||
running for the first time, so you might want to do something else in the
|
|
||||||
meantime.
|
|
||||||
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
|
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
|
||||||
5. `docker compose run web rails ldap:setup`
|
5. `docker compose run web rails ldap:setup`
|
||||||
6. `docker compose run web rails db:setup`
|
6. `docker compose run web rails db:setup`
|
||||||
@@ -30,44 +28,38 @@ have the password "user is user".
|
|||||||
|
|
||||||
### Rails app
|
### Rails app
|
||||||
|
|
||||||
_Note: when using Docker Compose, prefix the following commands with `docker-compose
|
|
||||||
run web`._
|
|
||||||
|
|
||||||
Installing dependencies:
|
Installing dependencies:
|
||||||
|
|
||||||
bundle install
|
bundle install
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
Migrating the local database (after schema changes):
|
Setting up local database (SQLite):
|
||||||
|
|
||||||
|
bundle exec rails db:create
|
||||||
bundle exec rails db:migrate
|
bundle exec rails db:migrate
|
||||||
|
|
||||||
Running the dev server, and auto-building CSS files on change _(automatic with Docker Compose)_:
|
Running the dev server and auto-building CSS files on change:
|
||||||
|
|
||||||
bin/dev
|
bin/dev
|
||||||
|
|
||||||
Running the background workers (requires Redis) _(automatic with Docker Compose)_:
|
Running the background workers (requires Redis):
|
||||||
|
|
||||||
bundle exec sidekiq -C config/sidekiq.yml
|
bundle exec sidekiq -C config/sidekiq.yml
|
||||||
|
|
||||||
Running the test suite:
|
Running all specs:
|
||||||
|
|
||||||
bundle exec rspec
|
bundle exec rspec
|
||||||
|
|
||||||
Running the test suite with Docker Compose requires overriding the Rails
|
### Docker (Compose)
|
||||||
environment:
|
|
||||||
|
|
||||||
docker-compose run -e "RAILS_ENV=test" web rspec
|
There is a working Docker Compose config file, which define a number of services including
|
||||||
|
an app server for Rails as well as a local 389ds (LDAP) server.
|
||||||
|
|
||||||
### Docker Compose
|
For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`,
|
||||||
|
listening on port 389 on your machine.
|
||||||
|
|
||||||
Services/containers are configured in `docker-compose.yml`.
|
You can pick and choose your services adding them by name (listed in `docker-compose.yml`) at
|
||||||
|
the end of the docker compose command. eg. `docker compose up ldap redis`
|
||||||
You can run services selectively, for example if you want to run the Rails app
|
|
||||||
and test suite on the host machine. Just add the service names of the
|
|
||||||
containers you want to run to the `up` command, like so:
|
|
||||||
|
|
||||||
docker-compose up ldap redis
|
|
||||||
|
|
||||||
#### LDAP server
|
#### LDAP server
|
||||||
|
|
||||||
@@ -84,15 +76,13 @@ Now you can seed the back-end with data using this Rails task:
|
|||||||
The setup task will first delete any existing entries in the directory tree
|
The setup task will first delete any existing entries in the directory tree
|
||||||
("dc=kosmos,dc=org"), and then create our development entries.
|
("dc=kosmos,dc=org"), and then create our development entries.
|
||||||
|
|
||||||
Note that all 389ds data is stored in the `389ds-data` volume. So if you want
|
Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
|
||||||
to start over with a fresh installation, delete both that volume as well as the
|
with a fresh installation, delete both that directory as well as the container.
|
||||||
container.
|
|
||||||
|
|
||||||
#### Minio / remoteStorage
|
#### Minio / RS
|
||||||
|
|
||||||
If you want to run remoteStorage accounts locally, you will have to create the
|
If you want to run remoteStorage accounts locally, you will have to create the
|
||||||
respective bucket first. With the `minio` container running (run by default
|
respective bucket first:
|
||||||
when using Docker Compose), follow these steps:
|
|
||||||
|
|
||||||
* `docker compose up web redis minio liquor-cabinet`
|
* `docker compose up web redis minio liquor-cabinet`
|
||||||
* Head to http://localhost:9001 and log in with user `minioadmin`, password
|
* Head to http://localhost:9001 and log in with user `minioadmin`, password
|
||||||
|
|||||||
@@ -32,6 +32,11 @@
|
|||||||
focus:ring-blue-400 focus:ring-opacity-75;
|
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 {
|
.btn-red {
|
||||||
@apply bg-red-600 hover:bg-red-700 text-white
|
@apply bg-red-600 hover:bg-red-700 text-white
|
||||||
focus:ring-red-500 focus:ring-opacity-75;
|
focus:ring-red-500 focus:ring-opacity-75;
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<% if @image_url %>
|
|
||||||
<%= image_tag @image_url, class: "h-full w-full" %>
|
|
||||||
<% else %>
|
|
||||||
<%= render partial: "icons/remotestorage", locals: { custom_class: "h-full w-full p-0.5 text-gray-200" } %>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module AppCatalog
|
|
||||||
class WebAppIconComponent < ViewComponent::Base
|
|
||||||
def initialize(web_app:)
|
|
||||||
if web_app&.icon&.attached?
|
|
||||||
@image_url = image_url_for(web_app.icon)
|
|
||||||
elsif web_app&.apple_touch_icon&.attached?
|
|
||||||
@image_url = image_url_for(web_app.apple_touch_icon)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def image_url_for(attachment)
|
|
||||||
if Setting.s3_enabled?
|
|
||||||
s3_image_url(attachment)
|
|
||||||
else
|
|
||||||
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
|
<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">
|
<div class="px-6 sm:px-12 pt-2 sm:pt-4">
|
||||||
<%= render partial: @tabnav_partial %>
|
<%= render partial: @tabnav_partial %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<!-- Modal Container -->
|
<!-- Modal Container -->
|
||||||
<div data-modal-target="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">
|
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
|
||||||
<!-- Modal Card -->
|
<!-- Modal Card -->
|
||||||
<div class="m-1 bg-white rounded shadow">
|
<div class="m-1 bg-white rounded shadow">
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class NotificationComponent < ViewComponent::Base
|
|||||||
'alert-octagon'
|
'alert-octagon'
|
||||||
when 'alert'
|
when 'alert'
|
||||||
'alert-octagon'
|
'alert-octagon'
|
||||||
|
when 'warning'
|
||||||
|
'alert-octagon'
|
||||||
else
|
else
|
||||||
'info'
|
'info'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="h-16 w-16 flex-none">
|
<div class="h-16 w-16 flex-none">
|
||||||
<%= render AppCatalog::WebAppIconComponent.new(web_app: @web_app) %>
|
<% if @web_app.icon.attached? %>
|
||||||
|
<%= image_tag s3_image_url(@web_app.icon), class: "h-full w-full" %>
|
||||||
|
<% elsif @web_app.apple_touch_icon.attached? %>
|
||||||
|
<%= image_tag s3_image_url(@web_app.apple_touch_icon), class: "h-full w-full" %>
|
||||||
|
<% else %>
|
||||||
|
<%= render partial: "icons/remotestorage", locals: { custom_class: "h-full w-full p-0.5 text-gray-200" } %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<h4 class="mb-1 text-lg font-bold">
|
<h4 class="mb-1 text-lg font-bold">
|
||||||
<%= @web_app&.name || @auth.app_name %>
|
<%= @web_app.name %>
|
||||||
</h4>
|
</h4>
|
||||||
<p class="text-sm text-gray-500">
|
<p class="text-sm text-gray-500">
|
||||||
<%= @auth.client_id %>
|
<%= @auth.client_id %>
|
||||||
|
|||||||
@@ -3,18 +3,16 @@ class Admin::DonationsController < Admin::BaseController
|
|||||||
before_action :set_current_section, only: [:index, :show, :new, :edit]
|
before_action :set_current_section, only: [:index, :show, :new, :edit]
|
||||||
|
|
||||||
# GET /donations
|
# GET /donations
|
||||||
# GET /donations.json
|
|
||||||
def index
|
def index
|
||||||
@pagy, @donations = pagy(Donation.all.order('created_at desc'))
|
@pagy, @donations = pagy(Donation.completed.order('paid_at desc'))
|
||||||
|
|
||||||
@stats = {
|
@stats = {
|
||||||
overall_sats: @donations.all.sum("amount_sats"),
|
overall_sats: @donations.sum("amount_sats"),
|
||||||
donor_count: Donation.distinct.count(:user_id)
|
donor_count: @donations.distinct.count(:user_id)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /donations/1
|
# GET /donations/1
|
||||||
# GET /donations/1.json
|
|
||||||
def show
|
def show
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -28,54 +26,41 @@ class Admin::DonationsController < Admin::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
# POST /donations
|
# POST /donations
|
||||||
# POST /donations.json
|
|
||||||
def create
|
def create
|
||||||
@donation = Donation.new(donation_params)
|
@donation = Donation.new(donation_params)
|
||||||
|
|
||||||
respond_to do |format|
|
if @donation.paid_at == nil
|
||||||
if @donation.save
|
@donation.errors.add(:paid_at, message: "is required")
|
||||||
format.html do
|
render :new, status: :unprocessable_entity and return
|
||||||
redirect_to admin_donation_url(@donation), flash: {
|
end
|
||||||
success: 'Donation was successfully created.'
|
|
||||||
}
|
if @donation.save
|
||||||
end
|
redirect_to admin_donation_url(@donation), flash: {
|
||||||
format.json { render :show, status: :created, location: @donation }
|
success: 'Donation was successfully created.'
|
||||||
else
|
}
|
||||||
format.html { render :new, status: :unprocessable_entity }
|
else
|
||||||
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
render :new, status: :unprocessable_entity
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# PATCH/PUT /donations/1
|
# PUT /donations/1
|
||||||
# PATCH/PUT /donations/1.json
|
|
||||||
def update
|
def update
|
||||||
respond_to do |format|
|
if @donation.update(donation_params)
|
||||||
if @donation.update(donation_params)
|
redirect_to admin_donation_url(@donation), flash: {
|
||||||
format.html do
|
success: 'Donation was successfully updated.'
|
||||||
redirect_to admin_donation_url(@donation), flash: {
|
}
|
||||||
success: 'Donation was successfully updated.'
|
else
|
||||||
}
|
render :edit, status: :unprocessable_entity
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# DELETE /donations/1
|
# DELETE /donations/1
|
||||||
# DELETE /donations/1.json
|
|
||||||
def destroy
|
def destroy
|
||||||
@donation.destroy
|
@donation.destroy
|
||||||
respond_to do |format|
|
|
||||||
format.html do redirect_to admin_donations_url, flash: {
|
redirect_to admin_donations_url, flash: {
|
||||||
success: 'Donation was successfully destroyed.'
|
success: 'Donation was successfully destroyed.'
|
||||||
}
|
}
|
||||||
end
|
|
||||||
format.json { head :no_content }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -86,7 +71,10 @@ class Admin::DonationsController < Admin::BaseController
|
|||||||
|
|
||||||
# Only allow a list of trusted parameters through.
|
# Only allow a list of trusted parameters through.
|
||||||
def donation_params
|
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
|
end
|
||||||
|
|
||||||
def set_current_section
|
def set_current_section
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
class Admin::Settings::RegistrationsController < Admin::SettingsController
|
class Admin::Settings::RegistrationsController < Admin::SettingsController
|
||||||
def show
|
def index
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def create
|
||||||
update_settings
|
update_settings
|
||||||
|
|
||||||
redirect_to admin_settings_registrations_path, flash: {
|
redirect_to admin_settings_registrations_path, flash: {
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
class Admin::Settings::ServicesController < Admin::SettingsController
|
class Admin::Settings::ServicesController < Admin::SettingsController
|
||||||
before_action :set_service, only: [:show, :update]
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
redirect_to admin_settings_service_path("btcpay")
|
@service = params[:s]
|
||||||
|
|
||||||
|
if @service.blank?
|
||||||
|
redirect_to admin_settings_services_path(params: { s: "btcpay" })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def create
|
||||||
end
|
service = params.require(:service)
|
||||||
|
|
||||||
def update
|
|
||||||
update_settings
|
update_settings
|
||||||
|
|
||||||
redirect_to admin_settings_service_path(@service), flash: {
|
redirect_to admin_settings_services_path(params: { s: service }), flash: {
|
||||||
success: "Settings saved"
|
success: "Settings saved"
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_subsection
|
|
||||||
@subsection = "services"
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_service
|
|
||||||
@service = params[:service]
|
|
||||||
|
|
||||||
if @service.blank?
|
|
||||||
redirect_to admin_settings_services_path and return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Admin::SettingsController < Admin::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @errors.any?
|
if @errors.any?
|
||||||
render :show and return
|
render :index and return
|
||||||
end
|
end
|
||||||
|
|
||||||
changed_keys.each do |key|
|
changed_keys.each do |key|
|
||||||
|
|||||||
@@ -41,4 +41,26 @@ class ApplicationController < ActionController::Base
|
|||||||
def after_sign_in_path_for(user)
|
def after_sign_in_path_for(user)
|
||||||
session[:user_return_to] || root_path
|
session[:user_return_to] || root_path
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -1,10 +1,128 @@
|
|||||||
class Contributions::DonationsController < ApplicationController
|
class Contributions::DonationsController < ApplicationController
|
||||||
before_action :authenticate_user!
|
include BtcpayHelper
|
||||||
|
|
||||||
# GET /donations
|
before_action :authenticate_user!
|
||||||
# GET /donations.json
|
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
|
def index
|
||||||
@donations = current_user.donations.completed
|
|
||||||
@current_section = :contributions
|
@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
|
end
|
||||||
|
|
||||||
|
# POST /contributions/donations
|
||||||
|
def create
|
||||||
|
if params[:currency] == "sats"
|
||||||
|
fiat_amount = nil
|
||||||
|
fiat_currency = nil
|
||||||
|
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
|
end
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ require "rqrcode"
|
|||||||
require "lnurl"
|
require "lnurl"
|
||||||
|
|
||||||
class Services::LightningController < ApplicationController
|
class Services::LightningController < ApplicationController
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :authenticate_with_lndhub
|
|
||||||
before_action :set_current_section
|
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
|
def index
|
||||||
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
@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
|
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
|
def set_current_section
|
||||||
@current_section = :services
|
@current_section = :services
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_balance
|
def require_service_available
|
||||||
lndhub = Lndhub.new
|
http_status :not_found unless Setting.lndhub_enabled?
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_transactions
|
def fetch_transactions
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
include Pagy::Frontend
|
include Pagy::Frontend
|
||||||
|
|
||||||
def sats_to_btc(sats)
|
|
||||||
sats.to_f / 100000000
|
|
||||||
end
|
|
||||||
|
|
||||||
def main_nav_class(current_section, link_to_section)
|
def main_nav_class(current_section, link_to_section)
|
||||||
if 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"
|
"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"
|
@subject = "New invitations added to your account"
|
||||||
mail to: @user.email, subject: @subject
|
mail to: @user.email, subject: @subject
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bitcoin_donation_confirmed
|
||||||
|
@user = params[:user]
|
||||||
|
@donation = params[:donation]
|
||||||
|
@subject = "Donation confirmed"
|
||||||
|
mail to: @user.email, subject: @subject
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class AppCatalog::WebApp < ApplicationRecord
|
class AppCatalog::WebApp < ApplicationRecord
|
||||||
store :metadata, coder: JSON
|
store :metadata, coder: JSON
|
||||||
|
|
||||||
has_many :remote_storage_authorizations, dependent: :destroy
|
has_many :remote_storage_authorizations
|
||||||
|
|
||||||
has_one_attached :icon
|
has_one_attached :icon
|
||||||
has_one_attached :apple_touch_icon
|
has_one_attached :apple_touch_icon
|
||||||
|
|||||||
@@ -4,12 +4,25 @@ class Donation < ApplicationRecord
|
|||||||
|
|
||||||
# Validations
|
# Validations
|
||||||
validates_presence_of :user
|
validates_presence_of :user
|
||||||
validates_presence_of :amount_sats
|
validates_presence_of :donation_method,
|
||||||
validates_presence_of :paid_at
|
inclusion: { in: %w[ custom btcpay lndhub ] }
|
||||||
|
validates_presence_of :payment_status, allow_nil: true,
|
||||||
# Hooks
|
inclusion: { in: %w[ processing settled ] }
|
||||||
# TODO before_create :store_fiat_value
|
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
|
#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
|
end
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ class Setting < RailsSettings::Base
|
|||||||
field :redis_url, type: :string,
|
field :redis_url, type: :string,
|
||||||
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
||||||
|
|
||||||
field :s3_enabled, type: :boolean,
|
|
||||||
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Registrations
|
# Registrations
|
||||||
#
|
#
|
||||||
@@ -51,6 +48,9 @@ class Setting < RailsSettings::Base
|
|||||||
field :btcpay_enabled, type: :boolean,
|
field :btcpay_enabled, type: :boolean,
|
||||||
default: ENV["BTCPAY_API_URL"].present?
|
default: ENV["BTCPAY_API_URL"].present?
|
||||||
|
|
||||||
|
field :btcpay_public_url, type: :string,
|
||||||
|
default: ENV["BTCPAY_PUBLIC_URL"].presence
|
||||||
|
|
||||||
field :btcpay_store_id, type: :string,
|
field :btcpay_store_id, type: :string,
|
||||||
default: ENV["BTCPAY_STORE_ID"].presence
|
default: ENV["BTCPAY_STORE_ID"].presence
|
||||||
|
|
||||||
@@ -157,7 +157,13 @@ class Setting < RailsSettings::Base
|
|||||||
# Nostr
|
# Nostr
|
||||||
#
|
#
|
||||||
|
|
||||||
field :nostr_enabled, type: :boolean, default: true
|
field :nostr_enabled, type: :boolean, default: false
|
||||||
|
|
||||||
|
#
|
||||||
|
# OpenCollective
|
||||||
|
#
|
||||||
|
|
||||||
|
field :opencollective_enabled, type: :boolean, default: true
|
||||||
|
|
||||||
#
|
#
|
||||||
# RemoteStorage
|
# RemoteStorage
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ module AppCatalogManager
|
|||||||
@app.metadata[prop] = metadata.send(prop) if prop
|
@app.metadata[prop] = metadata.send(prop) if prop
|
||||||
end
|
end
|
||||||
|
|
||||||
@app.save!
|
|
||||||
|
|
||||||
# TODO move icon downloads to separate, async job
|
|
||||||
|
|
||||||
if icon = metadata.select_icon(sizes: "256x256") ||
|
if icon = metadata.select_icon(sizes: "256x256") ||
|
||||||
icon = metadata.select_icon(sizes: "192x192")
|
icon = metadata.select_icon(sizes: "192x192")
|
||||||
attach_remote_image(:icon, icon)
|
attach_remote_image(:icon, icon)
|
||||||
@@ -31,6 +27,8 @@ module AppCatalogManager
|
|||||||
if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon")
|
if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon")
|
||||||
attach_remote_image(:apple_touch_icon, apple_touch_icon)
|
attach_remote_image(:apple_touch_icon, apple_touch_icon)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@app.save!
|
||||||
rescue Manifique::Error => e
|
rescue Manifique::Error => e
|
||||||
msg = "Fetching web app manifest failed for #{e.url}: #{e.type}"
|
msg = "Fetching web app manifest failed for #{e.url}: #{e.type}"
|
||||||
Rails.logger.warn(msg)
|
Rails.logger.warn(msg)
|
||||||
@@ -44,19 +42,14 @@ module AppCatalogManager
|
|||||||
else
|
else
|
||||||
download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}"
|
download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}"
|
||||||
end
|
end
|
||||||
filename = "#{attachment_name}-#{Time.now.to_i}.png"
|
filename = "#{attachment_name}.png"
|
||||||
key = "web_apps/#{@app.id}/icons/#{filename}"
|
key = "web_apps/#{@app.id}/icons/#{attachment_name}.png"
|
||||||
|
|
||||||
begin
|
begin
|
||||||
tempfile = Down.download(download_url)
|
tempfile = Down.download(download_url)
|
||||||
@app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
|
@app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
|
||||||
rescue Down::NotFound
|
rescue Down::NotFound
|
||||||
msg = "Download of \"#{attachment_name}\" failed: NotFound error for #{download_url}"
|
Rails.logger.warn "Icon download failed: NotFound error for #{download_url}"
|
||||||
Rails.logger.warn(msg)
|
|
||||||
Sentry.capture_message(msg)
|
|
||||||
rescue => e
|
|
||||||
Rails.logger.warn "Saving attachment \"#{attachment_name}\" failed: \"#{e.message}\""
|
|
||||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
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
|
module BtcpayManager
|
||||||
class FetchLightningWalletBalance < BtcpayManagerService
|
class FetchLightningWalletBalance < BtcpayManagerService
|
||||||
def call
|
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
|
confirmed_balance: res["offchain"]["local"].to_i / 1000 # msats to sats
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module BtcpayManager
|
module BtcpayManager
|
||||||
class FetchOnchainWalletBalance < BtcpayManagerService
|
class FetchOnchainWalletBalance < BtcpayManagerService
|
||||||
def call
|
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
|
balance: (res["balance"].to_f * 100000000).to_i, # BTC to sats
|
||||||
|
|||||||
@@ -2,23 +2,35 @@
|
|||||||
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
|
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
|
||||||
#
|
#
|
||||||
class BtcpayManagerService < ApplicationService
|
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
|
private
|
||||||
|
|
||||||
def get(endpoint)
|
def base_url
|
||||||
res = Faraday.get("#{base_url}/#{endpoint}", {}, {
|
@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",
|
"Content-Type" => "application/json",
|
||||||
"Accept" => "application/json",
|
"Accept" => "application/json",
|
||||||
"Authorization" => "token #{auth_token}"
|
"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)
|
JSON.parse(res.body)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
class Lndhub
|
class Lndhub < ApplicationService
|
||||||
attr_accessor :auth_token
|
attr_accessor :auth_token
|
||||||
|
|
||||||
def initialize
|
def post(path, payload)
|
||||||
@base_url = ENV["LNDHUB_API_URL"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def post(endpoint, payload)
|
|
||||||
headers = { "Content-Type" => "application/json" }
|
headers = { "Content-Type" => "application/json" }
|
||||||
if auth_token
|
if auth_token
|
||||||
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||||
end
|
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
|
log_error(res) if res.status != 200
|
||||||
|
|
||||||
JSON.parse(res.body)
|
JSON.parse(res.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(endpoint, auth_token)
|
def get(path, auth_token)
|
||||||
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
|
res = Faraday.get(endpoint_url(path), {}, {
|
||||||
"Content-Type" => "application/json",
|
"Content-Type" => "application/json",
|
||||||
"Accept" => "application/json",
|
"Accept" => "application/json",
|
||||||
"Authorization" => "Bearer #{auth_token}"
|
"Authorization" => "Bearer #{auth_token}"
|
||||||
@@ -42,7 +38,7 @@ class Lndhub
|
|||||||
self.auth_token
|
self.auth_token
|
||||||
end
|
end
|
||||||
|
|
||||||
def balance(user_token=nil)
|
def fetch_balance(user_token=nil)
|
||||||
get "balance", user_token || auth_token
|
get "balance", user_token || auth_token
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -72,4 +68,14 @@ class Lndhub
|
|||||||
Sentry.capture_message("Lndhub API request failed: #{res.body}")
|
Sentry.capture_message("Lndhub API request failed: #{res.body}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def base_url
|
||||||
|
@base_url ||= Setting.lndhub_api_url
|
||||||
|
end
|
||||||
|
|
||||||
|
def endpoint_url(path)
|
||||||
|
"#{base_url}/#{path.gsub(/^\//, '')}"
|
||||||
|
end
|
||||||
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
|
class LndhubV2 < Lndhub
|
||||||
|
|
||||||
def post(endpoint, payload, options={})
|
def post(path, payload, options={})
|
||||||
headers = { "Content-Type" => "application/json" }
|
headers = { "Content-Type" => "application/json" }
|
||||||
if auth_token
|
if auth_token
|
||||||
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||||
elsif options[:admin_token]
|
elsif options[:admin_token]
|
||||||
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
|
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
|
||||||
end
|
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
|
log_error(res) if res.status != 200
|
||||||
|
|
||||||
JSON.parse(res.body)
|
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.label :user_id %>
|
||||||
<%= form.collection_select :user_id, User.where(ou: Setting.primary_domain).order(:cn), :id, :cn, {} %>
|
<%= 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.label :amount_sats, "Amount BTC (sats)" %>
|
||||||
<%= form.number_field :amount_sats %>
|
<%= form.number_field :amount_sats %>
|
||||||
|
|
||||||
<%= form.label :amount_eur, "Amount EUR (cents)" %>
|
<%= form.label :fiat_amount, "Fiat Amount (cents)" %>
|
||||||
<%= form.number_field :amount_eur %>
|
<%= form.number_field :fiat_amount %>
|
||||||
|
|
||||||
<%= form.label :amount_usd, "Amount USD (cents)"%>
|
<%= form.label :fiat_currency, "Fiat Currency" %>
|
||||||
<%= form.number_field :amount_usd %>
|
<%= form.select :fiat_currency, options_for_select([
|
||||||
|
["EUR", "EUR"], ["USD", "USD"], ["sats", "sats"]
|
||||||
|
], selected: donation.fiat_currency) %>
|
||||||
|
|
||||||
<%= form.label :public_name %>
|
<%= form.label :public_name %>
|
||||||
<%= form.text_field :public_name %>
|
<%= form.text_field :public_name %>
|
||||||
|
|||||||
@@ -25,9 +25,8 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th class="text-right">Amount BTC</th>
|
<th class="text-right">Sats</th>
|
||||||
<th class="text-right">in EUR</th>
|
<th class="text-right">Fiat Amount</th>
|
||||||
<th class="text-right">in USD</th>
|
|
||||||
<th class="pl-2">Public name</th>
|
<th class="pl-2">Public name</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -37,9 +36,8 @@
|
|||||||
<% @donations.each do |donation| %>
|
<% @donations.each do |donation| %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td>
|
<td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td>
|
||||||
<td class="text-right"><%= sats_to_btc donation.amount_sats %></td>
|
<td class="text-right"><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></td>
|
||||||
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
|
<td class="text-right"><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %></td>
|
||||||
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
|
|
||||||
<td class="pl-2"><%= donation.public_name %></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><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
json.array! @donations, partial: "donations/donation", as: :donation
|
|
||||||
@@ -8,17 +8,17 @@
|
|||||||
<th>User</th>
|
<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.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Donation Method</th>
|
||||||
|
<td><%= @donation.donation_method %></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Amount sats</th>
|
<th>Amount sats</th>
|
||||||
<td><%= @donation.amount_sats %></td>
|
<td><%= @donation.amount_sats %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Amount EUR</th>
|
<th>Fiat amount</th>
|
||||||
<td><%= @donation.amount_eur %></td>
|
<td><% if @donation.fiat_amount.present? %><%= number_to_currency @donation.fiat_amount.to_f / 100, unit: "" %> <%= @donation.fiat_currency %><% end %></td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Amount USD</th>
|
|
||||||
<td><%= @donation.amount_usd %></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Public name</th>
|
<th>Public name</th>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
json.partial! "donations/donation", donation: @donation
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<%= render HeaderComponent.new(title: "Settings") %>
|
<%= render HeaderComponent.new(title: "Settings") %>
|
||||||
|
|
||||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
|
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
|
||||||
<%= form_for(Setting.new, url: admin_settings_registrations_path, method: :put) do |f| %>
|
<%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %>
|
||||||
<section>
|
<section>
|
||||||
<h3>Registrations</h3>
|
<h3>Registrations</h3>
|
||||||
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
<%= render HeaderComponent.new(title: "Settings") %>
|
<%= render HeaderComponent.new(title: "Settings") %>
|
||||||
|
|
||||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
|
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
|
||||||
<%= form_for(Setting.new, url: admin_settings_service_path(@service), method: :put) do |f| %>
|
<%= form_for(Setting.new, url: admin_settings_services_path) do |f| %>
|
||||||
|
<%= hidden_field_tag :service, @service %>
|
||||||
|
|
||||||
<% if @errors && @errors.any? %>
|
<% if @errors && @errors.any? %>
|
||||||
<section>
|
<section>
|
||||||
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
|
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
|
||||||
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 %>
|
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
|
||||||
<section>
|
<section>
|
||||||
<% if @donations.any? %>
|
<p class="mb-12">
|
||||||
<p class="mb-12">
|
Your financial contributions to the development and upkeep of Kosmos
|
||||||
Your financial contributions to the development and upkeep of Kosmos
|
software and services.
|
||||||
software and services.
|
</p>
|
||||||
</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 %>
|
|
||||||
</section>
|
</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 %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<%
|
<%
|
||||||
# TODO remove when https://github.com/hotwired/turbo/issues/203 is fixed
|
# TODO remove when https://github.com/hotwired/turbo/issues/203 is fixed
|
||||||
enable_turbo = session[:user_return_to].blank? ||
|
enable_turbo = !session[:user_return_to] || !session[:user_return_to].match?('/discourse/connect')
|
||||||
['/discourse/connect', '/rs/oauth'].none? { |s| session[:user_return_to].match(s) }
|
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<%= render HeaderCompactComponent.new(title: "Log in") %>
|
<%= render HeaderCompactComponent.new(title: "Log in") %>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg width="24" height="24" class="icon-remotestorage <%= custom_class %>" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" version="1.1" viewBox="0 0 250 249.9" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" class="<%= custom_class %>" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" version="1.1" viewBox="0 0 250 249.9" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="translate(-66.822 -.16484)">
|
<g transform="translate(-66.822 -.16484)">
|
||||||
<polygon id="polygon1" fill="currentColor" transform="matrix(.29308 0 0 .29308 83.528 -.028385)" points="228 181 370 100 511 181 652 263 370 425 87 263 87 263 0 213 0 213 0 311 0 378 0 427 0 476 86 525 185 582 370 689 554 582 653 525 653 590 653 592 370 754 0 542 0 640 185 747 370 853 554 747 739 640 739 525 739 476 739 427 739 378 653 427 370 589 86 427 86 361 185 418 370 524 554 418 653 361 739 311 739 213 554 107 370 0 185 107 58 180 144 230"/>
|
<polygon id="polygon1" fill="currentColor" transform="matrix(.29308 0 0 .29308 83.528 -.028385)" points="228 181 370 100 511 181 652 263 370 425 87 263 87 263 0 213 0 213 0 311 0 378 0 427 0 476 86 525 185 582 370 689 554 582 653 525 653 590 653 592 370 754 0 542 0 640 185 747 370 853 554 747 739 640 739 525 739 476 739 427 739 378 653 427 370 589 86 427 86 361 185 418 370 524 554 418 653 361 739 311 739 213 554 107 370 0 185 107 58 180 144 230"/>
|
||||||
</g>
|
</g>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 867 B After Width: | Height: | Size: 848 B |
@@ -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!
|
||||||
@@ -31,7 +31,6 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if Flipper.enabled?(:avatar_upload, current_user) %>
|
|
||||||
<label class="block">
|
<label class="block">
|
||||||
<p class="font-bold mb-1">
|
<p class="font-bold mb-1">
|
||||||
Avatar
|
Avatar
|
||||||
@@ -57,7 +56,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<p class="mt-8 pt-6 border-t border-gray-200 text-right">
|
<p class="mt-8 pt-6 border-t border-gray-200 text-right">
|
||||||
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
name: "Services", path: admin_settings_services_path, icon: "grid",
|
name: "Services", path: admin_settings_services_path, icon: "grid",
|
||||||
active: controller_name == "services"
|
active: current_page?(admin_settings_services_path)
|
||||||
) %>
|
) %>
|
||||||
<% if controller_name == "services" %>
|
<% if current_page?(admin_settings_services_path) %>
|
||||||
<%= render partial: "shared/admin_sidenav_settings_services" %>
|
<%= render partial: "shared/admin_sidenav_settings_services" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
|
|||||||
@@ -1,77 +1,77 @@
|
|||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "BTCPay",
|
name: "BTCPay",
|
||||||
path: admin_settings_service_path("btcpay"),
|
path: admin_settings_services_path(params: { s: "btcpay" }),
|
||||||
text_icon: Setting.btcpay_enabled? ? "◉" : "○",
|
text_icon: Setting.btcpay_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_service_path("btcpay")),
|
active: current_page?(admin_settings_services_path(params: { s: "btcpay" })),
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "Discourse",
|
name: "Discourse",
|
||||||
path: admin_settings_service_path("discourse"),
|
path: admin_settings_services_path(params: { s: "discourse" }),
|
||||||
text_icon: Setting.discourse_enabled? ? "◉" : "○",
|
text_icon: Setting.discourse_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_service_path("discourse")),
|
active: current_page?(admin_settings_services_path(params: { s: "discourse" })),
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "Drone CI",
|
name: "Drone CI",
|
||||||
path: admin_settings_service_path("droneci"),
|
path: admin_settings_services_path(params: { s: "droneci" }),
|
||||||
text_icon: Setting.droneci_enabled? ? "◉" : "○",
|
text_icon: Setting.droneci_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_service_path("droneci")),
|
active: current_page?(admin_settings_services_path(params: { s: "droneci" })),
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "E-Mail",
|
name: "E-Mail",
|
||||||
path: admin_settings_service_path("email"),
|
path: admin_settings_services_path(params: { s: "email" }),
|
||||||
text_icon: Setting.email_enabled? ? "◉" : "○",
|
text_icon: Setting.email_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_services_path(params: { s: "email" })),
|
active: current_page?(admin_settings_services_path(params: { s: "email" })),
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "ejabberd",
|
name: "ejabberd",
|
||||||
path: admin_settings_service_path("ejabberd"),
|
path: admin_settings_services_path(params: { s: "ejabberd" }),
|
||||||
text_icon: Setting.ejabberd_enabled? ? "◉" : "○",
|
text_icon: Setting.ejabberd_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_service_path("ejabberd")),
|
active: current_page?(admin_settings_services_path(params: { s: "ejabberd" })),
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "Gitea",
|
name: "Gitea",
|
||||||
path: admin_settings_service_path("gitea"),
|
path: admin_settings_services_path(params: { s: "gitea" }),
|
||||||
text_icon: Setting.gitea_enabled? ? "◉" : "○",
|
text_icon: Setting.gitea_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_service_path("gitea")),
|
active: current_page?(admin_settings_services_path(params: { s: "gitea" })),
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "LNDHub",
|
name: "LNDHub",
|
||||||
path: admin_settings_service_path("lndhub"),
|
path: admin_settings_services_path(params: { s: "lndhub" }),
|
||||||
text_icon: Setting.lndhub_enabled? ? "◉" : "○",
|
text_icon: Setting.lndhub_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_service_path("lndhub")),
|
active: current_page?(admin_settings_services_path(params: { s: "lndhub" })),
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "Mastodon",
|
name: "Mastodon",
|
||||||
path: admin_settings_service_path("mastodon"),
|
path: admin_settings_services_path(params: { s: "mastodon" }),
|
||||||
text_icon: Setting.mastodon_enabled? ? "◉" : "○",
|
text_icon: Setting.mastodon_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_service_path("mastodon")),
|
active: current_page?(admin_settings_services_path(params: { s: "mastodon" })),
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "MediaWiki",
|
name: "MediaWiki",
|
||||||
path: admin_settings_service_path("mediawiki"),
|
path: admin_settings_services_path(params: { s: "mediawiki" }),
|
||||||
text_icon: Setting.mediawiki_enabled? ? "◉" : "○",
|
text_icon: Setting.mediawiki_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_service_path("mediawiki")),
|
active: current_page?(admin_settings_services_path(params: { s: "mediawiki" })),
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "Nostr",
|
name: "Nostr",
|
||||||
path: admin_settings_service_path("nostr"),
|
path: admin_settings_services_path(params: { s: "nostr" }),
|
||||||
text_icon: Setting.nostr_enabled? ? "◉" : "○",
|
text_icon: Setting.nostr_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_service_path("nostr")),
|
active: current_page?(admin_settings_services_path(params: { s: "nostr" })),
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "RemoteStorage",
|
name: "RemoteStorage",
|
||||||
path: admin_settings_service_path("remotestorage"),
|
path: admin_settings_services_path(params: { s: "remotestorage" }),
|
||||||
text_icon: Setting.remotestorage_enabled? ? "◉" : "○",
|
text_icon: Setting.remotestorage_enabled? ? "◉" : "○",
|
||||||
active: current_page?(admin_settings_service_path("remotestorage")),
|
active: current_page?(admin_settings_services_path(params: { s: "remotestorage" })),
|
||||||
) %>
|
) %>
|
||||||
|
|||||||
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 %>
|
||||||
@@ -71,7 +71,7 @@ Rails.application.configure do
|
|||||||
# Allow requests from any IP
|
# Allow requests from any IP
|
||||||
config.web_console.permissions = '0.0.0.0/0'
|
config.web_console.permissions = '0.0.0.0/0'
|
||||||
|
|
||||||
if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
if ENV["S3_ENABLED"]
|
||||||
config.active_storage.service = :s3
|
config.active_storage.service = :s3
|
||||||
else
|
else
|
||||||
config.active_storage.service = :local
|
config.active_storage.service = :local
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ Rails.application.configure do
|
|||||||
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
|
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
|
||||||
config.action_mailer.raise_delivery_errors = true
|
config.action_mailer.raise_delivery_errors = true
|
||||||
|
|
||||||
if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
if ENV["S3_ENABLED"]
|
||||||
config.active_storage.service = :s3
|
config.active_storage.service = :s3
|
||||||
else
|
else
|
||||||
config.active_storage.service = :local
|
config.active_storage.service = :local
|
||||||
|
|||||||
@@ -52,9 +52,10 @@ Rails.application.configure do
|
|||||||
|
|
||||||
config.active_job.queue_adapter = :test
|
config.active_job.queue_adapter = :test
|
||||||
|
|
||||||
if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
if ENV["S3_ENABLED"]
|
||||||
config.active_storage.service = :s3
|
config.active_storage.service = :s3
|
||||||
else
|
else
|
||||||
config.active_storage.service = :local
|
# Store attachments on the local disk (in ./tmp)
|
||||||
|
config.active_storage.service = :test
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
namespace :contributions do
|
namespace :contributions do
|
||||||
root to: 'donations#index'
|
root to: 'donations#index'
|
||||||
|
resources :donations, only: ['index', 'create'] do
|
||||||
|
member do
|
||||||
|
get 'confirm_btcpay'
|
||||||
|
end
|
||||||
|
end
|
||||||
get 'projects', to: 'projects#index'
|
get 'projects', to: 'projects#index'
|
||||||
resources :donations, only: ['index']
|
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :invitations, only: ['index', 'show', 'create', 'destroy']
|
resources :invitations, only: ['index', 'show', 'create', 'destroy']
|
||||||
@@ -93,8 +97,8 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
|
|
||||||
namespace :settings do
|
namespace :settings do
|
||||||
resource 'registrations', only: ['show', 'update']
|
resources 'registrations', only: ['index', 'create']
|
||||||
resources 'services', param: 'service', only: ['index', 'show', 'update']
|
resources 'services', only: ['index', 'create']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
local:
|
local:
|
||||||
service: Disk
|
service: Disk
|
||||||
root: <%= ENV["ACTIVE_STORAGE_PATH"] || Rails.root.join("storage") %>
|
root: <%= Rails.root.join("storage") %>
|
||||||
|
|
||||||
test:
|
test:
|
||||||
service: Disk
|
service: Disk
|
||||||
root: <%= Rails.root.join("tmp/storage") %>
|
root: <%= Rails.root.join("tmp/storage") %>
|
||||||
|
|
||||||
<% if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false" %>
|
<% if ENV["S3_ENABLED"] %>
|
||||||
s3:
|
s3:
|
||||||
service: S3
|
service: S3
|
||||||
endpoint: <%= ENV["S3_ENDPOINT"] %>
|
endpoint: <%= ENV["S3_ENDPOINT"] %>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -60,7 +60,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_16_124640) do
|
|||||||
t.string "payment_method"
|
t.string "payment_method"
|
||||||
t.string "btcpay_invoice_id"
|
t.string "btcpay_invoice_id"
|
||||||
t.string "payment_status"
|
t.string "payment_status"
|
||||||
t.index ["payment_status"], name: "index_donations_on_payment_status"
|
|
||||||
t.index ["user_id"], name: "index_donations_on_user_id"
|
t.index ["user_id"], name: "index_donations_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ services:
|
|||||||
ldap:
|
ldap:
|
||||||
image: 4teamwork/389ds:latest
|
image: 4teamwork/389ds:latest
|
||||||
volumes:
|
volumes:
|
||||||
- 389ds-data:/data
|
- ./tmp/389ds:/data
|
||||||
networks:
|
networks:
|
||||||
- external_network
|
- external_network
|
||||||
- internal_network
|
- internal_network
|
||||||
@@ -16,12 +16,11 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
networks:
|
networks:
|
||||||
- external_network
|
|
||||||
- internal_network
|
- internal_network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'redis-cli', 'ping']
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
volumes:
|
volumes:
|
||||||
- redis-data:/data
|
- ./tmp/redis:/data
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
@@ -43,10 +42,8 @@ services:
|
|||||||
LDAP_ADMIN_PASSWORD: passthebutter
|
LDAP_ADMIN_PASSWORD: passthebutter
|
||||||
LDAP_USE_TLS: "false"
|
LDAP_USE_TLS: "false"
|
||||||
REDIS_URL: redis://redis:6379/0
|
REDIS_URL: redis://redis:6379/0
|
||||||
ACTIVE_STORAGE_PATH: "/akkounts/tmp/attachments"
|
|
||||||
RS_REDIS_URL: redis://redis:6379/1
|
RS_REDIS_URL: redis://redis:6379/1
|
||||||
RS_STORAGE_URL: "http://localhost:4567"
|
RS_STORAGE_URL: "http://localhost:4567"
|
||||||
S3_ENABLED: false
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- ldap
|
- ldap
|
||||||
- redis
|
- redis
|
||||||
@@ -70,7 +67,6 @@ services:
|
|||||||
REDIS_URL: redis://redis:6379/0
|
REDIS_URL: redis://redis:6379/0
|
||||||
RS_REDIS_URL: redis://redis:6379/1
|
RS_REDIS_URL: redis://redis:6379/1
|
||||||
RS_STORAGE_URL: "http://localhost:4567"
|
RS_STORAGE_URL: "http://localhost:4567"
|
||||||
S3_ENABLED: false
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- ldap
|
- ldap
|
||||||
- redis
|
- redis
|
||||||
@@ -85,10 +81,10 @@ services:
|
|||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
- "9001:9001"
|
- "9001:9001"
|
||||||
volumes:
|
volumes:
|
||||||
- minio-data:/data
|
- ./tmp/minio:/data
|
||||||
|
|
||||||
liquor-cabinet:
|
liquor-cabinet:
|
||||||
image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-rc.1
|
image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-beta.2
|
||||||
networks:
|
networks:
|
||||||
- external_network
|
- external_network
|
||||||
- internal_network
|
- internal_network
|
||||||
@@ -120,11 +116,3 @@ networks:
|
|||||||
external_network:
|
external_network:
|
||||||
internal_network:
|
internal_network:
|
||||||
internal: true
|
internal: true
|
||||||
|
|
||||||
volumes:
|
|
||||||
389ds-data:
|
|
||||||
driver: local
|
|
||||||
minio-data:
|
|
||||||
driver: local
|
|
||||||
redis-data:
|
|
||||||
driver: local
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"postcss-preset-env": "^7.8.3",
|
"postcss-preset-env": "^7.8.3",
|
||||||
"tailwindcss": "^3.2.4"
|
"tailwindcss": "^3.2.4"
|
||||||
},
|
},
|
||||||
"version": "0.9.0",
|
"version": "0.8.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css",
|
"build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css",
|
||||||
"build:css": "yarn run build:css:tailwind"
|
"build:css": "yarn run build:css:tailwind"
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
require "rails_helper"
|
|
||||||
|
|
||||||
RSpec.describe AppCatalog::WebAppIconComponent, type: :component do
|
|
||||||
describe "No web app given" do
|
|
||||||
it "renders the default icon" do
|
|
||||||
expect(
|
|
||||||
render_inline(described_class.new(web_app: nil)) {}.to_html
|
|
||||||
).to include("icon-remotestorage")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -23,35 +23,35 @@ RSpec.describe 'Admin/global settings', type: :feature do
|
|||||||
scenario "Opening service settings shows page for first service" do
|
scenario "Opening service settings shows page for first service" do
|
||||||
visit admin_settings_services_path
|
visit admin_settings_services_path
|
||||||
|
|
||||||
expect(current_url).to eq(admin_settings_service_url("btcpay"))
|
expect(current_url).to eq(admin_settings_services_url(params: { s: "btcpay" }))
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "View service settings" do
|
scenario "View service settings" do
|
||||||
visit admin_settings_service_path("ejabberd")
|
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||||
|
|
||||||
expect(page).to have_content("Enable ejabberd integration")
|
expect(page).to have_content("Enable ejabberd integration")
|
||||||
expect(page).to have_field("API URL", with: "http://xmpp.example.com/api")
|
expect(page).to have_field("API URL", with: "http://xmpp.example.com/api")
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "Disable a service integration" do
|
scenario "Disable a service integration" do
|
||||||
visit admin_settings_service_path("ejabberd")
|
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||||
expect(page).to have_checked_field("setting[ejabberd_enabled]")
|
expect(page).to have_checked_field("setting[ejabberd_enabled]")
|
||||||
|
|
||||||
uncheck "setting[ejabberd_enabled]"
|
uncheck "setting[ejabberd_enabled]"
|
||||||
click_button "Save"
|
click_button "Save"
|
||||||
|
|
||||||
expect(current_url).to eq(admin_settings_service_url("ejabberd"))
|
expect(current_url).to eq(admin_settings_services_url(params: { s: "ejabberd" }))
|
||||||
expect(page).to_not have_checked_field("setting[ejabberd_enabled]")
|
expect(page).to_not have_checked_field("setting[ejabberd_enabled]")
|
||||||
expect(page).to_not have_field("API URL", disabled: true)
|
expect(page).to_not have_field("API URL", disabled: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "Resettable fields" do
|
scenario "Resettable fields" do
|
||||||
visit admin_settings_service_path("ejabberd")
|
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||||
expect(page).to have_field("API URL", with: "http://xmpp.example.com/api")
|
expect(page).to have_field("API URL", with: "http://xmpp.example.com/api")
|
||||||
expect(page).to_not have_css('input#setting_ejabberd_api_url+button')
|
expect(page).to_not have_css('input#setting_ejabberd_api_url+button')
|
||||||
|
|
||||||
Setting.ejabberd_api_url = "http://example.com/foo"
|
Setting.ejabberd_api_url = "http://example.com/foo"
|
||||||
visit admin_settings_service_path("ejabberd")
|
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||||
expect(page).to have_field("API URL", with: "http://example.com/foo")
|
expect(page).to have_field("API URL", with: "http://example.com/foo")
|
||||||
expect(page).to have_css('input#setting_ejabberd_api_url+button')
|
expect(page).to have_css('input#setting_ejabberd_api_url+button')
|
||||||
end
|
end
|
||||||
|
|||||||
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
|
||||||
@@ -12,8 +12,6 @@ RSpec.describe 'Profile settings', type: :feature do
|
|||||||
uid: user.cn, ou: user.ou, display_name: "Mark"
|
uid: user.cn, ou: user.ou, display_name: "Mark"
|
||||||
})
|
})
|
||||||
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
|
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
|
||||||
|
|
||||||
Flipper.enable "avatar_upload"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
feature "Update display name" do
|
feature "Update display name" do
|
||||||
|
|||||||
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'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe ApplicationHelper do
|
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
|
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
|
||||||
233
spec/requests/contributions/donations_spec.rb
Normal file
233
spec/requests/contributions/donations_spec.rb
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
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
|
||||||
|
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
|
||||||
@@ -15,7 +15,7 @@ RSpec.describe "WebFinger", type: :request do
|
|||||||
res = JSON.parse(response.body)
|
res = JSON.parse(response.body)
|
||||||
rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
|
rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
|
||||||
|
|
||||||
expect(rs_link["href"]).to eql("#{Setting.rs_storage_url}/tony")
|
expect(rs_link["href"]).to eql("https://storage.kosmos.org/tony")
|
||||||
|
|
||||||
oauth_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-4.2"]
|
oauth_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-4.2"]
|
||||||
expect(oauth_url).to eql("http://www.example.com/rs/oauth/tony")
|
expect(oauth_url).to eql("http://www.example.com/rs/oauth/tony")
|
||||||
|
|||||||
Reference in New Issue
Block a user