diff --git a/.drone.yml b/.drone.yml index ed52d05..8694c49 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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: diff --git a/.env.example b/.env.example index cd29496..c63d0bb 100644 --- a/.env.example +++ b/.env.example @@ -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='' diff --git a/.env.production b/.env.production deleted file mode 100644 index d99e4fe..0000000 --- a/.env.production +++ /dev/null @@ -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' diff --git a/Gemfile b/Gemfile index 8824e1b..169f39e 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,7 @@ gem 'net-ldap' # Utilities gem "rqrcode", "~> 2.0" +gem 'rails-settings-cached', '~> 2.8.3' # HTTP requests gem 'faraday' diff --git a/Gemfile.lock b/Gemfile.lock index d80ab81..5a658b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/stylesheets/components/forms.css b/app/assets/stylesheets/components/forms.css index 6c68083..61c7b1e 100644 --- a/app/assets/stylesheets/components/forms.css +++ b/app/assets/stylesheets/components/forms.css @@ -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; diff --git a/app/components/quickstats_container_component.html.erb b/app/components/quickstats_container_component.html.erb new file mode 100644 index 0000000..466f09c --- /dev/null +++ b/app/components/quickstats_container_component.html.erb @@ -0,0 +1,3 @@ +
+ <%= content %> +
diff --git a/app/components/quickstats_container_component.rb b/app/components/quickstats_container_component.rb new file mode 100644 index 0000000..ac229b7 --- /dev/null +++ b/app/components/quickstats_container_component.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class QuickstatsContainerComponent < ViewComponent::Base +end diff --git a/app/components/quickstats_item_component.html.erb b/app/components/quickstats_item_component.html.erb new file mode 100644 index 0000000..4d27fb6 --- /dev/null +++ b/app/components/quickstats_item_component.html.erb @@ -0,0 +1,18 @@ +
+
+ <%= @title %> +
+
+ <% if @type == :number %> + <%= number_with_delimiter @value %> + <% else %> + <%= @value %> + <% end %> + <% if @unit %> + <%= @unit %> + <% end %> + <% if @meta %> + <%= @meta %> + <% end %> +
+
diff --git a/app/components/quickstats_item_component.rb b/app/components/quickstats_item_component.rb new file mode 100644 index 0000000..a93b79e --- /dev/null +++ b/app/components/quickstats_item_component.rb @@ -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 diff --git a/app/components/wallet_summary_component.html.erb b/app/components/wallet_summary_component.html.erb index b02e0d8..dc510de 100644 --- a/app/components/wallet_summary_component.html.erb +++ b/app/components/wallet_summary_component.html.erb @@ -7,10 +7,14 @@

<% if @balance %> - <%= number_with_delimiter @balance %> sats
+ <%= number_with_delimiter @balance %> + sats +
Available balance <% else %> - n/a sats
+ n/a + sats +
Balance unavailable <% end %>

diff --git a/app/controllers/admin/donations_controller.rb b/app/controllers/admin/donations_controller.rb index e6c37cc..ffbae14 100644 --- a/app/controllers/admin/donations_controller.rb +++ b/app/controllers/admin/donations_controller.rb @@ -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 diff --git a/app/controllers/admin/invitations_controller.rb b/app/controllers/admin/invitations_controller.rb index 7bfa7be..ca9b407 100644 --- a/app/controllers/admin/invitations_controller.rb +++ b/app/controllers/admin/invitations_controller.rb @@ -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 diff --git a/app/controllers/admin/ldap_users_controller.rb b/app/controllers/admin/ldap_users_controller.rb index 2e2c3dd..5109041 100644 --- a/app/controllers/admin/ldap_users_controller.rb +++ b/app/controllers/admin/ldap_users_controller.rb @@ -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 diff --git a/app/controllers/admin/lightning_controller.rb b/app/controllers/admin/lightning_controller.rb new file mode 100644 index 0000000..2b3dfe7 --- /dev/null +++ b/app/controllers/admin/lightning_controller.rb @@ -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 diff --git a/app/controllers/admin/settings/registrations_controller.rb b/app/controllers/admin/settings/registrations_controller.rb new file mode 100644 index 0000000..3f2019e --- /dev/null +++ b/app/controllers/admin/settings/registrations_controller.rb @@ -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 diff --git a/app/controllers/admin/settings/services_controller.rb b/app/controllers/admin/settings/services_controller.rb new file mode 100644 index 0000000..ebdad4e --- /dev/null +++ b/app/controllers/admin/settings/services_controller.rb @@ -0,0 +1,9 @@ +class Admin::Settings::ServicesController < Admin::SettingsController + + def index + end + + def update + end + +end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb new file mode 100644 index 0000000..8353f9b --- /dev/null +++ b/app/controllers/admin/settings_controller.rb @@ -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 diff --git a/app/controllers/invitations_controller.rb b/app/controllers/invitations_controller.rb index 147cb6a..3bb038a 100644 --- a/app/controllers/invitations_controller.rb +++ b/app/controllers/invitations_controller.rb @@ -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 } diff --git a/app/models/lndhub_account.rb b/app/models/lndhub_account.rb new file mode 100644 index 0000000..b5eb437 --- /dev/null +++ b/app/models/lndhub_account.rb @@ -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 diff --git a/app/models/lndhub_account_ledger.rb b/app/models/lndhub_account_ledger.rb new file mode 100644 index 0000000..6858275 --- /dev/null +++ b/app/models/lndhub_account_ledger.rb @@ -0,0 +1,3 @@ +class LndhubAccountLedger < LndhubBase + self.table_name = "account_ledgers" +end diff --git a/app/models/lndhub_base.rb b/app/models/lndhub_base.rb new file mode 100644 index 0000000..d8a75ea --- /dev/null +++ b/app/models/lndhub_base.rb @@ -0,0 +1,4 @@ +class LndhubBase < ActiveRecord::Base + self.abstract_class = true + establish_connection :lndhub +end diff --git a/app/models/lndhub_user.rb b/app/models/lndhub_user.rb new file mode 100644 index 0000000..e5645c5 --- /dev/null +++ b/app/models/lndhub_user.rb @@ -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 diff --git a/app/models/setting.rb b/app/models/setting.rb new file mode 100644 index 0000000..fb4dae5 --- /dev/null +++ b/app/models/setting.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index a627661..e123ff4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/admin/dashboard/index.html.erb b/app/views/admin/dashboard/index.html.erb index bb66283..49d9e30 100644 --- a/app/views/admin/dashboard/index.html.erb +++ b/app/views/admin/dashboard/index.html.erb @@ -1,7 +1,12 @@ <%= render HeaderComponent.new(title: "Admin Panel") %> <%= render MainSimpleComponent.new do %> -

- With great power comes great responsibility. -

+
+

+ <%= image_tag("/img/illustrations/undraw_vault_re_s4my.svg", class: 'h-48') %> +

+

+ With great power comes great responsibility. +

+
<% end %> diff --git a/app/views/admin/donations/index.html.erb b/app/views/admin/donations/index.html.erb index baf51cb..0f57949 100644 --- a/app/views/admin/donations/index.html.erb +++ b/app/views/admin/donations/index.html.erb @@ -1,7 +1,26 @@ <%= render HeaderComponent.new(title: "Donations") %> <%= render MainSimpleComponent.new do %> +
+ <%= 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 %> +
+ +
<% if @donations.any? %> +

Recent Donations

@@ -11,7 +30,7 @@ - + @@ -23,11 +42,13 @@ - - - - + + <% end %> @@ -37,6 +58,7 @@ No donations yet.

<% end %> +

<%= link_to 'Record an out-of-system donation', new_admin_donation_path, class: 'btn-md btn-gray' %> diff --git a/app/views/admin/invitations/index.html.erb b/app/views/admin/invitations/index.html.erb index 4bad1fb..1bc5517 100644 --- a/app/views/admin/invitations/index.html.erb +++ b/app/views/admin/invitations/index.html.erb @@ -2,16 +2,28 @@ <%= render MainSimpleComponent.new do %>

-

- There are currently <%= @invitations_unused_count %> - unused invitations available to existing users. - <%= @users_with_referrals_count %> users have successfully - invited new users. -

+ <%= 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 %>
<% if @invitations_used.any? %>
-

Accepted (<%= @invitations_used.length %>)

+

Recently Accepted

in USD Public name Date
<% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %> <% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %> <%= donation.public_name %><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d") : "" %><%= 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?' } %><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %> + <%= 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?' } %> +
diff --git a/app/views/admin/ldap_users/index.html.erb b/app/views/admin/ldap_users/index.html.erb index 85637b0..e39bd6f 100644 --- a/app/views/admin/ldap_users/index.html.erb +++ b/app/views/admin/ldap_users/index.html.erb @@ -1,34 +1,54 @@ <%= render HeaderComponent.new(title: "LDAP Users: #{@ou}") %> <%= render MainSimpleComponent.new do %> - - +
+ <%= 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 %> +
-
- - - - - - - - - - <% @entries.each do |entry| %> - - - - - - - <% end %> - -
UIDE-MailAdmin
<%= entry[:uid] %><%= entry[:mail] %><%= entry[:admin] %>
+ <% if @orgs.length > 1 %> +
+ +
    + <% @orgs.each do |org| %> +
  • + <%= link_to org[:ou], admin_ldap_users_path(ou: org[:ou]), class: "ks-text-link" %> +
  • + <% end %> +
+
+ <% end %> + +
+ + + + + + + + + + + <% @entries.each do |entry| %> + + + + + + + <% end %> + +
UIDE-MailAdmin
<%= entry[:uid] %><%= entry[:mail] %><%= entry[:admin] %>
+
<% end %> diff --git a/app/views/admin/lightning/index.html.erb b/app/views/admin/lightning/index.html.erb new file mode 100644 index 0000000..2c099cb --- /dev/null +++ b/app/views/admin/lightning/index.html.erb @@ -0,0 +1,48 @@ +<%= render HeaderComponent.new(title: "Lightning Network") %> + +<%= render MainSimpleComponent.new do %> +
+ <%= 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 %> +
+ +
+

Accounts

+ + + + + + + + + + <% @accounts.each do |account| %> + + + + + + <% end %> + +
LN AccountUserBalance
+ <%= account.login %> + + <% if user = @users.find{ |u| u[2] == account.login } %> + <%= "#{user[0]}@#{user[1]}" %> + <% end %> + <%= number_with_delimiter account.balance.to_i.to_s %>
+
+<% end %> diff --git a/app/views/admin/settings/registrations/index.html.erb b/app/views/admin/settings/registrations/index.html.erb new file mode 100644 index 0000000..2131e41 --- /dev/null +++ b/app/views/admin/settings/registrations/index.html.erb @@ -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| %> +
+

Registrations

+ <% if @errors && @errors.any? %> +
+
    + <% @errors.full_messages.each do |msg| %> +
  • <%= msg %>
  • + <% end %> +
+
+ <% end %> + + +
+ +
+

+ <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %> +

+
+ <% end %> +<% end %> diff --git a/app/views/admin/settings/services/index.html.erb b/app/views/admin/settings/services/index.html.erb new file mode 100644 index 0000000..c0dad64 --- /dev/null +++ b/app/views/admin/settings/services/index.html.erb @@ -0,0 +1,39 @@ +<%= render HeaderComponent.new(title: "Settings") %> + +<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %> +
+

Lightning Network

+ <%= form_for(Setting.new, url: admin_settings_services_path) do |f| %> + <% if @errors && @errors.any? %> +
+
    + <% @errors.full_messages.each do |msg| %> +
  • <%= msg %>
  • + <% end %> +
+
+ <% end %> + +
    +
  • +
    + +

    LNDHub configuration present and wallet features enabled

    +
    + <%= f.check_box :lndhub_enabled, checked: Setting.lndhub_enabled?, + disabled: true, + class: "relative ml-4 inline-flex flex-shrink-0" %> +
  • +
  • +
    + +

    LNDHub database configuration present and admin panel enabled

    +
    + <%= f.check_box :lndhub_admin_enabled, checked: Setting.lndhub_admin_enabled?, + disabled: true, + class: "relative ml-4 inline-flex flex-shrink-0" %> +
  • +
+ <% end %> +
+<% end %> diff --git a/app/views/icons/_grid.html.erb b/app/views/icons/_grid.html.erb index 8ef2e9d..57f9632 100644 --- a/app/views/icons/_grid.html.erb +++ b/app/views/icons/_grid.html.erb @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/views/settings/account/index.html.erb b/app/views/settings/account/index.html.erb index 708a174..279d37d 100644 --- a/app/views/settings/account/index.html.erb +++ b/app/views/settings/account/index.html.erb @@ -4,10 +4,10 @@

Password

Use the following button to request an email with a password reset link:

-

- <%= form_with(url: settings_reset_password_path, method: :post) do %> + <%= form_with(url: settings_reset_password_path, method: :post) do %> +

<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %> - <% end %> -

+

+ <% end %>
<% end %> diff --git a/app/views/shared/_admin_nav.html.erb b/app/views/shared/_admin_nav.html.erb index 881cd93..dd8c8b8 100644 --- a/app/views/shared/_admin_nav.html.erb +++ b/app/views/shared/_admin_nav.html.erb @@ -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) %> diff --git a/app/views/shared/_admin_sidenav_settings.html.erb b/app/views/shared/_admin_sidenav_settings.html.erb new file mode 100644 index 0000000..d675242 --- /dev/null +++ b/app/views/shared/_admin_sidenav_settings.html.erb @@ -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 +) %> diff --git a/config/database.yml b/config/database.yml index 10df6c5..7f9bef7 100644 --- a/config/database.yml +++ b/config/database.yml @@ -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"] %> diff --git a/config/routes.rb b/config/routes.rb index 5ed99bd..7559393 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20230217084310_create_settings.rb b/db/migrate/20230217084310_create_settings.rb new file mode 100644 index 0000000..6fc5a0c --- /dev/null +++ b/db/migrate/20230217084310_create_settings.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 486c465..6b3cf56 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/public/img/illustrations/undraw_vault_re_s4my.svg b/public/img/illustrations/undraw_vault_re_s4my.svg new file mode 100644 index 0000000..408326c --- /dev/null +++ b/public/img/illustrations/undraw_vault_re_s4my.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spec/components/wallet_summary_component_spec.rb b/spec/components/wallet_summary_component_spec.rb index 7acdb04..a253964 100644 --- a/spec/components/wallet_summary_component_spec.rb +++ b/spec/components/wallet_summary_component_spec.rb @@ -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 diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb index 3120d5b..f0e7edb 100644 --- a/spec/features/admin/dashboard_spec.rb +++ b/spec/features/admin/dashboard_spec.rb @@ -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 diff --git a/spec/features/admin/settings_spec.rb b/spec/features/admin/settings_spec.rb new file mode 100644 index 0000000..bd586c7 --- /dev/null +++ b/spec/features/admin/settings_spec.rb @@ -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 diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 8b730c2..9959661 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -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