From a3f0d0f2cfacbe5c396831fa3989bde1b414e0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 23 Feb 2023 19:00:03 +0800 Subject: [PATCH 01/26] Fix deprecation warnings --- app/models/user.rb | 3 +-- config/application.rb | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index e123ff4..b8226ff 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -27,8 +27,7 @@ class User < ApplicationRecord scope :confirmed, -> { where.not(confirmed_at: nil) } scope :pending, -> { where(confirmed_at: nil) } - lockbox_encrypts :ln_login - lockbox_encrypts :ln_password + has_encrypted :ln_login, :ln_password # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable diff --git a/config/application.rb b/config/application.rb index 2e9e8ed..3731f5a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -22,7 +22,7 @@ Bundler.require(*Rails.groups) module Akkounts class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.0 + config.load_defaults 7.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers From 3c2fe7c15d46f2cf955f4705a10c6b5086c4791d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 23 Feb 2023 20:13:08 +0800 Subject: [PATCH 02/26] Remove ln_login from users Not needed anymore, removing in favor of unencrypted `ln_account`. --- .env.test | 1 - app/controllers/wallet_controller.rb | 2 +- app/jobs/create_lndhub_account_job.rb | 3 +-- app/services/lndhub.rb | 4 ++-- app/services/lndhub_v2.rb | 2 +- db/migrate/20230223115536_remove_ln_login_from_users.rb | 5 +++++ db/schema.rb | 2 +- spec/jobs/create_lndhub_account_job_spec.rb | 2 +- 8 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 db/migrate/20230223115536_remove_ln_login_from_users.rb diff --git a/.env.test b/.env.test index 03daa76..9a1c70f 100644 --- a/.env.test +++ b/.env.test @@ -2,7 +2,6 @@ EJABBERD_API_URL='http://xmpp.example.com/api' BTCPAY_API_URL='http://btcpay.example.com/api/v1' -LNDHUB_LEGACY_API_URL='http://localhost:3023' LNDHUB_API_URL='http://localhost:3026' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' diff --git a/app/controllers/wallet_controller.rb b/app/controllers/wallet_controller.rb index 535eed1..a7920d4 100644 --- a/app/controllers/wallet_controller.rb +++ b/app/controllers/wallet_controller.rb @@ -7,7 +7,7 @@ class WalletController < ApplicationController before_action :fetch_balance def index - @wallet_url = "lndhub://#{current_user.ln_login}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}" + @wallet_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}" qrcode = RQRCode::QRCode.new(@wallet_url) @svg = qrcode.as_svg( diff --git a/app/jobs/create_lndhub_account_job.rb b/app/jobs/create_lndhub_account_job.rb index 52d2a2c..6569bcf 100644 --- a/app/jobs/create_lndhub_account_job.rb +++ b/app/jobs/create_lndhub_account_job.rb @@ -2,13 +2,12 @@ class CreateLndhubAccountJob < ApplicationJob queue_as :default def perform(user) - return if user.ln_login.present? && user.ln_password.present? + return if user.ln_account.present? && user.ln_password.present? lndhub = LndhubV2.new credentials = lndhub.create_account user.update! ln_account: credentials["login"], - ln_login: credentials["login"], # TODO remove when production is migrated ln_password: credentials["password"] end end diff --git a/app/services/lndhub.rb b/app/services/lndhub.rb index c9547d3..9febebf 100644 --- a/app/services/lndhub.rb +++ b/app/services/lndhub.rb @@ -2,7 +2,7 @@ class Lndhub attr_accessor :auth_token def initialize - @base_url = ENV["LNDHUB_LEGACY_API_URL"] + @base_url = ENV["LNDHUB_API_URL"] end def post(endpoint, payload) @@ -42,7 +42,7 @@ class Lndhub end def authenticate(user) - credentials = post "auth?type=auth", { login: user.ln_login, password: user.ln_password } + credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password } self.auth_token = credentials["access_token"] self.auth_token end diff --git a/app/services/lndhub_v2.rb b/app/services/lndhub_v2.rb index bb4c4b9..693f812 100644 --- a/app/services/lndhub_v2.rb +++ b/app/services/lndhub_v2.rb @@ -39,7 +39,7 @@ class LndhubV2 end def authenticate(user) - credentials = post "auth?type=auth", { login: user.ln_login, password: user.ln_password } + credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password } self.auth_token = credentials["access_token"] self.auth_token end diff --git a/db/migrate/20230223115536_remove_ln_login_from_users.rb b/db/migrate/20230223115536_remove_ln_login_from_users.rb new file mode 100644 index 0000000..82ab8f1 --- /dev/null +++ b/db/migrate/20230223115536_remove_ln_login_from_users.rb @@ -0,0 +1,5 @@ +class RemoveLnLoginFromUsers < ActiveRecord::Migration[7.0] + def change + remove_column :users, :ln_login_cyphertext + end +end diff --git a/db/schema.rb b/db/schema.rb index 6b3cf56..f87e1b0 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_02_17_084310) do +ActiveRecord::Schema[7.0].define(version: 2023_02_23_115536) do create_table "donations", force: :cascade do |t| t.integer "user_id" t.integer "amount_sats" diff --git a/spec/jobs/create_lndhub_account_job_spec.rb b/spec/jobs/create_lndhub_account_job_spec.rb index aa2d179..51ffe2d 100644 --- a/spec/jobs/create_lndhub_account_job_spec.rb +++ b/spec/jobs/create_lndhub_account_job_spec.rb @@ -19,7 +19,7 @@ RSpec.describe CreateLndhubAccountJob, type: :job do .with { |req| req.body == '{}' } user.reload - expect(user.ln_login).to eq("abc123") + expect(user.ln_account).to eq("abc123") expect(user.ln_password).to eq("def456") end From 1a2482434cd2e4d495901bf865e7a892d628cb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 23 Feb 2023 21:53:12 +0800 Subject: [PATCH 03/26] Rename admin users controller/route Started out as a simple helper page to list LDAP users, but turning into proper user management now. --- .../admin/{ldap_users_controller.rb => users_controller.rb} | 4 ++-- app/helpers/ldap_users_helper.rb | 2 -- app/helpers/users_helper.rb | 2 ++ app/views/admin/{ldap_users => users}/index.html.erb | 4 ++-- app/views/shared/_admin_nav.html.erb | 4 ++-- config/routes.rb | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename app/controllers/admin/{ldap_users_controller.rb => users_controller.rb} (80%) delete mode 100644 app/helpers/ldap_users_helper.rb create mode 100644 app/helpers/users_helper.rb rename app/views/admin/{ldap_users => users}/index.html.erb (88%) diff --git a/app/controllers/admin/ldap_users_controller.rb b/app/controllers/admin/users_controller.rb similarity index 80% rename from app/controllers/admin/ldap_users_controller.rb rename to app/controllers/admin/users_controller.rb index 5109041..70a7e1d 100644 --- a/app/controllers/admin/ldap_users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,4 +1,4 @@ -class Admin::LdapUsersController < Admin::BaseController +class Admin::UsersController < Admin::BaseController before_action :set_current_section def index @@ -15,6 +15,6 @@ class Admin::LdapUsersController < Admin::BaseController private def set_current_section - @current_section = :ldap_users + @current_section = :users end end diff --git a/app/helpers/ldap_users_helper.rb b/app/helpers/ldap_users_helper.rb deleted file mode 100644 index 81e2eff..0000000 --- a/app/helpers/ldap_users_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module LdapUsersHelper -end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..2310a24 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,2 @@ +module UsersHelper +end diff --git a/app/views/admin/ldap_users/index.html.erb b/app/views/admin/users/index.html.erb similarity index 88% rename from app/views/admin/ldap_users/index.html.erb rename to app/views/admin/users/index.html.erb index e39bd6f..d78a4af 100644 --- a/app/views/admin/ldap_users/index.html.erb +++ b/app/views/admin/users/index.html.erb @@ -1,4 +1,4 @@ -<%= render HeaderComponent.new(title: "LDAP Users: #{@ou}") %> +<%= render HeaderComponent.new(title: "Users: #{@ou}") %> <%= render MainSimpleComponent.new do %>
@@ -22,7 +22,7 @@
    <% @orgs.each do |org| %>
  • - <%= link_to org[:ou], admin_ldap_users_path(ou: org[:ou]), class: "ks-text-link" %> + <%= link_to org[:ou], admin_users_path(ou: org[:ou]), class: "ks-text-link" %>
  • <% end %>
diff --git a/app/views/shared/_admin_nav.html.erb b/app/views/shared/_admin_nav.html.erb index dd8c8b8..7762431 100644 --- a/app/views/shared/_admin_nav.html.erb +++ b/app/views/shared/_admin_nav.html.erb @@ -1,7 +1,7 @@ <%= 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 "Users", admin_users_path, + class: main_nav_class(@current_section, :users) %> <%= link_to "Invitations", admin_invitations_path, class: main_nav_class(@current_section, :invitations) %> <%= link_to "Donations", admin_donations_path, diff --git a/config/routes.rb b/config/routes.rb index 7559393..f86a64b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -39,7 +39,7 @@ Rails.application.routes.draw do namespace :admin do root to: 'dashboard#index' - get 'ldap_users', to: 'ldap_users#index' + resources 'users', only: ['index'] get 'invitations', to: 'invitations#index' resources :donations get 'lightning', to: 'lightning#index' From ffed39802422db91cf29fc6c3f182a0ee88b7f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 23 Feb 2023 22:09:23 +0800 Subject: [PATCH 04/26] Add admin user details page --- app/controllers/admin/users_controller.rb | 9 +++++++++ app/views/admin/users/show.html.erb | 6 ++++++ config/routes.rb | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 app/views/admin/users/show.html.erb diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 70a7e1d..65ee39d 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,4 +1,5 @@ class Admin::UsersController < Admin::BaseController + before_action :set_user, only: [:show] before_action :set_current_section def index @@ -12,8 +13,16 @@ class Admin::UsersController < Admin::BaseController } end + def show + end + private + def set_user + address = params[:address].split("@") + @user = User.where(cn: address.first, ou: address.last).first + end + def set_current_section @current_section = :users end diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb new file mode 100644 index 0000000..021c5c6 --- /dev/null +++ b/app/views/admin/users/show.html.erb @@ -0,0 +1,6 @@ +<%= render HeaderComponent.new(title: "User: #{@user.address}") %> + +<%= render MainSimpleComponent.new do %> +
+
+<% end %> diff --git a/config/routes.rb b/config/routes.rb index f86a64b..ee16ee8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -39,7 +39,7 @@ Rails.application.routes.draw do namespace :admin do root to: 'dashboard#index' - resources 'users', only: ['index'] + resources 'users', param: 'address', only: ['index', 'show'], constraints: { address: /.*/ } get 'invitations', to: 'invitations#index' resources :donations get 'lightning', to: 'lightning#index' From 55abbcc5ad904aab5aa429e9a5a139a3aaf355b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 23 Feb 2023 23:55:32 +0800 Subject: [PATCH 05/26] WIP user page --- app/assets/stylesheets/components/tables.css | 6 +++++- app/controllers/admin/users_controller.rb | 1 + app/views/admin/users/show.html.erb | 21 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/components/tables.css b/app/assets/stylesheets/components/tables.css index 9b5e1fc..1a385b2 100644 --- a/app/assets/stylesheets/components/tables.css +++ b/app/assets/stylesheets/components/tables.css @@ -7,10 +7,14 @@ @apply text-left; } - table th { + table thead th { @apply pb-3.5 text-sm font-normal uppercase text-gray-500; } + table tbody th { + @apply text-left font-normal text-gray-500; + } + table th:not(:last-of-type), table td:not(:last-of-type) { @apply pr-2; diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 65ee39d..fbe0252 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -14,6 +14,7 @@ class Admin::UsersController < Admin::BaseController end def show + @inviter = Invitation.where(invited_user_id: @user.id).first.try(:user) end private diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index 021c5c6..8fbf1c1 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -2,5 +2,26 @@ <%= render MainSimpleComponent.new do %>
+

Account

+ + + + + + + + + + + + + + + + + + + +
Created at<%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %>
Confirmed at<%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %>
Email<%= @user.email %>
Invited by<%= @inviter ? link_to(@inviter.address, admin_user_path(@inviter.address), class: 'ks-text-link') : "—" %>
<% end %> From a0727e709f67e44d9759597f83967e6444cf73e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 25 Feb 2023 15:27:28 +0800 Subject: [PATCH 06/26] Add table class for rows with dividers --- app/assets/stylesheets/components/tables.css | 12 +++++++++++- app/views/admin/donations/index.html.erb | 2 +- app/views/admin/invitations/index.html.erb | 2 +- app/views/admin/lightning/index.html.erb | 2 +- app/views/admin/users/index.html.erb | 2 +- app/views/admin/users/show.html.erb | 2 +- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/components/tables.css b/app/assets/stylesheets/components/tables.css index 1a385b2..14fb4af 100644 --- a/app/assets/stylesheets/components/tables.css +++ b/app/assets/stylesheets/components/tables.css @@ -20,7 +20,17 @@ @apply pr-2; } - table td { + table td, tbody th { @apply py-2; } + + table.divided { + @apply divide-y divide-gray-300; + } + table.divided tbody { + @apply divide-y divide-gray-200; + } + table.divided td, table.divided tbody th { + @apply py-3; + } } diff --git a/app/views/admin/donations/index.html.erb b/app/views/admin/donations/index.html.erb index 0f57949..0876cd7 100644 --- a/app/views/admin/donations/index.html.erb +++ b/app/views/admin/donations/index.html.erb @@ -21,7 +21,7 @@
<% if @donations.any? %>

Recent Donations

- +
diff --git a/app/views/admin/invitations/index.html.erb b/app/views/admin/invitations/index.html.erb index 1bc5517..a49491e 100644 --- a/app/views/admin/invitations/index.html.erb +++ b/app/views/admin/invitations/index.html.erb @@ -24,7 +24,7 @@ <% if @invitations_used.any? %>

Recently Accepted

-
User
+
diff --git a/app/views/admin/lightning/index.html.erb b/app/views/admin/lightning/index.html.erb index 2c099cb..04c4507 100644 --- a/app/views/admin/lightning/index.html.erb +++ b/app/views/admin/lightning/index.html.erb @@ -20,7 +20,7 @@

Accounts

-
Token
+
diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb index d78a4af..8bc4db3 100644 --- a/app/views/admin/users/index.html.erb +++ b/app/views/admin/users/index.html.erb @@ -30,7 +30,7 @@ <% end %>
-
LN Account
+
diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index 8fbf1c1..ef8dda2 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -3,7 +3,7 @@ <%= render MainSimpleComponent.new do %>

Account

-
UID
+
From e675970f4c54c101e83d50504d3d2d625e517a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 25 Feb 2023 15:28:02 +0800 Subject: [PATCH 07/26] Add view helper for colored badges --- app/helpers/application_helper.rb | 7 +++++++ tailwind.config.js | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 248d897..037be7c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -10,5 +10,12 @@ module ApplicationHelper "text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block" end end + + # Colors available: gray, red, yellow, green, blue, purple, pink + # (Add more colors by adding classes to the safelist in tailwind.config.js) + def badge(text, color) + tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800" + end + end diff --git a/tailwind.config.js b/tailwind.config.js index 20a464f..a86d1b2 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -7,6 +7,15 @@ module.exports = { './app/helpers/**/*.rb', './app/javascript/**/*.js' ], + safelist: [ + 'bg-gray-100', 'text-gray-800', + 'bg-red-100', 'text-red-800', + 'bg-yellow-100', 'text-yellow-800', + 'bg-green-100', 'text-green-800', + 'bg-blue-100', 'text-blue-800', + 'bg-purple-100', 'text-purple-800', + 'bg-pink-100', 'text-pink-800', + ], theme: { extend: { fontFamily: { @@ -16,5 +25,5 @@ module.exports = { }, plugins: [ require('@tailwindcss/forms') - ], + ] } From 30fb9805e505572c84ab9ea63569bf517df20990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 25 Feb 2023 15:29:46 +0800 Subject: [PATCH 08/26] Add associations between users via invitations --- app/models/invitation.rb | 1 + app/models/user.rb | 4 ++++ app/views/admin/invitations/index.html.erb | 4 ++-- app/views/admin/users/show.html.erb | 6 +++++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/models/invitation.rb b/app/models/invitation.rb index e4910a5..cfff859 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -1,6 +1,7 @@ class Invitation < ApplicationRecord # Relations belongs_to :user + belongs_to :invitee, class_name: "User", foreign_key: 'invited_user_id', optional: true # Validations validates_presence_of :user diff --git a/app/models/user.rb b/app/models/user.rb index b8226ff..dde6c1c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,10 @@ class User < ApplicationRecord # Relations has_many :invitations, dependent: :destroy + has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id' + has_one :inviter, through: :invitation, source: :user + has_many :invitees, through: :invitations + has_many :donations, dependent: :nullify has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user", diff --git a/app/views/admin/invitations/index.html.erb b/app/views/admin/invitations/index.html.erb index a49491e..51905c0 100644 --- a/app/views/admin/invitations/index.html.erb +++ b/app/views/admin/invitations/index.html.erb @@ -38,8 +38,8 @@ - - + + <% end %> diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index ef8dda2..5354ef0 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -19,7 +19,11 @@ - +
Created at
<%= invitation.token %> <%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %><%= invitation.user.address %><%= User.find(invitation.invited_user_id).address %><%= link_to invitation.user.address, admin_user_path(invitation.user.address), class: "ks-text-link" %><%= link_to invitation.invitee.address, admin_user_path(invitation.invitee.address), class: "ks-text-link" %>
Invited by<%= @inviter ? link_to(@inviter.address, admin_user_path(@inviter.address), class: 'ks-text-link') : "—" %> + <% if @user.inviter %> + <%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %> + <% else %>—<% end %> +
From 678e80a25df0dcc2333dedb8381491560f005309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 25 Feb 2023 15:30:23 +0800 Subject: [PATCH 09/26] Retrieve ldap entry from user model --- app/models/user.rb | 6 ++++++ app/services/ldap_service.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index dde6c1c..3c4be54 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -70,6 +70,12 @@ class User < ApplicationRecord end end + def ldap_entry + return @ldap_entry if defined?(@ldap_entry) + ldap = LdapService.new + @ldap_entry = ldap.fetch_users(uid: self.cn, ou: self.ou).first + end + def address "#{self.cn}@#{self.ou}" end diff --git a/app/services/ldap_service.rb b/app/services/ldap_service.rb index d68b992..baff4c9 100644 --- a/app/services/ldap_service.rb +++ b/app/services/ldap_service.rb @@ -43,7 +43,7 @@ class LdapService < ApplicationService end attributes = %w{dn cn uid mail admin} - filter = Net::LDAP::Filter.eq("uid", "*") + filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes) entries.sort_by! { |e| e.cn[0] } From 8eb487600c81b18202e91fd79808d14c966e0160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 25 Feb 2023 15:31:19 +0800 Subject: [PATCH 10/26] Switch admin users index from pure LDAP to database --- app/controllers/admin/users_controller.rb | 9 +++++---- app/views/admin/users/index.html.erb | 13 ++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index fbe0252..f408ac7 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -3,10 +3,11 @@ class Admin::UsersController < Admin::BaseController before_action :set_current_section def index - ldap = LdapService.new - @ou = params[:ou] || "kosmos.org" - @orgs = ldap.fetch_organizations - @entries = ldap.fetch_users(ou: @ou) + ldap = LdapService.new + @ou = params[:ou] || "kosmos.org" + @orgs = ldap.fetch_organizations + @users = User.where(ou: @ou).order(cn: :asc).to_a + @stats = { users_confirmed: User.where(ou: @ou).confirmed.count, users_pending: User.where(ou: @ou).pending.count diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb index 8bc4db3..9e60ac7 100644 --- a/app/views/admin/users/index.html.erb +++ b/app/views/admin/users/index.html.erb @@ -34,18 +34,17 @@ UID - E-Mail - Admin + Status + Roles - <% @entries.each do |entry| %> + <% @users.each do |user| %> - <%= entry[:uid] %> - <%= entry[:mail] %> - <%= entry[:admin] %> - + <%= link_to(user.cn, admin_user_path(user.address), class: 'ks-text-link') %> + <%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %> + <%= user.is_admin? ? badge("admin", :red) : "" %> <% end %> From 1a55e5e89514704f5686879f7f747877da6df088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 25 Feb 2023 15:32:13 +0800 Subject: [PATCH 11/26] Link users everywhere in admin panel --- app/views/admin/donations/index.html.erb | 3 +- app/views/admin/donations/show.html.erb | 57 ++++++++++++------------ app/views/admin/lightning/index.html.erb | 2 +- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/app/views/admin/donations/index.html.erb b/app/views/admin/donations/index.html.erb index 0876cd7..1074843 100644 --- a/app/views/admin/donations/index.html.erb +++ b/app/views/admin/donations/index.html.erb @@ -33,11 +33,10 @@ - <% @donations.each do |donation| %> - <%= donation.user.address %> + <%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %> <%= sats_to_btc donation.amount_sats %> <% 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 %> diff --git a/app/views/admin/donations/show.html.erb b/app/views/admin/donations/show.html.erb index 0c1d54c..57b0531 100644 --- a/app/views/admin/donations/show.html.erb +++ b/app/views/admin/donations/show.html.erb @@ -1,35 +1,34 @@ <%= render HeaderComponent.new(title: "Donations") %> <%= render MainSimpleComponent.new do %> -

- User: - <%= @donation.user.address %> -

- -

- Amount sats: - <%= @donation.amount_sats %> -

- -

- Amount eur: - <%= @donation.amount_eur %> -

- -

- Amount usd: - <%= @donation.amount_usd %> -

- -

- Public name: - <%= @donation.public_name %> -

- -

- Date: - <%= @donation.paid_at %> -

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
User<%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %>
Amount sats<%= @donation.amount_sats %>
Amount EUR<%= @donation.amount_eur %>
Amount USD<%= @donation.amount_usd %>
Public name<%= @donation.public_name %>
Date<%= @donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") %>

<%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'ks-text-link' %> | diff --git a/app/views/admin/lightning/index.html.erb b/app/views/admin/lightning/index.html.erb index 04c4507..8cb0757 100644 --- a/app/views/admin/lightning/index.html.erb +++ b/app/views/admin/lightning/index.html.erb @@ -36,7 +36,7 @@ <% if user = @users.find{ |u| u[2] == account.login } %> - <%= "#{user[0]}@#{user[1]}" %> + <%= link_to "#{user[0]}@#{user[1]}", admin_user_path("#{user[0]}@#{user[1]}"), class: "ks-text-link" %> <% end %> <%= number_with_delimiter account.balance.to_i.to_s %> From 27dd4163f060a5e9609989a5cd8a551eacd12fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 25 Feb 2023 15:32:50 +0800 Subject: [PATCH 12/26] Add more data to admin user page --- app/views/admin/users/show.html.erb | 33 ++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index 5354ef0..2cc45c7 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -11,12 +11,22 @@ Confirmed at - <%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %> + + <% if @user.confirmed_at %> + <%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %> + <% else %> + <%= badge "pending", :yellow %> + <% end %> + Email <%= @user.email %> + + Roles + <%= @user.is_admin? ? badge("admin", :red) : "—" %> + Invited by @@ -25,6 +35,27 @@ <% else %>—<% end %> + + Invitations available + + <%= @user.invitations.count %> + + + + Invited users + + <% if @user.invitees.length > 0 %> +

    + <% @user.invitees.order(cn: :asc).each do |invitee| %> +
  • <%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %>
  • + <% end %> +
+ <% else %>—<% end %> + + + + +
From 6d20ac9a1c104116b726efbb75e85d5e864ae219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 25 Feb 2023 15:33:03 +0800 Subject: [PATCH 13/26] Add lndhub info to admin user page --- app/controllers/admin/users_controller.rb | 4 +++- app/models/lndhub_user.rb | 14 +++++++++++- app/views/admin/users/show.html.erb | 27 +++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f408ac7..b23f468 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -15,7 +15,9 @@ class Admin::UsersController < Admin::BaseController end def show - @inviter = Invitation.where(invited_user_id: @user.id).first.try(:user) + if Setting.lndhub_admin_enabled? + @lndhub_user = @user.lndhub_user + end end private diff --git a/app/models/lndhub_user.rb b/app/models/lndhub_user.rb index e5645c5..f467089 100644 --- a/app/models/lndhub_user.rb +++ b/app/models/lndhub_user.rb @@ -10,6 +10,18 @@ class LndhubUser < LndhubBase foreign_key: "login" def balance - accounts.current.first.ledgers.sum("account_ledgers.amount") + accounts.current.first.ledgers.sum("account_ledgers.amount").to_i.abs + end + + def sum_outgoing + accounts.outgoing.first.ledgers.sum("account_ledgers.amount").to_i.abs + end + + def sum_incoming + accounts.incoming.first.ledgers.sum("account_ledgers.amount").to_i.abs + end + + def sum_fees + accounts.fees.first.ledgers.sum("account_ledgers.amount").to_i.abs end end diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index 2cc45c7..e4ddd66 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -56,7 +56,34 @@ + + <% if Setting.lndhub_admin_enabled? %> +
+

LndHub

+ <% if @lndhub_user %> + + + + + + + + + + + + + + + + + +
AccountBalanceIncomingOutgoingFees
<%= @user.ln_account %><%= number_with_delimiter @lndhub_user.balance %> sats<%= number_with_delimiter @lndhub_user.sum_incoming %> sats<%= number_with_delimiter @lndhub_user.sum_outgoing %> sats<%= number_with_delimiter @lndhub_user.sum_fees %> sats
+ <% else %> +

No LndHub user found for account <%= @user.ln_account %>. + <% end %>

+ <% end %> <% end %> From eec4533fea90249b508ca45f1ab26af07ad2efaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 26 Feb 2023 11:32:03 +0800 Subject: [PATCH 14/26] Improve markup --- app/assets/stylesheets/components/base.css | 4 ++-- app/views/admin/settings/registrations/index.html.erb | 4 ++-- app/views/admin/settings/services/index.html.erb | 4 ++-- app/views/admin/users/show.html.erb | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/components/base.css b/app/assets/stylesheets/components/base.css index b8fbc69..6ea47c7 100644 --- a/app/assets/stylesheets/components/base.css +++ b/app/assets/stylesheets/components/base.css @@ -32,11 +32,11 @@ @apply pt-0; } - main p { + main p:not(:last-child) { @apply mb-4 leading-6; } - main ul { + main ul:not(:last-child) { @apply mb-6; } diff --git a/app/views/admin/settings/registrations/index.html.erb b/app/views/admin/settings/registrations/index.html.erb index 2131e41..68e1827 100644 --- a/app/views/admin/settings/registrations/index.html.erb +++ b/app/views/admin/settings/registrations/index.html.erb @@ -22,14 +22,14 @@ <%= f.text_area :reserved_usernames, value: Setting.reserved_usernames.join("\n"), class: "h-44 mb-2" %> -

+

One username per line

-

+

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

diff --git a/app/views/admin/settings/services/index.html.erb b/app/views/admin/settings/services/index.html.erb index c0dad64..a4a4fec 100644 --- a/app/views/admin/settings/services/index.html.erb +++ b/app/views/admin/settings/services/index.html.erb @@ -18,7 +18,7 @@
  • -

    LNDHub configuration present and wallet features enabled

    +

    LNDHub configuration present and wallet features enabled

    <%= f.check_box :lndhub_enabled, checked: Setting.lndhub_enabled?, disabled: true, @@ -27,7 +27,7 @@
  • -

    LNDHub database configuration present and admin panel enabled

    +

    LNDHub database configuration present and admin panel enabled

    <%= f.check_box :lndhub_admin_enabled, checked: Setting.lndhub_admin_enabled?, disabled: true, diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index e4ddd66..126aed9 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -45,7 +45,7 @@ Invited users <% if @user.invitees.length > 0 %> -
      +
        <% @user.invitees.order(cn: :asc).each do |invitee| %>
      • <%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %>
      • <% end %> From 1c3e893b6bd53d2980ffa212b25462fc436aa997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 26 Feb 2023 11:32:26 +0800 Subject: [PATCH 15/26] Fix height of link element buttons --- app/assets/stylesheets/components/buttons.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/components/buttons.css b/app/assets/stylesheets/components/buttons.css index da79cec..5da9116 100644 --- a/app/assets/stylesheets/components/buttons.css +++ b/app/assets/stylesheets/components/buttons.css @@ -1,6 +1,6 @@ @layer components { .btn { - @apply font-semibold rounded-md leading-none cursor-pointer text-center + @apply inline-block font-semibold rounded-md leading-none cursor-pointer text-center transition-colors duration-75 focus:outline-none focus:ring-4; } From 5f74212603f1da3bfc42594bc79df88c04f8f4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 26 Feb 2023 11:33:11 +0800 Subject: [PATCH 16/26] Improve admin donation pages --- app/views/admin/donations/_form.html.erb | 48 +++++----------- app/views/admin/donations/edit.html.erb | 7 +-- app/views/admin/donations/new.html.erb | 4 +- app/views/admin/donations/show.html.erb | 70 +++++++++++++----------- 4 files changed, 53 insertions(+), 76 deletions(-) diff --git a/app/views/admin/donations/_form.html.erb b/app/views/admin/donations/_form.html.erb index 1405b08..0522cb0 100644 --- a/app/views/admin/donations/_form.html.erb +++ b/app/views/admin/donations/_form.html.erb @@ -10,46 +10,24 @@ <% end %> -
        -

        - <%= form.label :user_id %> - <%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn %> -

        -
        +
        + <%= form.label :user_id %> + <%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn, {} %> -
        -

        - <%= form.label :amount_sats, "Amount BTC (sats)" %> - <%= form.number_field :amount_sats %> -

        -
        + <%= form.label :amount_sats, "Amount BTC (sats)" %> + <%= form.number_field :amount_sats %> -
        -

        - <%= form.label :amount_eur, "Amount EUR (cents)" %> - <%= form.number_field :amount_eur %> -

        -
        + <%= form.label :amount_eur, "Amount EUR (cents)" %> + <%= form.number_field :amount_eur %> -
        -

        - <%= form.label :amount_usd, "Amount USD (cents)"%> - <%= form.number_field :amount_usd %> -

        -
        + <%= form.label :amount_usd, "Amount USD (cents)"%> + <%= form.number_field :amount_usd %> -
        -

        - <%= form.label :public_name %> - <%= form.text_field :public_name %> -

        -
        + <%= form.label :public_name %> + <%= form.text_field :public_name %> -
        -

        - <%= form.label :paid_at %> - <%= form.text_field :paid_at %> -

        + <%= form.label :paid_at %> + <%= form.text_field :paid_at %>

        diff --git a/app/views/admin/donations/edit.html.erb b/app/views/admin/donations/edit.html.erb index ba590b2..27340f1 100644 --- a/app/views/admin/donations/edit.html.erb +++ b/app/views/admin/donations/edit.html.erb @@ -1,12 +1,9 @@ -<%= render HeaderComponent.new(title: "Donations") %> +<%= render HeaderComponent.new(title: "Donation ##{@donation.id}") %> <%= render MainSimpleComponent.new do %> -

        Editing Donation

        - <%= render 'form', donation: @donation, url: admin_donation_path(@donation) %>

        - <%= link_to 'Show', admin_donation_path(@donation), class: 'ks-text-link' %> | - <%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %> + <%= link_to 'Cancel', admin_donation_path(@donation), class: 'btn-sm btn-gray' %>

        <% end %> diff --git a/app/views/admin/donations/new.html.erb b/app/views/admin/donations/new.html.erb index d831813..176b659 100644 --- a/app/views/admin/donations/new.html.erb +++ b/app/views/admin/donations/new.html.erb @@ -1,8 +1,6 @@ -<%= render HeaderComponent.new(title: "Donations") %> +<%= render HeaderComponent.new(title: "Add Donation") %> <%= render MainSimpleComponent.new do %> -

        New Donation

        - <%= render 'form', donation: @donation, url: admin_donations_path %>

        diff --git a/app/views/admin/donations/show.html.erb b/app/views/admin/donations/show.html.erb index 57b0531..1526789 100644 --- a/app/views/admin/donations/show.html.erb +++ b/app/views/admin/donations/show.html.erb @@ -1,37 +1,41 @@ -<%= render HeaderComponent.new(title: "Donations") %> +<%= render HeaderComponent.new(title: "Donation ##{@donation.id}") %> <%= render MainSimpleComponent.new do %> - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        User<%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %>
        Amount sats<%= @donation.amount_sats %>
        Amount EUR<%= @donation.amount_eur %>
        Amount USD<%= @donation.amount_usd %>
        Public name<%= @donation.public_name %>
        Date<%= @donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") %>
        +

        + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        User<%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %>
        Amount sats<%= @donation.amount_sats %>
        Amount EUR<%= @donation.amount_eur %>
        Amount USD<%= @donation.amount_usd %>
        Public name<%= @donation.public_name %>
        Date<%= @donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") %>
        +
        -

        - <%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'ks-text-link' %> | - <%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %> -

        +
        +

        + <%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'btn-md btn-blue mr-1' %> + <%= link_to 'Back', admin_donations_path, class: 'btn-md btn-gray' %> +

        +
        <% end %> From c8e405d93aebbb366790601b3d4a39abfc263e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 26 Feb 2023 18:41:18 +0800 Subject: [PATCH 17/26] Fix inline tailwind styles not being applied --- app/assets/stylesheets/components/base.css | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/components/base.css b/app/assets/stylesheets/components/base.css index 6ea47c7..b4fa491 100644 --- a/app/assets/stylesheets/components/base.css +++ b/app/assets/stylesheets/components/base.css @@ -32,14 +32,22 @@ @apply pt-0; } - main p:not(:last-child) { + main p { @apply mb-4 leading-6; } - main ul:not(:last-child) { + main p:last-child { + @apply mb-0; + } + + main ul { @apply mb-6; } + main ul:last-child { + @apply mb-0; + } + main ul li { @apply leading-6; } From 52cc2a815103eadfa3a8ae6f6c68312019ac35ea Mon Sep 17 00:00:00 2001 From: Garret Alfert Date: Sun, 26 Feb 2023 13:10:49 +0100 Subject: [PATCH 18/26] Fix numbering in quickstart steps --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fca3437..4fe0bfb 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ so: in the log output 4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"` 5. `docker compose run web rails ldap:setup` -5. `docker compose run web rails db:setup` +6. `docker compose run web rails db:setup` After these steps, you should have a working Rails app with a handful of test users running on [http://localhost:3000](http://localhost:3000). From b2a1b8caf5c454c9dd4c544f057aff34a6c1ddff Mon Sep 17 00:00:00 2001 From: Garret Alfert Date: Sun, 26 Feb 2023 13:15:33 +0100 Subject: [PATCH 19/26] Remove "admin" from default reserved usernames Blocking admin prevents seeding the DB, which creates an admin user --- app/models/setting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/setting.rb b/app/models/setting.rb index fb4dae5..94444c0 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -3,7 +3,7 @@ class Setting < RailsSettings::Base cache_prefix { "v1" } field :reserved_usernames, type: :array, default: %w[ - account accounts admin donations mail webmaster support + account accounts donations mail webmaster support ] field :lndhub_enabled, default: (ENV["LNDHUB_API_URL"].present?.to_s || "false"), type: :boolean From 75ffd4e2f1af0072dc9158f9e7d0bfad3c7ffd8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 25 Feb 2023 21:35:38 +0800 Subject: [PATCH 20/26] Add service attribute to LDAP user entry --- app/controllers/admin/users_controller.rb | 2 + .../users/confirmations_controller.rb | 17 +++ app/models/user.rb | 54 ++++++-- app/services/ldap_service.rb | 24 ++-- app/views/admin/users/show.html.erb | 116 +++++++++++------- config/routes.rb | 2 +- spec/models/user_spec.rb | 85 ++++++++++++- 7 files changed, 234 insertions(+), 66 deletions(-) create mode 100644 app/controllers/users/confirmations_controller.rb diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index b23f468..84e0ff2 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -18,6 +18,8 @@ class Admin::UsersController < Admin::BaseController if Setting.lndhub_admin_enabled? @lndhub_user = @user.lndhub_user end + + @services_enabled = @user.services_enabled end private diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb new file mode 100644 index 0000000..340b538 --- /dev/null +++ b/app/controllers/users/confirmations_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Users::ConfirmationsController < Devise::ConfirmationsController + # GET /resource/confirmation?confirmation_token=abcdef + def show + self.resource = resource_class.confirm_by_token(params[:confirmation_token]) + yield resource if block_given? + + if resource.errors.empty? + set_flash_message!(:success, :confirmed) + resource.devise_after_confirmation + respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) } + else + respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new } + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 3c4be54..31599c4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -42,9 +42,9 @@ class User < ApplicationRecord def ldap_before_save self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first - - dn = Devise::LDAP::Adapter.get_ldap_param(self.cn, "dn") - self.ou = dn.split(',').select{|e| e[0..1] == "ou"}.first.delete_prefix("ou=") + self.ou = dn.split(',') + .select{|e| e[0..1] == "ou"}.first + .delete_prefix("ou=") if self.confirmed_at.blank? && self.confirmation_token.blank? # User had an account with a trusted email address before akkounts was a thing @@ -52,6 +52,10 @@ class User < ApplicationRecord end end + def devise_after_confirmation + enable_service %w[discourse gitea wiki xmpp] + end + def reset_password(new_password, new_password_confirmation) self.password = new_password self.password_confirmation = new_password_confirmation @@ -70,12 +74,6 @@ class User < ApplicationRecord end end - def ldap_entry - return @ldap_entry if defined?(@ldap_entry) - ldap = LdapService.new - @ldap_entry = ldap.fetch_users(uid: self.cn, ou: self.ou).first - end - def address "#{self.cn}@#{self.ou}" end @@ -90,4 +88,42 @@ class User < ApplicationRecord lndhub.authenticate self lndhub.addinvoice payload end + + def dn + return @dn if defined?(@dn) + @dn = Devise::LDAP::Adapter.get_dn(self.cn) + end + + def ldap_entry + ldap.fetch_users(uid: self.cn, ou: self.ou).first + end + + def services_enabled + ldap_entry[:service] || [] + end + + def enable_service(service) + current_services = services_enabled + new_services = Array(service).map(&:to_s) + services = (current_services + new_services).uniq + ldap.replace_attribute(dn, :service, services) + end + + def disable_service(service) + current_services = services_enabled + disabled_services = Array(service).map(&:to_s) + services = (current_services - disabled_services).uniq + ldap.replace_attribute(dn, :service, services) + end + + def disable_all_services + ldap.delete_attribute(dn,:service) + end + + private + + def ldap + return @ldap_service if defined?(@ldap_service) + @ldap_service = LdapService.new + end end diff --git a/app/services/ldap_service.rb b/app/services/ldap_service.rb index baff4c9..5a572c5 100644 --- a/app/services/ldap_service.rb +++ b/app/services/ldap_service.rb @@ -3,6 +3,18 @@ class LdapService < ApplicationService @suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org" end + def add_attribute(dn, attr, values) + ldap_client.add_attribute dn, attr, values + end + + def replace_attribute(dn, attr, values) + ldap_client.replace_attribute dn, attr, values + end + + def delete_attribute(dn, attr) + ldap_client.delete_attribute dn, attr + end + def add_entry(dn, attrs, interactive=false) puts "Adding entry: #{dn}" if interactive res = ldap_client.add dn: dn, attributes: attrs @@ -10,10 +22,6 @@ class LdapService < ApplicationService res end - def add_attribute(dn, attr, value) - ldap_client.add_attribute dn, attr, value - end - def delete_entry(dn, interactive=false) puts "Deleting entry: #{dn}" if interactive res = ldap_client.delete dn: dn @@ -42,18 +50,17 @@ class LdapService < ApplicationService treebase = ldap_config["base"] end - attributes = %w{dn cn uid mail admin} + attributes = %w{dn cn uid mail admin service} filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") 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 + admin: e.try(:admin) ? 'admin' : nil, + service: e.try(:service) } end end @@ -131,5 +138,4 @@ class LdapService < ApplicationService def ldap_config ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env] end - end diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index 126aed9..4679e6f 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -1,63 +1,97 @@ <%= render HeaderComponent.new(title: "User: #{@user.address}") %> <%= render MainSimpleComponent.new do %> +
        +
        +

        Account

        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        Created at<%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %>
        Confirmed at + <% if @user.confirmed_at %> + <%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %> + <% else %> + <%= badge "pending", :yellow %> + <% end %> +
        Email<%= @user.email %>
        Roles<%= @user.is_admin? ? badge("admin", :red) : "—" %>
        Invited by + <% if @user.inviter %> + <%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %> + <% else %>—<% end %> +
        Invitations available + <%= @user.invitations.count %> +
        Invited users + <% if @user.invitees.length > 0 %> +
          + <% @user.invitees.order(cn: :asc).each do |invitee| %> +
        • <%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %>
        • + <% end %> +
        + <% else %>—<% end %> +
        +
        + +
        + +
        +
        +
        -

        Account

        - +

        Services

        +
        - - + + - - + + - - + + - - + + - - - - - - - - - - + +
        Created at<%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %>Discourse<%= check_box_tag 'service_discourse', 'enabled', @services_enabled.include?("discourse"), disabled: true %>
        Confirmed at - <% if @user.confirmed_at %> - <%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %> - <% else %> - <%= badge "pending", :yellow %> - <% end %> - Gitea<%= check_box_tag 'service_gitea', 'enabled', @services_enabled.include?("gitea"), disabled: true %>
        Email<%= @user.email %>Mastodon<%= check_box_tag 'service_mastodon', 'enabled', @services_enabled.include?("mastodon"), disabled: true %>
        Roles<%= @user.is_admin? ? badge("admin", :red) : "—" %>Wiki<%= check_box_tag 'service_wiki', 'enabled', @services_enabled.include?("wiki"), disabled: true %>
        Invited by - <% if @user.inviter %> - <%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %> - <% else %>—<% end %> -
        Invitations available - <%= @user.invitations.count %> -
        Invited users - <% if @user.invitees.length > 0 %> -
          - <% @user.invitees.order(cn: :asc).each do |invitee| %> -
        • <%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %>
        • - <% end %> -
        - <% else %>—<% end %> -
        XMPP<%= check_box_tag 'service_xmpp', 'enabled', @services_enabled.include?("xmpp"), disabled: true %>
        - <% if Setting.lndhub_admin_enabled? %> + <% if Setting.lndhub_admin_enabled? && @user.confirmed? %>

        LndHub

        <% if @lndhub_user %> diff --git a/config/routes.rb b/config/routes.rb index ee16ee8..e137a92 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ require 'sidekiq/web' Rails.application.routes.draw do - devise_for :users + devise_for :users, :controllers => { :confirmations => "users/confirmations" } get 'welcome', to: 'welcome#index' get 'check_your_email', to: 'welcome#check_your_email' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ed135d9..b1724f1 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,7 +1,16 @@ require 'rails_helper' RSpec.describe User, type: :model do - let(:user) { create :user } + let(:user) { create :user, cn: "philipp" } + let(:dn) { "cn=philipp,ou=kosmos.org,cn=users,dc=kosmos,dc=org" } + + describe "#address" do + let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" } + + it "returns the user address" do + expect(user.address).to eq("jimmy@kosmos.org") + end + end describe "#is_admin?" do it "returns true when admin flag is set in LDAP" do @@ -21,11 +30,75 @@ RSpec.describe User, type: :model do end end - describe "#address" do - let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" } - - it "returns the user address" do - expect(user.address).to eq("jimmy@kosmos.org") + describe "#services_enabled" do + it "returns the entries from the LDAP service attribute" do + expect(user).to receive(:ldap_entry).and_return({ + uid: user.cn, ou: user.ou, mail: user.email, admin: nil, + service: ["discourse", "gitea", "wiki", "xmpp"] + }) + expect(user.services_enabled).to eq(["discourse", "gitea", "wiki", "xmpp"]) end end + + describe "#enable_service" do + before do + allow(user).to receive(:ldap_entry).and_return({ + uid: user.cn, ou: user.ou, mail: user.email, admin: nil, + service: ["discourse", "gitea"] + }) + allow(user).to receive(:dn).and_return(dn) + end + + it "adds the service to the LDAP entry" do + expect_any_instance_of(LdapService).to receive(:replace_attribute) + .with(dn, :service, ["discourse", "gitea", "wiki"]).and_return(true) + + user.enable_service(:wiki) + end + + it "adds multiple service to the LDAP entry" do + expect_any_instance_of(LdapService).to receive(:replace_attribute) + .with(dn, :service, ["discourse", "gitea", "wiki", "xmpp"]).and_return(true) + + user.enable_service([:wiki, :xmpp]) + end + end + + describe "#disable_service" do + before do + allow(user).to receive(:ldap_entry).and_return({ + uid: user.cn, ou: user.ou, mail: user.email, admin: nil, + service: ["discourse", "gitea", "xmpp"] + }) + allow(user).to receive(:dn).and_return(dn) + end + + it "removes the service from the LDAP entry" do + expect_any_instance_of(LdapService).to receive(:replace_attribute) + .with(dn, :service, ["discourse", "gitea"]).and_return(true) + + user.disable_service(:xmpp) + end + + it "removes multiple services from the LDAP entry" do + expect_any_instance_of(LdapService).to receive(:replace_attribute) + .with(dn, :service, ["discourse"]).and_return(true) + + user.disable_service([:xmpp, "gitea"]) + end + end + + describe "#disable_all_services" do + before do + allow(user).to receive(:dn).and_return(dn) + end + + it "removes all services from the LDAP entry" do + expect_any_instance_of(LdapService).to receive(:delete_attribute) + .with(dn, :service).and_return(true) + + user.disable_all_services + end + end + end From 7cff849d790c42eb5b71e0188692a92d41f75cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 1 Mar 2023 17:07:13 +0800 Subject: [PATCH 21/26] Add more users when seeding db --- db/seeds.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/seeds.rb b/db/seeds.rb index 98ae840..3671ad2 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,7 +10,7 @@ Sidekiq::Testing.inline! do ldap.add_attribute "cn=admin,ou=kosmos.org,cn=users,dc=kosmos,dc=org", :admin, "true" - 5.times do |n| + 35.times do |n| username = Faker::Name.unique.first_name.downcase email = Faker::Internet.unique.email From 3aad27c7bd12ef7b7b0a26bbd16b3e83b38815b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 1 Mar 2023 17:08:24 +0800 Subject: [PATCH 22/26] Add Pagy gem, config, styles --- Gemfile | 1 + Gemfile.lock | 2 + .../stylesheets/application.tailwind.css | 1 + .../stylesheets/components/pagination.css | 45 ++++ app/controllers/admin/base_controller.rb | 2 +- app/helpers/application_helper.rb | 4 +- config/initializers/pagy.rb | 250 ++++++++++++++++++ 7 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 app/assets/stylesheets/components/pagination.css create mode 100644 config/initializers/pagy.rb diff --git a/Gemfile b/Gemfile index 169f39e..97c230e 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,7 @@ gem 'net-ldap' # Utilities gem "rqrcode", "~> 2.0" gem 'rails-settings-cached', '~> 2.8.3' +gem 'pagy', '~> 6.0', '>= 6.0.2' # HTTP requests gem 'faraday' diff --git a/Gemfile.lock b/Gemfile.lock index 5a658b5..3e28d0f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -178,6 +178,7 @@ GEM nokogiri (1.13.9-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) + pagy (6.0.2) pg (1.2.3) public_suffix (5.0.0) puma (4.3.12) @@ -327,6 +328,7 @@ DEPENDENCIES listen (~> 3.2) lockbox net-ldap + pagy (~> 6.0, >= 6.0.2) pg (~> 1.2.3) puma (~> 4.1) rails (~> 7.0.2) diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 7a9da2e..5ee4651 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -7,4 +7,5 @@ @import "components/forms"; @import "components/links"; @import "components/notifications"; +@import "components/pagination"; @import "components/tables"; diff --git a/app/assets/stylesheets/components/pagination.css b/app/assets/stylesheets/components/pagination.css new file mode 100644 index 0000000..f6ab6c6 --- /dev/null +++ b/app/assets/stylesheets/components/pagination.css @@ -0,0 +1,45 @@ +@layer components { + .pagy-nav.pagination { + @apply isolate inline-flex -space-x-px rounded-md shadow-sm; + } + + .pagy-nav .page:not(.prev):not(.next) { + @apply hidden sm:inline-block; + } + + .pagy-nav .page.next a { + @apply relative inline-flex items-center rounded-r-md border + border-gray-300 bg-white px-3 py-2 text-sm font-medium + text-gray-500 hover:bg-gray-100 focus:z-20; + } + + .pagy-nav .page.prev a { + @apply relative inline-flex items-center rounded-l-md border + border-gray-300 bg-white px-3 py-2 text-sm font-medium + text-gray-500 hover:bg-gray-100 focus:z-20; + } + + .pagy-nav .page.next.disabled { + @apply relative inline-flex items-center rounded-r-md border + border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium + text-gray-400 focus:z-20; + } + + .pagy-nav .page.prev.disabled { + @apply relative inline-flex items-center rounded-l-md border + border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium + text-gray-400 focus:z-20; + } + + .pagy-nav .page a, .page.gap { + @apply bg-white border-gray-300 text-gray-500 hover:bg-gray-100 relative + inline-flex items-center border px-4 py-2 text-sm font-medium + focus:z-20; + } + + .pagy-nav .page.active { + @apply z-10 border-indigo-500 bg-indigo-50 text-indigo-600 relative + inline-flex items-center border px-4 py-2 text-sm font-medium + focus:z-20; + } +} diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 957328e..d504235 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -1,4 +1,5 @@ class Admin::BaseController < ApplicationController + include Pagy::Backend before_action :authenticate_user! before_action :authorize_admin @@ -7,5 +8,4 @@ class Admin::BaseController < ApplicationController def set_context @context = :admin end - end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 037be7c..b4b4f69 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,4 +1,6 @@ module ApplicationHelper + include Pagy::Frontend + def sats_to_btc(sats) sats.to_f / 100000000 end @@ -16,6 +18,4 @@ module ApplicationHelper def badge(text, color) tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800" end - end - diff --git a/config/initializers/pagy.rb b/config/initializers/pagy.rb new file mode 100644 index 0000000..16b8843 --- /dev/null +++ b/config/initializers/pagy.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +# Pagy initializer file (6.0.2) +# Customize only what you really need and notice that the core Pagy works also without any of the following lines. +# Should you just cherry pick part of this file, please maintain the require-order of the extras + + +# Pagy DEFAULT Variables +# See https://ddnexus.github.io/pagy/docs/api/pagy#variables +# All the Pagy::DEFAULT are set for all the Pagy instances but can be overridden per instance by just passing them to +# Pagy.new|Pagy::Countless.new|Pagy::Calendar::*.new or any of the #pagy* controller methods + + +# Instance variables +# See https://ddnexus.github.io/pagy/docs/api/pagy#instance-variables +# Pagy::DEFAULT[:page] = 1 # default +# Pagy::DEFAULT[:items] = 20 # default +# Pagy::DEFAULT[:outset] = 0 # default + + +# Other Variables +# See https://ddnexus.github.io/pagy/docs/api/pagy#other-variables +# Pagy::DEFAULT[:size] = [1,4,4,1] # default +# Pagy::DEFAULT[:page_param] = :page # default +# The :params can be also set as a lambda e.g ->(params){ params.exclude('useless').merge!('custom' => 'useful') } +# Pagy::DEFAULT[:params] = {} # default +# Pagy::DEFAULT[:fragment] = '#fragment' # example +# Pagy::DEFAULT[:link_extra] = 'data-remote="true"' # example +# Pagy::DEFAULT[:i18n_key] = 'pagy.item_name' # default +# Pagy::DEFAULT[:cycle] = true # example +# Pagy::DEFAULT[:request_path] = "/foo" # example + + +# Extras +# See https://ddnexus.github.io/pagy/categories/extra + + +# Backend Extras + +# Arel extra: For better performance utilizing grouped ActiveRecord collections: +# See: https://ddnexus.github.io/pagy/docs/extras/arel +# require 'pagy/extras/arel' + +# Array extra: Paginate arrays efficiently, avoiding expensive array-wrapping and without overriding +# See https://ddnexus.github.io/pagy/docs/extras/array +# require 'pagy/extras/array' + +# Calendar extra: Add pagination filtering by calendar time unit (year, quarter, month, week, day) +# See https://ddnexus.github.io/pagy/docs/extras/calendar +# require 'pagy/extras/calendar' +# Default for each unit +# Pagy::Calendar::Year::DEFAULT[:order] = :asc # Time direction of pagination +# Pagy::Calendar::Year::DEFAULT[:format] = '%Y' # strftime format +# +# Pagy::Calendar::Quarter::DEFAULT[:order] = :asc # Time direction of pagination +# Pagy::Calendar::Quarter::DEFAULT[:format] = '%Y-Q%q' # strftime format +# +# Pagy::Calendar::Month::DEFAULT[:order] = :asc # Time direction of pagination +# Pagy::Calendar::Month::DEFAULT[:format] = '%Y-%m' # strftime format +# +# Pagy::Calendar::Week::DEFAULT[:order] = :asc # Time direction of pagination +# Pagy::Calendar::Week::DEFAULT[:format] = '%Y-%W' # strftime format +# +# Pagy::Calendar::Day::DEFAULT[:order] = :asc # Time direction of pagination +# Pagy::Calendar::Day::DEFAULT[:format] = '%Y-%m-%d' # strftime format +# +# Uncomment the following lines, if you need calendar localization without using the I18n extra +# module LocalizePagyCalendar +# def localize(time, opts) +# ::I18n.l(time, **opts) +# end +# end +# Pagy::Calendar.prepend LocalizePagyCalendar + +# Countless extra: Paginate without any count, saving one query per rendering +# See https://ddnexus.github.io/pagy/docs/extras/countless +# require 'pagy/extras/countless' +# Pagy::DEFAULT[:countless_minimal] = false # default (eager loading) + +# Elasticsearch Rails extra: Paginate `ElasticsearchRails::Results` objects +# See https://ddnexus.github.io/pagy/docs/extras/elasticsearch_rails +# Default :pagy_search method: change only if you use also +# the searchkick or meilisearch extra that defines the same +# Pagy::DEFAULT[:elasticsearch_rails_pagy_search] = :pagy_search +# Default original :search method called internally to do the actual search +# Pagy::DEFAULT[:elasticsearch_rails_search] = :search +# require 'pagy/extras/elasticsearch_rails' + +# Headers extra: http response headers (and other helpers) useful for API pagination +# See http://ddnexus.github.io/pagy/extras/headers +# require 'pagy/extras/headers' +# Pagy::DEFAULT[:headers] = { page: 'Current-Page', +# items: 'Page-Items', +# count: 'Total-Count', +# pages: 'Total-Pages' } # default + +# Meilisearch extra: Paginate `Meilisearch` result objects +# See https://ddnexus.github.io/pagy/docs/extras/meilisearch +# Default :pagy_search method: change only if you use also +# the elasticsearch_rails or searchkick extra that define the same method +# Pagy::DEFAULT[:meilisearch_pagy_search] = :pagy_search +# Default original :search method called internally to do the actual search +# Pagy::DEFAULT[:meilisearch_search] = :ms_search +# require 'pagy/extras/meilisearch' + +# Metadata extra: Provides the pagination metadata to Javascript frameworks like Vue.js, react.js, etc. +# See https://ddnexus.github.io/pagy/docs/extras/metadata +# you must require the frontend helpers internal extra (BEFORE the metadata extra) ONLY if you need also the :sequels +# require 'pagy/extras/frontend_helpers' +# require 'pagy/extras/metadata' +# For performance reasons, you should explicitly set ONLY the metadata you use in the frontend +# Pagy::DEFAULT[:metadata] = %i[scaffold_url page prev next last] # example + +# Searchkick extra: Paginate `Searchkick::Results` objects +# See https://ddnexus.github.io/pagy/docs/extras/searchkick +# Default :pagy_search method: change only if you use also +# the elasticsearch_rails or meilisearch extra that defines the same +# DEFAULT[:searchkick_pagy_search] = :pagy_search +# Default original :search method called internally to do the actual search +# Pagy::DEFAULT[:searchkick_search] = :search +# require 'pagy/extras/searchkick' +# uncomment if you are going to use Searchkick.pagy_search +# Searchkick.extend Pagy::Searchkick + + +# Frontend Extras + +# Bootstrap extra: Add nav, nav_js and combo_nav_js helpers and templates for Bootstrap pagination +# See https://ddnexus.github.io/pagy/docs/extras/bootstrap +# require 'pagy/extras/bootstrap' + +# Bulma extra: Add nav, nav_js and combo_nav_js helpers and templates for Bulma pagination +# See https://ddnexus.github.io/pagy/docs/extras/bulma +# require 'pagy/extras/bulma' + +# Foundation extra: Add nav, nav_js and combo_nav_js helpers and templates for Foundation pagination +# See https://ddnexus.github.io/pagy/docs/extras/foundation +# require 'pagy/extras/foundation' + +# Materialize extra: Add nav, nav_js and combo_nav_js helpers for Materialize pagination +# See https://ddnexus.github.io/pagy/docs/extras/materialize +# require 'pagy/extras/materialize' + +# Navs extra: Add nav_js and combo_nav_js javascript helpers +# Notice: the other frontend extras add their own framework-styled versions, +# so require this extra only if you need the unstyled version +# See https://ddnexus.github.io/pagy/docs/extras/navs +# require 'pagy/extras/navs' + +# Semantic extra: Add nav, nav_js and combo_nav_js helpers for Semantic UI pagination +# See https://ddnexus.github.io/pagy/docs/extras/semantic +# require 'pagy/extras/semantic' + +# UIkit extra: Add nav helper and templates for UIkit pagination +# See https://ddnexus.github.io/pagy/docs/extras/uikit +# require 'pagy/extras/uikit' + +# Multi size var used by the *_nav_js helpers +# See https://ddnexus.github.io/pagy/docs/extras/navs#steps +# Pagy::DEFAULT[:steps] = { 0 => [2,3,3,2], 540 => [3,5,5,3], 720 => [5,7,7,5] } # example + + +# Feature Extras + +# Gearbox extra: Automatically change the number of items per page depending on the page number +# See https://ddnexus.github.io/pagy/docs/extras/gearbox +# require 'pagy/extras/gearbox' +# set to false only if you want to make :gearbox_extra an opt-in variable +# Pagy::DEFAULT[:gearbox_extra] = false # default true +# Pagy::DEFAULT[:gearbox_items] = [15, 30, 60, 100] # default + +# Items extra: Allow the client to request a custom number of items per page with an optional selector UI +# See https://ddnexus.github.io/pagy/docs/extras/items +# require 'pagy/extras/items' +# set to false only if you want to make :items_extra an opt-in variable +# Pagy::DEFAULT[:items_extra] = false # default true +# Pagy::DEFAULT[:items_param] = :items # default +# Pagy::DEFAULT[:max_items] = 100 # default + +# Overflow extra: Allow for easy handling of overflowing pages +# See https://ddnexus.github.io/pagy/docs/extras/overflow +# require 'pagy/extras/overflow' +# Pagy::DEFAULT[:overflow] = :empty_page # default (other options: :last_page and :exception) + +# Support extra: Extra support for features like: incremental, infinite, auto-scroll pagination +# See https://ddnexus.github.io/pagy/docs/extras/support +# require 'pagy/extras/support' + +# Trim extra: Remove the page=1 param from links +# See https://ddnexus.github.io/pagy/docs/extras/trim +# require 'pagy/extras/trim' +# set to false only if you want to make :trim_extra an opt-in variable +# Pagy::DEFAULT[:trim_extra] = false # default true + +# Standalone extra: Use pagy in non Rack environment/gem +# See https://ddnexus.github.io/pagy/docs/extras/standalone +# require 'pagy/extras/standalone' +# Pagy::DEFAULT[:url] = 'http://www.example.com/subdir' # optional default + + +# Rails +# Enable the .js file required by the helpers that use javascript +# (pagy*_nav_js, pagy*_combo_nav_js, and pagy_items_selector_js) +# See https://ddnexus.github.io/pagy/docs/api/javascript + +# With the asset pipeline +# Sprockets need to look into the pagy javascripts dir, so add it to the assets paths +# Rails.application.config.assets.paths << Pagy.root.join('javascripts') + +# I18n + +# Pagy internal I18n: ~18x faster using ~10x less memory than the i18n gem +# See https://ddnexus.github.io/pagy/docs/api/i18n +# Notice: No need to configure anything in this section if your app uses only "en" +# or if you use the i18n extra below +# +# Examples: +# load the "de" built-in locale: +# Pagy::I18n.load(locale: 'de') +# +# load the "de" locale defined in the custom file at :filepath: +# Pagy::I18n.load(locale: 'de', filepath: 'path/to/pagy-de.yml') +# +# load the "de", "en" and "es" built-in locales: +# (the first passed :locale will be used also as the default_locale) +# Pagy::I18n.load({ locale: 'de' }, +# { locale: 'en' }, +# { locale: 'es' }) +# +# load the "en" built-in locale, a custom "es" locale, +# and a totally custom locale complete with a custom :pluralize proc: +# (the first passed :locale will be used also as the default_locale) +# Pagy::I18n.load({ locale: 'en' }, +# { locale: 'es', filepath: 'path/to/pagy-es.yml' }, +# { locale: 'xyz', # not built-in +# filepath: 'path/to/pagy-xyz.yml', +# pluralize: lambda{ |count| ... } ) + + +# I18n extra: uses the standard i18n gem which is ~18x slower using ~10x more memory +# than the default pagy internal i18n (see above) +# See https://ddnexus.github.io/pagy/docs/extras/i18n +# require 'pagy/extras/i18n' + +# Default i18n key +# Pagy::DEFAULT[:i18n_key] = 'pagy.item_name' # default + + +# When you are done setting your own default freeze it, so it will not get changed accidentally +Pagy::DEFAULT.freeze From cbbb4c6e4751269fdad286bf355369bcaec331e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 1 Mar 2023 17:08:36 +0800 Subject: [PATCH 23/26] Add pagination to admin pages --- app/controllers/admin/donations_controller.rb | 3 ++- app/controllers/admin/invitations_controller.rb | 3 ++- app/controllers/admin/users_controller.rb | 2 +- app/views/admin/donations/index.html.erb | 3 ++- app/views/admin/invitations/index.html.erb | 3 ++- app/views/admin/users/index.html.erb | 3 ++- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/controllers/admin/donations_controller.rb b/app/controllers/admin/donations_controller.rb index ffbae14..c29fb80 100644 --- a/app/controllers/admin/donations_controller.rb +++ b/app/controllers/admin/donations_controller.rb @@ -5,7 +5,8 @@ class Admin::DonationsController < Admin::BaseController # GET /donations # GET /donations.json def index - @donations = Donation.all.order('created_at desc') + @pagy, @donations = pagy(Donation.all.order('created_at desc')) + @stats = { overall_sats: @donations.all.sum("amount_sats"), donor_count: Donation.distinct.count(:user_id) diff --git a/app/controllers/admin/invitations_controller.rb b/app/controllers/admin/invitations_controller.rb index ca9b407..97a33b3 100644 --- a/app/controllers/admin/invitations_controller.rb +++ b/app/controllers/admin/invitations_controller.rb @@ -1,7 +1,8 @@ class Admin::InvitationsController < Admin::BaseController def index @current_section = :invitations - @invitations_used = Invitation.used.order('used_at desc') + @pagy, @invitations_used = pagy(Invitation.used.order('used_at desc')) + @stats = { available: Invitation.unused.count, accepted: @invitations_used.length, diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index b23f468..2081e7a 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -6,7 +6,7 @@ class Admin::UsersController < Admin::BaseController ldap = LdapService.new @ou = params[:ou] || "kosmos.org" @orgs = ldap.fetch_organizations - @users = User.where(ou: @ou).order(cn: :asc).to_a + @pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc)) @stats = { users_confirmed: User.where(ou: @ou).confirmed.count, diff --git a/app/views/admin/donations/index.html.erb b/app/views/admin/donations/index.html.erb index 1074843..f13e94b 100644 --- a/app/views/admin/donations/index.html.erb +++ b/app/views/admin/donations/index.html.erb @@ -21,7 +21,7 @@
        <% if @donations.any? %>

        Recent Donations

        - +
        @@ -52,6 +52,7 @@ <% end %>
        User
        + <%== pagy_nav @pagy %> <% else %>

        No donations yet. diff --git a/app/views/admin/invitations/index.html.erb b/app/views/admin/invitations/index.html.erb index 51905c0..19e51be 100644 --- a/app/views/admin/invitations/index.html.erb +++ b/app/views/admin/invitations/index.html.erb @@ -24,7 +24,7 @@ <% if @invitations_used.any? %>

        Recently Accepted

        - +
        @@ -44,6 +44,7 @@ <% end %>
        Token
        + <%== pagy_nav @pagy %>
        <% end %> <% end %> diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb index 9e60ac7..e0eb90d 100644 --- a/app/views/admin/users/index.html.erb +++ b/app/views/admin/users/index.html.erb @@ -30,7 +30,7 @@ <% end %>
        - +
        @@ -49,5 +49,6 @@ <% end %>
        UID
        + <%== pagy_nav @pagy %>
        <% end %> From 251a170f2b02032d280beaff6af962bd74267d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 1 Mar 2023 17:14:44 +0800 Subject: [PATCH 24/26] Add documentation link for Pagy --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fe0bfb..b2e7205 100644 --- a/README.md +++ b/README.md @@ -81,12 +81,15 @@ with a fresh installation, delete both that directory as well as the container. ## Documentation +### Rails + * [Ruby on Rails](https://guides.rubyonrails.org/) -* [Sass](https://sass-lang.com/documentation) +* [Pagination](https://ddnexus.github.io/pagy/) ### Front-end * [Tailwind CSS](https://tailwindcss.com/) +* [Sass](https://sass-lang.com/documentation) ### Testing From 6c2a97e7e589d7fc97eb22cb42223429b333ecd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 1 Mar 2023 22:47:41 +0800 Subject: [PATCH 25/26] Improve design of service grid on dashboard --- .../stylesheets/application.tailwind.css | 1 + .../components/dashboard_services.css | 5 + app/views/dashboard/index.html.erb | 117 +++++++++++------- public/img/logos/icon_discourse.svg | 8 ++ public/img/logos/icon_droneci.svg | 9 ++ public/img/logos/icon_gitea.png | Bin 0 -> 46270 bytes public/img/logos/icon_lightning.svg | 1 + public/img/logos/icon_mastodon.svg | 48 +++++++ public/img/logos/icon_mediawiki.svg | 1 + public/img/logos/icon_xmpp.svg | 114 +++++++++++++++++ 10 files changed, 259 insertions(+), 45 deletions(-) create mode 100644 app/assets/stylesheets/components/dashboard_services.css create mode 100644 public/img/logos/icon_discourse.svg create mode 100644 public/img/logos/icon_droneci.svg create mode 100644 public/img/logos/icon_gitea.png create mode 100644 public/img/logos/icon_lightning.svg create mode 100644 public/img/logos/icon_mastodon.svg create mode 100644 public/img/logos/icon_mediawiki.svg create mode 100644 public/img/logos/icon_xmpp.svg diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 5ee4651..acf8aa9 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -4,6 +4,7 @@ @import "components/base"; @import "components/buttons"; +@import "components/dashboard_services"; @import "components/forms"; @import "components/links"; @import "components/notifications"; diff --git a/app/assets/stylesheets/components/dashboard_services.css b/app/assets/stylesheets/components/dashboard_services.css new file mode 100644 index 0000000..f725347 --- /dev/null +++ b/app/assets/stylesheets/components/dashboard_services.css @@ -0,0 +1,5 @@ +@layer components { + .services > div > a { + background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 0, rgba(255,255,255,0.88) 100%); + } +} diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 9f39bb4..9e24f78 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -2,60 +2,87 @@ <%= render MainSimpleComponent.new do %>
        -

        +

        Your Kosmos account and password currently give you access to these services:

        -
        -
        -

        - <%= link_to "Chat", "https://wiki.kosmos.org/Services:Chat", class: "ks-text-link" %> -

        -

        - Chat rooms and instant messaging (XMPP/Jabber) -

        +
        +
        + <%= link_to "https://wiki.kosmos.org/Services:Chat", + class: "block h-full px-6 py-6 rounded-md" do %> +

        Chat

        +

        + Federated chat rooms and instant messaging +

        + <% end %>
        -
        -

        - <%= link_to "Discourse", "https://community.kosmos.org", class: "ks-text-link" %> -

        -

        - Kosmos community forums and user support/help site -

        +
        + <%= link_to "https://community.kosmos.org", + class: "block h-full px-6 py-6 rounded-md" do %> +

        Discourse

        +

        + Kosmos community forums and user support/help site +

        + <% end %>
        -
        -

        - <%= render partial: "icons/zap", locals: { custom_class: "text-amber-500 h-4 w-4 inline" } %> - <%= link_to "Lightning Wallet", wallet_path, class: "ks-text-link" %> -

        -

        - Send and receive sats over the Bitcoin Lightning Network -

        +
        + <%= link_to "https://wiki.kosmos.org", + class: "block h-full px-6 py-6 rounded-md" do %> +

        Wiki

        +

        + Kosmos documentation and knowledge base +

        + <% end %>
        -
        -

        - <%= link_to "Wiki", "https://wiki.kosmos.org", class: "ks-text-link" %> -

        -

        - Kosmos documentation and knowledge base -

        +
        + <%= link_to wallet_path, + class: "block h-full px-6 py-6 rounded-md" do %> +

        Wallet

        +

        + Send and receive sats over the Bitcoin Lightning Network +

        + <% end %>
        -
        -

        - <%= link_to "Gitea", "https://gitea.kosmos.org", class: "ks-text-link" %> -

        -

        - Code hosting and collaboration for software projects -

        +
        + <%= link_to "https://gitea.kosmos.org", + class: "block h-full px-6 py-6 rounded-md" do %> +

        Gitea

        +

        + Code hosting and collaboration for software projects +

        + <% end %>
        -
        -

        - <%= link_to "Drone CI", "https://drone.kosmos.org", class: "ks-text-link" %> -

        -

        - Continuous integration for software projects on Gitea -

        +
        + <%= link_to "https://drone.kosmos.org", + class: "block h-full px-6 py-6 rounded-md" do %> +

        Drone CI

        +

        + Continuous integration for software projects on Gitea +

        + <% end %>
        + + + + + + + + + +
        <% end %> diff --git a/public/img/logos/icon_discourse.svg b/public/img/logos/icon_discourse.svg new file mode 100644 index 0000000..4cbb8c8 --- /dev/null +++ b/public/img/logos/icon_discourse.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/img/logos/icon_droneci.svg b/public/img/logos/icon_droneci.svg new file mode 100644 index 0000000..10f926a --- /dev/null +++ b/public/img/logos/icon_droneci.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/img/logos/icon_gitea.png b/public/img/logos/icon_gitea.png new file mode 100644 index 0000000000000000000000000000000000000000..1538cd1852b7674ddc70c042913cc4bfd05277a3 GIT binary patch literal 46270 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 literal 0 HcmV?d00001 diff --git a/public/img/logos/icon_lightning.svg b/public/img/logos/icon_lightning.svg new file mode 100644 index 0000000..406841e --- /dev/null +++ b/public/img/logos/icon_lightning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/logos/icon_mastodon.svg b/public/img/logos/icon_mastodon.svg new file mode 100644 index 0000000..7bd752a --- /dev/null +++ b/public/img/logos/icon_mastodon.svg @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/public/img/logos/icon_mediawiki.svg b/public/img/logos/icon_mediawiki.svg new file mode 100644 index 0000000..fdadd25 --- /dev/null +++ b/public/img/logos/icon_mediawiki.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/logos/icon_xmpp.svg b/public/img/logos/icon_xmpp.svg new file mode 100644 index 0000000..058c67a --- /dev/null +++ b/public/img/logos/icon_xmpp.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From dfb12b8f62db319930cb6cb0101e0f4e5bdd13d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 2 Mar 2023 15:54:03 +0800 Subject: [PATCH 26/26] Fix typo --- .../20230223115536_remove_ln_login_ciphertext_from_users.rb | 5 +++++ db/migrate/20230223115536_remove_ln_login_from_users.rb | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20230223115536_remove_ln_login_ciphertext_from_users.rb delete mode 100644 db/migrate/20230223115536_remove_ln_login_from_users.rb diff --git a/db/migrate/20230223115536_remove_ln_login_ciphertext_from_users.rb b/db/migrate/20230223115536_remove_ln_login_ciphertext_from_users.rb new file mode 100644 index 0000000..da1bf86 --- /dev/null +++ b/db/migrate/20230223115536_remove_ln_login_ciphertext_from_users.rb @@ -0,0 +1,5 @@ +class RemoveLnLoginCiphertextFromUsers < ActiveRecord::Migration[7.0] + def change + remove_column :users, :ln_login_ciphertext + end +end diff --git a/db/migrate/20230223115536_remove_ln_login_from_users.rb b/db/migrate/20230223115536_remove_ln_login_from_users.rb deleted file mode 100644 index 82ab8f1..0000000 --- a/db/migrate/20230223115536_remove_ln_login_from_users.rb +++ /dev/null @@ -1,5 +0,0 @@ -class RemoveLnLoginFromUsers < ActiveRecord::Migration[7.0] - def change - remove_column :users, :ln_login_cyphertext - end -end