diff --git a/Gemfile b/Gemfile index d691dee..20d1473 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ gem 'devise_ldap_authenticatable' gem 'net-ldap' # Utilities +gem 'aasm' gem "image_processing", "~> 1.12.2" gem "rqrcode", "~> 2.0" gem 'rails-settings-cached', '~> 2.8.3' diff --git a/Gemfile.lock b/Gemfile.lock index 396c65e..c96c0c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,8 @@ GEM remote: https://rubygems.org/ specs: + aasm (5.5.0) + concurrent-ruby (~> 1.0) actioncable (8.0.2) actionpack (= 8.0.2) activesupport (= 8.0.2) @@ -526,6 +528,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + aasm aws-sdk-s3 bcrypt (~> 3.1) capybara diff --git a/README.md b/README.md index 530186b..4b69035 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ command: ### Front-end +* [Icons](https://feathericons.com) * [Tailwind CSS](https://tailwindcss.com/) * [Sass](https://sass-lang.com/documentation) * [Stimulus](https://stimulus.hotwired.dev/handbook/) diff --git a/app/components/sidenav_link_component.rb b/app/components/sidenav_link_component.rb index 9436a67..8a3614b 100644 --- a/app/components/sidenav_link_component.rb +++ b/app/components/sidenav_link_component.rb @@ -29,7 +29,7 @@ class SidenavLinkComponent < ViewComponent::Base def class_names_icon(path) if @active - "text-teal-500 group-hover:text-teal-500 flex-shrink-0 -ml-1 mr-3 h-6 w-6" + "text-teal-600 group-hover:text-teal-600 flex-shrink-0 -ml-1 mr-3 h-6 w-6" elsif @disabled "text-gray-300 group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6" else diff --git a/app/controllers/admin/donations_controller.rb b/app/controllers/admin/donations_controller.rb index 51ea4e3..c12a868 100644 --- a/app/controllers/admin/donations_controller.rb +++ b/app/controllers/admin/donations_controller.rb @@ -4,11 +4,22 @@ class Admin::DonationsController < Admin::BaseController # GET /donations def index - @pagy, @donations = pagy(Donation.completed.order('paid_at desc')) + @username = params[:username].presence + + pending_scope = Donation.incomplete.joins(:user).order('paid_at desc') + completed_scope = Donation.completed.joins(:user).order('paid_at desc') + + if @username + pending_scope = pending_scope.where(users: { cn: @username }) + completed_scope = completed_scope.where(users: { cn: @username }) + end + + @pending_donations = pending_scope + @pagy, @donations = pagy(completed_scope) @stats = { - overall_sats: @donations.sum("amount_sats"), - donor_count: Donation.completed.count(:user_id) + overall_sats: completed_scope.sum("amount_sats"), + donor_count: completed_scope.distinct.count(:user_id) } end diff --git a/app/controllers/admin/invitations_controller.rb b/app/controllers/admin/invitations_controller.rb index 97a33b3..c5c0290 100644 --- a/app/controllers/admin/invitations_controller.rb +++ b/app/controllers/admin/invitations_controller.rb @@ -1,12 +1,28 @@ class Admin::InvitationsController < Admin::BaseController + before_action :set_current_section + def index - @current_section = :invitations - @pagy, @invitations_used = pagy(Invitation.used.order('used_at desc')) + @username = params[:username].presence + accepted_scope = Invitation.used.order('used_at desc') + unused_scope = Invitation.unused + + if @username + accepted_scope = accepted_scope.joins(:user).where(users: { cn: @username }) + unused_scope = unused_scope.joins(:user).where(users: { cn: @username }) + end + + @pagy, @invitations_used = pagy(accepted_scope) @stats = { - available: Invitation.unused.count, - accepted: @invitations_used.length, - users_with_referrals: Invitation.used.distinct.count(:user_id) + available: unused_scope.count, + accepted: accepted_scope.count, + users_with_referrals: accepted_scope.distinct.count(:user_id) } end + + private + + def set_current_section + @current_section = :invitations + end end diff --git a/app/controllers/admin/settings/membership_controller.rb b/app/controllers/admin/settings/membership_controller.rb new file mode 100644 index 0000000..f01e958 --- /dev/null +++ b/app/controllers/admin/settings/membership_controller.rb @@ -0,0 +1,23 @@ +class Admin::Settings::MembershipController < Admin::SettingsController + def show + end + + def update + update_settings + + redirect_to admin_settings_membership_path, flash: { + success: "Settings saved" + } + end + + private + + def setting_params + params.require(:setting).permit([ + :member_status_contributor, + :member_status_sustainer, + :user_index_show_contributors, + :user_index_show_sustainers + ]) + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index e73d208..e6504e4 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -4,18 +4,30 @@ 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 + @show_contributors = Setting.user_index_show_contributors + @show_sustainers = Setting.user_index_show_sustainers + + @contributors = ldap.search_users(:memberStatus, :contributor, :cn) if @show_contributors + @sustainers = ldap.search_users(:memberStatus, :sustainer, :cn) if @show_sustainers + @admins = ldap.search_users(:admin, true, :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 } + @stats[:users_contributing] = @contributors.size if @show_contributors + @stats[:users_paying] = @sustainers.size if @show_sustainers end # GET /admin/users/:username def show + @invitees = @user.invitees + @recent_invitees = @user.invitees.order(created_at: :desc).limit(5) + @more_invitees = (@invitees - @recent_invitees).count + if Setting.lndhub_admin_enabled? @lndhub_user = @user.lndhub_user end diff --git a/app/controllers/contributions/donations_controller.rb b/app/controllers/contributions/donations_controller.rb index b9f46c4..99e614e 100644 --- a/app/controllers/contributions/donations_controller.rb +++ b/app/controllers/contributions/donations_controller.rb @@ -11,7 +11,7 @@ class Contributions::DonationsController < ApplicationController def index @current_section = :contributions @donations_completed = current_user.donations.completed.order('paid_at desc') - @donations_pending = current_user.donations.processing.order('created_at desc') + @donations_processing = current_user.donations.processing.order('created_at desc') if Setting.lndhub_enabled? begin @@ -81,14 +81,11 @@ class Contributions::DonationsController < ApplicationController case invoice["status"] when "Settled" - @donation.paid_at = DateTime.now - @donation.payment_status = "settled" - @donation.save! + @donation.complete! flash_message = { success: "Thank you!" } when "Processing" unless @donation.processing? - @donation.payment_status = "processing" - @donation.save! + @donation.start_processing! flash_message = { success: "Thank you! We will send you an email when the payment is confirmed." } BtcpayCheckDonationJob.set(wait: 20.seconds).perform_later(@donation) end diff --git a/app/jobs/btcpay_check_donation_job.rb b/app/jobs/btcpay_check_donation_job.rb index 3cfdbcb..839dda8 100644 --- a/app/jobs/btcpay_check_donation_job.rb +++ b/app/jobs/btcpay_check_donation_job.rb @@ -10,9 +10,7 @@ class BtcpayCheckDonationJob < ApplicationJob case invoice["status"] when "Settled" - donation.paid_at = DateTime.now - donation.payment_status = "settled" - donation.save! + donation.complete! NotificationMailer.with(user: donation.user) .bitcoin_donation_confirmed diff --git a/app/models/concerns/settings/membership_settings.rb b/app/models/concerns/settings/membership_settings.rb new file mode 100644 index 0000000..bf4712c --- /dev/null +++ b/app/models/concerns/settings/membership_settings.rb @@ -0,0 +1,18 @@ +module Settings + module MembershipSettings + extend ActiveSupport::Concern + + included do + field :member_status_contributor, type: :string, + default: "Contributor" + field :member_status_sustainer, type: :string, + default: "Sustainer" + + # Admin panel + field :user_index_show_contributors, type: :boolean, + default: false + field :user_index_show_sustainers, type: :boolean, + default: false + end + end +end diff --git a/app/models/donation.rb b/app/models/donation.rb index 9799ae6..f3e585c 100644 --- a/app/models/donation.rb +++ b/app/models/donation.rb @@ -1,22 +1,42 @@ class Donation < ApplicationRecord - # Relations + include AASM + belongs_to :user - # Validations validates_presence_of :user validates_presence_of :donation_method, inclusion: { in: %w[ custom btcpay lndhub ] } validates_presence_of :payment_status, allow_nil: true, - inclusion: { in: %w[ processing settled ] } + inclusion: { in: %w[ pending processing settled ] } validates_presence_of :paid_at, allow_nil: true validates_presence_of :amount_sats, allow_nil: true validates_presence_of :fiat_amount, allow_nil: true validates_presence_of :fiat_currency, allow_nil: true, inclusion: { in: %w[ EUR USD ] } - #Scopes + scope :pending, -> { where(payment_status: "pending") } scope :processing, -> { where(payment_status: "processing") } - scope :completed, -> { where(payment_status: "settled") } + scope :completed, -> { where(payment_status: "settled") } + scope :incomplete, -> { where.not(payment_status: "settled") } + + aasm column: :payment_status do + state :pending, initial: true + state :processing + state :settled + + event :start_processing do + transitions from: :pending, to: :processing + end + + event :complete do + transitions from: :processing, to: :settled, after: [:set_paid_at, :set_sustainer_status] + transitions from: :pending, to: :settled, after: [:set_paid_at, :set_sustainer_status] + end + end + + def pending? + payment_status == "pending" + end def processing? payment_status == "processing" @@ -25,4 +45,17 @@ class Donation < ApplicationRecord def completed? payment_status == "settled" end + + private + + def set_paid_at + update paid_at: DateTime.now if paid_at.nil? + end + + def set_sustainer_status + user.add_member_status :sustainer + rescue => e + Sentry.capture_exception(e) if Setting.sentry_enabled? + Rails.logger.error("Failed to set memberStatus: #{e.message}") + end end diff --git a/app/models/setting.rb b/app/models/setting.rb index aa8072d..2c31ca8 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -16,6 +16,7 @@ class Setting < RailsSettings::Base include Settings::LightningNetworkSettings include Settings::MastodonSettings include Settings::MediaWikiSettings + include Settings::MembershipSettings include Settings::NostrSettings include Settings::OpenCollectiveSettings include Settings::RemoteStorageSettings 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/_username_search_form.html.erb b/app/views/admin/_username_search_form.html.erb new file mode 100644 index 0000000..b318c86 --- /dev/null +++ b/app/views/admin/_username_search_form.html.erb @@ -0,0 +1,11 @@ +<%= form_with url: path, method: :get, local: true, class: "flex gap-1" do %> + <%= text_field_tag :username, @username, placeholder: 'Filter by username' %> + <%= button_tag type: 'submit', name: nil, title: "Filter", class: 'btn-md btn-icon btn-outline' do %> + <%= render partial: "icons/filter", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %> + <% end %> + <% if @username %> + <%= link_to path, title: "Remove filter", class: 'btn-md btn-icon btn-outline' do %> + <%= render partial: "icons/x", locals: { custom_class: "text-red-600 h-4 w-4 inline" } %> + <% end %> + <% end %> +<% end %> diff --git a/app/views/admin/donations/_list.html.erb b/app/views/admin/donations/_list.html.erb new file mode 100644 index 0000000..aa405ec --- /dev/null +++ b/app/views/admin/donations/_list.html.erb @@ -0,0 +1,34 @@ + + + + + + + + + + + + + <% donations.each do |donation| %> + + + + + + + + + <% end %> + +
UserSatsFiat AmountPublic nameDate
<%= link_to donation.user.cn, admin_user_path(donation.user.cn), class: 'ks-text-link' %><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %><%= donation.public_name %><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : donation.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %> + <%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %> + <%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %> + <%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red', + data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %> +
+<% if defined?(pagy) %> +
+ <%== pagy_nav pagy %> +
+<% end %> diff --git a/app/views/admin/donations/index.html.erb b/app/views/admin/donations/index.html.erb index 90a755e..2e80ba2 100644 --- a/app/views/admin/donations/index.html.erb +++ b/app/views/admin/donations/index.html.erb @@ -5,7 +5,7 @@ <%= render QuickstatsContainerComponent.new do %> <%= render QuickstatsItemComponent.new( type: :number, - title: 'Overall', + title: 'Received', value: @stats[:overall_sats], unit: 'sats' ) %> @@ -19,41 +19,28 @@
- <% if @donations.any? %> -

Recent Donations

- - - - - - - - - - - - - <% @donations.each do |donation| %> - - - - - - - - - <% end %> - -
UserSatsFiat AmountPublic nameDate
<%= link_to donation.user.cn, admin_user_path(donation.user.cn), class: 'ks-text-link' %><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %><%= donation.public_name %><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %> - <%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %> - <%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %> - <%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red', - data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %> -
- <%== pagy_nav @pagy %> + <%= render partial: "admin/username_search_form", + locals: { path: admin_donations_path } %> +
+ + <% if @pending_donations.present? %> +
+

Pending

+ <%= render partial: "admin/donations/list", locals: { + donations: @pending_donations + } %> +
+ <% end %> + +
+ <% if @donations.present? %> +

Received

+ <%= render partial: "admin/donations/list", locals: { + donations: @donations, pagy: @pagy + } %> <% else %>

- No donations yet. + No donations received yet.

<% end %>
diff --git a/app/views/admin/donations/show.html.erb b/app/views/admin/donations/show.html.erb index 00b32cc..3e3ad65 100644 --- a/app/views/admin/donations/show.html.erb +++ b/app/views/admin/donations/show.html.erb @@ -25,7 +25,15 @@ <%= @donation.public_name %> - Date + Payment status + <%= @donation.payment_status %> + + + Created at + <%= @donation.created_at&.strftime("%Y-%m-%d (%H:%M UTC)") %> + + + Paid at <%= @donation.paid_at&.strftime("%Y-%m-%d (%H:%M UTC)") %> diff --git a/app/views/admin/invitations/index.html.erb b/app/views/admin/invitations/index.html.erb index 7c2cd43..acc5606 100644 --- a/app/views/admin/invitations/index.html.erb +++ b/app/views/admin/invitations/index.html.erb @@ -21,9 +21,15 @@ ) %> <% end %> + +
+ <%= render partial: "admin/username_search_form", + locals: { path: admin_invitations_path } %> +
+ <% if @invitations_used.any? %>
-

Recently Accepted

+

Accepted

diff --git a/app/views/admin/settings/membership/show.html.erb b/app/views/admin/settings/membership/show.html.erb new file mode 100644 index 0000000..5b403d6 --- /dev/null +++ b/app/views/admin/settings/membership/show.html.erb @@ -0,0 +1,53 @@ +<%= render HeaderComponent.new(title: "Settings") %> + +<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %> + <%= form_for(Setting.new, url: admin_settings_membership_path, method: :put) do |f| %> +
+

Membership

+ + <% if @errors && @errors.any? %> + <%= render partial: "admin/settings/errors", locals: { errors: @errors } %> + <% end %> + + +
+ +
+

Admin panel

+ + +
+ +
+

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

+
+ <% end %> +<% end %> diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb index 830057a..66bf6f6 100644 --- a/app/views/admin/users/index.html.erb +++ b/app/views/admin/users/index.html.erb @@ -13,6 +13,20 @@ title: 'Pending', value: @stats[:users_pending], ) %> + <% if @show_contributors %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: Setting.member_status_contributor.pluralize, + value: @stats[:users_contributing], + ) %> + <% end %> + <% if @show_sustainers %> + <%= render QuickstatsItemComponent.new( + type: :number, + title: Setting.member_status_sustainer.pluralize, + value: @stats[:users_paying], + ) %> + <% end %> <% end %> @@ -29,8 +43,12 @@ <% @users.each do |user| %> - - + + <% end %> diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb index 2c101b8..367b5a7 100644 --- a/app/views/admin/users/show.html.erb +++ b/app/views/admin/users/show.html.erb @@ -32,6 +32,30 @@ + + + + + + + + <% end %> diff --git a/app/views/contributions/donations/index.html.erb b/app/views/contributions/donations/index.html.erb index 3e03617..2e9d33c 100644 --- a/app/views/contributions/donations/index.html.erb +++ b/app/views/contributions/donations/index.html.erb @@ -22,17 +22,17 @@ - <% if @donations_pending.any? %> + <% if @donations_processing.any? %>

Pending

<%= render partial: "contributions/donations/list", - locals: { donations: @donations_pending } %> + locals: { donations: @donations_processing } %>
<% end %> <% if @donations_completed.any? %>
-

Past contributions

+

Contributions

<%= render partial: "contributions/donations/list", locals: { donations: @donations_completed } %>
diff --git a/app/views/icons/_filter.html.erb b/app/views/icons/_filter.html.erb index 38a47e0..5adc889 100644 --- a/app/views/icons/_filter.html.erb +++ b/app/views/icons/_filter.html.erb @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/views/icons/_server.html.erb b/app/views/icons/_server.html.erb index 54ce094..f97725e 100644 --- a/app/views/icons/_server.html.erb +++ b/app/views/icons/_server.html.erb @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/views/icons/_users.html.erb b/app/views/icons/_users.html.erb index aacf6b0..81970a1 100644 --- a/app/views/icons/_users.html.erb +++ b/app/views/icons/_users.html.erb @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/views/shared/_admin_sidenav_settings.html.erb b/app/views/shared/_admin_sidenav_settings.html.erb index 48c5ced..14efb00 100644 --- a/app/views/shared/_admin_sidenav_settings.html.erb +++ b/app/views/shared/_admin_sidenav_settings.html.erb @@ -3,7 +3,11 @@ active: current_page?(admin_settings_registrations_path) ) %> <%= render SidenavLinkComponent.new( - name: "Services", path: admin_settings_services_path, icon: "grid", + name: "Membership", path: admin_settings_membership_path, icon: "users", + active: current_page?(admin_settings_membership_path) +) %> +<%= render SidenavLinkComponent.new( + name: "Services", path: admin_settings_services_path, icon: "server", active: controller_name == "services" ) %> <% if controller_name == "services" %> diff --git a/config/routes.rb b/config/routes.rb index 759abcc..115fadd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -109,6 +109,7 @@ Rails.application.routes.draw do namespace :settings do resource 'registrations', only: ['show', 'update'] + resource 'membership', only: ['show', 'update'], controller: 'membership' resources 'services', param: 'service', only: ['index', 'show', 'update'] end end diff --git a/db/migrate/20250527113805_update_payment_status_to_pending.rb b/db/migrate/20250527113805_update_payment_status_to_pending.rb new file mode 100644 index 0000000..f334c21 --- /dev/null +++ b/db/migrate/20250527113805_update_payment_status_to_pending.rb @@ -0,0 +1,6 @@ +class UpdatePaymentStatusToPending < ActiveRecord::Migration[8.0] + def change + Donation.where(payment_status: nil).update_all(payment_status: "pending") + Donation.where.not(payment_status: %w[pending processing settled]).update_all(payment_status: "pending") + end +end diff --git a/db/schema.rb b/db/schema.rb index 1140431..b7746ac 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[8.0].define(version: 2025_05_17_105755) do +ActiveRecord::Schema[8.0].define(version: 2025_05_27_113805) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false 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/jobs/btcpay_check_donation_job_spec.rb b/spec/jobs/btcpay_check_donation_job_spec.rb index 71cd1df..6e12adb 100644 --- a/spec/jobs/btcpay_check_donation_job_spec.rb +++ b/spec/jobs/btcpay_check_donation_job_spec.rb @@ -8,16 +8,18 @@ RSpec.describe BtcpayCheckDonationJob, type: :job do user.donations.create!( donation_method: "btcpay", btcpay_invoice_id: "K4e31MhbLKmr3D7qoNYRd3", - paid_at: nil, payment_status: "processing", - fiat_amount: 120, fiat_currency: "USD" + paid_at: nil, + payment_status: "processing", + fiat_amount: 120, + fiat_currency: "USD" ) end before do allow_any_instance_of(User).to receive(:ldap_entry).and_return({ - uid: user.cn, ou: user.ou, mail: user.email, admin: nil, - display_name: nil + uid: user.cn, ou: user.ou, mail: user.email, admin: nil, display_name: nil }) + allow_any_instance_of(User).to receive(:add_member_status) end after(:each) do @@ -65,15 +67,20 @@ RSpec.describe BtcpayCheckDonationJob, type: :job do it "notifies the user via email" do perform_enqueued_jobs(only: described_class) { job } expect(enqueued_jobs.size).to eq(1) - job = enqueued_jobs.select{|j| j['job_class'] == "ActionMailer::MailDeliveryJob"}.first + job = enqueued_jobs.select { |j| j['job_class'] == "ActionMailer::MailDeliveryJob" }.first expect(job['arguments'][0]).to eq('NotificationMailer') expect(job['arguments'][1]).to eq('bitcoin_donation_confirmed') - expect(job['arguments'][3]['params']['user']['_aj_globalid']).to eq('gid://akkounts/User/1') + expect(job['arguments'][3]['params']['user']['_aj_globalid']).to eq(user.to_global_id.to_s) end it "does not enqueue itself again" do expect_any_instance_of(described_class).not_to receive(:re_enqueue_job) perform_enqueued_jobs(only: described_class) { job } end + + it "updates the user's member status" do + expect_any_instance_of(User).to receive(:add_member_status).with(:sustainer) + perform_enqueued_jobs(only: described_class) { job } + end end end 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 diff --git a/spec/requests/contributions/donations_spec.rb b/spec/requests/contributions/donations_spec.rb index 01397eb..e6d1392 100644 --- a/spec/requests/contributions/donations_spec.rb +++ b/spec/requests/contributions/donations_spec.rb @@ -177,7 +177,7 @@ RSpec.describe "Donations", type: :request do .to_return(status: 200, headers: {}, body: invoice) stub_request(:get, "http://btcpay.example.com/api/v1/stores/123456/invoices/MCkDbf2cUgBuuisUCgnRnb/payment-methods") .to_return(status: 200, headers: {}, body: payments) - + allow(user).to receive(:add_member_status).with(:sustainer).and_return(["sustainer"]) get confirm_btcpay_contributions_donation_path(subject) end @@ -185,11 +185,16 @@ RSpec.describe "Donations", type: :request do subject.reload expect(subject.paid_at).not_to be_nil expect(subject.amount_sats).to eq(2061) + expect(subject.payment_status).to eq("settled") end it "redirects to the donations index" do expect(response).to redirect_to(contributions_donations_url) end + + it "updates the user's member status" do + expect(user).to have_received(:add_member_status).with(:sustainer) + end end describe "amount in sats" do
<%= 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) : "" %> + <% if @show_contributors %><%= @contributors.include?(user.cn) ? badge("contributor", :green) : "" %><% end %> + <% if @show_sustainers %><%= @sustainers.include?(user.cn) ? badge("sustainer", :green) : "" %><% end %> + <%= @admins.include?(user.cn) ? badge("admin", :red) : "" %>
Roles <%= @user.is_admin? ? badge("admin", :red) : "—" %>
Status + <% if @user.is_contributing_member? || @user.is_paying_member? %> + <%= @user.is_contributing_member? ? badge("contributor", :green) : "" %> + <%= @user.is_paying_member? ? badge("sustainer", :green) : "" %> + <% else %> + — + <% end %> +
Donations + <% if @user.donations.any? %> + <%= link_to admin_donations_path(username: @user.cn), class: "ks-text-link" do %> + <%= @user.donations.completed.count %> for + <%= number_with_delimiter @user.donations.completed.sum("amount_sats") %> sats + <% end %> + <% else %> + — + <% end %> +
Invited by @@ -75,10 +99,13 @@
Invited users - <% if @user.invitees.length > 0 %> + <% if @invitees.any? %>
    - <% @user.invitees.order(cn: :asc).each do |invitee| %> -
  • <%= link_to invitee.cn, admin_user_path(invitee.cn), class: 'ks-text-link' %>
  • + <% @recent_invitees.each do |invitee| %> +
  • <%= link_to invitee.cn, admin_user_path(invitee.cn), class: "ks-text-link" %>
  • + <% end %> + <% if @more_invitees > 0 %> +
  • and <%= link_to "#{@more_invitees} more", admin_invitations_path(username: @user.cn), class: "ks-text-link" %>
  • <% end %>
<% else %>—<% end %> @@ -267,7 +294,9 @@ ) %>
+ <% if @user.nostr_pubkey.present? %> <%= link_to "Open profile", "https://njump.me/#{@user.nostr_pubkey_bech32}", class: "btn-sm btn-gray" %> + <% end %>