require 'nostr' class User < ApplicationRecord include EmailValidatable attr_accessor :current_password attr_accessor :display_name attr_accessor :avatar_new attr_accessor :pgp_pubkey serialize :preferences, coder: UserPreferences # # 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_many :remote_storage_authorizations has_many :zaps has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user", primary_key: "lndhub_username", foreign_key: "login" has_many :accounts, through: :lndhub_user # # Attachments # has_one_attached :avatar # # Validations # validates_uniqueness_of :cn, scope: :ou validates_length_of :cn, minimum: 3 validates_format_of :cn, with: /\A([a-z0-9\-])*\z/, if: Proc.new{ |u| u.cn.present? }, message: "is invalid. Please use only letters, numbers and -" validates_format_of :cn, without: /\A-/, if: Proc.new{ |u| u.cn.present? }, message: "is invalid. Usernames need to start with a letter." # FIXME This needs a server restart to apply values validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i, message: "has already been taken", unless: Proc.new{ |u| u.persisted? } validates_uniqueness_of :email validates :email, email: true validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true, if: -> { defined?(@display_name) } validate :acceptable_avatar validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? } # # Scopes # scope :confirmed, -> { where.not(confirmed_at: nil) } scope :pending, -> { where(confirmed_at: nil) } scope :all_except, -> (user) { where.not(id: user) } # # Encrypted database columns # encrypts :lndhub_password # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :ldap_authenticatable, :confirmable, :recoverable, :validatable, :timeoutable, :rememberable # # Methods # def ldap_before_save self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first 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 self.confirmed_at = DateTime.now end end def devise_after_confirmation if ldap_entry[:mail] != self.email # E-Mail update confirmed LdapManager::UpdateEmail.call(dn: self.dn, address: self.email) else # E-Mail from signup confirmed (i.e. account activation) enable_default_services # TODO enable in development when we have easy setup of ejabberd etc. return if Rails.env.development? || !Setting.ejabberd_enabled? XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present? XmppSetDefaultBookmarksJob.perform_later(self) end end def send_devise_notification(notification, *args) devise_mailer.send(notification, self, *args).deliver_later end def reset_password(new_password, new_password_confirmation) self.password = new_password self.password_confirmation = new_password_confirmation return false unless valid? Devise::LDAP::Adapter.update_password(login_with, new_password) clear_reset_password_token save end def is_admin? @admin ||= if admin = Devise::LDAP::Adapter.get_ldap_param(self.cn, :admin) !!admin.first else false end end def address "#{self.cn}@#{self.ou}" end def mastodon_address return nil unless Setting.mastodon_enabled? "#{self.cn.gsub("-", "_")}@#{Setting.mastodon_address_domain}" end def valid_attribute?(attribute_name) self.valid? self.errors[attribute_name].blank? end def enable_default_services enable_service Setting.default_services end def dn return @dn if defined?(@dn) @dn = Devise::LDAP::Adapter.get_dn(self.cn) end def ldap_entry(reload: false) return @ldap_entry if defined?(@ldap_entry) && !reload @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 @display_name ||= ldap_entry[:display_name] end # TODO Variant keys are currently broken for some reason # (They use the same key as the main blob, when it should be # "/variants/#{key)" # def avatar_variant(size: :medium) # dimensions = case size # when :large then [400, 400] # when :medium then [256, 256] # when :small then [64, 64] # else [256, 256] # end # format = avatar.content_type == "image/png" ? :png : :jpeg # avatar.variant(resize_to_fill: dimensions, format: format) # end def nostr_pubkey @nostr_pubkey ||= ldap_entry[:nostr_key] end def nostr_pubkey_bech32 return nil unless nostr_pubkey.present? Nostr::PublicKey.new(nostr_pubkey).to_bech32 end def pgp_pubkey @pgp_pubkey ||= ldap_entry[:pgp_key] end def gnupg_key return nil unless pgp_pubkey.present? GPGME::Key.import(pgp_pubkey) GPGME::Key.get(pgp_fpr) end def pgp_pubkey_contains_user_address? gnupg_key.uids.map(&:email).include?(address) end def wkd_hash ZBase32.encode(Digest::SHA1.digest(cn)) end def services_enabled ldap_entry[:services_enabled] || [] end def service_enabled?(name) services_enabled.map(&:to_sym).include?(name.to_sym) end def enable_service(service) add_to_ldap_array :services_enabled, :serviceEnabled, service ldap_entry(reload: true)[:services_enabled] end def disable_service(service) remove_from_ldap_array :services_enabled, :serviceEnabled, service ldap_entry(reload: true)[:services_enabled] end def disable_all_services 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 def ldap return @ldap_service if defined?(@ldap_service) @ldap_service = LdapService.new end def acceptable_avatar return unless avatar_new.present? if avatar_new.size > 1.megabyte errors.add(:avatar, "must be less than 1MB file size") end acceptable_types = ["image/jpeg", "image/png"] unless acceptable_types.include?(avatar_new.content_type) errors.add(:avatar, "must be a JPEG or PNG file") end end def acceptable_pgp_key_format unless GPGME::Key.valid?(pgp_pubkey) errors.add(:pgp_pubkey, 'is not a valid armored PGP public key block') end end end