Use a multi-step process, caching results in between, while informing the user about the current steps/progress
275 lines
8.4 KiB
Ruby
275 lines
8.4 KiB
Ruby
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,
|
|
:fetch_nostr_metadata
|
|
]
|
|
|
|
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
|
|
UserManager::UpdateAvatar.call(user: @user)
|
|
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
|
|
|
|
def fetch_nostr_metadata
|
|
return render plain: "no pubkey", status: :bad_request if @user.nostr_pubkey.blank?
|
|
|
|
cache_key = "nostr_metadata:#{@user.id}"
|
|
|
|
case params[:step]
|
|
when "relays"
|
|
nip65_event = NostrManager::DiscoverUserRelays.call(pubkey: @user.nostr_pubkey)
|
|
write_relays = nip65_event ? extract_write_relays(nip65_event) : []
|
|
|
|
if nip65_event && write_relays.any? && stale?(nip65_event)
|
|
auth_event = NostrManager::DiscoverUserRelays.call(
|
|
pubkey: @user.nostr_pubkey, relays: write_relays
|
|
)
|
|
nip65_event = auth_event if auth_event
|
|
write_relays = extract_write_relays(nip65_event) if auth_event
|
|
end
|
|
|
|
Rails.cache.write("#{cache_key}:nip65", nip65_event, expires_in: 5.minutes)
|
|
Rails.cache.write("#{cache_key}:write_relays", write_relays, expires_in: 5.minutes)
|
|
render json: { next: "profile" }
|
|
|
|
when "profile"
|
|
write_relays = Rails.cache.read("#{cache_key}:write_relays") || []
|
|
relays = write_relays.any? ? write_relays : nil
|
|
profile = NostrManager::DiscoverUserProfile.call(
|
|
pubkey: @user.nostr_pubkey, relays: relays
|
|
)
|
|
Rails.cache.write("#{cache_key}:profile", profile, expires_in: 5.minutes)
|
|
render json: { next: "render" }
|
|
|
|
when "render", nil
|
|
@nip65_event = Rails.cache.read("#{cache_key}:nip65")
|
|
@profile = Rails.cache.read("#{cache_key}:profile")
|
|
render partial: "nostr_metadata", formats: :html
|
|
|
|
else
|
|
render plain: "invalid step", status: :bad_request
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def set_main_nav_section
|
|
@current_section = :settings
|
|
end
|
|
|
|
def extract_write_relays(nip65_event)
|
|
nip65_event["tags"]
|
|
.select { |t| t[0] == "r" && t[2] != "read" }
|
|
.map { |t| t[1] }
|
|
end
|
|
|
|
def stale?(event, threshold = 30.minutes)
|
|
Time.at(event["created_at"]) < threshold.ago
|
|
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
|