Encrypt passwords

This commit is contained in:
Ozzie Isaacs 2022-07-02 17:45:24 +02:00
parent b1c70d5b4a
commit 3bde8a5d95
12 changed files with 234 additions and 62 deletions

1
.gitignore vendored
View File

@ -33,3 +33,4 @@ settings.yaml
gdrive_credentials gdrive_credentials
client_secrets.json client_secrets.json
gmail.json gmail.json
/.key

View File

@ -28,6 +28,7 @@ import mimetypes
from flask import Flask from flask import Flask
from .MyLoginManager import MyLoginManager from .MyLoginManager import MyLoginManager
from flask_principal import Principal from flask_principal import Principal
from flask_limiter import Limiter
from . import logger from . import logger
from .cli import CliParameter from .cli import CliParameter
@ -81,7 +82,7 @@ app.config.update(
lm = MyLoginManager() lm = MyLoginManager()
config = config_sql._ConfigSQL() config = config_sql.ConfigSQL()
cli_param = CliParameter() cli_param = CliParameter()
@ -96,6 +97,7 @@ web_server = WebServer()
updater_thread = Updater() updater_thread = Updater()
limiter = Limiter(key_func=True, headers_enabled=True)
def create_app(): def create_app():
if csrf: if csrf:
@ -106,7 +108,12 @@ def create_app():
ub.init_db(cli_param.settings_path, cli_param.user_credentials) ub.init_db(cli_param.settings_path, cli_param.user_credentials)
# pylint: disable=no-member # pylint: disable=no-member
config_sql.load_configuration(config, ub.session, cli_param) encrypt_key, error = config_sql.get_encryption_key(os.path.dirname(cli_param.settings_path))
config_sql.load_configuration(ub.session, encrypt_key)
config.init_config(ub.session, encrypt_key, cli_param)
if error:
log.error(error)
lm.login_view = 'web.login' lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous lm.anonymous_user = ub.Anonymous
@ -150,7 +157,7 @@ def create_app():
if os.environ.get('FLASK_DEBUG'): if os.environ.get('FLASK_DEBUG'):
cache_buster.init_cache_busting(app) cache_buster.init_cache_busting(app)
log.info('Starting Calibre Web...') log.info('Starting Calibre Web...')
limiter.init_app(app)
Principal(app) Principal(app)
lm.init_app(app) lm.init_app(app)
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session)) app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
@ -165,7 +172,7 @@ def create_app():
services.ldap.init_app(app, config) services.ldap.init_app(app, config)
if services.goodreads_support: if services.goodreads_support:
services.goodreads_support.connect(config.config_goodreads_api_key, services.goodreads_support.connect(config.config_goodreads_api_key,
config.config_goodreads_api_secret, config.config_goodreads_api_secret_e,
config.config_use_goodreads) config.config_use_goodreads)
config.store_calibre_uuid(calibre_db, db.Library_Id) config.store_calibre_uuid(calibre_db, db.Library_Id)
# Register scheduled tasks # Register scheduled tasks

View File

@ -206,12 +206,12 @@ def admin():
commit = version['version'] commit = version['version']
all_user = ub.session.query(ub.User).all() all_user = ub.session.query(ub.User).all()
email_settings = config.get_mail_settings() # email_settings = mail_config.get_mail_settings()
schedule_time = format_time(datetime_time(hour=config.schedule_start_time), format="short") schedule_time = format_time(datetime_time(hour=config.schedule_start_time), format="short")
t = timedelta(hours=config.schedule_duration // 60, minutes=config.schedule_duration % 60) t = timedelta(hours=config.schedule_duration // 60, minutes=config.schedule_duration % 60)
schedule_duration = format_timedelta(t, threshold=.99) schedule_duration = format_timedelta(t, threshold=.99)
return render_title_template("admin.html", allUser=all_user, email=email_settings, config=config, commit=commit, return render_title_template("admin.html", allUser=all_user, config=config, commit=commit,
feature_support=feature_support, schedule_time=schedule_time, feature_support=feature_support, schedule_time=schedule_time,
schedule_duration=schedule_duration, schedule_duration=schedule_duration,
title=_(u"Admin page"), page="admin") title=_(u"Admin page"), page="admin")
@ -1062,7 +1062,7 @@ def _config_checkbox_int(to_save, x):
def _config_string(to_save, x): def _config_string(to_save, x):
return config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y) return config.set_from_dictionary(to_save, x, lambda y: y.strip().strip(u'\u200B\u200C\u200D\ufeff') if y else y)
def _configuration_gdrive_helper(to_save): def _configuration_gdrive_helper(to_save):
@ -1151,9 +1151,9 @@ def _configuration_ldap_helper(to_save):
reboot_required |= _config_string(to_save, "config_ldap_cert_path") reboot_required |= _config_string(to_save, "config_ldap_cert_path")
reboot_required |= _config_string(to_save, "config_ldap_key_path") reboot_required |= _config_string(to_save, "config_ldap_key_path")
_config_string(to_save, "config_ldap_group_name") _config_string(to_save, "config_ldap_group_name")
if to_save.get("config_ldap_serv_password", "") != "": if to_save.get("config_ldap_serv_password_e", "") != "":
reboot_required |= 1 reboot_required |= 1
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8') config.set_from_dictionary(to_save, "config_ldap_serv_password_e")
config.save() config.save()
if not config.config_ldap_provider_url \ if not config.config_ldap_provider_url \
@ -1165,7 +1165,7 @@ def _configuration_ldap_helper(to_save):
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS: if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE: if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password): if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password_e):
return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account and Password')) return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account and Password'))
else: else:
if not config.config_ldap_serv_username: if not config.config_ldap_serv_username:
@ -1233,7 +1233,7 @@ def new_user():
kobo_support=kobo_support, registered_oauth=oauth_check) kobo_support=kobo_support, registered_oauth=oauth_check)
@admi.route("/admin/mailsettings") @admi.route("/admin/mailsettings", methods=["GET"])
@login_required @login_required
@admin_required @admin_required
def edit_mailsettings(): def edit_mailsettings():
@ -1266,11 +1266,12 @@ def update_mailsettings():
else: else:
_config_int(to_save, "mail_port") _config_int(to_save, "mail_port")
_config_int(to_save, "mail_use_ssl") _config_int(to_save, "mail_use_ssl")
_config_string(to_save, "mail_password") _config_string(to_save, "mail_password_e")
_config_int(to_save, "mail_size", lambda y: int(y)*1024*1024) _config_int(to_save, "mail_size", lambda y: int(y)*1024*1024)
config.mail_server = to_save.get('mail_server', "").strip() _config_string(to_save, "mail_server")
config.mail_from = to_save.get('mail_from', "").strip() _config_string(to_save, "mail_from")
config.mail_login = to_save.get('mail_login', "").strip() _config_string(to_save, "mail_login")
try: try:
config.save() config.save()
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:
@ -1749,10 +1750,10 @@ def _configuration_update_helper():
# Goodreads configuration # Goodreads configuration
_config_checkbox(to_save, "config_use_goodreads") _config_checkbox(to_save, "config_use_goodreads")
_config_string(to_save, "config_goodreads_api_key") _config_string(to_save, "config_goodreads_api_key")
_config_string(to_save, "config_goodreads_api_secret") _config_string(to_save, "config_goodreads_api_secret_e")
if services.goodreads_support: if services.goodreads_support:
services.goodreads_support.connect(config.config_goodreads_api_key, services.goodreads_support.connect(config.config_goodreads_api_key,
config.config_goodreads_api_secret, config.config_goodreads_api_secret_e,
config.config_use_goodreads) config.config_use_goodreads)
_config_int(to_save, "config_updatechannel") _config_int(to_save, "config_updatechannel")

View File

@ -23,6 +23,10 @@ import json
from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from sqlalchemy.sql.expression import text from sqlalchemy.sql.expression import text
from sqlalchemy import exists
from cryptography.fernet import Fernet
import cryptography.exceptions
from base64 import urlsafe_b64decode
try: try:
# Compatibility with sqlalchemy 2.0 # Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base from sqlalchemy.orm import declarative_base
@ -56,7 +60,8 @@ class _Settings(_Base):
mail_port = Column(Integer, default=25) mail_port = Column(Integer, default=25)
mail_use_ssl = Column(SmallInteger, default=0) mail_use_ssl = Column(SmallInteger, default=0)
mail_login = Column(String, default='mail@example.com') mail_login = Column(String, default='mail@example.com')
mail_password = Column(String, default='mypassword') mail_password_e = Column(String)
mail_password = Column(String)
mail_from = Column(String, default='automailer <mail@example.com>') mail_from = Column(String, default='automailer <mail@example.com>')
mail_size = Column(Integer, default=25*1024*1024) mail_size = Column(Integer, default=25*1024*1024)
mail_server_type = Column(SmallInteger, default=0) mail_server_type = Column(SmallInteger, default=0)
@ -106,6 +111,7 @@ class _Settings(_Base):
config_use_goodreads = Column(Boolean, default=False) config_use_goodreads = Column(Boolean, default=False)
config_goodreads_api_key = Column(String) config_goodreads_api_key = Column(String)
config_goodreads_api_secret_e = Column(String)
config_goodreads_api_secret = Column(String) config_goodreads_api_secret = Column(String)
config_register_email = Column(Boolean, default=False) config_register_email = Column(Boolean, default=False)
config_login_type = Column(Integer, default=0) config_login_type = Column(Integer, default=0)
@ -116,7 +122,8 @@ class _Settings(_Base):
config_ldap_port = Column(SmallInteger, default=389) config_ldap_port = Column(SmallInteger, default=389)
config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE) config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE)
config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org') config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org')
config_ldap_serv_password = Column(String, default="") config_ldap_serv_password_e = Column(String)
config_ldap_serv_password = Column(String)
config_ldap_encryption = Column(SmallInteger, default=0) config_ldap_encryption = Column(SmallInteger, default=0)
config_ldap_cacert_path = Column(String, default="") config_ldap_cacert_path = Column(String, default="")
config_ldap_cert_path = Column(String, default="") config_ldap_cert_path = Column(String, default="")
@ -159,19 +166,117 @@ class _Settings(_Base):
return self.__class__.__name__ return self.__class__.__name__
class MailConfigSQL(object):
def __init__(self):
self.__dict__["dirty"] = list()
def init_config(self, session, secret_key):
self._session = session
self._settings = None
self._fernet = Fernet(secret_key)
self.load()
def _read_from_storage(self):
if self._settings is None:
log.debug("_MailConfigSQL._read_from_storage")
self._settings = self._session.query(_Mail_Settings).first()
return self._settings
def to_dict(self):
storage = {}
for k, v in self.__dict__.items():
if k[0] != '_' and not k.endswith("password") and not k.endswith("secret") and not k == "cli":
storage[k] = v
return storage
def load(self):
"""Load all configuration values from the underlying storage."""
s = self._read_from_storage() # type: _Settings
for k, v in s.__dict__.items():
if k[0] != '_':
if v is None:
# if the storage column has no value, apply the (possible) default
column = s.__class__.__dict__.get(k)
if column.default is not None:
v = column.default.arg
if k.endswith("enc") and v is not None:
try:
setattr(s, k, self._fernet.decrypt(v).decode())
except cryptography.exceptions.InvalidKey:
setattr(s, k, None)
else:
setattr(self, k, v)
self.__dict__["dirty"] = list()
def save(self):
"""Apply all configuration values to the underlying storage."""
s = self._read_from_storage() # type: _Settings
for k in self.dirty:
if k[0] == '_':
continue
if hasattr(s, k):
if k.endswith("enc"):
setattr(s, k, self._fernet.encrypt(self.__dict__[k].encode()))
else:
setattr(s, k, self.__dict__[k])
log.debug("_MailConfigSQL updating storage")
self._session.merge(s)
try:
self._session.commit()
except OperationalError as e:
log.error('Database error: %s', e)
self._session.rollback()
self.load()
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
"""Possibly updates a field of this object.
The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor.
:returns: `True` if the field has changed value
"""
new_value = dictionary.get(field, default)
if new_value is None:
return False
if field not in self.__dict__:
log.warning("_ConfigSQL trying to set unknown field '%s' = %r", field, new_value)
return False
if convertor is not None:
if encode:
new_value = convertor(new_value.encode(encode))
else:
new_value = convertor(new_value)
current_value = self.__dict__.get(field)
if current_value == new_value:
return False
setattr(self, field, new_value)
return True
def __setattr__(self, attr_name, attr_value):
super().__setattr__(attr_name, attr_value)
self.__dict__["dirty"].append(attr_name)
# Class holds all application specific settings in calibre-web # Class holds all application specific settings in calibre-web
class _ConfigSQL(object): class ConfigSQL(object):
# pylint: disable=no-member # pylint: disable=no-member
def __init__(self): def __init__(self):
pass self.__dict__["dirty"] = list()
def init_config(self, session, cli): def init_config(self, session, secret_key, cli):
self._session = session self._session = session
self._settings = None self._settings = None
self.db_configured = None self.db_configured = None
self.config_calibre_dir = None self.config_calibre_dir = None
self.load() self._fernet = Fernet(secret_key)
self.cli = cli self.cli = cli
self.load()
change = False change = False
if self.config_converterpath == None: # pylint: disable=access-member-before-definition if self.config_converterpath == None: # pylint: disable=access-member-before-definition
@ -300,10 +405,10 @@ class _ConfigSQL(object):
setattr(self, field, new_value) setattr(self, field, new_value)
return True return True
def toDict(self): def to_dict(self):
storage = {} storage = {}
for k, v in self.__dict__.items(): for k, v in self.__dict__.items():
if k[0] != '_' and not k.endswith("password") and not k.endswith("secret") and not k == "cli": if k[0] != '_' and not k.endswith("_e") and not k == "cli":
storage[k] = v storage[k] = v
return storage return storage
@ -317,6 +422,12 @@ class _ConfigSQL(object):
column = s.__class__.__dict__.get(k) column = s.__class__.__dict__.get(k)
if column.default is not None: if column.default is not None:
v = column.default.arg v = column.default.arg
if k.endswith("_e") and v is not None:
try:
setattr(self, k, self._fernet.decrypt(v).decode())
except cryptography.fernet.InvalidToken:
setattr(self, k, "")
else:
setattr(self, k, v) setattr(self, k, v)
have_metadata_db = bool(self.config_calibre_dir) have_metadata_db = bool(self.config_calibre_dir)
@ -339,16 +450,20 @@ class _ConfigSQL(object):
except OperationalError as e: except OperationalError as e:
log.error('Database error: %s', e) log.error('Database error: %s', e)
self._session.rollback() self._session.rollback()
self.__dict__["dirty"] = list()
def save(self): def save(self):
"""Apply all configuration values to the underlying storage.""" """Apply all configuration values to the underlying storage."""
s = self._read_from_storage() # type: _Settings s = self._read_from_storage() # type: _Settings
for k, v in self.__dict__.items(): for k in self.dirty:
if k[0] == '_': if k[0] == '_':
continue continue
if hasattr(s, k): if hasattr(s, k):
setattr(s, k, v) if k.endswith("_e"):
setattr(s, k, self._fernet.encrypt(self.__dict__[k].encode()))
else:
setattr(s, k, self.__dict__[k])
log.debug("_ConfigSQL updating storage") log.debug("_ConfigSQL updating storage")
self._session.merge(s) self._session.merge(s)
@ -364,7 +479,6 @@ class _ConfigSQL(object):
log.error(error) log.error(error)
log.warning("invalidating configuration") log.warning("invalidating configuration")
self.db_configured = False self.db_configured = False
# self.config_calibre_dir = None
self.save() self.save()
def store_calibre_uuid(self, calibre_db, Library_table): def store_calibre_uuid(self, calibre_db, Library_table):
@ -376,8 +490,40 @@ class _ConfigSQL(object):
except AttributeError: except AttributeError:
pass pass
def __setattr__(self, attr_name, attr_value):
super().__setattr__(attr_name, attr_value)
self.__dict__["dirty"].append(attr_name)
def _migrate_table(session, orm_class):
def _encrypt_fields(session, secret_key):
try:
session.query(exists().where(_Settings.mail_password_e)).scalar()
except OperationalError:
with session.bind.connect() as conn:
conn.execute("ALTER TABLE settings ADD column 'mail_password_e' String")
conn.execute("ALTER TABLE settings ADD column 'config_goodreads_api_secret_e' String")
conn.execute("ALTER TABLE settings ADD column 'config_ldap_serv_password_e' String")
session.commit()
crypter = Fernet(secret_key)
settings = session.query(_Settings.mail_password, _Settings.config_goodreads_api_secret,
_Settings.config_ldap_serv_password).first()
if settings.mail_password:
session.query(_Settings).update(
{_Settings.mail_password_e: crypter.encrypt(settings.mail_password.encode())})
if settings.config_goodreads_api_secret:
session.query(_Settings).update(
{_Settings.config_goodreads_api_secret_e:
crypter.encrypt(settings.config_goodreads_api_secret.encode())})
if settings.config_ldap_serv_password:
session.query(_Settings).update(
{_Settings.config_ldap_serv_password_e:
crypter.encrypt(settings.config_ldap_serv_password.encode())})
session.commit()
def _migrate_table(session, orm_class, secret_key=None):
if secret_key:
_encrypt_fields(session, secret_key)
changed = False changed = False
for column_name, column in orm_class.__dict__.items(): for column_name, column in orm_class.__dict__.items():
@ -453,22 +599,18 @@ def autodetect_kepubify_binary():
return "" return ""
def _migrate_database(session): def _migrate_database(session, secret_key):
# make sure the table is created, if it does not exist # make sure the table is created, if it does not exist
_Base.metadata.create_all(session.bind) _Base.metadata.create_all(session.bind)
_migrate_table(session, _Settings) _migrate_table(session, _Settings, secret_key)
_migrate_table(session, _Flask_Settings) _migrate_table(session, _Flask_Settings)
def load_configuration(conf, session, cli): def load_configuration(session, secret_key):
_migrate_database(session) _migrate_database(session, secret_key)
if not session.query(_Settings).count(): if not session.query(_Settings).count():
session.add(_Settings()) session.add(_Settings())
session.commit() session.commit()
# conf = _ConfigSQL()
conf.init_config(session, cli)
# return conf
def get_flask_session_key(_session): def get_flask_session_key(_session):
@ -478,3 +620,25 @@ def get_flask_session_key(_session):
_session.add(flask_settings) _session.add(flask_settings)
_session.commit() _session.commit()
return flask_settings.flask_session_key return flask_settings.flask_session_key
def get_encryption_key(key_path):
key_file = os.path.join(key_path, ".key")
generate = True
error = ""
if os.path.exists(key_file) and os.path.getsize(key_file) > 32:
with open(key_file, "rb") as f:
key = f.read()
try:
urlsafe_b64decode(key)
generate = False
except ValueError:
pass
if generate:
key = Fernet.generate_key()
try:
with open(key_file, "wb") as f:
f.write(key)
except PermissionError as e:
error = e
return key, error

View File

@ -65,7 +65,7 @@ def send_debug():
file_list.remove(element) file_list.remove(element)
memory_zip = BytesIO() memory_zip = BytesIO()
with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf: with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr('settings.txt', json.dumps(config.toDict(), sort_keys=True, indent=2)) zf.writestr('settings.txt', json.dumps(config.to_dict(), sort_keys=True, indent=2))
zf.writestr('libs.txt', json.dumps(collect_stats(), sort_keys=True, indent=2, cls=lazyEncoder)) zf.writestr('libs.txt', json.dumps(collect_stats(), sort_keys=True, indent=2, cls=lazyEncoder))
for fp in file_list: for fp in file_list:
zf.write(fp, os.path.basename(fp)) zf.write(fp, os.path.basename(fp))

View File

@ -19,7 +19,6 @@
import os import os
import io import io
import sys
import mimetypes import mimetypes
import re import re
import shutil import shutil

View File

@ -44,15 +44,15 @@ def init_app(app, config):
app.config['LDAP_SCHEMA'] = 'ldap' app.config['LDAP_SCHEMA'] = 'ldap'
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS: if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE: if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
if config.config_ldap_serv_password is None: if config.config_ldap_serv_password_e is None:
config.config_ldap_serv_password = '' config.config_ldap_serv_password_e = ''
app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password) app.config['LDAP_PASSWORD'] = config.config_ldap_serv_password_e
else: else:
app.config['LDAP_PASSWORD'] = base64.b64decode("") app.config['LDAP_PASSWORD'] = ""
app.config['LDAP_USERNAME'] = config.config_ldap_serv_username app.config['LDAP_USERNAME'] = config.config_ldap_serv_username
else: else:
app.config['LDAP_USERNAME'] = "" app.config['LDAP_USERNAME'] = ""
app.config['LDAP_PASSWORD'] = base64.b64decode("") app.config['LDAP_PASSWORD'] = ""
if bool(config.config_ldap_cert_path): if bool(config.config_ldap_cert_path):
app.config['LDAP_CUSTOM_OPTIONS'].update({ app.config['LDAP_CUSTOM_OPTIONS'].update({
pyLDAP.OPT_X_TLS_REQUIRE_CERT: pyLDAP.OPT_X_TLS_DEMAND, pyLDAP.OPT_X_TLS_REQUIRE_CERT: pyLDAP.OPT_X_TLS_DEMAND,

View File

@ -202,8 +202,8 @@ class TaskEmail(CalibreTask):
self.asyncSMTP.set_debuglevel(1) self.asyncSMTP.set_debuglevel(1)
if use_ssl == 1: if use_ssl == 1:
self.asyncSMTP.starttls() self.asyncSMTP.starttls()
if self.settings["mail_password"]: if self.settings["mail_password_e"]:
self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"])) self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password_e"]))
# Convert message to something to send # Convert message to something to send
fp = StringIO() fp = StringIO()

View File

@ -61,27 +61,27 @@
<div class="col"> <div class="col">
<h2>{{_('E-mail Server Settings')}}</h2> <h2>{{_('E-mail Server Settings')}}</h2>
{% if config.get_mail_server_configured() %} {% if config.get_mail_server_configured() %}
{% if email.mail_server_type == 0 %} {% if config.mail_server_type == 0 %}
<div class="col-xs-12 col-sm-12"> <div class="col-xs-12 col-sm-12">
<div class="row"> <div class="row">
<div class="col-xs-6 col-sm-3">{{_('SMTP Hostname')}}</div> <div class="col-xs-6 col-sm-3">{{_('SMTP Hostname')}}</div>
<div class="col-xs-6 col-sm-3">{{email.mail_server}}</div> <div class="col-xs-6 col-sm-3">{{config.mail_server}}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-sm-3">{{_('SMTP Port')}}</div> <div class="col-xs-6 col-sm-3">{{_('SMTP Port')}}</div>
<div class="col-xs-6 col-sm-3">{{email.mail_port}}</div> <div class="col-xs-6 col-sm-3">{{config.mail_port}}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-sm-3">{{_('Encryption')}}</div> <div class="col-xs-6 col-sm-3">{{_('Encryption')}}</div>
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(email.mail_use_ssl) }}</div> <div class="col-xs-6 col-sm-3">{{ display_bool_setting(config.mail_use_ssl) }}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-sm-3">{{_('SMTP Login')}}</div> <div class="col-xs-6 col-sm-3">{{_('SMTP Login')}}</div>
<div class="col-xs-6 col-sm-3">{{email.mail_login}}</div> <div class="col-xs-6 col-sm-3">{{config.mail_login}}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-sm-3">{{_('From E-mail')}}</div> <div class="col-xs-6 col-sm-3">{{_('From E-mail')}}</div>
<div class="col-xs-6 col-sm-3">{{email.mail_from}}</div> <div class="col-xs-6 col-sm-3">{{config.mail_from}}</div>
</div> </div>
</div> </div>
{% else %} {% else %}
@ -92,7 +92,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-sm-3">{{_('From E-mail')}}</div> <div class="col-xs-6 col-sm-3">{{_('From E-mail')}}</div>
<div class="col-xs-6 col-sm-3">{{email.mail_gmail_token['email']}}</div> <div class="col-xs-6 col-sm-3">{{config.mail_gmail_token['email']}}</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -159,8 +159,8 @@
<input type="text" class="form-control" id="config_goodreads_api_key" name="config_goodreads_api_key" value="{% if config.config_goodreads_api_key != None %}{{ config.config_goodreads_api_key }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_goodreads_api_key" name="config_goodreads_api_key" value="{% if config.config_goodreads_api_key != None %}{{ config.config_goodreads_api_key }}{% endif %}" autocomplete="off">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="config_goodreads_api_secret">{{_('Goodreads API Secret')}}</label> <label for="config_goodreads_api_secret_e">{{_('Goodreads API Secret')}}</label>
<input type="text" class="form-control" id="config_goodreads_api_secret" name="config_goodreads_api_secret" value="{% if config.config_goodreads_api_secret != None %}{{ config.config_goodreads_api_secret }}{% endif %}" autocomplete="off"> <input type="password" class="form-control" id="config_goodreads_api_secret_e" name="config_goodreads_api_secret_e" value="{% if config.config_goodreads_api_secret_e != None %}{{ config.config_goodreads_api_secret_e }}{% endif %}" autocomplete="off">
</div> </div>
</div> </div>
{% endif %} {% endif %}
@ -245,8 +245,8 @@
</div> </div>
<div data-related="ldap-auth-password-2"> <div data-related="ldap-auth-password-2">
<div class="form-group"> <div class="form-group">
<label for="config_ldap_serv_password">{{_('LDAP Administrator Password')}}</label> <label for="config_ldap_serv_password_e">{{_('LDAP Administrator Password')}}</label>
<input type="password" class="form-control" id="config_ldap_serv_password" name="config_ldap_serv_password" value="" autocomplete="off"> <input type="password" class="form-control" id="config_ldap_serv_password_e" name="config_ldap_serv_password_e" value="" autocomplete="off">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@ -48,8 +48,8 @@
<input type="text" class="form-control" name="mail_login" id="mail_login" value="{{content.mail_login}}"> <input type="text" class="form-control" name="mail_login" id="mail_login" value="{{content.mail_login}}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="mail_password">{{_('SMTP Password')}}</label> <label for="mail_password_e">{{_('SMTP Password')}}</label>
<input type="password" class="form-control" name="mail_password" id="mail_password" value="{{content.mail_password}}"> <input type="password" class="form-control" name="mail_password_e" id="mail_password_e" value="{{content.mail_password_e}}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="mail_from">{{_('From E-mail')}}</label> <label for="mail_from">{{_('From E-mail')}}</label>

View File

@ -33,7 +33,7 @@ scholarly>=1.2.0,<1.7
markdown2>=2.0.0,<2.5.0 markdown2>=2.0.0,<2.5.0
html2text>=2020.1.16,<2022.1.1 html2text>=2020.1.16,<2022.1.1
python-dateutil>=2.1,<2.9.0 python-dateutil>=2.1,<2.9.0
beautifulsoup4>=4.0.1,<4.11.0 beautifulsoup4>=4.0.1,<4.12.0
cchardet>=2.0.0,<2.2.0 cchardet>=2.0.0,<2.2.0
# Comics # Comics