Jul 2nd - Optimized code 3
This commit is contained in:
+1
-1
@@ -369,7 +369,7 @@ def _seed_settings():
|
|||||||
('email_ingestion_host', '', 'IMAP server hostname (e.g. imap.gmail.com)'),
|
('email_ingestion_host', '', 'IMAP server hostname (e.g. imap.gmail.com)'),
|
||||||
('email_ingestion_port', '993', 'IMAP SSL port'),
|
('email_ingestion_port', '993', 'IMAP SSL port'),
|
||||||
('email_ingestion_user', '', 'Mailbox username / email address'),
|
('email_ingestion_user', '', 'Mailbox username / email address'),
|
||||||
('email_ingestion_password', '', 'Mailbox password (stored in plaintext — use a dedicated app password)'),
|
('email_ingestion_password', '', 'Mailbox password (encrypted at rest — use a dedicated app password)'),
|
||||||
('email_ingestion_folder', 'INBOX', 'IMAP folder to watch for new mail'),
|
('email_ingestion_folder', 'INBOX', 'IMAP folder to watch for new mail'),
|
||||||
('email_ingestion_move_to', 'Processed', 'IMAP folder to move processed mail into'),
|
('email_ingestion_move_to', 'Processed', 'IMAP folder to move processed mail into'),
|
||||||
('email_ingestion_interval', '5', 'Poll interval in minutes'),
|
('email_ingestion_interval', '5', 'Poll interval in minutes'),
|
||||||
|
|||||||
+10
-4
@@ -1318,10 +1318,12 @@ def settings():
|
|||||||
'email_ingestion_move_to' : request.form.get('email_ingestion_move_to', 'Processed').strip(),
|
'email_ingestion_move_to' : request.form.get('email_ingestion_move_to', 'Processed').strip(),
|
||||||
'email_ingestion_interval': request.form.get('email_ingestion_interval', '5').strip(),
|
'email_ingestion_interval': request.form.get('email_ingestion_interval', '5').strip(),
|
||||||
}
|
}
|
||||||
# Password: only update if a new value was provided
|
# Password: only update if a new value was provided. Encrypted
|
||||||
|
# at rest — see app/services/crypto_service.py.
|
||||||
new_pw = request.form.get('email_ingestion_password', '').strip()
|
new_pw = request.form.get('email_ingestion_password', '').strip()
|
||||||
if new_pw:
|
if new_pw:
|
||||||
fields['email_ingestion_password'] = new_pw
|
from app.services.crypto_service import encrypt_secret
|
||||||
|
fields['email_ingestion_password'] = encrypt_secret(new_pw)
|
||||||
|
|
||||||
changes = []
|
changes = []
|
||||||
for key, value in fields.items():
|
for key, value in fields.items():
|
||||||
@@ -1412,7 +1414,10 @@ def settings():
|
|||||||
'host' : SystemSetting.get('email_ingestion_host', ''),
|
'host' : SystemSetting.get('email_ingestion_host', ''),
|
||||||
'port' : SystemSetting.get('email_ingestion_port', '993'),
|
'port' : SystemSetting.get('email_ingestion_port', '993'),
|
||||||
'user' : SystemSetting.get('email_ingestion_user', ''),
|
'user' : SystemSetting.get('email_ingestion_user', ''),
|
||||||
'password': SystemSetting.get('email_ingestion_password', ''),
|
# The stored password is never sent back to the browser — the field
|
||||||
|
# is always rendered blank with a placeholder indicating whether one
|
||||||
|
# is already configured (see settings.html).
|
||||||
|
'has_password': bool(SystemSetting.get('email_ingestion_password', '')),
|
||||||
'folder' : SystemSetting.get('email_ingestion_folder', 'INBOX'),
|
'folder' : SystemSetting.get('email_ingestion_folder', 'INBOX'),
|
||||||
'move_to' : SystemSetting.get('email_ingestion_move_to', 'Processed'),
|
'move_to' : SystemSetting.get('email_ingestion_move_to', 'Processed'),
|
||||||
'interval' : SystemSetting.get('email_ingestion_interval', '5'),
|
'interval' : SystemSetting.get('email_ingestion_interval', '5'),
|
||||||
@@ -1477,7 +1482,8 @@ def email_ingestion_test():
|
|||||||
|
|
||||||
# Fall back to the stored password if the admin left the field blank
|
# Fall back to the stored password if the admin left the field blank
|
||||||
if not password:
|
if not password:
|
||||||
password = SystemSetting.get('email_ingestion_password', '')
|
from app.services.crypto_service import decrypt_secret
|
||||||
|
password = decrypt_secret(SystemSetting.get('email_ingestion_password', ''))
|
||||||
|
|
||||||
if not host or not user or not password:
|
if not host or not user or not password:
|
||||||
return jsonify(ok=False, message='Host, username, and password are required.')
|
return jsonify(ok=False, message='Host, username, and password are required.')
|
||||||
|
|||||||
@@ -86,22 +86,6 @@ def save_attachment(file, ticket_id=None, comment_id=None, uploader_id=None):
|
|||||||
)
|
)
|
||||||
db.session.add(att)
|
db.session.add(att)
|
||||||
return att
|
return att
|
||||||
filename = secure_filename(file.filename)
|
|
||||||
ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
|
|
||||||
stored_name = f"{uuid.uuid4().hex}.{ext}"
|
|
||||||
upload_dir = current_app.config['UPLOAD_FOLDER']
|
|
||||||
file.save(os.path.join(upload_dir, stored_name))
|
|
||||||
att = Attachment(
|
|
||||||
ticket_id = ticket_id,
|
|
||||||
comment_id = comment_id,
|
|
||||||
filename = filename,
|
|
||||||
stored_name= stored_name,
|
|
||||||
file_size = os.path.getsize(os.path.join(upload_dir, stored_name)),
|
|
||||||
mime_type = file.content_type,
|
|
||||||
uploaded_by= uploader_id,
|
|
||||||
)
|
|
||||||
db.session.add(att)
|
|
||||||
return att
|
|
||||||
|
|
||||||
|
|
||||||
# ─── Dashboard ────────────────────────────────────────────────────────────────
|
# ─── Dashboard ────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Symmetric encryption for secrets stored in SystemSetting (currently just
|
||||||
|
the IMAP mailbox password used by email ingestion).
|
||||||
|
|
||||||
|
The Fernet key is derived from SECRET_KEY rather than a separately managed
|
||||||
|
key, so no extra key-rotation story is needed for a single-tenant on-prem
|
||||||
|
app — rotating SECRET_KEY (which already invalidates sessions) also
|
||||||
|
invalidates encrypted secrets, which is an acceptable trade-off here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
|
||||||
|
def _fernet() -> Fernet:
|
||||||
|
secret = current_app.config['SECRET_KEY'].encode()
|
||||||
|
key = base64.urlsafe_b64encode(hashlib.sha256(secret).digest())
|
||||||
|
return Fernet(key)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_secret(plain: str) -> str:
|
||||||
|
"""Encrypt a secret for storage. Empty input passes through unchanged."""
|
||||||
|
if not plain:
|
||||||
|
return ''
|
||||||
|
return _fernet().encrypt(plain.encode()).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_secret(stored: str) -> str:
|
||||||
|
"""Decrypt a secret previously written by encrypt_secret().
|
||||||
|
|
||||||
|
Falls back to returning the value unchanged if it isn't a valid Fernet
|
||||||
|
token. This covers values saved before encryption was introduced, so
|
||||||
|
existing configurations (e.g. email ingestion set up before this change)
|
||||||
|
keep working without a manual data migration — the value is simply
|
||||||
|
re-encrypted the next time it's saved through the settings form.
|
||||||
|
"""
|
||||||
|
if not stored:
|
||||||
|
return ''
|
||||||
|
try:
|
||||||
|
return _fernet().decrypt(stored.encode()).decode()
|
||||||
|
except (InvalidToken, ValueError):
|
||||||
|
return stored
|
||||||
@@ -295,7 +295,8 @@ def _run_ingestion(app):
|
|||||||
host = _get_setting('email_ingestion_host', '')
|
host = _get_setting('email_ingestion_host', '')
|
||||||
port = int(_get_setting('email_ingestion_port', '993'))
|
port = int(_get_setting('email_ingestion_port', '993'))
|
||||||
username = _get_setting('email_ingestion_user', '')
|
username = _get_setting('email_ingestion_user', '')
|
||||||
password = _get_setting('email_ingestion_password', '')
|
from app.services.crypto_service import decrypt_secret
|
||||||
|
password = decrypt_secret(_get_setting('email_ingestion_password', ''))
|
||||||
folder = _get_setting('email_ingestion_folder', 'INBOX')
|
folder = _get_setting('email_ingestion_folder', 'INBOX')
|
||||||
move_to = _get_setting('email_ingestion_move_to', 'Processed')
|
move_to = _get_setting('email_ingestion_move_to', 'Processed')
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="password" class="form-control" name="email_ingestion_password"
|
<input type="password" class="form-control" name="email_ingestion_password"
|
||||||
value="{{ email_settings.password }}" placeholder="Leave blank to keep current"/>
|
autocomplete="new-password"
|
||||||
|
placeholder="{{ 'Leave blank to keep current (already set)' if email_settings.has_password else 'Not set' }}"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Watch Folder</label>
|
<label class="form-label">Watch Folder</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user