Add email service and settings
This commit is contained in:
parent
aab6793b86
commit
958d18d61a
2
Gemfile
2
Gemfile
@ -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'
|
||||||
|
@ -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
|
||||||
|
34
app/controllers/services/email_controller.rb
Normal file
34
app/controllers/services/email_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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?
|
||||||
|
12
app/services/ldap_manager/update_email_maildrop.rb
Normal file
12
app/services/ldap_manager/update_email_maildrop.rb
Normal 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
|
12
app/services/ldap_manager/update_email_password.rb
Normal file
12
app/services/ldap_manager/update_email_password.rb
Normal 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
|
@ -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
|
||||||
|
16
app/views/admin/settings/services/_email.html.erb
Normal file
16
app/views/admin/settings/services/_email.html.erb
Normal 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>
|
@ -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
|
||||||
|
@ -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 |
35
app/views/services/email/new_password.html.erb
Normal file
35
app/views/services/email/new_password.html.erb
Normal 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 %>
|
128
app/views/services/email/show.html.erb
Normal file
128
app/views/services/email/show.html.erb
Normal 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 %>
|
34
app/views/settings/_email.html.erb
Normal file
34
app/views/settings/_email.html.erb
Normal 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 %>
|
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
BIN
public/img/logos/icon_k9-mail.png
Normal file
BIN
public/img/logos/icon_k9-mail.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
BIN
public/img/logos/icon_thunderbird.png
Normal file
BIN
public/img/logos/icon_thunderbird.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
62
spec/features/settings/email_spec.rb
Normal file
62
spec/features/settings/email_spec.rb
Normal 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
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user