diff --git a/Gemfile b/Gemfile index df0e515..7c8e759 100644 --- a/Gemfile +++ b/Gemfile @@ -22,7 +22,7 @@ gem 'jbuilder', '~> 2.7' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use Active Model has_secure_password -# gem 'bcrypt', '~> 3.1.7' +gem 'bcrypt', '~> 3.1.7' # Configuration gem 'dotenv-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 3e551f1..444b10e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -259,6 +259,7 @@ GEM method_source (1.0.0) mini_magick (4.12.0) mini_mime (1.1.5) + mini_portile2 (2.8.5) minitest (5.20.0) multipart-post (2.3.0) net-imap (0.3.7) @@ -272,6 +273,9 @@ GEM net-smtp (0.4.0) net-protocol nio4r (2.5.9) + nokogiri (1.15.4) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) nokogiri (1.15.4-arm64-darwin) racc (~> 1.4) nokogiri (1.15.4-x86_64-linux) @@ -422,6 +426,8 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) + sqlite3 (1.6.7) + mini_portile2 (~> 2.8.0) sqlite3 (1.6.7-arm64-darwin) sqlite3 (1.6.7-x86_64-linux) stimulus-rails (1.3.0) @@ -466,6 +472,7 @@ PLATFORMS DEPENDENCIES aws-sdk-s3 + bcrypt (~> 3.1.7) byebug (~> 11.1) capybara cssbundling-rails diff --git a/app/controllers/services/email_controller.rb b/app/controllers/services/email_controller.rb new file mode 100644 index 0000000..516505b --- /dev/null +++ b/app/controllers/services/email_controller.rb @@ -0,0 +1,34 @@ +class Services::EmailController < Services::BaseController + before_action :authenticate_user! + before_action :require_service_available + before_action :require_feature_enabled + + def show + ldap_entry = current_user.ldap_entry + + @service_enabled = ldap_entry[:email_password].present? + @maildrop = ldap_entry[:email_maildrop] + @email_forwarding_active = @maildrop.present? && + @maildrop.split("@").first != current_user.cn + end + + def new_password + if session[:new_email_password].present? + @new_password = session.delete(:new_email_password) + else + redirect_to setting_path(:email) + end + end + + private + + def require_service_available + http_status :not_found unless Setting.email_enabled? + end + + def require_feature_enabled + unless Flipper.enabled?(:email, current_user) + http_status :forbidden + end + end +end diff --git a/app/controllers/services/remotestorage_controller.rb b/app/controllers/services/remotestorage_controller.rb index 67c7e76..632e5c7 100644 --- a/app/controllers/services/remotestorage_controller.rb +++ b/app/controllers/services/remotestorage_controller.rb @@ -1,7 +1,7 @@ class Services::RemotestorageController < Services::BaseController before_action :authenticate_user! - before_action :require_feature_enabled before_action :require_service_available + before_action :require_feature_enabled # Dashboard def show @@ -14,13 +14,13 @@ class Services::RemotestorageController < Services::BaseController private + def require_service_available + http_status :not_found unless Setting.remotestorage_enabled? + end + def require_feature_enabled unless Flipper.enabled?(:remotestorage, current_user) http_status :forbidden end end - - def require_service_available - http_status :not_found unless Setting.remotestorage_enabled? - end end diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 8f30419..f1fa357 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -1,10 +1,11 @@ -require 'securerandom' +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] - before_action :set_user, only: [:show, :update, :update_email] + 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) @@ -40,7 +41,7 @@ class SettingsController < ApplicationController end def update_email - if @user.valid_ldap_authentication?(email_params[:current_password]) + 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.' @@ -56,6 +57,28 @@ class SettingsController < ApplicationController end end + 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(@user.dn, hashed_password) + + if @user.ldap_entry[:email_maildrop] != @user.address + LdapManager::UpdateEmailMaildrop.call(@user.dn, @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 + def reset_password current_user.send_reset_password_instructions sign_out current_user @@ -111,7 +134,8 @@ class SettingsController < ApplicationController def set_settings_section @settings_section = params[:section] allowed_sections = [ - :profile, :account, :lightning, :remotestorage, :xmpp, :experiments + :profile, :account, :xmpp, :email, :lightning, :remotestorage, + :experiments ] unless allowed_sections.include?(@settings_section.to_sym) @@ -132,7 +156,11 @@ class SettingsController < ApplicationController end def email_params - params.require(:user).permit(:email, :current_password) + params.require(:user).permit(:email) + end + + def security_params + params.require(:user).permit(:current_password) end def nostr_event_params @@ -140,4 +168,14 @@ class SettingsController < ApplicationController :id, :pubkey, :created_at, :kind, :tags, :content, :sig ]) 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 end diff --git a/app/javascript/controllers/settings/email/password_controller.js b/app/javascript/controllers/settings/email/password_controller.js new file mode 100644 index 0000000..6c8784e --- /dev/null +++ b/app/javascript/controllers/settings/email/password_controller.js @@ -0,0 +1,27 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = [ "resetPasswordButton", "currentPasswordField" ] + static values = { validationFailed: Boolean } + + connect () { + if (this.validationFailedValue) return; + + this.element.querySelectorAll(".initial-hidden").forEach(el => { + el.classList.add("hidden"); + }) + this.element.querySelectorAll(".initial-visible").forEach(el => { + el.classList.remove("hidden"); + }) + } + + showPasswordReset () { + this.element.querySelectorAll(".initial-visible").forEach(el => { + el.classList.add("hidden"); + }) + this.element.querySelectorAll(".initial-hidden").forEach(el => { + el.classList.remove("hidden"); + }) + this.currentPasswordFieldTarget.select(); + } +} diff --git a/app/models/setting.rb b/app/models/setting.rb index 9cafa2e..27acdcc 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -168,4 +168,30 @@ class Setting < RailsSettings::Base field :rs_redis_url, type: :string, default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1" + + + # + # E-Mail Service + # + + field :email_enabled, type: :boolean, + default: ENV["EMAIL_SMTP_HOST"].present? + + # field :email_smtp_host, type: :string, + # default: ENV["EMAIL_SMTP_HOST"].presence + # + # field :email_smtp_port, type: :string, + # default: ENV["EMAIL_SMTP_PORT"].presence || 587 + # + # field :email_smtp_enable_starttls, type: :string, + # default: ENV["EMAIL_SMTP_PORT"].presence || true + # + # field :email_auth_method, type: :string, + # default: ENV["EMAIL_AUTH_METHOD"].presence || "plain" + # + # field :email_imap_host, type: :string, + # default: ENV["EMAIL_IMAP_HOST"].presence + # + # field :email_imap_port, type: :string, + # default: ENV["EMAIL_IMAP_PORT"].presence || 993 end diff --git a/app/models/user.rb b/app/models/user.rb index acda860..b31016a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,7 @@ class User < ApplicationRecord attr_accessor :display_name attr_accessor :avatar_new + attr_accessor :current_password serialize :preferences, UserPreferences @@ -90,11 +91,12 @@ class User < ApplicationRecord # E-Mail update confirmed LdapManager::UpdateEmail.call(self.dn, self.email) else - # TODO Make configurable # E-Mail from signup confirmed (i.e. account activation) - enable_service %w[ discourse gitea mediawiki xmpp ] - #TODO enable in development when we have easy setup of ejabberd etc. + # TODO Make configurable, only activate globally enabled services + enable_service %w[ discourse email gitea mediawiki xmpp ] + + # 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? diff --git a/app/services/ldap_manager/update_email_maildrop.rb b/app/services/ldap_manager/update_email_maildrop.rb new file mode 100644 index 0000000..d26cf16 --- /dev/null +++ b/app/services/ldap_manager/update_email_maildrop.rb @@ -0,0 +1,12 @@ +module LdapManager + class UpdateEmailMaildrop < LdapManagerService + def initialize(dn, address) + @dn = dn + @address = address + end + + def call + replace_attribute @dn, :mailRoutingAddress, @address + end + end +end diff --git a/app/services/ldap_manager/update_email_password.rb b/app/services/ldap_manager/update_email_password.rb new file mode 100644 index 0000000..e9cde6a --- /dev/null +++ b/app/services/ldap_manager/update_email_password.rb @@ -0,0 +1,12 @@ +module LdapManager + class UpdateEmailPassword < LdapManagerService + def initialize(dn, password_hash) + @dn = dn + @password_hash = password_hash + end + + def call + replace_attribute @dn, :mailpassword, @password_hash + end + end +end diff --git a/app/services/ldap_service.rb b/app/services/ldap_service.rb index eac64c2..1c56df6 100644 --- a/app/services/ldap_service.rb +++ b/app/services/ldap_service.rb @@ -50,8 +50,11 @@ class LdapService < ApplicationService treebase = ldap_config["base"] end - attributes = %w{dn cn uid mail displayName admin service} - filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") + attributes = %w[ + dn cn uid mail displayName admin service + mailRoutingAddress mailpassword + ] + filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes) entries.sort_by! { |e| e.cn[0] } @@ -61,7 +64,9 @@ class LdapService < ApplicationService mail: e.try(:mail) ? e.mail.first : nil, display_name: e.try(:displayName) ? e.displayName.first : nil, admin: e.try(:admin) ? 'admin' : nil, - service: e.try(:service) + service: e.try(:service), + email_maildrop: e.try(:mailRoutingAddress), + email_password: e.try(:mailpassword) } end end diff --git a/app/views/admin/settings/services/_email.html.erb b/app/views/admin/settings/services/_email.html.erb new file mode 100644 index 0000000..4bf021a --- /dev/null +++ b/app/views/admin/settings/services/_email.html.erb @@ -0,0 +1,16 @@ +
+ A no-bullshit email account +
+ <% end %> ++ Sync your data between apps and devices +
+ <% end %> +- Sync your data between apps and devices -
- <% end %> -+ Your email password has been updated. +
++ Please store the new one in a password manager or write it down somewhere: +
++ <%= label_tag :new_password, 'New password', class: 'hidden' %> + <%= text_field_tag :new_password, @new_password, disabled: true, class: 'text-xl grow', + data: { "clipboard-target": "source"} %> + + +
++ <%= link_to "Done", services_email_path, class: "btn-md btn-blue w-full" %> +
+ <%= render QrCodeModalComponent.new(qr_content: @new_password) %> ++ Send and receive electronic mail. +
++ disabled="disabled" + data-clipboard-target="source" /> + + +
+ <%= render QrCodeModalComponent.new(qr_content: "xmpp:"+current_user.address) %> ++ Your email password is different from your main account password. You can + reset your email password in the + <%= link_to "email settings", setting_path(:email), class: "ks-text-link" %>. +
++ Use the following button to generate a new email password: +
++ +
++ <%= f.label :current_password, 'Current account password', class: 'font-bold' %> +
++ <%= f.password_field :current_password, class: "w-full", required: true, + data: { 'settings--email--password-target': "currentPasswordField" } %> +
+ <% if @validation_errors.present? && @validation_errors[:current_password].present? %> +<%= @validation_errors[:current_password].first %>
+ <% end %> ++ <%= f.submit "Create new email password", class: "btn-md btn-blue w-full md:w-auto" %> +
+