06/01 Optimize codes
This commit is contained in:
+34
-2
@@ -322,12 +322,36 @@ def import_confirm():
|
||||
|
||||
ALLOWED_RECEIPT_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}
|
||||
|
||||
# Magic bytes → (canonical_extension, mime_type)
|
||||
_MAGIC = [
|
||||
(b'\xff\xd8\xff', 'jpg', 'image/jpeg'),
|
||||
(b'\x89PNG\r\n\x1a\n', 'png', 'image/png'),
|
||||
(b'GIF87a', 'gif', 'image/gif'),
|
||||
(b'GIF89a', 'gif', 'image/gif'),
|
||||
(b'%PDF-', 'pdf', 'application/pdf'),
|
||||
# WEBP: RIFF????WEBP (bytes 0-3 = RIFF, bytes 8-11 = WEBP)
|
||||
]
|
||||
|
||||
|
||||
def _allowed_receipt(filename):
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in ALLOWED_RECEIPT_EXTENSIONS
|
||||
|
||||
|
||||
def _check_magic(header_bytes):
|
||||
"""
|
||||
Inspect the first 12 bytes of a file and return (ext, mime) if recognised,
|
||||
or (None, None) if the bytes don't match any allowed type.
|
||||
"""
|
||||
for magic, ext, mime in _MAGIC:
|
||||
if header_bytes[:len(magic)] == magic:
|
||||
return ext, mime
|
||||
# WEBP special case: RIFF at 0, WEBP at 8
|
||||
if header_bytes[:4] == b'RIFF' and header_bytes[8:12] == b'WEBP':
|
||||
return 'webp', 'image/webp'
|
||||
return None, None
|
||||
|
||||
|
||||
@settings_bp.route('/receipt/upload/<int:txn_id>', methods=['POST'])
|
||||
@login_required
|
||||
def upload_receipt(txn_id):
|
||||
@@ -350,7 +374,15 @@ def upload_receipt(txn_id):
|
||||
flash('File too large. Maximum 10MB.', 'warning')
|
||||
return redirect(request.referrer or url_for('transactions.index'))
|
||||
|
||||
ext = f.filename.rsplit('.', 1)[1].lower()
|
||||
# Validate actual file content via magic bytes (prevents renamed-file attacks)
|
||||
header = f.read(12)
|
||||
f.seek(0)
|
||||
magic_ext, magic_mime = _check_magic(header)
|
||||
if magic_ext is None:
|
||||
flash('File content does not match an allowed type (PNG, JPG, GIF, PDF, WEBP).', 'warning')
|
||||
return redirect(request.referrer or url_for('transactions.index'))
|
||||
|
||||
ext = magic_ext # use the extension we detected, not what the user claimed
|
||||
unique_name = f'{uuid.uuid4().hex}.{ext}'
|
||||
upload_dir = current_app.config.get('UPLOAD_FOLDER', '/home/pfm/app/uploads')
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
@@ -367,7 +399,7 @@ def upload_receipt(txn_id):
|
||||
filename=unique_name,
|
||||
original_filename=secure_filename(f.filename),
|
||||
file_size=file_size,
|
||||
mime_type=f.content_type,
|
||||
mime_type=magic_mime, # use validated type, not the browser-supplied header
|
||||
)
|
||||
db.session.add(receipt)
|
||||
db.session.flush()
|
||||
|
||||
+20
-3
@@ -226,14 +226,30 @@ def sync_confirm():
|
||||
parsed.append(r)
|
||||
|
||||
imported, skipped = import_transactions(parsed, ta)
|
||||
flash(f'Imported {imported} transaction(s). Skipped {skipped} duplicate(s).', 'success')
|
||||
flash(f'Imported {imported} transaction(s) from {ta.account_name}. '
|
||||
f'Skipped {skipped} duplicate(s).', 'success')
|
||||
|
||||
# Advance the sync queue if this came from a "Sync All" run
|
||||
from flask import session as flask_session
|
||||
queue = flask_session.pop('teller_sync_queue', [])
|
||||
if queue:
|
||||
next_id = queue[0]
|
||||
flask_session['teller_sync_queue'] = queue[1:]
|
||||
flash(f'{len(queue)} account(s) remaining in sync queue.', 'info')
|
||||
return redirect(url_for('teller.sync_preview_view', teller_account_id=next_id))
|
||||
|
||||
return redirect(url_for('transactions.index'))
|
||||
|
||||
|
||||
@teller_bp.route('/sync/all', methods=['POST'])
|
||||
@login_required
|
||||
def sync_all():
|
||||
"""Sync all mapped accounts — redirects to first account's preview."""
|
||||
"""
|
||||
Queue all mapped accounts for sequential sync.
|
||||
Stores the full list in session, then redirects to the first account's preview.
|
||||
After each confirm the queue advances automatically.
|
||||
"""
|
||||
from flask import session as flask_session
|
||||
enrollments = TellerEnrollment.query.filter_by(is_active=True).all()
|
||||
mapped = []
|
||||
for e in enrollments:
|
||||
@@ -245,7 +261,8 @@ def sync_all():
|
||||
flash('No accounts mapped for sync.', 'warning')
|
||||
return redirect(url_for('teller.index'))
|
||||
|
||||
# For simplicity, sync first account; user can chain through others
|
||||
# Store the full queue; first item will be previewed immediately
|
||||
flask_session['teller_sync_queue'] = [ta.id for ta in mapped[1:]]
|
||||
return redirect(url_for('teller.sync_preview_view', teller_account_id=mapped[0].id))
|
||||
|
||||
|
||||
|
||||
@@ -140,6 +140,9 @@ def new():
|
||||
db.session.commit()
|
||||
calc_balance(txn.account_id)
|
||||
flash(f'{"Income" if txn_type == "income" else "Expense"} added.', 'success')
|
||||
if txn_type == 'expense':
|
||||
from app.services.alert_service import check_and_flash_budget_alerts
|
||||
check_and_flash_budget_alerts(flash)
|
||||
return redirect(url_for('transactions.index', tab=txn_type))
|
||||
|
||||
return render_template('transactions/form.html',
|
||||
@@ -174,6 +177,9 @@ def edit(id):
|
||||
calc_balance(old_account_id)
|
||||
calc_balance(txn.account_id)
|
||||
flash('Transaction updated.', 'success')
|
||||
if txn.transaction_type == 'expense':
|
||||
from app.services.alert_service import check_and_flash_budget_alerts
|
||||
check_and_flash_budget_alerts(flash)
|
||||
return redirect(url_for('transactions.index', tab=txn.transaction_type))
|
||||
|
||||
return render_template('transactions/form.html',
|
||||
|
||||
Reference in New Issue
Block a user