Add email service and settings

This commit is contained in:
Râu Cao 2023-12-23 12:26:34 +01:00
parent aab6793b86
commit 958d18d61a
Signed by: raucao
GPG Key ID: 37036C356E56CC51
24 changed files with 506 additions and 35 deletions

View File

@ -22,7 +22,7 @@ gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production # Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0' # gem 'redis', '~> 4.0'
# Use Active Model has_secure_password # Use Active Model has_secure_password
# gem 'bcrypt', '~> 3.1.7' gem 'bcrypt', '~> 3.1.7'
# Configuration # Configuration
gem 'dotenv-rails' gem 'dotenv-rails'

View File

@ -259,6 +259,7 @@ GEM
method_source (1.0.0) method_source (1.0.0)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.20.0) minitest (5.20.0)
multipart-post (2.3.0) multipart-post (2.3.0)
net-imap (0.3.7) net-imap (0.3.7)
@ -272,6 +273,9 @@ GEM
net-smtp (0.4.0) net-smtp (0.4.0)
net-protocol net-protocol
nio4r (2.5.9) nio4r (2.5.9)
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.15.4-arm64-darwin) nokogiri (1.15.4-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.15.4-x86_64-linux) nokogiri (1.15.4-x86_64-linux)
@ -422,6 +426,8 @@ GEM
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.6.7)
mini_portile2 (~> 2.8.0)
sqlite3 (1.6.7-arm64-darwin) sqlite3 (1.6.7-arm64-darwin)
sqlite3 (1.6.7-x86_64-linux) sqlite3 (1.6.7-x86_64-linux)
stimulus-rails (1.3.0) stimulus-rails (1.3.0)
@ -466,6 +472,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
aws-sdk-s3 aws-sdk-s3
bcrypt (~> 3.1.7)
byebug (~> 11.1) byebug (~> 11.1)
capybara capybara
cssbundling-rails cssbundling-rails

View File

@ -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

View File

@ -1,7 +1,7 @@
class Services::RemotestorageController < Services::BaseController class Services::RemotestorageController < Services::BaseController
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_feature_enabled
before_action :require_service_available before_action :require_service_available
before_action :require_feature_enabled
# Dashboard # Dashboard
def show def show
@ -14,13 +14,13 @@ class Services::RemotestorageController < Services::BaseController
private private
def require_service_available
http_status :not_found unless Setting.remotestorage_enabled?
end
def require_feature_enabled def require_feature_enabled
unless Flipper.enabled?(:remotestorage, current_user) unless Flipper.enabled?(:remotestorage, current_user)
http_status :forbidden http_status :forbidden
end end
end end
def require_service_available
http_status :not_found unless Setting.remotestorage_enabled?
end
end end

View File

@ -1,10 +1,11 @@
require 'securerandom' require "securerandom"
require "bcrypt"
class SettingsController < ApplicationController class SettingsController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_main_nav_section before_action :set_main_nav_section
before_action :set_settings_section, 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] before_action :set_user, only: [:show, :update, :update_email, :reset_email_password]
def index def index
redirect_to setting_path(:profile) redirect_to setting_path(:profile)
@ -40,7 +41,7 @@ class SettingsController < ApplicationController
end end
def update_email 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] if @user.update email: email_params[:email]
redirect_to setting_path(:account), flash: { redirect_to setting_path(:account), flash: {
notice: 'Please confirm your new address using the confirmation link we just sent you.' notice: 'Please confirm your new address using the confirmation link we just sent you.'
@ -56,6 +57,28 @@ class SettingsController < ApplicationController
end end
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 def reset_password
current_user.send_reset_password_instructions current_user.send_reset_password_instructions
sign_out current_user sign_out current_user
@ -111,7 +134,8 @@ class SettingsController < ApplicationController
def set_settings_section def set_settings_section
@settings_section = params[:section] @settings_section = params[:section]
allowed_sections = [ allowed_sections = [
:profile, :account, :lightning, :remotestorage, :xmpp, :experiments :profile, :account, :xmpp, :email, :lightning, :remotestorage,
:experiments
] ]
unless allowed_sections.include?(@settings_section.to_sym) unless allowed_sections.include?(@settings_section.to_sym)
@ -132,7 +156,11 @@ class SettingsController < ApplicationController
end end
def email_params 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 end
def nostr_event_params def nostr_event_params
@ -140,4 +168,14 @@ class SettingsController < ApplicationController
:id, :pubkey, :created_at, :kind, :tags, :content, :sig :id, :pubkey, :created_at, :kind, :tags, :content, :sig
]) ])
end 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 end

View File

@ -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();
}
}

View File

@ -168,4 +168,30 @@ class Setting < RailsSettings::Base
field :rs_redis_url, type: :string, field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1" 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 end

View File

@ -3,6 +3,7 @@ class User < ApplicationRecord
attr_accessor :display_name attr_accessor :display_name
attr_accessor :avatar_new attr_accessor :avatar_new
attr_accessor :current_password
serialize :preferences, UserPreferences serialize :preferences, UserPreferences
@ -90,11 +91,12 @@ class User < ApplicationRecord
# E-Mail update confirmed # E-Mail update confirmed
LdapManager::UpdateEmail.call(self.dn, self.email) LdapManager::UpdateEmail.call(self.dn, self.email)
else else
# TODO Make configurable
# E-Mail from signup confirmed (i.e. account activation) # 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? return if Rails.env.development? || !Setting.ejabberd_enabled?
XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present? XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present?

View File

@ -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

View File

@ -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

View File

@ -50,8 +50,11 @@ class LdapService < ApplicationService
treebase = ldap_config["base"] treebase = ldap_config["base"]
end end
attributes = %w{dn cn uid mail displayName admin service} attributes = %w[
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") 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 = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] } entries.sort_by! { |e| e.cn[0] }
@ -61,7 +64,9 @@ class LdapService < ApplicationService
mail: e.try(:mail) ? e.mail.first : nil, mail: e.try(:mail) ? e.mail.first : nil,
display_name: e.try(:displayName) ? e.displayName.first : nil, display_name: e.try(:displayName) ? e.displayName.first : nil,
admin: e.try(:admin) ? 'admin' : 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
end end

View File

@ -0,0 +1,16 @@
<h3>E-Mail</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :email_enabled,
enabled: Setting.email_enabled?,
title: "Enable E-Mail service integration",
description: "Enable/configure LDAP attributes for use with a mail server"
) %>
<%# <% if Setting.email_enabled? %>
<%# <%= render FormElements::FieldsetResettableSettingComponent.new(
<%# key: :gitea_public_url,
<%# title: "Public URL"
<%# ) %>
<%# <% end %>
</ul>

View File

@ -32,6 +32,17 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if Setting.email_enabled? &&
Flipper.enabled?(:email, current_user) %>
<div class="border border-gray-300 rounded-md hover:border-gray-400">
<%= link_to services_email_path, class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">E-Mail</h3>
<p class="text-gray-600">
A no-bullshit email account
</p>
<% end %>
</div>
<% end %>
<% if Setting.discourse_enabled? %> <% if Setting.discourse_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:95%] bg-center bg-no-repeat bg-[length:95%] bg-center bg-no-repeat
@ -58,6 +69,18 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if Setting.remotestorage_enabled? &&
Flipper.enabled?(:remotestorage, current_user) %>
<div class="border border-gray-300 rounded-md hover:border-gray-400">
<%= link_to services_storage_path,
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Storage</h3>
<p class="text-gray-600">
Sync your data between apps and devices
</p>
<% end %>
</div>
<% end %>
<% if Setting.gitea_enabled? %> <% if Setting.gitea_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <div class="border border-gray-300 rounded-md hover:border-gray-400
bg-cover bg-center bg-no-repeat bg-cover bg-center bg-no-repeat
@ -84,18 +107,6 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if Setting.remotestorage_enabled? &&
Flipper.enabled?(:remotestorage, current_user) %>
<div class="border border-gray-300 rounded-md hover:border-gray-400">
<%= link_to services_storage_path,
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Storage</h3>
<p class="text-gray-600">
Sync your data between apps and devices
</p>
<% end %>
</div>
<% end %>
<% if Setting.mediawiki_enabled? %> <% if Setting.mediawiki_enabled? %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <div class="border border-gray-300 rounded-md hover:border-gray-400
bg-cover bg-[center_top_-20px] bg-no-repeat bg-cover bg-[center_top_-20px] bg-no-repeat

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mail"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mail <%= custom_class %>"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 375 B

View File

@ -0,0 +1,35 @@
<%= render HeaderCompactComponent.new(title: "New E-Mail Password") %>
<%= render MainCompactComponent.new do %>
<section data-controller="modal" data-action="keydown.esc->modal#close">
<p class="font-bold">
Your email password has been updated.
</p>
<p class="mb-8">
Please store the new one in a password manager or write it down somewhere:
</p>
<p data-controller="clipboard" class="flex gap-1 w-full mb-10">
<%= 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"} %>
<button id="copy-new-password" class="btn-md btn-icon btn-outline shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard">
<span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
<span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
</button>
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
data-action="click->modal#open" title="Show QR code">
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</button>
</p>
<p class="mb-0">
<%= link_to "Done", services_email_path, class: "btn-md btn-blue w-full" %>
</p>
<%= render QrCodeModalComponent.new(qr_content: @new_password) %>
</section>
<% end %>

View File

@ -0,0 +1,128 @@
<%= render HeaderComponent.new(title: "E-Mail") %>
<%= render MainSimpleComponent.new do %>
<section>
<p class="mb-6">
Send and receive electronic mail.
</p>
</section>
<section data-controller="modal" data-action="keydown.esc->modal#close">
<h3>Your E-Mail Address</h3>
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
<input type="text" id="user_address" class="grow"
value=<%= current_user.address %> disabled="disabled"
data-clipboard-target="source" />
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard">
<span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
<span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
</button>
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
data-action="click->modal#open" title="Show QR code">
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</button>
</p>
<%= render QrCodeModalComponent.new(qr_content: "xmpp:"+current_user.address) %>
</section>
<section>
<h3>E-Mail Password</h3>
<p>
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" %>.
</p>
</section>
<section>
<h3>Recommended Apps</h3>
<div data-controller="tabs"
data-tabs-active-tab-class="-mb-px border-gray-200 border-l border-t border-r rounded-t text-indigo-600 hover:text-indigo-600"
data-tabs-inactive-tab-class="text-gray-500 hover:text-gray-700"
class="mb-12">
<select data-action="tabs#change" data-tabs-target="select"
class="block w-full mb-8 sm:hidden">
<optgroup label="Mobile">
<option>Android</option>
</optgroup>
<optgroup label="Desktop">
<option>Linux</option>
<option>Windows</option>
<option>macOS</option>
</optgroup>
</select>
<ul class="hidden sm:flex list-reset mb-8 border-gray-200 border-b">
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Android
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Linux
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Windows
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
macOS
</a>
</li>
</ul>
<div id="apps-android" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "K-9 Mail",
description: "Soon to become Thunderbird Mobile",
icon_path: "/img/logos/icon_k9-mail.png",
links: [
["Website", "https://k9mail.app"],
["Google Play", "https://play.google.com/store/apps/details?id=com.fsck.k9"],
["F-Droid", "https://f-droid.org/en/packages/com.fsck.k9/"],
]
) %>
</div>
<div id="apps-linux" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Thunderbird",
description: "The most popular open-source email app",
icon_path: "/img/logos/icon_thunderbird.png",
links: [
["Website", "https://www.thunderbird.net"]
]
) %>
</div>
<div id="apps-windows" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Thunderbird",
description: "The most popular open-source email app",
icon_path: "/img/logos/icon_thunderbird.png",
links: [
["Website", "https://www.thunderbird.net"]
]
) %>
</div>
<div id="apps-mac" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Thunderbird",
description: "The most popular open-source email app",
icon_path: "/img/logos/icon_thunderbird.png",
links: [
["Website", "https://www.thunderbird.net"]
]
) %>
</div>
</div>
</section>
<% end %>

View File

@ -0,0 +1,34 @@
<%= tag.section data: {
controller: "settings--email--password",
"settings--email--password-validation-failed-value": @validation_errors.present?
} do %>
<h3>E-Mail Password</h3>
<%= form_for(@user, url: reset_email_password_settings_path, method: "post") do |f| %>
<%= hidden_field_tag :section, "email" %>
<p class="mb-8">
Use the following button to generate a new email password:
</p>
<p class="hidden initial-visible">
<button type="button" id="edit-email" class="btn-md btn-gray"
data-settings--email--password-target="resetPasswordButton"
data-action="settings--email--password#showPasswordReset">
Reset email password
</button>
</p>
<div class="initial-hidden">
<p class="mt-4 mb-2">
<%= f.label :current_password, 'Current account password', class: 'font-bold' %>
</p>
<p class="sm:w-3/5">
<%= f.password_field :current_password, class: "w-full", required: true,
data: { 'settings--email--password-target': "currentPasswordField" } %>
</p>
<% if @validation_errors.present? && @validation_errors[:current_password].present? %>
<p class="error-msg"><%= @validation_errors[:current_password].first %></p>
<% end %>
<p class="mt-6">
<%= f.submit "Create new email password", class: "btn-md btn-blue w-full md:w-auto" %>
</p>
</div>
<% end %>
<% end %>

View File

@ -19,6 +19,13 @@
text_icon: Setting.droneci_enabled? ? "◉" : "○", text_icon: Setting.droneci_enabled? ? "◉" : "○",
active: current_page?(admin_settings_services_path(params: { s: "droneci" })), active: current_page?(admin_settings_services_path(params: { s: "droneci" })),
) %> ) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "E-Mail",
path: admin_settings_services_path(params: { s: "email" }),
text_icon: Setting.email_enabled? ? "◉" : "○",
active: current_page?(admin_settings_services_path(params: { s: "email" })),
) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "ejabberd", name: "ejabberd",

View File

@ -12,13 +12,21 @@
active: @settings_section.to_s == "xmpp" active: @settings_section.to_s == "xmpp"
) %> ) %>
<% end %> <% end %>
<% if Setting.email_enabled? &&
Flipper.enabled?(:email, current_user) %>
<%= render SidenavLinkComponent.new(
name: "E-Mail", path: setting_path(:email), icon: "mail",
active: @settings_section.to_s == "email"
) %>
<% end %>
<% if Setting.lndhub_enabled %> <% if Setting.lndhub_enabled %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
name: "Lightning", path: setting_path(:lightning), icon: "zap", name: "Lightning", path: setting_path(:lightning), icon: "zap",
active: @settings_section.to_s == "lightning" active: @settings_section.to_s == "lightning"
) %> ) %>
<% end %> <% end %>
<% if Setting.remotestorage_enabled %> <% if Setting.remotestorage_enabled? &&
Flipper.enabled?(:remotestorage, current_user) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
name: "Storage", path: setting_path(:remotestorage), icon: "remotestorage", name: "Storage", path: setting_path(:remotestorage), icon: "remotestorage",
active: @settings_section.to_s == "remotestorage" active: @settings_section.to_s == "remotestorage"

View File

@ -23,6 +23,12 @@ Rails.application.routes.draw do
resource :mastodon, only: [:show], controller: 'mastodon' resource :mastodon, only: [:show], controller: 'mastodon'
resource :email, only: [:show], controller: 'email' do
member do
get 'new_password'
end
end
resources :lightning, only: [:index] do resources :lightning, only: [:index] do
collection do collection do
get 'transactions' get 'transactions'
@ -44,6 +50,7 @@ Rails.application.routes.draw do
collection do collection do
post 'update_email' post 'update_email'
post 'reset_password' post 'reset_password'
post 'reset_email_password'
post 'set_nostr_pubkey' post 'set_nostr_pubkey'
delete 'nostr_pubkey', to: 'settings#remove_nostr_pubkey' delete 'nostr_pubkey', to: 'settings#remove_nostr_pubkey'
end end

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,62 @@
require 'rails_helper'
RSpec.describe 'E-Mail settings', type: :feature do
let(:user) { create :user }
feature "Reset email password" do
before do
login_as user, :scope => :user
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
.with("invalid password").and_return(false)
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
.with("valid password").and_return(true)
end
scenario 'fails with invalid password' do
expect(LdapManager::UpdateEmailPassword).not_to receive(:call)
expect(LdapManager::UpdateEmailMaildrop).not_to receive(:call)
visit setting_path(:email)
fill_in 'Current account password', with: "invalid password"
click_button "Create new email password"
within ".error-msg" do
expect(page).to have_content("Wrong password")
end
end
scenario 'works with valid password' do
allow_any_instance_of(User).to receive(:dn)
.and_return("cn=#{user.cn},ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ uid: user.cn, ou: user.ou, email_maildrop: user.address })
expect(LdapManager::UpdateEmailPassword).to receive(:call).and_return(true)
expect(LdapManager::UpdateEmailMaildrop).not_to receive(:call)
visit setting_path(:email)
fill_in 'Current account password', with: "valid password"
click_button "Create new email password"
expect(current_url).to eq(new_password_services_email_url)
end
scenario 'updates the maildrop attribute if necessary' do
allow_any_instance_of(User).to receive(:dn)
.and_return("cn=#{user.cn},ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ uid: user.cn, ou: user.ou, email_maildrop: "mahafaly@example.com" })
expect(LdapManager::UpdateEmailPassword).to receive(:call).and_return(true)
expect(LdapManager::UpdateEmailMaildrop).to receive(:call)
.with(user.dn, user.address).and_return(true)
visit setting_path(:email)
fill_in 'Current account password', with: "valid password"
click_button "Create new email password"
expect(current_url).to eq(new_password_services_email_url)
end
end
end

View File

@ -66,9 +66,9 @@ RSpec.describe User, type: :model do
it "returns the entries from the LDAP service attribute" do it "returns the entries from the LDAP service attribute" do
expect(user).to receive(:ldap_entry).and_return({ expect(user).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, mail: user.email, admin: nil, uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
service: ["discourse", "gitea", "wiki", "xmpp"] service: ["discourse", "email", "gitea", "wiki", "xmpp"]
}) })
expect(user.services_enabled).to eq(["discourse", "gitea", "wiki", "xmpp"]) expect(user.services_enabled).to eq(["discourse", "email", "gitea", "wiki", "xmpp"])
end end
end end
@ -147,7 +147,7 @@ RSpec.describe User, type: :model do
after { clear_enqueued_jobs } after { clear_enqueued_jobs }
it "enables default services" do it "enables default services" do
expect(user).to receive(:enable_service).with(%w[ discourse gitea mediawiki xmpp ]) expect(user).to receive(:enable_service).with(%w[ discourse email gitea mediawiki xmpp ])
user.send :devise_after_confirmation user.send :devise_after_confirmation
end end