require "securerandom" require "bcrypt" class SettingsController < ApplicationController before_action :authenticate_user! before_action :set_main_nav_section before_action :set_settings_section, only: [:show, :update, :update_email, :reset_email_password] before_action :set_user, only: [:show, :update, :update_email, :reset_email_password] def index redirect_to setting_path(:profile) end def show case @settings_section when "lightning" @notifications_enabled = @user.preferences[:lightning_notify_sats_received] != "disabled" || @user.preferences[:lightning_notify_zap_received] != "disabled" when "nostr" session[:shared_secret] ||= SecureRandom.base64(12) end end # PUT /settings/:section def update @user.preferences.merge!(user_params[:preferences] || {}) @user.display_name = user_params[:display_name] @user.avatar_new = user_params[:avatar_new] @user.pgp_pubkey = user_params[:pgp_pubkey] if @user.save if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name]) LdapManager::UpdateDisplayName.call(dn: @user.dn, display_name: @user.display_name) end if @user.avatar_new.present? if store_user_avatar LdapManager::UpdateAvatar.call(user: @user) XmppSetAvatarJob.perform_later(user: @user) if Setting.ejabberd_enabled? else @validation_errors = @user.errors render :show, status: :unprocessable_entity and return end end if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key]) UserManager::UpdatePgpKey.call(user: @user) end redirect_to setting_path(@settings_section), flash: { success: 'Settings saved.' } else @validation_errors = @user.errors render :show, status: :unprocessable_entity end end # POST /settings/update_email def update_email if @user.valid_ldap_authentication?(security_params[:current_password]) if @user.update email: email_params[:email] redirect_to setting_path(:account), flash: { notice: 'Please confirm your new address using the confirmation link we just sent you.' } else @validation_errors = @user.errors render :show, status: :unprocessable_entity end else redirect_to setting_path(:account), flash: { error: 'Password did not match your current password. Try again.' } end end # POST /settings/reset_email_password def reset_email_password @user.current_password = security_params[:current_password] if @user.valid_ldap_authentication?(@user.current_password) @user.current_password = nil session[:new_email_password] = generate_email_password hashed_password = hash_email_password(session[:new_email_password]) LdapManager::UpdateEmailPassword.call(dn: @user.dn, password_hash: hashed_password) if @user.ldap_entry[:email_maildrop] != @user.address LdapManager::UpdateEmailMaildrop.call(dn: @user.dn, address: @user.address) end redirect_to new_password_services_email_path else @validation_errors = { current_password: [ "Wrong password. Try again!" ] } render :show, status: :forbidden end end # POST /settings/reset_password def reset_password current_user.send_reset_password_instructions sign_out current_user msg = "We have sent you an email with a link to reset your password." redirect_to check_your_email_path, notice: msg end # POST /settings/set_nostr_pubkey def set_nostr_pubkey signed_event = Nostr::Event.new(**nostr_event_from_params) is_valid_sig = signed_event.verify_signature is_valid_auth = NostrManager::VerifyAuth.call( event: signed_event, challenge: session[:shared_secret] ) unless is_valid_sig && is_valid_auth flash[:alert] = "Public key could not be verified" http_status :unprocessable_entity and return end user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey) if user_with_pubkey.present? && (user_with_pubkey != current_user) flash[:alert] = "Public key already in use for a different account" http_status :unprocessable_entity and return end LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event.pubkey) session[:shared_secret] = nil flash[:success] = "Public key verification successful" http_status :ok end # DELETE /settings/nostr_pubkey def remove_nostr_pubkey # TODO require current pubkey or password to delete LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: nil) redirect_to setting_path(:nostr), flash: { success: 'Public key removed from account' } end private def set_main_nav_section @current_section = :settings end def set_settings_section @settings_section = params[:section] allowed_sections = [ :profile, :account, :xmpp, :email, :lightning, :remotestorage, :nostr ] unless allowed_sections.include?(@settings_section.to_sym) redirect_to setting_path(:profile) end end def set_user @user = current_user end def user_params params.require(:user).permit( :display_name, :avatar_new, :pgp_pubkey, preferences: UserPreferences.pref_keys ) end def email_params params.require(:user).permit(:email) end def security_params params.require(:user).permit(:current_password) end def generate_email_password characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join end def hash_email_password(password) salt = BCrypt::Engine.generate_salt BCrypt::Engine.hash_secret(password, salt) end def store_user_avatar io = @user.avatar_new.tempfile img_data = UserManager::ProcessAvatar.call(io: io) if img_data.blank? @user.errors.add(:avatar, "failed to process file") false end tempfile = Tempfile.create tempfile.binmode tempfile.write(img_data) tempfile.rewind hash = Digest::SHA256.hexdigest(img_data) ext = @user.avatar_new.content_type == "image/png" ? "png" : "jpg" filename = "#{hash}.#{ext}" if filename == @user.avatar.filename.to_s @user.errors.add(:avatar, "must be a new file/picture") false else key = "users/#{@user.cn}/avatars/#{filename}" @user.avatar.attach io: tempfile, key: key, filename: filename @user.save end end end