From 393f85e45ce71f7b5fe847c55bbbc15e9eeb2e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 27 May 2025 13:32:58 +0400 Subject: [PATCH] WIP Add member/contributor status to users --- app/controllers/admin/users_controller.rb | 16 ++++-- .../concerns/settings/member_settings.rb | 10 ++++ app/models/user.rb | 52 +++++++++++++++---- app/services/ldap_service.rb | 29 +++++++---- app/views/admin/users/index.html.erb | 18 ++++++- app/views/admin/users/show.html.erb | 7 +++ lib/tasks/ldap.rake | 4 +- schemas/ldap/member_status.ldif | 8 +++ spec/models/user_spec.rb | 2 +- 9 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 app/models/concerns/settings/member_settings.rb create mode 100644 schemas/ldap/member_status.ldif diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index e73d208..105241f 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -4,13 +4,19 @@ class Admin::UsersController < Admin::BaseController # GET /admin/users def index - ldap = LdapService.new - @ou = Setting.primary_domain - @pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc)) + ldap = LdapService.new + ou = Setting.primary_domain + + @admins = ldap.search_users(:admin, true, :cn) + @contributors = ldap.search_users(:memberStatus, :contributor, :cn) + @sustainers = ldap.search_users(:memberStatus, :sustainer, :cn) + @pagy, @users = pagy(User.where(ou: ou).order(cn: :asc)) @stats = { - users_confirmed: User.where(ou: @ou).confirmed.count, - users_pending: User.where(ou: @ou).pending.count + users_confirmed: User.where(ou: ou).confirmed.count, + users_pending: User.where(ou: ou).pending.count, + users_contributing: @contributors.size, + users_paying: @sustainers.size } end diff --git a/app/models/concerns/settings/member_settings.rb b/app/models/concerns/settings/member_settings.rb new file mode 100644 index 0000000..09c648b --- /dev/null +++ b/app/models/concerns/settings/member_settings.rb @@ -0,0 +1,10 @@ +module Settings + module MemberSettings + extend ActiveSupport::Concern + + included do + field :member_default_status, type: :string, + default: ENV["MEMBER_DEFAULT_STATUS"].presence + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index a0c49b0..4479800 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -163,7 +163,21 @@ class User < ApplicationRecord def ldap_entry(reload: false) return @ldap_entry if defined?(@ldap_entry) && !reload - @ldap_entry = ldap.fetch_users(uid: self.cn, ou: self.ou).first + @ldap_entry = ldap.fetch_users(cn: self.cn).first + end + + def add_to_ldap_array(attr_key, ldap_attr, value) + current_entries = ldap_entry[attr_key.to_sym] || [] + new_entries = Array(value).map(&:to_s) + entries = (current_entries + new_entries).uniq.sort + ldap.replace_attribute(dn, ldap_attr.to_sym, entries) + end + + def remove_from_ldap_array(attr_key, ldap_attr, value) + current_entries = ldap_entry[attr_key.to_sym] || [] + entries_to_remove = Array(value).map(&:to_s) + entries = (current_entries - entries_to_remove).uniq.sort + ldap.replace_attribute(dn, ldap_attr.to_sym, entries) end def display_name @@ -220,21 +234,39 @@ class User < ApplicationRecord end def enable_service(service) - current_services = services_enabled - new_services = Array(service).map(&:to_s) - services = (current_services + new_services).uniq.sort - ldap.replace_attribute(dn, :serviceEnabled, services) + add_to_ldap_array :services_enabled, :serviceEnabled, service + ldap_entry(reload: true)[:services_enabled] end def disable_service(service) - current_services = services_enabled - disabled_services = Array(service).map(&:to_s) - services = (current_services - disabled_services).uniq.sort - ldap.replace_attribute(dn, :serviceEnabled, services) + remove_from_ldap_array :services_enabled, :serviceEnabled, service + ldap_entry(reload: true)[:services_enabled] end def disable_all_services - ldap.delete_attribute(dn,:service) + ldap.delete_attribute(dn, :serviceEnabled) + end + + def member_status + ldap_entry[:member_status] || [] + end + + def add_member_status(status) + add_to_ldap_array :member_status, :memberStatus, status + ldap_entry(reload: true)[:member_status] + end + + def remove_member_status(status) + remove_from_ldap_array :member_status, :memberStatus, status + ldap_entry(reload: true)[:member_status] + end + + def is_contributing_member? + member_status.map(&:to_sym).include?(:contributor) + end + + def is_paying_member? + member_status.map(&:to_sym).include?(:sustainer) end private diff --git a/app/services/ldap_service.rb b/app/services/ldap_service.rb index 316a58a..a7e9ff5 100644 --- a/app/services/ldap_service.rb +++ b/app/services/ldap_service.rb @@ -50,19 +50,17 @@ class LdapService < ApplicationService end def fetch_users(args={}) - if args[:ou] - treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}" - else - treebase = ldap_config["base"] - end - attributes = %w[ - dn cn uid mail displayName admin serviceEnabled + dn cn uid mail displayName admin serviceEnabled memberStatus mailRoutingAddress mailpassword nostrKey pgpKey ] - filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") + filter = Net::LDAP::Filter.eq('objectClass', 'person') & + Net::LDAP::Filter.eq("cn", args[:cn] || "*") - entries = client.search(base: treebase, filter: filter, attributes: attributes) + entries = client.search( + base: ldap_config["base"], filter: filter, + attributes: attributes + ) entries.sort_by! { |e| e.cn[0] } entries = entries.collect do |e| { @@ -71,6 +69,7 @@ class LdapService < ApplicationService display_name: e.try(:displayName) ? e.displayName.first : nil, admin: e.try(:admin) ? 'admin' : nil, services_enabled: e.try(:serviceEnabled), + member_status: e.try(:memberStatus), email_maildrop: e.try(:mailRoutingAddress), email_password: e.try(:mailpassword), nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil, @@ -79,10 +78,20 @@ class LdapService < ApplicationService end end + def search_users(search_attr, value, return_attr) + filter = Net::LDAP::Filter.eq('objectClass', 'person') & + Net::LDAP::Filter.eq(search_attr.to_s, value.to_s) & + Net::LDAP::Filter.present('cn') + entries = client.search( + base: ldap_config["base"], filter: filter, + attributes: [return_attr] + ) + entries.map { |entry| entry[return_attr].first }.compact + end + def fetch_organizations attributes = %w{dn ou description} filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit") - # filter = Net::LDAP::Filter.eq("objectClass", "*") treebase = "cn=users,#{ldap_suffix}" entries = client.search(base: treebase, filter: filter, attributes: attributes) diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb index 830057a..c2521e8 100644 --- a/app/views/admin/users/index.html.erb +++ b/app/views/admin/users/index.html.erb @@ -13,6 +13,16 @@ title: 'Pending', value: @stats[:users_pending], ) %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Contributors', + value: @stats[:users_contributing], + ) %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: 'Sustainers', + value: @stats[:users_paying], + ) %> <% end %> @@ -29,8 +39,12 @@ <% @users.each do |user| %> <%= link_to(user.cn, admin_user_path(user.cn), class: 'ks-text-link') %> - <%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %> - <%= user.is_admin? ? badge("admin", :red) : "" %> + + <%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %> + <%= @contributors.include?(user.cn) ? badge("contributor", :green) : "" %> + <%= @sustainers.include?(user.cn) ? badge("sustainer", :green) : "" %> + + <%= @admins.include?(user.cn) ? badge("admin", :red) : "" %> <% end %> diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index 2c101b8..b5bfd5d 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -32,6 +32,13 @@ Roles <%= @user.is_admin? ? badge("admin", :red) : "—" %> + + Status + + <%= @user.is_contributing_member? ? badge("contributor", :green) : "" %> + <%= @user.is_paying_member? ? badge("sustainer", :green) : "" %> + + Invited by diff --git a/lib/tasks/ldap.rake b/lib/tasks/ldap.rake index 244a7d2..9f078bf 100644 --- a/lib/tasks/ldap.rake +++ b/lib/tasks/ldap.rake @@ -21,7 +21,7 @@ namespace :ldap do desc "Add custom attributes to schema" task add_custom_attributes: :environment do |t, args| - %w[ admin service_enabled nostr_key pgp_key ].each do |name| + %w[ admin service_enabled member_status nostr_key pgp_key ].each do |name| Rake::Task["ldap:modify_ldap_schema"].invoke(name, "add") Rake::Task['ldap:modify_ldap_schema'].reenable end @@ -29,7 +29,7 @@ namespace :ldap do desc "Delete custom attributes from schema" task delete_custom_attributes: :environment do |t, args| - %w[ admin service_enabled nostr_key pgp_key ].each do |name| + %w[ admin service_enabled member_status nostr_key pgp_key ].each do |name| Rake::Task["ldap:modify_ldap_schema"].invoke(name, "delete") Rake::Task['ldap:modify_ldap_schema'].reenable end diff --git a/schemas/ldap/member_status.ldif b/schemas/ldap/member_status.ldif new file mode 100644 index 0000000..4b55f6f --- /dev/null +++ b/schemas/ldap/member_status.ldif @@ -0,0 +1,8 @@ +dn: cn=schema +changetype: modify +add: attributeTypes +attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.3 + NAME 'memberStatus' + DESC 'Current member/contributor status' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a4e0f57..9b96edc 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -154,7 +154,7 @@ RSpec.describe User, type: :model do it "removes all services from the LDAP entry" do expect_any_instance_of(LdapService).to receive(:delete_attribute) - .with(dn, :service).and_return(true) + .with(dn, :serviceEnabled).and_return(true) user.disable_all_services end