Add configurable settings, admin settings pages, reserved usernames #81

Merged
raucao merged 8 commits from feature/settings into feature/admin_stats 2023-02-23 07:42:21 +00:00
22 changed files with 260 additions and 10 deletions

View File

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

View File

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

View File

@ -1,6 +1,6 @@
@layer components { @layer components {
input[type=text], input[type=email], input[type=password], 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 @apply mt-1 rounded-md bg-gray-100 focus:bg-white
border-transparent focus:border-transparent focus:ring-2 border-transparent focus:border-transparent focus:ring-2
focus:ring-blue-600 focus:ring-opacity-75; focus:ring-blue-600 focus:ring-opacity-75;

View File

@ -33,7 +33,11 @@ class Admin::DonationsController < Admin::BaseController
respond_to do |format| respond_to do |format|
if @donation.save 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 } format.json { render :show, status: :created, location: @donation }
else else
format.html { render :new } format.html { render :new }
@ -47,7 +51,11 @@ class Admin::DonationsController < Admin::BaseController
def update def update
respond_to do |format| respond_to do |format|
if @donation.update(donation_params) 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 } format.json { render :show, status: :ok, location: @donation }
else else
format.html { render :edit } format.html { render :edit }
@ -61,7 +69,10 @@ class Admin::DonationsController < Admin::BaseController
def destroy def destroy
@donation.destroy @donation.destroy
respond_to do |format| 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 } format.json { head :no_content }
end end
end end

View File

@ -13,7 +13,7 @@ class Admin::LightningController < Admin::BaseController
end end
def check_feature_enabled def check_feature_enabled
if ENV["LNDHUB_ADMIN_UI"].empty? if !Setting.lndhub_admin_enabled?
flash[:alert] = "Lightning Admin UI not enabled" flash[:alert] = "Lightning Admin UI not enabled"
redirect_to admin_root_path and return redirect_to admin_root_path and return
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

@ -27,7 +27,10 @@ class InvitationsController < ApplicationController
respond_to do |format| respond_to do |format|
if @invitation.save 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 } format.json { render :show, status: :created, location: @invitation }
else else
format.html { render :new } format.html { render :new }

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

@ -12,6 +12,15 @@ class User < ApplicationRecord
validates_uniqueness_of :cn validates_uniqueness_of :cn
validates_length_of :cn, :minimum => 3 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_uniqueness_of :email
validates :email, email: true validates :email, email: true

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

@ -6,7 +6,9 @@
class: main_nav_class(@current_section, :invitations) %> class: main_nav_class(@current_section, :invitations) %>
<%= link_to "Donations", admin_donations_path, <%= link_to "Donations", admin_donations_path,
class: main_nav_class(@current_section, :donations) %> class: main_nav_class(@current_section, :donations) %>
<% if ENV["LNDHUB_ADMIN_UI"] %> <% if Setting.lndhub_admin_enabled? %>
<%= link_to "Lightning", admin_lightning_path, <%= link_to "Lightning", admin_lightning_path,
class: main_nav_class(@current_section, :lightning) %> class: main_nav_class(@current_section, :lightning) %>
<% end %> <% 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

@ -43,6 +43,11 @@ Rails.application.routes.draw do
get 'invitations', to: 'invitations#index' get 'invitations', to: 'invitations#index'
resources :donations resources :donations
get 'lightning', to: 'lightning#index' get 'lightning', to: 'lightning#index'
namespace :settings do
resources 'registrations', only: ['index', 'create']
resources 'services', only: ['index', 'create']
end
end end
authenticate :user, ->(user) { user.is_admin? } do 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. # 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| create_table "donations", force: :cascade do |t|
t.integer "user_id" t.integer "user_id"
t.integer "amount_sats" 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" t.index ["user_id"], name: "index_invitations_on_user_id"
end 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| create_table "users", force: :cascade do |t|
t.string "cn" t.string "cn"
t.string "ou" t.string "ou"

View File

@ -4,7 +4,9 @@ RSpec.describe 'Admin dashboard', type: :feature do
let(:user) { create :user } let(:user) { create :user }
before do 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 login_as user, :scope => :user
end 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" fill_in "user_cn", with: "t"
click_button "Continue" click_button "Continue"
expect(page).to have_content("Username is too short") 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" fill_in "user_cn", with: "jimmy"
click_button "Continue" click_button "Continue"
expect(page).to have_content("Username has already been taken") 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") expect(page).to have_content("confirm your address")
end end
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
end end