Merge pull request 'Add lndhub admin panel, quick stats for admin pages' (#80) from feature/admin_stats into master
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #80
Reviewed-by: bumi <bumi@noreply.kosmos.org>
This commit is contained in:
Râu Cao 2023-02-23 07:43:15 +00:00
commit 84337c3a7d
45 changed files with 604 additions and 129 deletions

View File

@ -28,7 +28,7 @@ steps:
- bundle install --jobs=3 --retry=3
- yarn install
- rake css:build
- rake spec
- bundle exec rspec
- name: rebuild-cache
image: drillster/drone-volume-cache
volumes:

View File

@ -3,11 +3,17 @@ LDAP_PORT=389
LDAP_ADMIN_PASSWORD=passthebutter
LDAP_SUFFIX="dc=kosmos,dc=org"
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
BTCPAY_API_URL='http://localhost:23001/api/v1'
LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
LNDHUB_ADMIN_UI=true
LNDHUB_PG_HOST=localhost
LNDHUB_PG_PORT=5432
LNDHUB_PG_DATABASE=lndhub
LNDHUB_PG_USERNAME=lndhub
LNDHUB_PG_PASSWORD=''

View File

@ -1,9 +0,0 @@
EJABBERD_API_URL='https://xmpp.kosmos.org:5443/api'
BTCPAY_API_URL='http://10.1.1.163:23001/api/v1'
LNDHUB_LEGACY_API_URL='http://10.1.1.163:3026'
LNDHUB_API_URL='http://10.1.1.163:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
WEBHOOKS_ALLOWED_IPS='10.1.1.163'

View File

@ -38,6 +38,7 @@ gem 'net-ldap'
# Utilities
gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3'
# HTTP requests
gem 'faraday'

View File

@ -206,6 +206,9 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.3)
loofah (~> 2.3)
rails-settings-cached (2.8.3)
activerecord (>= 5.0.0)
railties (>= 5.0.0)
railties (7.0.4)
actionpack (= 7.0.4)
activesupport (= 7.0.4)
@ -327,6 +330,7 @@ DEPENDENCIES
pg (~> 1.2.3)
puma (~> 4.1)
rails (~> 7.0.2)
rails-settings-cached (~> 2.8.3)
rqrcode (~> 2.0)
rspec-rails
sidekiq (< 7)

View File

@ -1,6 +1,6 @@
@layer components {
input[type=text], input[type=email], input[type=password],
input[type=number], select {
input[type=number], select, textarea {
@apply mt-1 rounded-md bg-gray-100 focus:bg-white
border-transparent focus:border-transparent focus:ring-2
focus:ring-blue-600 focus:ring-opacity-75;

View File

@ -0,0 +1,3 @@
<dl class="grid grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-12">
<%= content %>
</dl>

View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
class QuickstatsContainerComponent < ViewComponent::Base
end

View File

@ -0,0 +1,18 @@
<div class="">
<dt class="mb-2 text-gray-500">
<%= @title %>
</dt>
<dd>
<% if @type == :number %>
<span class="text-2xl"><%= number_with_delimiter @value %></span>
<% else %>
<span class="text-2xl"><%= @value %></span>
<% end %>
<% if @unit %>
<span><%= @unit %></span>
<% end %>
<% if @meta %>
<span class="text-gray-500"><%= @meta %></span>
<% end %>
</dd>
</div>

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class QuickstatsItemComponent < ViewComponent::Base
def initialize(type:, title:, value:, unit: nil, meta: nil, icon_name: nil, icon_color_class: nil)
@type = type
@title = title
@value = value
@unit = unit
@meta = meta
@icon_name = icon_name
@icon_color_class = icon_color_class
end
end

View File

@ -7,10 +7,14 @@
<div class="md:col-span-4 mt-4 md:mt-0">
<p class="font-mono md:text-right mb-0 p-4 border border-gray-300 rounded-lg overflow-hidden">
<% if @balance %>
<span class="text-xl"><%= number_with_delimiter @balance %> sats</span><br>
<span class="text-2xl"><%= number_with_delimiter @balance %></span>
<span class="text-xl">sats</span>
<br>
<span class="text-sm text-gray-500">Available balance</span>
<% else %>
<span class="text-xl">n/a sats</span><br>
<span class="text-2xl">n/a</span>
<span class="text-xl">sats</span>
<br>
<span class="text-sm text-gray-500">Balance unavailable</span>
<% end %>
</p>

View File

@ -5,7 +5,11 @@ class Admin::DonationsController < Admin::BaseController
# GET /donations
# GET /donations.json
def index
@donations = Donation.all
@donations = Donation.all.order('created_at desc')
@stats = {
overall_sats: @donations.all.sum("amount_sats"),
donor_count: Donation.distinct.count(:user_id)
}
end
# GET /donations/1
@ -29,7 +33,11 @@ class Admin::DonationsController < Admin::BaseController
respond_to do |format|
if @donation.save
format.html { redirect_to admin_donation_url(@donation), notice: 'Donation was successfully created.' }
format.html do
redirect_to admin_donation_url(@donation), flash: {
success: 'Donation was successfully created.'
}
end
format.json { render :show, status: :created, location: @donation }
else
format.html { render :new }
@ -43,7 +51,11 @@ class Admin::DonationsController < Admin::BaseController
def update
respond_to do |format|
if @donation.update(donation_params)
format.html { redirect_to admin_donation_url(@donation), notice: 'Donation was successfully updated.' }
format.html do
redirect_to admin_donation_url(@donation), flash: {
success: 'Donation was successfully updated.'
}
end
format.json { render :show, status: :ok, location: @donation }
else
format.html { render :edit }
@ -57,7 +69,10 @@ class Admin::DonationsController < Admin::BaseController
def destroy
@donation.destroy
respond_to do |format|
format.html { redirect_to admin_donations_url, notice: 'Donation was successfully destroyed.' }
format.html do redirect_to admin_donations_url, flash: {
success: 'Donation was successfully destroyed.'
}
end
format.json { head :no_content }
end
end

View File

@ -1,8 +1,11 @@
class Admin::InvitationsController < Admin::BaseController
def index
@current_section = :invitations
@invitations_unused_count = Invitation.unused.count
@users_with_referrals_count = Invitation.used.distinct.count(:user_id)
@invitations_used = Invitation.used.order('used_at desc')
@stats = {
available: Invitation.unused.count,
accepted: @invitations_used.length,
users_with_referrals: Invitation.used.distinct.count(:user_id)
}
end
end

View File

@ -2,43 +2,18 @@ class Admin::LdapUsersController < Admin::BaseController
before_action :set_current_section
def index
attributes = %w{dn cn uid mail admin}
filter = Net::LDAP::Filter.eq("uid", "*")
ldap = LdapService.new
@ou = params[:ou] || "kosmos.org"
treebase = "ou=#{@ou},cn=users,dc=kosmos,dc=org"
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] }
@entries = entries.collect do |e|
{
uid: e.uid.first,
mail: e.try(:mail) ? e.mail.first : nil,
admin: e.try(:admin) ? 'admin' : nil
# password: e.userpassword.first
}
end
# ldap_client.get_operation_result
@orgs = ldap.fetch_organizations
@entries = ldap.fetch_users(ou: @ou)
@stats = {
users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: @ou).pending.count
}
end
private
def ldap_client
ldap_client ||= Net::LDAP.new host: ldap_config['host'],
port: ldap_config['port'],
# encryption: ldap_config['ssl'],
auth: {
method: :simple,
username: ldap_config['admin_user'],
password: ldap_config['admin_password']
}
end
def ldap_config
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
end
def set_current_section
@current_section = :ldap_users
end

View File

@ -0,0 +1,21 @@
class Admin::LightningController < Admin::BaseController
before_action :check_feature_enabled
def index
@current_section = :lightning
@users = User.pluck(:cn, :ou, :ln_account)
@accounts = LndhubAccount.with_balances.order(balance: :desc).to_a
@ln = {}
@ln[:current_balance] = LndhubAccount.current.joins(:ledgers).sum("account_ledgers.amount")
@ln[:users_with_sats] = @accounts.length
end
def check_feature_enabled
if !Setting.lndhub_admin_enabled?
flash[:alert] = "Lightning Admin UI not enabled"
redirect_to admin_root_path and return
end
end
end

View File

@ -0,0 +1,38 @@
class Admin::Settings::RegistrationsController < Admin::SettingsController
def index
end
def create
@errors = ActiveModel::Errors.new(Setting.new)
setting_params.keys.each do |key|
next if setting_params[key].nil?
setting = Setting.new(var: key)
setting.value = setting_params[key].strip
unless setting.valid?
@errors.merge!(setting.errors)
end
end
if @errors.any?
render :index
end
setting_params.keys.each do |key|
Setting.send("#{key}=", setting_params[key].strip) unless setting_params[key].nil?
end
redirect_to admin_settings_registrations_path, flash: {
success: "Settings saved"
}
end
private
def setting_params
params.require(:setting).permit(:reserved_usernames)
end
end

View File

@ -0,0 +1,9 @@
class Admin::Settings::ServicesController < Admin::SettingsController
def index
end
def update
end
end

View File

@ -0,0 +1,12 @@
class Admin::SettingsController < Admin::BaseController
before_action :set_current_section
def index
end
private
def set_current_section
@current_section = :settings
end
end

View File

@ -5,7 +5,7 @@ class InvitationsController < ApplicationController
# GET /invitations
def index
@invitations_unused = current_user.invitations.unused
@invitations_used = current_user.invitations.used
@invitations_used = current_user.invitations.used.order('used_at desc')
@current_section = :invitations
end
@ -27,7 +27,10 @@ class InvitationsController < ApplicationController
respond_to do |format|
if @invitation.save
format.html { redirect_to @invitation, notice: 'Invitation was successfully created.' }
format.html do redirect_to @invitation, flash: {
success: 'Invitation was successfully created.'
}
end
format.json { render :show, status: :created, location: @invitation }
else
format.html { render :new }

View File

@ -0,0 +1,21 @@
class LndhubAccount < LndhubBase
self.table_name = "accounts"
self.inheritance_column = :_type_disabled
has_many :ledgers, class_name: "LndhubAccountLedger",
foreign_key: "account_id"
belongs_to :user, class_name: "LndhubUser",
foreign_key: "user_id"
scope :current, -> { where(type: "current") }
scope :outgoing, -> { where(type: "outgoing") }
scope :incoming, -> { where(type: "incoming") }
scope :fees, -> { where(type: "fees") }
scope :with_balances, -> {
current.joins(:user).joins(:ledgers)
.group("accounts.id", "users.login")
.select("accounts.id, users.login, SUM(account_ledgers.amount) AS balance")
}
end

View File

@ -0,0 +1,3 @@
class LndhubAccountLedger < LndhubBase
self.table_name = "account_ledgers"
end

View File

@ -0,0 +1,4 @@
class LndhubBase < ActiveRecord::Base
self.abstract_class = true
establish_connection :lndhub
end

15
app/models/lndhub_user.rb Normal file
View File

@ -0,0 +1,15 @@
class LndhubUser < LndhubBase
self.table_name = "users"
self.inheritance_column = :_type_disabled
has_many :accounts, class_name: "LndhubAccount",
foreign_key: "user_id"
belongs_to :user, class_name: "User",
primary_key: "ln_account",
foreign_key: "login"
def balance
accounts.current.first.ledgers.sum("account_ledgers.amount")
end
end

11
app/models/setting.rb Normal file
View File

@ -0,0 +1,11 @@
# RailsSettings Model
class Setting < RailsSettings::Base
cache_prefix { "v1" }
field :reserved_usernames, type: :array, default: %w[
account accounts admin donations mail webmaster support
]
field :lndhub_enabled, default: (ENV["LNDHUB_API_URL"].present?.to_s || "false"), type: :boolean
field :lndhub_admin_enabled, default: (ENV["LNDHUB_ADMIN_UI"] || "false"), type: :boolean
end

View File

@ -5,11 +5,28 @@ class User < ApplicationRecord
has_many :invitations, dependent: :destroy
has_many :donations, dependent: :nullify
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
primary_key: "ln_account", foreign_key: "login"
has_many :accounts, through: :lndhub_user
validates_uniqueness_of :cn
validates_length_of :cn, :minimum => 3
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
if: Proc.new{ |u| u.cn.present? },
message: "is invalid. Please use only letters, numbers and -"
validates_format_of :cn, without: /\A-/,
if: Proc.new{ |u| u.cn.present? },
message: "is invalid. Usernames need to start with a letter."
validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i,
message: "has already been taken"
validates_uniqueness_of :email
validates :email, email: true
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :pending, -> { where(confirmed_at: nil) }
lockbox_encrypts :ln_login
lockbox_encrypts :ln_password

View File

@ -1,7 +1,12 @@
<%= render HeaderComponent.new(title: "Admin Panel") %>
<%= render MainSimpleComponent.new do %>
<p class="text-center">
With great power comes great responsibility.
</p>
<div class="text-center">
<p class="my-12 inline-flex align-center items-center">
<%= image_tag("/img/illustrations/undraw_vault_re_s4my.svg", class: 'h-48') %>
</p>
<p class="text-gray-500">
With great power comes great responsibility.
</p>
</div>
<% end %>

View File

@ -1,7 +1,26 @@
<%= render HeaderComponent.new(title: "Donations") %>
<%= render MainSimpleComponent.new do %>
<section>
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Overall',
value: @stats[:overall_sats],
unit: 'sats'
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Donors',
value: @stats[:donor_count],
meta: "/ #{User.count} users"
) %>
<% end %>
</section>
<section>
<% if @donations.any? %>
<h3>Recent Donations</h3>
<table>
<thead>
<tr>
@ -11,7 +30,7 @@
<th class="text-right">in USD</th>
<th class="pl-2">Public name</th>
<th>Date</th>
<th colspan="3"></th>
<th></th>
</tr>
</thead>
@ -23,11 +42,13 @@
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
<td class="pl-2"><%= donation.public_name %></td>
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d") : "" %></td>
<td><%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %></td>
<td><%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %></td>
<td><%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %></td>
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td>
<td class="text-right">
<%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
<%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>
</td>
</tr>
<% end %>
</tbody>
@ -37,6 +58,7 @@
No donations yet.
</p>
<% end %>
</section>
<p class="mt-12">
<%= link_to 'Record an out-of-system donation', new_admin_donation_path, class: 'btn-md btn-gray' %>

View File

@ -2,16 +2,28 @@
<%= render MainSimpleComponent.new do %>
<section>
<p>
There are currently <strong><%= @invitations_unused_count %>
unused invitations</strong> available to existing users.
<strong><%= @users_with_referrals_count %> users</strong> have successfully
invited new users.
</p>
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Available',
value: @stats[:available],
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Accepted',
value: @stats[:accepted],
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Users with referrals',
value: @stats[:users_with_referrals],
meta: "/ #{User.count}"
) %>
<% end %>
</section>
<% if @invitations_used.any? %>
<section>
<h3>Accepted (<%= @invitations_used.length %>)</h3>
<h3>Recently Accepted</h3>
<table>
<thead>
<tr>

View File

@ -1,34 +1,54 @@
<%= render HeaderComponent.new(title: "LDAP Users: #{@ou}") %>
<%= render MainSimpleComponent.new do %>
<h3 class="hidden">Domains</h3>
<ul class="mb-10">
<li class="inline-block">
<%= link_to 'kosmos.org', admin_ldap_users_path, class: "ks-text-link" %>
</li>
<li class="inline-block ml-6">
<%= link_to '5apps.com', admin_ldap_users_path(ou: '5apps.com'), class: "ks-text-link" %>
</li>
</ul>
<section>
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Confirmed',
value: @stats[:users_confirmed],
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Pending',
value: @stats[:users_pending],
) %>
<% end %>
</section>
<table>
<thead>
<tr>
<th>UID</th>
<th>E-Mail</th>
<th>Admin</th>
<!-- <th>Password</th> -->
</tr>
</thead>
<tbody>
<% @entries.each do |entry| %>
<tr>
<td><%= entry[:uid] %></td>
<td><%= entry[:mail] %></td>
<td><%= entry[:admin] %></td>
<!-- <td><%= entry[:password] %></td> -->
</tr>
<% end %>
</tbody>
</table>
<% if @orgs.length > 1 %>
<section>
<h3 class="hidden">Domains</h3>
<ul>
<% @orgs.each do |org| %>
<li class="inline-block">
<%= link_to org[:ou], admin_ldap_users_path(ou: org[:ou]), class: "ks-text-link" %>
</li>
<% end %>
</ul>
</section>
<% end %>
<section>
<table>
<thead>
<tr>
<th>UID</th>
<th>E-Mail</th>
<th>Admin</th>
<!-- <th>Password</th> -->
</tr>
</thead>
<tbody>
<% @entries.each do |entry| %>
<tr>
<td><%= entry[:uid] %></td>
<td><%= entry[:mail] %></td>
<td><%= entry[:admin] %></td>
<!-- <td><%= entry[:password] %></td> -->
</tr>
<% end %>
</tbody>
</table>
</section>
<% end %>

View File

@ -0,0 +1,48 @@
<%= render HeaderComponent.new(title: "Lightning Network") %>
<%= render MainSimpleComponent.new do %>
<section>
<%= render QuickstatsContainerComponent.new do %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Current user balance',
value: @ln[:current_balance],
unit: 'sats'
) %>
<%= render QuickstatsItemComponent.new(
type: :number,
title: 'Users with sats',
value: @ln[:users_with_sats],
meta: "/ #{User.count}"
) %>
<% end %>
</section>
<section>
<h3>Accounts</h3>
<table>
<thead>
<tr>
<th>LN Account</th>
<th>User</th>
<th>Balance</th>
</tr>
</thead>
<tbody>
<% @accounts.each do |account| %>
<tr>
<td class="font-mono">
<%= account.login %>
</td>
<td>
<% if user = @users.find{ |u| u[2] == account.login } %>
<%= "#{user[0]}@#{user[1]}" %>
<% end %>
</td>
<td><%= number_with_delimiter account.balance.to_i.to_s %></td>
</tr>
<% end %>
</tbody>
</table>
</section>
<% end %>

View File

@ -0,0 +1,37 @@
<%= render HeaderComponent.new(title: "Settings") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
<%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %>
<section>
<h3>Registrations</h3>
<% if @errors && @errors.any? %>
<div>
<ul>
<% @errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<label class="block">
<p class="font-bold mb-1">Reserved usernames</p>
<p class="text-gray-500">
These usernames cannot be registered as accounts:
</p>
<%= f.text_area :reserved_usernames,
value: Setting.reserved_usernames.join("\n"),
class: "h-44 mb-2" %>
<p class="mb-0 text-sm text-gray-500">
One username per line
</p>
</label>
</section>
<section>
<p class="mb-0 pt-6 border-t border-gray-200">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p>
</section>
<% end %>
<% end %>

View File

@ -0,0 +1,39 @@
<%= render HeaderComponent.new(title: "Settings") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
<section>
<h3>Lightning Network</h3>
<%= form_for(Setting.new, url: admin_settings_services_path) do |f| %>
<% if @errors && @errors.any? %>
<div>
<ul>
<% @errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<ul role="list" class="mt-2 divide-y divide-gray-200">
<li class="flex items-center justify-between py-6">
<div class="flex flex-col">
<label class="font-bold mb-1">Enable LNDHub integration</label>
<p class="text-gray-500 mb-0">LNDHub configuration present and wallet features enabled</p>
</div>
<%= f.check_box :lndhub_enabled, checked: Setting.lndhub_enabled?,
disabled: true,
class: "relative ml-4 inline-flex flex-shrink-0" %>
</li>
<li class="flex items-center justify-between py-6">
<div class="flex flex-col">
<label class="font-bold mb-1">Enable LNDHub admin panel</label>
<p class="text-gray-500 mb-0">LNDHub database configuration present and admin panel enabled</p>
</div>
<%= f.check_box :lndhub_admin_enabled, checked: Setting.lndhub_admin_enabled?,
disabled: true,
class: "relative ml-4 inline-flex flex-shrink-0" %>
</li>
</ul>
<% end %>
</section>
<% end %>

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-grid"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-grid <%= custom_class %>"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>

Before

Width:  |  Height:  |  Size: 404 B

After

Width:  |  Height:  |  Size: 425 B

View File

@ -4,10 +4,10 @@
<section>
<h3>Password</h3>
<p class="mb-8">Use the following button to request an email with a password reset link:</p>
<p>
<%= form_with(url: settings_reset_password_path, method: :post) do %>
<%= form_with(url: settings_reset_password_path, method: :post) do %>
<p>
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
<% end %>
</p>
</p>
<% end %>
</section>
<% end %>

View File

@ -1,8 +1,14 @@
<%= link_to "Dashboard", admin_root_path,
class: main_nav_class(@current_section, :dashboard) %>
<%= link_to "Users", admin_ldap_users_path,
class: main_nav_class(@current_section, :ldap_users) %>
<%= link_to "Invitations", admin_invitations_path,
class: main_nav_class(@current_section, :invitations) %>
<%= link_to "Donations", admin_donations_path,
class: main_nav_class(@current_section, :donations) %>
<%= link_to "LDAP Users", admin_ldap_users_path,
class: main_nav_class(@current_section, :ldap_users) %>
<% if Setting.lndhub_admin_enabled? %>
<%= link_to "Lightning", admin_lightning_path,
class: main_nav_class(@current_section, :lightning) %>
<% end %>
<%= link_to "Settings", admin_settings_registrations_path,
class: main_nav_class(@current_section, :settings) %>

View File

@ -0,0 +1,11 @@
<%= render SidenavLinkComponent.new(
name: "Registrations", path: admin_settings_registrations_path, icon: "user",
active: current_page?(admin_settings_registrations_path)
) %>
<%= render SidenavLinkComponent.new(
name: "Services", path: admin_settings_services_path, icon: "grid",
active: current_page?(admin_settings_services_path)
) %>
<%= render SidenavLinkComponent.new(
name: "Security", path: "#", icon: "shield", disabled: true
) %>

View File

@ -1,30 +1,49 @@
# SQLite. Versions 3.8.0 and up are supported.
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %>
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
primary:
<<: *default
database: db/development.sqlite3
lndhub:
<<: *default
adapter: postgresql
database_tasks: false
host: <%= ENV["LNDHUB_PG_HOST"] || 'localhost' %>
port: <%= ENV["LNDHUB_PG_PORT"] || 5432 %>
database: <%= ENV["LNDHUB_PG_DATABASE"] || 'lndhub' %>
username: <%= ENV["LNDHUB_PG_USERNAME"] || 'lndhub' %>
password: <%= ENV["LNDHUB_PG_PASSWORD"] %>
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: db/test.sqlite3
primary:
<<: *default
database: db/test.sqlite3
lndhub:
<<: *default
database_tasks: false
database: db/test.lndhub.sqlite3
production:
<<: *default
adapter: postgresql
database: akkounts
port: 5432
host: <%= Rails.application.credentials.postgres[:host] rescue nil %>
username: <%= Rails.application.credentials.postgres[:username] rescue nil %>
password: <%= Rails.application.credentials.postgres[:password] rescue nil %>
primary:
<<: *default
adapter: postgresql
database: akkounts
port: 5432
host: <%= Rails.application.credentials.postgres[:host] rescue nil %>
username: <%= Rails.application.credentials.postgres[:username] rescue nil %>
password: <%= Rails.application.credentials.postgres[:password] rescue nil %>
lndhub:
<<: *default
adapter: postgresql
database_tasks: false
host: <%= ENV["LNDHUB_PG_HOST"] || 'localhost' %>
port: <%= ENV["LNDHUB_PG_PORT"] || 5432 %>
database: <%= ENV["LNDHUB_PG_DATABASE"] || 'lndhub' %>
username: <%= ENV["LNDHUB_PG_USERNAME"] || 'lndhub' %>
password: <%= ENV["LNDHUB_PG_PASSWORD"] %>

View File

@ -39,9 +39,15 @@ Rails.application.routes.draw do
namespace :admin do
root to: 'dashboard#index'
get 'invitations', to: 'invitations#index'
get 'ldap_users', to: 'ldap_users#index'
get 'invitations', to: 'invitations#index'
resources :donations
get 'lightning', to: 'lightning#index'
namespace :settings do
resources 'registrations', only: ['index', 'create']
resources 'services', only: ['index', 'create']
end
end
authenticate :user, ->(user) { user.is_admin? } do

View File

@ -0,0 +1,15 @@
class CreateSettings < ActiveRecord::Migration[7.0]
def self.up
create_table :settings do |t|
t.string :var, null: false
t.text :value, null: true
t.timestamps
end
add_index :settings, %i(var), unique: true
end
def self.down
drop_table :settings
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_01_11_113139) do
ActiveRecord::Schema[7.0].define(version: 2023_02_17_084310) do
create_table "donations", force: :cascade do |t|
t.integer "user_id"
t.integer "amount_sats"
@ -34,6 +34,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_01_11_113139) do
t.index ["user_id"], name: "index_invitations_on_user_id"
end
create_table "settings", force: :cascade do |t|
t.string "var", null: false
t.text "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["var"], name: "index_settings_on_var", unique: true
end
create_table "users", force: :cascade do |t|
t.string "cn"
t.string "ou"

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -5,7 +5,7 @@ RSpec.describe WalletSummaryComponent, type: :component do
expect(
render_inline(described_class.new(balance: 2301000)) {}.css("section").to_html
).to include(
"2,301,000 sats"
"2,301,000"
)
end
end

View File

@ -4,7 +4,9 @@ RSpec.describe 'Admin dashboard', type: :feature do
let(:user) { create :user }
before do
allow(user).to receive(:is_admin?).and_return(true)
allow(Devise::LDAP::Adapter).to receive(:get_ldap_param)
.with(user.cn, :admin).and_return(["true"])
login_as user, :scope => :user
end

View File

@ -0,0 +1,21 @@
require 'rails_helper'
RSpec.describe 'Admin/global settings', type: :feature do
let(:user) { create :user }
before do
allow(Devise::LDAP::Adapter).to receive(:get_ldap_param)
.with(user.cn, :admin).and_return(["true"])
login_as user, :scope => :user
end
scenario 'Update reserved usernames' do
visit admin_settings_registrations_path
expect(Setting.reserved_usernames).not_to include(['Kosmos', 'Kredits'])
fill_in 'Reserved usernames', with: "Kosmos\nKredits"
click_button "Save"
expect(Setting.reserved_usernames).to eq(['Kosmos', 'Kredits'])
end
end

View File

@ -71,6 +71,12 @@ RSpec.describe "Signup", type: :feature do
fill_in "user_cn", with: "t"
click_button "Continue"
expect(page).to have_content("Username is too short")
fill_in "user_cn", with: "-tony"
click_button "Continue"
expect(page).to have_content("Username is invalid")
fill_in "user_cn", with: "$atoshi"
click_button "Continue"
expect(page).to have_content("Username is invalid")
fill_in "user_cn", with: "jimmy"
click_button "Continue"
expect(page).to have_content("Username has already been taken")
@ -103,5 +109,11 @@ RSpec.describe "Signup", type: :feature do
expect(page).to have_content("confirm your address")
end
end
scenario "Reserved usernames" do
fill_in "user_cn", with: "accounts"
click_button "Continue"
expect(page).to have_content("Username has already been taken")
end
end
end