30 Commits

Author SHA1 Message Date
be5fe00f20 Merge pull request 'Fix XMPP from-address config not being used' (#150) from bugfix/xmpp_from_address into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #150
2023-10-19 10:47:45 +00:00
Râu Cao
e9c4929726 Fix XMPP from-address config not being used
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-10-17 15:21:57 +02:00
14ff0c0e16 Merge pull request 'BTCPay settings, admin page, and new Lightning balance API' (#147) from feature/btcpay_configs into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #147
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-09-26 10:13:09 +00:00
Râu Cao
d939f5d649 Merge branch 'master' into feature/btcpay_configs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2023-09-20 19:12:24 +02:00
Râu Cao
69fffb29d8 Make publishing of BTCPay wallet balances optional
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2023-09-20 18:36:53 +02:00
Râu Cao
91d3b977e9 Fix spec 2023-09-20 18:26:50 +02:00
7a5fd46835 Merge pull request 'Add user avatars to LDAP, upload on profile settings page' (#148) from feature/123-user_avatars into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #148
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-09-13 13:01:25 +00:00
Râu Cao
9c4c5c2553 Use correct content type for image
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-09-13 14:49:16 +02:00
Râu Cao
8f819d12c0 Remove debug output 2023-09-13 14:48:51 +02:00
Râu Cao
b810e27480 Use custom docker image with libvips installed in CI
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-07 19:40:43 +02:00
Râu Cao
1949f1876f Use attr_reader instead of shared instance variables
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-07 19:22:15 +02:00
Râu Cao
2ba0116ca6 Fix wrong inheritance
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-07 19:17:46 +02:00
Râu Cao
2c2ddabdff Fix code being silly
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-07 19:15:14 +02:00
Râu Cao
dfcdbec0dd Add specs for avatar upload
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-09-07 11:42:42 +02:00
Râu Cao
3b67a8791c Add libvips package to Docker container 2023-09-07 11:42:24 +02:00
Râu Cao
d5ab532947 Store and retrieve avatars in/from LDAP exclusively
Some checks failed
continuous-integration/drone/push Build is failing
No need to keep them in two places at the same time. We can fetch them
from LDAP whenever we want to do something with them.
2023-09-06 20:42:26 +02:00
Râu Cao
50c63d5c38 Update user avatar in LDAP 2023-09-06 19:02:07 +02:00
Râu Cao
64d09cfb7f Use variant declarations instead of custom methods 2023-09-06 12:38:47 +02:00
Râu Cao
def44618ef Comments
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-06 12:16:00 +02:00
Râu Cao
9e5aeaf572 Add user avatars 2023-09-06 12:15:53 +02:00
Râu Cao
86f85a90f4 Add/configure ActiveStorage 2023-09-06 12:14:28 +02:00
d8a35ac3fd Merge pull request 'Fix wrong redirect after sign-in for RS OAuth' (#146) from bugfix/rs_oauth_login into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #146
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-09-05 11:03:02 +00:00
Râu Cao
5a5f62e98a Refactor BTCPay service and API, add lightning balance
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-04 16:02:54 +02:00
Râu Cao
074f9afcbb Fix descriptions not being shown for resettable form fields 2023-09-04 15:37:02 +02:00
Râu Cao
725fd2e5ea Move lndhub admin token to env var/setting 2023-09-04 15:36:22 +02:00
Râu Cao
8349ca5e12 Add admin settings page for BTCPay 2023-09-04 15:25:20 +02:00
Râu Cao
46d59e3371 Improve icons in admin service settings sidenav 2023-09-04 15:24:35 +02:00
Râu Cao
e8e6ee0bc4 Add configurable settings for BTCPay 2023-09-04 15:23:27 +02:00
Râu Cao
a91ee2bd0a Fix generated usernames in seeds potentially being too short
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-04 11:35:51 +02:00
Râu Cao
fcb6923c92 Fix wrong redirect after sign-in for RS OAuth
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
We use a custom auth method to pre-fill the username when reaching the
RS OAuth while signed out. However, it needs to redirect back to the RS
OAuth page after sign-in, and not to the root path.
2023-09-04 11:33:16 +02:00
55 changed files with 669 additions and 183 deletions

View File

@@ -17,7 +17,7 @@ steps:
branch: branch:
- master - master
- name: rspec - name: rspec
image: guildeducation/rails:2.7.2-14.20.0 image: gitea.kosmos.org/kosmos/akkounts-ci:0.1.0
environment: environment:
RAILS_ENV: test RAILS_ENV: test
REDIS_URL: redis://redis:6379/0 REDIS_URL: redis://redis:6379/0

View File

@@ -34,6 +34,8 @@ EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
EJABBERD_API_URL='https://xmpp.kosmos.org/api' EJABBERD_API_URL='https://xmpp.kosmos.org/api'
BTCPAY_API_URL='http://localhost:23001/api/v1' BTCPAY_API_URL='http://localhost:23001/api/v1'
BTCPAY_STORE_ID=''
BTCPAY_AUTH_TOKEN=''
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'

View File

@@ -2,13 +2,14 @@ PRIMARY_DOMAIN=kosmos.org
REDIS_URL='redis://localhost:6379/0' REDIS_URL='redis://localhost:6379/0'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
BTCPAY_STORE_ID='123456'
DISCOURSE_PUBLIC_URL='http://discourse.example.com' DISCOURSE_PUBLIC_URL='http://discourse.example.com'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw' DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
EJABBERD_API_URL='http://xmpp.example.com/api' EJABBERD_API_URL='http://xmpp.example.com/api'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
LNDHUB_API_URL='http://localhost:3026' LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946' LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@
!/tmp/pids/ !/tmp/pids/
!/tmp/pids/.keep !/tmp/pids/.keep
/storage
/public/assets /public/assets
.byebug_history .byebug_history

View File

@@ -4,7 +4,7 @@ FROM ruby:2.7.6
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \ RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
ldap-utils tini ldap-utils tini libvips
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get install -y nodejs RUN apt-get update && apt-get install -y nodejs

View File

@@ -37,6 +37,7 @@ gem 'devise_ldap_authenticatable'
gem 'net-ldap' gem 'net-ldap'
# Utilities # Utilities
gem "image_processing", "~> 1.12.2"
gem "rqrcode", "~> 2.0" gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3' gem 'rails-settings-cached', '~> 2.8.3'
gem 'pagy', '~> 6.0', '>= 6.0.2' gem 'pagy', '~> 6.0', '>= 6.0.2'

View File

@@ -182,6 +182,9 @@ GEM
hashdiff (1.0.1) hashdiff (1.0.1)
i18n (1.14.1) i18n (1.14.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (1.1.6) importmap-rails (1.1.6)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
@@ -220,6 +223,7 @@ GEM
marcel (1.0.2) marcel (1.0.2)
matrix (0.4.2) matrix (0.4.2)
method_source (1.0.0) method_source (1.0.0)
mini_magick (4.12.0)
mini_mime (1.1.2) mini_mime (1.1.2)
minitest (5.18.0) minitest (5.18.0)
multipart-post (2.3.0) multipart-post (2.3.0)
@@ -337,6 +341,8 @@ GEM
rubocop-ast (1.29.0) rubocop-ast (1.29.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rufus-scheduler (3.9.1) rufus-scheduler (3.9.1)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
@@ -435,6 +441,7 @@ DEPENDENCIES
flipper flipper
flipper-active_record flipper-active_record
flipper-ui flipper-ui
image_processing (~> 1.12.2)
importmap-rails importmap-rails
jbuilder (~> 2.7) jbuilder (~> 2.7)
letter_opener letter_opener

View File

@@ -6,7 +6,7 @@ module FormElements
@tag = tag @tag = tag
@positioning = :vertical @positioning = :vertical
@title = title @title = title
@descripton = description @description = description
@key = key.to_sym @key = key.to_sym
@type = type @type = type
@resettable = is_resettable?(@key) @resettable = is_resettable?(@key)

View File

@@ -1,4 +1,8 @@
<%= link_to @path, class: @link_class, title: (@disabled ? "Coming soon" : nil) do %> <%= link_to @path, class: @link_class, title: (@disabled ? "Coming soon" : nil) do %>
<% if @icon.present? %>
<%= render partial: "icons/#{@icon}", locals: { custom_class: @icon_class } %> <%= render partial: "icons/#{@icon}", locals: { custom_class: @icon_class } %>
<% elsif @text_icon.present? %>
<span class="mr-3"><%= @text_icon %></span>
<% end %>
<span class="truncate"><%= @name %></span> <span class="truncate"><%= @name %></span>
<% end %> <% end %>

View File

@@ -1,11 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class SidenavLinkComponent < ViewComponent::Base class SidenavLinkComponent < ViewComponent::Base
def initialize(name:, level: 1, path:, icon:, active: false, disabled: false) def initialize(name:, level: 1, path:, icon: nil, text_icon: nil,
active: false, disabled: false)
@name = name @name = name
@level = level @level = level
@path = path @path = path
@icon = icon @icon = icon
@text_icon = text_icon
@active = active @active = active
@disabled = disabled @disabled = disabled
@link_class = class_names_link(path) @link_class = class_names_link(path)

View File

@@ -3,7 +3,7 @@ class Admin::Settings::ServicesController < Admin::SettingsController
@service = params[:s] @service = params[:s]
if @service.blank? if @service.blank?
redirect_to admin_settings_services_path(params: { s: "discourse" }) redirect_to admin_settings_services_path(params: { s: "btcpay" })
end end
end end

View File

@@ -20,6 +20,8 @@ class Admin::UsersController < Admin::BaseController
end end
@services_enabled = @user.services_enabled @services_enabled = @user.services_enabled
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn, ou: @user.ou)
end end
private private

View File

@@ -0,0 +1,29 @@
class Api::BtcpayController < Api::BaseController
before_action :require_feature_enabled
def onchain_btc_balance
balance = BtcpayManager::FetchOnchainWalletBalance.call
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch BTC wallet balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
def lightning_btc_balance
balance = BtcpayManager::FetchLightningWalletBalance.call
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch BTC lightning balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
private
def require_feature_enabled
unless Setting.btcpay_publish_wallet_balances
http_status :not_found and return
end
end
end

View File

@@ -1,13 +0,0 @@
class Api::KreditsController < Api::BaseController
def onchain_btc_balance
btcpay = BtcPay.new
balance = btcpay.onchain_wallet_balance
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch kredits BTC wallet balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
end

View File

@@ -37,4 +37,8 @@ class ApplicationController < ActionController::Base
format.any { head status } format.any { head status }
end end
end end
def after_sign_in_path_for(user)
session[:user_return_to] || root_path
end
end end

View File

@@ -108,6 +108,7 @@ class Rs::OauthController < ApplicationController
def require_signed_in_with_username def require_signed_in_with_username
unless user_signed_in? unless user_signed_in?
username, org = params[:useraddress].split("@") username, org = params[:useraddress].split("@")
session[:user_return_to] = request.url
redirect_to new_user_session_path(cn: username, ou: org) redirect_to new_user_session_path(cn: username, ou: org)
end end
end end

View File

@@ -19,10 +19,15 @@ class SettingsController < ApplicationController
def update def update
@user.preferences.merge!(user_params[:preferences] || {}) @user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name] @user.display_name = user_params[:display_name]
@user.avatar_new = user_params[:avatar]
if @user.save if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name]) if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
LdapManager::UpdateDisplayName.call(@user.dn, user_params[:display_name]) LdapManager::UpdateDisplayName.call(@user.dn, @user.display_name)
end
if @user.avatar_new.present?
LdapManager::UpdateAvatar.call(@user.dn, @user.avatar_new)
end end
redirect_to setting_path(@settings_section), flash: { redirect_to setting_path(@settings_section), flash: {
@@ -117,7 +122,7 @@ class SettingsController < ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:display_name, preferences: [ params.require(:user).permit(:display_name, :avatar, preferences: [
:lightning_notify_sats_received, :lightning_notify_sats_received,
:xmpp_exchange_contacts_with_invitees :xmpp_exchange_contacts_with_invitees
]) ])

View File

@@ -30,7 +30,7 @@ class WebhooksController < ApplicationController
def notify_xmpp(address, amt_sats, memo) def notify_xmpp(address, amt_sats, memo)
payload = { payload = {
type: "normal", type: "normal",
from: Setting.primary_domain, from: Setting.xmpp_notifications_from_address,
to: address, to: address,
subject: "Sats received!", subject: "Sats received!",
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}" body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"

View File

@@ -36,7 +36,25 @@ class Setting < RailsSettings::Base
# #
field :sentry_enabled, type: :boolean, readonly: true, field :sentry_enabled, type: :boolean, readonly: true,
default: (ENV["SENTRY_DSN"].present?.to_s || false) default: ENV["SENTRY_DSN"].present?
#
# BTCPay Server
#
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
# #
# Discourse # Discourse
@@ -46,7 +64,7 @@ class Setting < RailsSettings::Base
default: ENV["DISCOURSE_PUBLIC_URL"].presence default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean, field :discourse_enabled, type: :boolean,
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false) default: ENV["DISCOURSE_PUBLIC_URL"].present?
field :discourse_connect_secret, type: :string, field :discourse_connect_secret, type: :string,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence default: ENV["DISCOURSE_CONNECT_SECRET"].presence
@@ -59,14 +77,14 @@ class Setting < RailsSettings::Base
default: ENV["DRONECI_PUBLIC_URL"].presence default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean, field :droneci_enabled, type: :boolean,
default: (ENV["DRONECI_PUBLIC_URL"].present?.to_s || false) default: ENV["DRONECI_PUBLIC_URL"].present?
# #
# ejabberd # ejabberd
# #
field :ejabberd_enabled, type: :boolean, field :ejabberd_enabled, type: :boolean,
default: (ENV["EJABBERD_API_URL"].present?.to_s || false) default: ENV["EJABBERD_API_URL"].present?
field :ejabberd_api_url, type: :string, field :ejabberd_api_url, type: :string,
default: ENV["EJABBERD_API_URL"].presence default: ENV["EJABBERD_API_URL"].presence
@@ -85,7 +103,7 @@ class Setting < RailsSettings::Base
default: ENV["GITEA_PUBLIC_URL"].presence default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean, field :gitea_enabled, type: :boolean,
default: (ENV["GITEA_PUBLIC_URL"].present?.to_s || false) default: ENV["GITEA_PUBLIC_URL"].present?
# #
# Lightning Network # Lightning Network
@@ -95,16 +113,19 @@ class Setting < RailsSettings::Base
default: ENV["LNDHUB_API_URL"].presence default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean, field :lndhub_enabled, type: :boolean,
default: (ENV["LNDHUB_API_URL"].present?.to_s || false) default: ENV["LNDHUB_API_URL"].present?
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean, field :lndhub_admin_enabled, type: :boolean,
default: (ENV["LNDHUB_ADMIN_UI"] || false) default: ENV["LNDHUB_ADMIN_UI"] || false
field :lndhub_public_key, type: :string, field :lndhub_public_key, type: :string,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "") default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean, field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present?.to_s || false } default: -> { self.lndhub_public_key.present? }
# #
# Mastodon # Mastodon
@@ -114,7 +135,7 @@ class Setting < RailsSettings::Base
default: ENV["MASTODON_PUBLIC_URL"].presence default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean, field :mastodon_enabled, type: :boolean,
default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false) default: ENV["MASTODON_PUBLIC_URL"].present?
field :mastodon_address_domain, type: :string, field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
@@ -127,7 +148,7 @@ class Setting < RailsSettings::Base
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean, field :mediawiki_enabled, type: :boolean,
default: (ENV["MEDIAWIKI_PUBLIC_URL"].present?.to_s || false) default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
# #
# Nostr # Nostr
@@ -140,7 +161,7 @@ class Setting < RailsSettings::Base
# #
field :remotestorage_enabled, type: :boolean, field :remotestorage_enabled, type: :boolean,
default: (ENV["RS_STORAGE_URL"].present?.to_s || false) default: ENV["RS_STORAGE_URL"].present?
field :rs_storage_url, type: :string, field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence default: ENV["RS_STORAGE_URL"].presence

View File

@@ -2,10 +2,14 @@ class User < ApplicationRecord
include EmailValidatable include EmailValidatable
attr_accessor :display_name attr_accessor :display_name
attr_accessor :avatar_new
serialize :preferences, UserPreferences serialize :preferences, UserPreferences
#
# Relations # Relations
#
has_many :invitations, dependent: :destroy has_many :invitations, dependent: :destroy
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id' has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user has_one :inviter, through: :invitation, source: :user
@@ -20,6 +24,10 @@ class User < ApplicationRecord
has_many :remote_storage_authorizations has_many :remote_storage_authorizations
#
# Validations
#
validates_uniqueness_of :cn, scope: :ou validates_uniqueness_of :cn, scope: :ou
validates_length_of :cn, minimum: 3 validates_length_of :cn, minimum: 3
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/, validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
@@ -40,10 +48,20 @@ class User < ApplicationRecord
validates_uniqueness_of :nostr_pubkey, allow_blank: true validates_uniqueness_of :nostr_pubkey, allow_blank: true
validate :acceptable_avatar
#
# Scopes
#
scope :confirmed, -> { where.not(confirmed_at: nil) } scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :pending, -> { where(confirmed_at: nil) } scope :pending, -> { where(confirmed_at: nil) }
scope :all_except, -> (user) { where.not(id: user) } scope :all_except, -> (user) { where.not(id: user) }
#
# Encrypted database columns
#
has_encrypted :ln_login, :ln_password has_encrypted :ln_login, :ln_password
# Include default devise modules. Others available are: # Include default devise modules. Others available are:
@@ -140,6 +158,10 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name] @display_name ||= ldap_entry[:display_name]
end end
def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn, ou: ou)
end
def services_enabled def services_enabled
ldap_entry[:service] || [] ldap_entry[:service] || []
end end
@@ -168,4 +190,17 @@ class User < ApplicationRecord
return @ldap_service if defined?(@ldap_service) return @ldap_service if defined?(@ldap_service)
@ldap_service = LdapService.new @ldap_service = LdapService.new
end end
def acceptable_avatar
return unless avatar_new.present?
if avatar_new.size > 1.megabyte
errors.add(:avatar, "file size is too large")
end
acceptable_types = ["image/jpeg", "image/png"]
unless acceptable_types.include?(avatar_new.content_type)
errors.add(:avatar, "must be a JPEG or PNG file")
end
end
end end

View File

@@ -1,32 +0,0 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcPay
def initialize
@base_url = ENV["BTCPAY_API_URL"]
@store_id = Rails.application.credentials.btcpay[:store_id]
@auth_token = Rails.application.credentials.btcpay[:auth_token]
end
def onchain_wallet_balance
res = get "stores/#{@store_id}/payment-methods/onchain/BTC/wallet"
{
balance: res["balance"].to_f,
unconfirmed_balance: res["unconfirmedBalance"].to_f,
confirmed_balance: res["confirmedBalance"].to_f
}
end
private
def get(endpoint)
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{@auth_token}"
})
JSON.parse(res.body)
end
end

View File

@@ -0,0 +1,11 @@
module BtcpayManager
class FetchLightningWalletBalance < BtcpayManagerService
def call
res = get "stores/#{store_id}/lightning/BTC/balance"
{
balance: res["offchain"]["local"].to_i / 1000 # msats to sats
}
end
end
end

View File

@@ -0,0 +1,13 @@
module BtcpayManager
class FetchOnchainWalletBalance < BtcpayManagerService
def call
res = get "stores/#{store_id}/payment-methods/onchain/BTC/wallet"
{
balance: (res["balance"].to_f * 100000000).to_i, # BTC to sats
unconfirmed_balance: (res["unconfirmedBalance"].to_f * 100000000).to_i,
confirmed_balance: (res["confirmedBalance"].to_f * 100000000).to_i
}
end
end
end

View File

@@ -0,0 +1,24 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcpayManagerService < ApplicationService
attr_reader :base_url, :store_id, :auth_token
def initialize
@base_url = Setting.btcpay_api_url
@store_id = Setting.btcpay_store_id
@auth_token = Setting.btcpay_auth_token
end
private
def get(endpoint)
res = Faraday.get("#{base_url}/#{endpoint}", {}, {
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{auth_token}"
})
JSON.parse(res.body)
end
end

View File

@@ -0,0 +1,17 @@
module LdapManager
class FetchAvatar < LdapManagerService
def initialize(cn:, ou: nil)
@cn = cn
@ou = ou
end
def call
treebase = @ou ? "ou=#{@ou},cn=users,#{suffix}" : ldap_config["base"]
attributes = %w{ jpegPhoto }
filter = Net::LDAP::Filter.eq("cn", @cn)
entry = ldap_client.search(base: treebase, filter: filter, attributes: attributes).first
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
end
end
end

View File

@@ -0,0 +1,27 @@
require "image_processing/vips"
module LdapManager
class UpdateAvatar < LdapManagerService
def initialize(dn, file)
@dn = dn
@img_data = process(file)
end
def call
replace_attribute @dn, :jpegPhoto, @img_data
end
private
def process(file)
processed = ImageProcessing::Vips
.resize_to_fill(512, 512)
.source(file)
.convert("jpeg")
.saver(strip: true)
.call
Base64.strict_encode64 processed.read
end
end
end

View File

@@ -1,2 +1,5 @@
class LdapManagerService < LdapService class LdapManagerService < LdapService
def suffix
@suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end
end end

View File

@@ -14,7 +14,7 @@ class LndhubV2 < Lndhub
end end
def create_account(payload={}) def create_account(payload={})
post "v2/users", payload, admin_token: Rails.application.credentials.lndhub[:admin_token] post "v2/users", payload, admin_token: Setting.lndhub_admin_token
end end
def create_invoice(payload) def create_invoice(payload)

View File

@@ -0,0 +1,37 @@
<h3>BTCPay Server</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :btcpay_enabled,
enabled: Setting.btcpay_enabled?,
title: "Enable BTCPay integration",
description: "BTCPay configuration present and features enabled"
) %>
<% if Setting.btcpay_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :btcpay_api_url,
title: "API URL"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :btcpay_store_id,
title: "Store ID"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :btcpay_auth_token,
type: :password,
title: "Auth Token"
) %>
</ul>
</section>
<section>
<h3>REST API</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :btcpay_publish_wallet_balances,
enabled: Setting.btcpay_publish_wallet_balances?,
title: "Publish wallet balances",
description: "Publish the store's on-chain and Lightning wallet balances"
) %>
<% end %>
</ul>

View File

@@ -9,29 +9,35 @@
) %> ) %>
<% if Setting.lndhub_enabled? %> <% if Setting.lndhub_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetResettableSettingComponent.new(
key: :lndhub_api_url, key: :lndhub_api_url,
title: "API URL" title: "API URL"
) %> ) %>
<% end %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_admin_enabled,
enabled: Setting.lndhub_admin_enabled?,
title: "Enable LNDHub admin panel",
description: "LNDHub database configuration present and admin panel enabled"
) %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_keysend_enabled,
enabled: Setting.lndhub_keysend_enabled?,
title: "Enable keysend payments",
description: "Allow users to receive invoice-less payments to their Lightning Address"
) %>
<% if Setting.lndhub_keysend_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetResettableSettingComponent.new(
key: :lndhub_public_key, key: :lndhub_admin_token,
title: "Public key", type: :password,
description: "The public key of the Lightning node used by LNDHub" title: "Admin token",
) %> description: "Auth token for creating new lndhub accounts"
) %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_admin_enabled,
enabled: Setting.lndhub_admin_enabled?,
title: "Enable LNDHub admin panel",
description: "LNDHub database configuration present and admin panel enabled"
) %>
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :lndhub_keysend_enabled,
enabled: Setting.lndhub_keysend_enabled?,
title: "Enable keysend payments",
description: "Allow users to receive invoice-less payments to their Lightning Address"
) %>
<% if Setting.lndhub_keysend_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :lndhub_public_key,
title: "Public key",
description: "The public key of the Lightning node used by LNDHub"
) %>
<% end %>
<% end %> <% end %>
</ul> </ul>

View File

@@ -63,6 +63,10 @@
</section> </section>
<section class="sm:flex-1 sm:pt-0"> <section class="sm:flex-1 sm:pt-0">
<h3>LDAP<h3>
<p>
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
</p>
<!-- <h3>Actions</h3> --> <!-- <h3>Actions</h3> -->
</section> </section>
</div> </div>

View File

@@ -1,33 +1,62 @@
<section> <section>
<h3>Profile</h3> <h3>Profile</h3>
<p class="mb-2"> <div class="mb-6">
<%= label :user_address, 'User address', class: 'font-bold' %> <p class="mb-2">
</p> <%= label :user_address, 'User address', class: 'font-bold' %>
<p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5"> </p>
<input type="text" id="user_address" class="grow" <p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
value=<%= @user.address %> disabled="disabled" <input type="text" id="user_address" class="grow"
data-clipboard-target="source" /> value=<%= @user.address %> disabled="disabled"
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0" data-clipboard-target="source" />
data-clipboard-target="trigger" data-action="clipboard#copy" <button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
title="Copy to clipboard"> data-clipboard-target="trigger" data-action="clipboard#copy"
<span class="content-initial"> title="Copy to clipboard">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %> <span class="content-initial">
</span> <%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
<span class="content-active hidden"> </span>
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %> <span class="content-active hidden">
</span> <%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</button> </span>
</p> </button>
<p class="text-sm text-gray-500"> </p>
Your user address for Chat and Lightning Network. <p class="text-sm text-gray-500">
</p> Your user address for Chat and Lightning Network.
</p>
</div>
<%= form_for(@user, url: setting_path(:profile), html: { :method => :put }) do |f| %> <%= form_for(@user, url: setting_path(:profile), html: { :method => :put }) do |f| %>
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Display name") do %> <%= render FormElements::FieldsetComponent.new(tag: "div", title: "Display name") do %>
<%= f.text_field :display_name, class: "w-full sm:w-3/5 mb-2" %> <%= f.text_field :display_name, class: "w-full sm:w-3/5" %>
<% if @validation_errors.present? && @validation_errors[:display_name].present? %> <% if @validation_errors.present? && @validation_errors[:display_name].present? %>
<p class="error-msg"><%= @validation_errors[:display_name].first %></p> <p class="error-msg mt-2"><%= @validation_errors[:display_name].first %></p>
<% end %> <% end %>
<% end %> <% end %>
<label class="block">
<p class="font-bold mb-1">
Avatar
</p>
<p class="text-gray-500">
Default profile picture
</p>
<div class="flex items-center gap-6">
<% if current_user.avatar.present? %>
<p class="flex-none">
<%= image_tag "data:image/jpeg;base64,#{current_user.avatar}", class: "h-24 w-24 rounded-lg" %>
</p>
<% end %>
<div class="grow">
<p class="mb-2">
<%= f.file_field :avatar, class: "" %>
<p class="text-sm text-gray-500">
JPEG or PNG image, not larger than 1 megabyte
</p>
<% if @validation_errors.present? && @validation_errors[:avatar].present? %>
<p class="error-msg mb-2"><%= @validation_errors[:avatar].first %></p>
<% end %>
</div>
</div>
</label>
<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" %>
</p> </p>

View File

@@ -1,63 +1,70 @@
<%= render SidenavLinkComponent.new(
level: 2,
name: "BTCPay",
path: admin_settings_services_path(params: { s: "btcpay" }),
text_icon: Setting.btcpay_enabled? ? "◉" : "○",
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_services_path(params: { s: "discourse" }), path: admin_settings_services_path(params: { s: "discourse" }),
icon: Setting.discourse_enabled? ? "check" : "x", text_icon: Setting.discourse_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "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_services_path(params: { s: "droneci" }), path: admin_settings_services_path(params: { s: "droneci" }),
icon: Setting.droneci_enabled? ? "check" : "x", text_icon: Setting.droneci_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "droneci" })), active: current_page?(admin_settings_services_path(params: { s: "droneci" })),
) %> ) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "ejabberd", name: "ejabberd",
path: admin_settings_services_path(params: { s: "ejabberd" }), path: admin_settings_services_path(params: { s: "ejabberd" }),
icon: Setting.ejabberd_enabled? ? "check" : "x", text_icon: Setting.ejabberd_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "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_services_path(params: { s: "gitea" }), path: admin_settings_services_path(params: { s: "gitea" }),
icon: Setting.gitea_enabled? ? "check" : "x", text_icon: Setting.gitea_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "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_services_path(params: { s: "lndhub" }), path: admin_settings_services_path(params: { s: "lndhub" }),
icon: Setting.lndhub_enabled? ? "check" : "x", text_icon: Setting.lndhub_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "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_services_path(params: { s: "mastodon" }), path: admin_settings_services_path(params: { s: "mastodon" }),
icon: Setting.mastodon_enabled? ? "check" : "x", text_icon: Setting.mastodon_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "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_services_path(params: { s: "mediawiki" }), path: admin_settings_services_path(params: { s: "mediawiki" }),
icon: Setting.mediawiki_enabled? ? "check" : "x", text_icon: Setting.mediawiki_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "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_services_path(params: { s: "nostr" }), path: admin_settings_services_path(params: { s: "nostr" }),
icon: Setting.nostr_enabled? ? "check" : "x", text_icon: Setting.nostr_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "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_services_path(params: { s: "remotestorage" }), path: admin_settings_services_path(params: { s: "remotestorage" }),
icon: Setting.remotestorage_enabled? ? "check" : "x", text_icon: Setting.remotestorage_enabled? ? "" : "",
active: current_page?(admin_settings_services_path(params: { s: "remotestorage" })), active: current_page?(admin_settings_services_path(params: { s: "remotestorage" })),
) %> ) %>

4
ci/Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
# syntax=docker/dockerfile:1
FROM guildeducation/rails:2.7.2-14.20.0
RUN apt-get update -qq && apt-get install -y --no-install-recommends ldap-utils libvips

View File

@@ -5,7 +5,7 @@ require "rails"
require "active_model/railtie" require "active_model/railtie"
require "active_job/railtie" require "active_job/railtie"
require "active_record/railtie" require "active_record/railtie"
# require "active_storage/engine" require "active_storage/engine"
require "action_controller/railtie" require "action_controller/railtie"
require "action_mailer/railtie" require "action_mailer/railtie"
require "action_mailbox/engine" require "action_mailbox/engine"

View File

@@ -1 +1 @@
yEs5CyuAbqphlDWgtw/YQvkPn+EN4ecen2dAjs7zvYErkRRWp99FinGlQIMe6NRkMLLLSIj2BwR/wlscn1kLpIfwGpxfSZ89srK3do6Mb5QogpxdUsnQB8qv5PTGRQFBcjM47s1Q5m0t+OKxGvOnLyKnQp+cVS2KFJMbSzQarW8wIZSz2gKArn9Ttk0kqUHMlJWNY7Yh6xIrrxlEalaTOVzPdtnF7u8Tobminu15eeWHMormMRz4dYSaDc6hUtfpdy1NzOHaeXIU9A9RY/iytxuIQNgcMAlcWbPe//rVk/unH2F8xqSOfed4h/nC08F/qq4z8va3kEXBSdW/G91aIDMu1mo0kX3YNibq8s25C/CfGpzw39ozJ9erTBH7hy6nfmxU6qZuWcTGDj3NOfKe/XIfDcpOjsqkT2IOFARrYodb67q23IuOufraK1/FD4LXu8l0S8/Oi0cqMjtPPs7tS0M1C3DrbmlEzGKETrHpmoKHqjA0rgOmK4ZZM9LeI+l8Z+fDpYcCak9fLGGxnjf+nKiYMSUtm9+1dwycG2lpBV6fbmIKHJWngO2jVGcycODkc525oUaAO4hdPMqrz1AdU3AzYmLJTxW3aZ4uL5NyEJ7TbUBC0HT7h2gEi/tUry4cfD2EsM9bCrCUNuMBrnPqd4r8AvORoqqYIw1IEsP0RgWa2+hfeG1QCjBRPFHQOcqo+W25CelivMe79qI08w0iC8S4hfOQO4QrmMgtd1BhcR+wVpVE3X9EJZi3Hl7z14hXcSic+gkswJMtVZcnJL4rmZ0iEW1mpqUuegsX5vB/4qPxiQyeB80pg8Q33shvUbixzSBkl6znmLSiIffsiDsGOsnuzfl/MUT+JBs3UswNt4tSp7nEwhUjKFHrZHrAJiGCdtIS6yDPGe3HfQv1JkQ+9A8zv88hRmzeIx2JyT/shtIqGo+4ZTJd5cma--Lij/n0+cpstyZD28--FOUhwW3y+0jdaYkKvG2xrg== tmI5vm7qZhaigr52jEBVWkRdj+EE+9OmPh3vWXC7kA/OHuuucpr7SodychuMkQDPLM0BLk88LFsqvRIR+mqnLWpRC+P9aeUFE6ohxSWzcAd7Y4sgxUD8zpCRPndrwTw0hxXXj1WZSYeWn4BoAB34aV+gYen2MajZF3a95hJGtS5yjgWxvLVkQQKqRDfykkfX6fCS0BPo5X7sT7m4xwCATD/D4219wajm5W3TIdkriHtwt28ZLspaRWA5e0UkzKf8+/Gaj2CrW7UWcvew8R93zQ5RA2/Sp3sDTVN+kLz9I9Q095lQC0ywCAEFYHeKmc2tjrzqRaAAWu06xmWLqGIg21G+A/UU9lUJOkIpxQACWoOfS2IoXR1nXhgXMopkz3aCBXDxKw554v4H2QyOceOsuRf2C685ibMqzQkKMmJ4tcbiOJL77DUc08JTjB8Dq4Ohr8sMzXbV/hATevjYoRP0XarLekqhLv90ZLuIVY16DwB0CzACeNBKeKbeLqJF51upRRWgi+gTbYpV04yUwnXdyssF8mydWocgihrTryBi8F6PsuhBGcaYdP+0yibnGxDCC4x2rupbBfMj2OIX7pYzgtIHB3Eo954Y+bCoggqbE/Qrb9VVXNMgtKgLt8EGWU2tg6wl9QicitIq87uLDAade93zTn6rmcKPywjMDo6jbVIs653ZdUhiKdHGdpnJccbgQ/iLSPB1umNnCeaEX5jM+K9zBvl7ZMCdSk1YIQ==--ekKumqLiSlVJNwMe--K/ecXmmMT1x+WnIXMbHBDw==

View File

@@ -70,4 +70,7 @@ Rails.application.configure do
# Allow requests from any IP # Allow requests from any IP
config.web_console.whiny_requests = false config.web_console.whiny_requests = false
# Store attachments on the local disk (in ./storage)
config.active_storage.service = :local
end end

View File

@@ -110,6 +110,10 @@ 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
# TODO make configurable
# Store attachments in S3-compatible back-end
config.active_storage.service = :local
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found). # the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true config.i18n.fallbacks = true

View File

@@ -51,4 +51,7 @@ Rails.application.configure do
} }
config.active_job.queue_adapter = :test config.active_job.queue_adapter = :test
# Store attachments on the local disk (in ./tmp)
config.active_storage.service = :test
end end

View File

@@ -54,7 +54,8 @@ Rails.application.routes.draw do
post 'webhooks/lndhub', to: 'webhooks#lndhub' post 'webhooks/lndhub', to: 'webhooks#lndhub'
namespace :api do namespace :api do
get 'kredits/onchain_btc_balance', to: 'kredits#onchain_btc_balance' get 'btcpay/onchain_btc_balance', to: 'btcpay#onchain_btc_balance'
get 'btcpay/lightning_btc_balance', to: 'btcpay#lightning_btc_balance'
end end
namespace :admin do namespace :admin do

View File

@@ -1,7 +1,7 @@
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local: local:
service: Disk service: Disk
root: <%= Rails.root.join("storage") %> root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>

View File

@@ -0,0 +1,57 @@
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :key ], unique: true
end
create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[primary_key_type, foreign_key_type]
end
end

View File

@@ -10,7 +10,35 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_05_23_120753) do ActiveRecord::Schema[7.0].define(version: 2023_09_06_073324) do
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
create_table "donations", force: :cascade do |t| create_table "donations", force: :cascade do |t|
t.integer "user_id" t.integer "user_id"
t.integer "amount_sats" t.integer "amount_sats"
@@ -94,5 +122,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_23_120753) do
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "remote_storage_authorizations", "users" add_foreign_key "remote_storage_authorizations", "users"
end end

View File

@@ -13,6 +13,7 @@ Sidekiq::Testing.inline! do
35.times do |n| 35.times do |n|
username = Faker::Name.unique.first_name.downcase username = Faker::Name.unique.first_name.downcase
email = Faker::Internet.unique.email email = Faker::Internet.unique.email
next if username.length < 3
CreateAccount.call( CreateAccount.call(
username: username, domain: "kosmos.org", email: email, username: username, domain: "kosmos.org", email: email,

View File

@@ -23,7 +23,7 @@ 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_services_url(params: { s: "discourse" })) expect(current_url).to eq(admin_settings_services_url(params: { s: "btcpay" }))
end end
scenario "View service settings" do scenario "View service settings" do

View File

@@ -54,6 +54,11 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do
context "when signed out" do context "when signed out" do
let(:user) { create :user } let(:user) { create :user }
before do
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
.with(user.password).and_return(true)
end
it "prefills the username field in the signin form" do it "prefills the username field in the signin form" do
visit new_rs_oauth_path(useraddress: user.address, visit new_rs_oauth_path(useraddress: user.address,
redirect_uri: "http://example.com", redirect_uri: "http://example.com",
@@ -62,5 +67,19 @@ RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do
expect(find("#user_cn").value).to eq(user.cn) expect(find("#user_cn").value).to eq(user.cn)
end end
it "redirects to the OAuth dialog after sign-in" do
auth_url = new_rs_oauth_url(useraddress: user.address,
redirect_uri: "http://example.com",
client_id: "http://example.com",
scope: "documents,[photos], contacts:r")
visit auth_url
fill_in "User", with: user.cn
fill_in "Password", with: user.password
click_button "Log in"
expect(current_url).to eq(auth_url)
end
end end
end end

View File

@@ -2,20 +2,19 @@ require 'rails_helper'
RSpec.describe 'Profile settings', type: :feature do RSpec.describe 'Profile settings', type: :feature do
let(:user) { create :user, cn: "mwahlberg" } let(:user) { create :user, cn: "mwahlberg" }
let(:avatar_base64) { File.read("#{Rails.root}/spec/fixtures/files/avatar-base64.txt") }
before do before do
login_as user, :scope => :user login_as user, :scope => :user
allow(user).to receive(:display_name).and_return("Mark")
allow_any_instance_of(User).to receive(:dn).and_return("cn=mwahlberg,ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, display_name: "Mark"
})
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
end end
feature "Update display name" do feature "Update display name" do
before do
allow(user).to receive(:display_name).and_return("Mark")
allow_any_instance_of(User).to receive(:dn).and_return("cn=mwahlberg,ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, display_name: "Mark"
})
end
scenario 'fails with validation error' do scenario 'fails with validation error' do
visit setting_path(:profile) visit setting_path(:profile)
fill_in 'Display name', with: "M" fill_in 'Display name', with: "M"
@@ -42,4 +41,59 @@ RSpec.describe 'Profile settings', type: :feature do
end end
end end
end end
feature "Update avatar" do
scenario "fails with validation error for wrong content type" do
visit setting_path(:profile)
attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/bitcoin.pdf"
click_button "Save"
expect(current_url).to eq(setting_url(:profile))
within ".error-msg" do
expect(page).to have_content("must be a JPEG or PNG file")
end
end
scenario "fails with validation error for file size too large" do
visit setting_path(:profile)
attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/fsociety-irc.png"
click_button "Save"
expect(current_url).to eq(setting_url(:profile))
within ".error-msg" do
expect(page).to have_content("file size is too large")
end
end
scenario 'works with valid JPG file' do
file_path = "#{Rails.root}/spec/fixtures/files/taipei.jpg"
expect_any_instance_of(LdapManager::UpdateAvatar).to receive(:replace_attribute)
.with(user.dn, :jpegPhoto, avatar_base64).and_return(true)
visit setting_path(:profile)
attach_file "Avatar", file_path
click_button "Save"
expect(current_url).to eq(setting_url(:profile))
within ".flash-msg" do
expect(page).to have_content("Settings saved")
end
end
scenario 'works with valid PNG file' do
file_path = "#{Rails.root}/spec/fixtures/files/bender.png"
expect(LdapManager::UpdateAvatar).to receive(:call).and_return(true)
visit setting_path(:profile)
attach_file "Avatar", file_path
click_button "Save"
expect(current_url).to eq(setting_url(:profile))
within ".flash-msg" do
expect(page).to have_content("Settings saved")
end
end
end
end end

1
spec/fixtures/files/avatar-base64.txt vendored Normal file

File diff suppressed because one or more lines are too long

BIN
spec/fixtures/files/bender.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
spec/fixtures/files/bitcoin.pdf vendored Normal file

Binary file not shown.

BIN
spec/fixtures/files/fsociety-irc.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
spec/fixtures/files/taipei.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View File

@@ -0,0 +1,103 @@
require 'rails_helper'
require 'webmock/rspec'
RSpec.describe "/api/btcpay", type: :request do
describe "GET /onchain_btc_balance" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/payment-methods/onchain/BTC/wallet")
.to_return(status: 200, headers: {}, body: {
balance: 0.91108606,
unconfirmedBalance: 0,
confirmedBalance: 0.91108606
}.to_json)
end
it "returns a formatted result for the onchain wallet balance" do
get api_btcpay_onchain_btc_balance_path
expect(response).to have_http_status(:ok)
res = JSON.parse(response.body)
expect(res["balance"]).to eq(91108606)
expect(res["unconfirmed_balance"]).to eq(0)
expect(res["confirmed_balance"]).to eq(91108606)
end
context "upstream request error" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/payment-methods/onchain/BTC/wallet")
.to_return(status: 500, headers: {}, body: "")
end
it "returns a formatted error" do
get api_btcpay_onchain_btc_balance_path
expect(response).to have_http_status(:server_error)
res = JSON.parse(response.body)
expect(res["error"]).not_to be_nil
end
end
context "feature disabled" do
before do
Setting.btcpay_publish_wallet_balances = false
end
it "returns a 404 status" do
get api_btcpay_onchain_btc_balance_path
expect(response).to have_http_status(:not_found)
end
end
end
describe "GET /lightning_btc_balance" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/lightning/BTC/balance")
.to_return(status: 200, headers: {}, body: {
offchain: {
local: 4200000000
},
}.to_json)
end
it "returns a formatted result for the onchain wallet balance" do
get api_btcpay_lightning_btc_balance_path
expect(response).to have_http_status(:ok)
res = JSON.parse(response.body)
expect(res["balance"]).to eq(4200000)
end
context "upstream request error" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/lightning/BTC/balance")
.to_return(status: 500, headers: {}, body: "")
end
it "returns a formatted error" do
get api_btcpay_lightning_btc_balance_path
expect(response).to have_http_status(:server_error)
res = JSON.parse(response.body)
expect(res["error"]).not_to be_nil
end
end
context "feature disabled" do
before do
Setting.btcpay_publish_wallet_balances = false
end
it "returns a 404 status" do
get api_btcpay_lightning_btc_balance_path
expect(response).to have_http_status(:not_found)
end
end
end
end

View File

@@ -1,43 +0,0 @@
require 'rails_helper'
require 'webmock/rspec'
RSpec.describe "/api/kredits", type: :request do
describe "GET /onchain_btc_balance" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/payment-methods/onchain/BTC/wallet")
.to_return(status: 200, headers: {}, body: {
balance: 0.91108606,
unconfirmedBalance: 0,
confirmedBalance: 0.91108606
}.to_json)
end
it "returns a formatted result for the onchain wallet balance" do
get api_kredits_onchain_btc_balance_path
expect(response).to have_http_status(:ok)
res = JSON.parse(response.body)
expect(res["balance"]).to eq(0.91108606)
expect(res["unconfirmed_balance"]).to eq(0)
expect(res["confirmed_balance"]).to eq(0.91108606)
end
context "upstream request error" do
before do
stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/payment-methods/onchain/BTC/wallet")
.to_return(status: 500, headers: {}, body: "")
end
it "returns a formatted error" do
get api_kredits_onchain_btc_balance_path
expect(response).to have_http_status(:server_error)
res = JSON.parse(response.body)
expect(res["error"]).not_to be_nil
end
end
end
end

View File

@@ -68,6 +68,7 @@ RSpec.describe "Webhooks", type: :request do
context "notification preference set to 'xmpp'" do context "notification preference set to 'xmpp'" do
before do before do
Setting.xmpp_notifications_from_address = "botka@kosmos.org"
user.update! preferences: { lightning_notify_sats_received: "xmpp" } user.update! preferences: { lightning_notify_sats_received: "xmpp" }
post "/webhooks/lndhub", params: payload.to_json post "/webhooks/lndhub", params: payload.to_json
end end
@@ -78,7 +79,7 @@ RSpec.describe "Webhooks", type: :request do
msg = enqueued_jobs.first["arguments"].first msg = enqueued_jobs.first["arguments"].first
expect(msg["type"]).to eq("normal") expect(msg["type"]).to eq("normal")
expect(msg["from"]).to eq("kosmos.org") expect(msg["from"]).to eq("botka@kosmos.org")
expect(msg["to"]).to eq(user.address) expect(msg["to"]).to eq(user.address)
expect(msg["subject"]).to eq("Sats received!") expect(msg["subject"]).to eq("Sats received!")
expect(msg["body"]).to match(/^12,300 sats received/) expect(msg["body"]).to match(/^12,300 sats received/)