Sep 16 - Optimize code, part 1
This commit is contained in:
@@ -281,7 +281,7 @@ def attendance_report():
|
||||
CONCAT(e.firstName, ' ', e.lastName) as employee_name,
|
||||
ad.verification_required,
|
||||
ad.verification_status,
|
||||
ad.verification_photo,
|
||||
NULL AS verification_photo, -- base64 image, never shown on this page
|
||||
COALESCE(ad.is_dynamic_qr, 0) as is_dynamic_qr
|
||||
FROM attendance_data ad
|
||||
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
|
||||
@@ -310,7 +310,7 @@ def attendance_report():
|
||||
CONCAT(e.firstName, ' ', e.lastName) as employee_name,
|
||||
ad.verification_required,
|
||||
ad.verification_status,
|
||||
ad.verification_photo,
|
||||
NULL AS verification_photo, -- base64 image, never shown on this page
|
||||
COALESCE(ad.is_dynamic_qr, 0) as is_dynamic_qr
|
||||
FROM attendance_data ad
|
||||
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
|
||||
|
||||
@@ -32,6 +32,7 @@ from utils.helpers import (
|
||||
staff_or_admin_required)
|
||||
from utils.geocoding import (calculate_location_accuracy_enhanced, process_location_data_enhanced,
|
||||
check_location_accuracy_column_exists)
|
||||
from utils.excel_safety import excel_hyperlink, neutralize_unexpected_formulas
|
||||
import openpyxl
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
@@ -462,7 +463,7 @@ def create_excel_export(selected_columns, column_names, filters):
|
||||
# Format coordinates with 10 decimal places
|
||||
lat_formatted = f"{float(qr_record.address_latitude):.10f}"
|
||||
lng_formatted = f"{float(qr_record.address_longitude):.10f}"
|
||||
hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
|
||||
hyperlink_formula = excel_hyperlink(f"http://maps.google.com/maps?q={lat_formatted},{lng_formatted}", address_text.strip())
|
||||
cell.value = hyperlink_formula
|
||||
logger_handler.logger.debug(f"Added QR address hyperlink for employee {attendance_record.employee_id}")
|
||||
else:
|
||||
@@ -475,7 +476,7 @@ def create_excel_export(selected_columns, column_names, filters):
|
||||
# Format coordinates with 10 decimal places
|
||||
lat_formatted = f"{float(attendance_record.latitude):.10f}"
|
||||
lng_formatted = f"{float(attendance_record.longitude):.10f}"
|
||||
hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
|
||||
hyperlink_formula = excel_hyperlink(f"http://maps.google.com/maps?q={lat_formatted},{lng_formatted}", address_text.strip())
|
||||
cell.value = hyperlink_formula
|
||||
logger_handler.logger.debug(f"Added check-in address hyperlink for employee {attendance_record.employee_id}")
|
||||
else:
|
||||
@@ -488,7 +489,7 @@ def create_excel_export(selected_columns, column_names, filters):
|
||||
# Format coordinates with 10 decimal places
|
||||
lat_formatted = f"{float(attendance_record.latitude):.10f}"
|
||||
lng_formatted = f"{float(attendance_record.longitude):.10f}"
|
||||
hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
|
||||
hyperlink_formula = excel_hyperlink(f"http://maps.google.com/maps?q={lat_formatted},{lng_formatted}", address_text.strip())
|
||||
cell.value = hyperlink_formula
|
||||
logger_handler.logger.debug(f"Added check-in address hyperlink (fallback) for employee {attendance_record.employee_id}")
|
||||
else:
|
||||
@@ -500,7 +501,7 @@ def create_excel_export(selected_columns, column_names, filters):
|
||||
# Format coordinates with 10 decimal places
|
||||
lat_formatted = f"{float(attendance_record.latitude):.10f}"
|
||||
lng_formatted = f"{float(attendance_record.longitude):.10f}"
|
||||
hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
|
||||
hyperlink_formula = excel_hyperlink(f"http://maps.google.com/maps?q={lat_formatted},{lng_formatted}", address_text.strip())
|
||||
cell.value = hyperlink_formula
|
||||
logger_handler.logger.debug(f"Added check-in address hyperlink (no accuracy data) for employee {attendance_record.employee_id}")
|
||||
else:
|
||||
@@ -585,6 +586,7 @@ def create_excel_export(selected_columns, column_names, filters):
|
||||
|
||||
# Save to BytesIO
|
||||
excel_buffer = io.BytesIO()
|
||||
neutralize_unexpected_formulas(wb) # formula-injection guard (utils/excel_safety.py)
|
||||
wb.save(excel_buffer)
|
||||
excel_buffer.seek(0)
|
||||
|
||||
@@ -801,7 +803,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
|
||||
# Format coordinates with 10 decimal places
|
||||
lat_formatted = f"{float(qr_record.address_latitude):.10f}"
|
||||
lng_formatted = f"{float(qr_record.address_longitude):.10f}"
|
||||
hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
|
||||
hyperlink_formula = excel_hyperlink(f"http://maps.google.com/maps?q={lat_formatted},{lng_formatted}", address_text.strip())
|
||||
cell.value = hyperlink_formula
|
||||
logger_handler.logger.debug(f"Added QR address hyperlink for employee {attendance_record.employee_id}")
|
||||
else:
|
||||
@@ -814,7 +816,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
|
||||
# Format coordinates with 10 decimal places
|
||||
lat_formatted = f"{float(attendance_record.latitude):.10f}"
|
||||
lng_formatted = f"{float(attendance_record.longitude):.10f}"
|
||||
hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
|
||||
hyperlink_formula = excel_hyperlink(f"http://maps.google.com/maps?q={lat_formatted},{lng_formatted}", address_text.strip())
|
||||
cell.value = hyperlink_formula
|
||||
logger_handler.logger.debug(f"Added check-in address hyperlink for employee {attendance_record.employee_id}")
|
||||
else:
|
||||
@@ -827,7 +829,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
|
||||
# Format coordinates with 10 decimal places
|
||||
lat_formatted = f"{float(attendance_record.latitude):.10f}"
|
||||
lng_formatted = f"{float(attendance_record.longitude):.10f}"
|
||||
hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
|
||||
hyperlink_formula = excel_hyperlink(f"http://maps.google.com/maps?q={lat_formatted},{lng_formatted}", address_text.strip())
|
||||
cell.value = hyperlink_formula
|
||||
logger_handler.logger.debug(f"Added check-in address hyperlink (fallback) for employee {attendance_record.employee_id}")
|
||||
else:
|
||||
@@ -839,7 +841,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
|
||||
# Format coordinates with 10 decimal places
|
||||
lat_formatted = f"{float(attendance_record.latitude):.10f}"
|
||||
lng_formatted = f"{float(attendance_record.longitude):.10f}"
|
||||
hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
|
||||
hyperlink_formula = excel_hyperlink(f"http://maps.google.com/maps?q={lat_formatted},{lng_formatted}", address_text.strip())
|
||||
cell.value = hyperlink_formula
|
||||
logger_handler.logger.debug(f"Added check-in address hyperlink (no accuracy data) for employee {attendance_record.employee_id}")
|
||||
else:
|
||||
@@ -933,6 +935,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
|
||||
|
||||
# Save to BytesIO
|
||||
excel_buffer = io.BytesIO()
|
||||
neutralize_unexpected_formulas(wb) # formula-injection guard (utils/excel_safety.py)
|
||||
wb.save(excel_buffer)
|
||||
excel_buffer.seek(0)
|
||||
|
||||
|
||||
+3
-2
@@ -86,10 +86,11 @@ def login():
|
||||
flash('Please enter both username and password.', 'error')
|
||||
return render_template('login.html')
|
||||
|
||||
# Rate-limit check — blocks IPs with 5+ failed attempts in 15 minutes
|
||||
# Rate-limit check — 5 failures for this IP + username, or 20 for the
|
||||
# username from any IP, within 15 minutes (see SecurityManager)
|
||||
from flask import current_app
|
||||
sec_mgr = getattr(current_app, 'security_manager', None)
|
||||
if sec_mgr and sec_mgr.is_auth_rate_limited():
|
||||
if sec_mgr and sec_mgr.is_auth_rate_limited(username):
|
||||
logger_handler.log_security_event(
|
||||
event_type="login_rate_limited",
|
||||
description=f"Login blocked by rate limiter for username: {username}",
|
||||
|
||||
@@ -17,6 +17,8 @@ from models.qrcode import QRCode
|
||||
from models.user import User
|
||||
from logger_handler import log_user_activity, log_database_operations
|
||||
from utils.helpers import (
|
||||
PAYROLL_AREA_ROLES,
|
||||
restrict_blueprint_to_roles,
|
||||
admin_required,
|
||||
has_admin_privileges,
|
||||
has_staff_level_access,
|
||||
@@ -25,6 +27,9 @@ from utils.helpers import (
|
||||
|
||||
bp = Blueprint('employees', __name__)
|
||||
|
||||
# Employee records (sidebar: admin, payroll, accounting) — checked for every route.
|
||||
restrict_blueprint_to_roles(bp, PAYROLL_AREA_ROLES)
|
||||
|
||||
|
||||
|
||||
@bp.route('/employees', endpoint='employees')
|
||||
|
||||
@@ -15,7 +15,7 @@ from datetime import datetime
|
||||
|
||||
from extensions import logger_handler
|
||||
from logger_handler import log_user_activity
|
||||
from utils.helpers import login_required
|
||||
from utils.helpers import login_required, restrict_blueprint_to_roles, PAYROLL_AREA_ROLES
|
||||
from legacy_attendance_service import (
|
||||
LegacyDbUnavailable,
|
||||
get_legacy_dashboard_stats,
|
||||
@@ -27,6 +27,9 @@ from legacy_attendance_service import (
|
||||
|
||||
bp = Blueprint('legacy_attendance', __name__)
|
||||
|
||||
# Legacy Attendance (sidebar: admin, payroll, accounting) — checked for every route.
|
||||
restrict_blueprint_to_roles(bp, PAYROLL_AREA_ROLES)
|
||||
|
||||
# Fixed dropdown values — confirmed values stored in the legacy `records.type` column
|
||||
LEGACY_RECORD_TYPES = ['CHECK IN', 'CHECK OUT']
|
||||
|
||||
|
||||
+19
-6
@@ -20,6 +20,8 @@ from werkzeug.utils import secure_filename
|
||||
from logger_handler import log_user_activity, log_database_operations
|
||||
from sqlalchemy import or_
|
||||
from utils.helpers import (
|
||||
QR_MANAGEMENT_ROLES,
|
||||
roles_required,
|
||||
admin_required,
|
||||
employee_id_regex_condition,
|
||||
expand_employee_id_filter,
|
||||
@@ -106,6 +108,7 @@ def get_unique_qr_locations():
|
||||
|
||||
|
||||
@bp.route('/qr-codes/create', methods=['GET', 'POST'], endpoint='create_qr_code')
|
||||
@roles_required(*QR_MANAGEMENT_ROLES) # not project managers (reports only)
|
||||
@login_required
|
||||
@log_database_operations('qr_code_creation')
|
||||
def create_qr_code():
|
||||
@@ -286,6 +289,7 @@ def create_qr_code():
|
||||
return render_template('create_qr_code.html', projects=projects, styles=styles)
|
||||
|
||||
@bp.route('/qr-codes/bulk-import', methods=['GET', 'POST'], endpoint='import_bulk_qr_codes')
|
||||
@roles_required(*QR_MANAGEMENT_ROLES)
|
||||
@login_required
|
||||
@log_database_operations('qr_code_bulk_import')
|
||||
def import_bulk_qr_codes():
|
||||
@@ -400,6 +404,7 @@ def import_bulk_qr_codes():
|
||||
|
||||
|
||||
@bp.route('/qr-codes/bulk-import/template', endpoint='download_qr_import_template')
|
||||
@roles_required(*QR_MANAGEMENT_ROLES)
|
||||
@login_required
|
||||
def download_qr_import_template():
|
||||
"""Download Excel template for bulk QR code import"""
|
||||
@@ -465,6 +470,7 @@ def download_qr_import_template():
|
||||
return redirect(url_for('qr_codes.import_bulk_qr_codes'))
|
||||
|
||||
@bp.route('/qr-codes/<int:qr_id>/edit', methods=['GET', 'POST'], endpoint='edit_qr_code')
|
||||
@roles_required(*QR_MANAGEMENT_ROLES)
|
||||
@login_required
|
||||
@log_database_operations('qr_code_edit')
|
||||
def edit_qr_code(qr_id):
|
||||
@@ -744,19 +750,23 @@ def qr_checkin(qr_url):
|
||||
# Get and validate employee ID
|
||||
employee_id = request.form.get('employee_id', '').strip()
|
||||
|
||||
# At most 4 digits — counted on the base ID, so an old-style typed
|
||||
# suffix ("1234SP") from a page cached before the numeric-only rule
|
||||
# still passes. Refused, never truncated: a shorter ID is another person.
|
||||
# 1 to 4 digits, numbers only — checked on the base ID, so an old-style
|
||||
# typed suffix ("1234SP") from a page cached before the numeric-only rule
|
||||
# still passes. Anything else is refused, never truncated (a shorter ID
|
||||
# is another person). This also keeps text such as "=HYPERLINK(...)" out
|
||||
# of attendance_data and every Excel export built from it.
|
||||
if employee_id:
|
||||
base_for_length, _ = parse_employee_id_for_work_type(employee_id)
|
||||
if len(re.sub(r'\D', '', base_for_length)) > CHECKIN_EMPLOYEE_ID_MAX_DIGITS:
|
||||
if (not re.fullmatch(r'[0-9]+', base_for_length)
|
||||
or len(base_for_length) > CHECKIN_EMPLOYEE_ID_MAX_DIGITS):
|
||||
logger_handler.logger.warning(
|
||||
f"Check-in rejected: employee ID '{employee_id}' has more than "
|
||||
f"Check-in rejected: employee ID {employee_id!r} is not 1-"
|
||||
f"{CHECKIN_EMPLOYEE_ID_MAX_DIGITS} digits (QR {qr_url})"
|
||||
)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Employee ID must be 4 digits or fewer. / El ID de empleado debe tener 4 dígitos o menos.'
|
||||
'message': ('Employee ID must be 1 to 4 digits, numbers only. / '
|
||||
'El ID de empleado debe tener de 1 a 4 dígitos, solo números.')
|
||||
}), 400
|
||||
|
||||
# --- ADDED: type of work selected on the check-in page ---
|
||||
@@ -1325,6 +1335,7 @@ def qr_last_work_type(qr_url):
|
||||
|
||||
|
||||
@bp.route('/qr-codes/<int:qr_id>/toggle-status', methods=['POST'], endpoint='toggle_qr_status')
|
||||
@roles_required('admin') # the dashboard shows Activate/Deactivate to admins only
|
||||
@login_required
|
||||
def toggle_qr_status(qr_id):
|
||||
"""Toggle QR code active/inactive status"""
|
||||
@@ -1406,6 +1417,7 @@ def open_qr_link(qr_id):
|
||||
}), 500
|
||||
|
||||
@bp.route('/qr-codes/<int:qr_id>/activate', methods=['POST'], endpoint='activate_qr_code')
|
||||
@roles_required('admin')
|
||||
@login_required
|
||||
def activate_qr_code(qr_id):
|
||||
"""Activate a QR code"""
|
||||
@@ -1433,6 +1445,7 @@ def activate_qr_code(qr_id):
|
||||
}), 500
|
||||
|
||||
@bp.route('/qr-codes/<int:qr_id>/deactivate', methods=['POST'], endpoint='deactivate_qr_code')
|
||||
@roles_required('admin')
|
||||
@login_required
|
||||
def deactivate_qr_code(qr_id):
|
||||
"""Deactivate a QR code"""
|
||||
|
||||
@@ -15,10 +15,15 @@ from models.project import Project
|
||||
from models.user import User
|
||||
from sqlalchemy import text
|
||||
from logger_handler import log_user_activity, log_database_operations
|
||||
from utils.helpers import login_required, staff_or_admin_required
|
||||
from utils.helpers import login_required, staff_or_admin_required, restrict_blueprint_to_roles, PAYROLL_AREA_ROLES
|
||||
from utils.excel_safety import csv_safe
|
||||
|
||||
bp = Blueprint('statistics', __name__)
|
||||
|
||||
# Company-wide statistics: admins in the sidebar; the export already allowed
|
||||
# payroll and accounting, so the same three roles are checked for every route.
|
||||
restrict_blueprint_to_roles(bp, PAYROLL_AREA_ROLES)
|
||||
|
||||
|
||||
|
||||
@bp.route('/statistics', endpoint='qr_statistics')
|
||||
@@ -278,7 +283,8 @@ def export_statistics():
|
||||
|
||||
# Write data
|
||||
for row in export_data:
|
||||
writer.writerow([
|
||||
# csv_safe: device / address text starting with = + - @ must not run as a formula
|
||||
writer.writerow([csv_safe(v) for v in [
|
||||
row.id, row.employee_id, row.employee_name,
|
||||
str(row.check_in_date), str(row.check_in_time),
|
||||
row.qr_code_name, row.qr_location, row.location_event,
|
||||
@@ -287,7 +293,7 @@ def export_statistics():
|
||||
row.latitude or '', row.longitude or '',
|
||||
row.address or '', row.location_name or '',
|
||||
str(row.created_timestamp)
|
||||
])
|
||||
]])
|
||||
|
||||
output.seek(0)
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ from sqlalchemy import text, or_
|
||||
from werkzeug.utils import secure_filename
|
||||
from logger_handler import log_user_activity, log_database_operations
|
||||
from utils.helpers import (
|
||||
PAYROLL_AREA_ROLES,
|
||||
restrict_blueprint_to_roles,
|
||||
admin_required,
|
||||
employee_id_regex_condition,
|
||||
expand_employee_id_filter,
|
||||
@@ -41,6 +43,10 @@ import openpyxl.cell.cell
|
||||
|
||||
bp = Blueprint('time_attendance', __name__)
|
||||
|
||||
# Time Attendance is a payroll area (sidebar: admin, payroll, accounting).
|
||||
# Checked for EVERY route here — imports and exports feed payroll.
|
||||
restrict_blueprint_to_roles(bp, PAYROLL_AREA_ROLES)
|
||||
|
||||
|
||||
def build_time_attendance_employee_filter(employee_ids):
|
||||
"""
|
||||
|
||||
@@ -23,6 +23,7 @@ from models.qrcode import QRCode
|
||||
from models.time_attendance import TimeAttendance
|
||||
from sqlalchemy import text
|
||||
from working_hours_calculator import WorkingHoursCalculator, round_time_to_quarter_hour, convert_minutes_to_base100, round_base100_hours
|
||||
from utils.excel_safety import excel_hyperlink, neutralize_unexpected_formulas
|
||||
import openpyxl
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side, numbers
|
||||
from openpyxl.utils import get_column_letter
|
||||
@@ -1377,6 +1378,7 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
|
||||
|
||||
# Save to BytesIO
|
||||
output = io.BytesIO()
|
||||
neutralize_unexpected_formulas(wb) # formula-injection guard (utils/excel_safety.py)
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
|
||||
@@ -2237,13 +2239,13 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename,
|
||||
building_address = ref_record.event_description or ''
|
||||
if building_address:
|
||||
encoded_addr = building_address.replace(' ', '+').replace(',', '%2C')
|
||||
hyperlink_formula = f'=HYPERLINK("https://www.google.com/maps/place/{encoded_addr}","{building_address}")'
|
||||
hyperlink_formula = excel_hyperlink(f"https://www.google.com/maps/place/{encoded_addr}", building_address)
|
||||
ws.cell(row=current_row, column=11, value=hyperlink_formula)
|
||||
|
||||
recorded_addr = ref_record.recorded_address or ''
|
||||
if recorded_addr:
|
||||
encoded_recorded = recorded_addr.replace(' ', '+').replace(',', '%2C')
|
||||
recorded_hyperlink = f'=HYPERLINK("https://www.google.com/maps/place/{encoded_recorded}","{recorded_addr}")'
|
||||
recorded_hyperlink = excel_hyperlink(f"https://www.google.com/maps/place/{encoded_recorded}", recorded_addr)
|
||||
ws.cell(row=current_row, column=12, value=recorded_hyperlink)
|
||||
|
||||
current_row += 1
|
||||
@@ -2363,6 +2365,7 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename,
|
||||
|
||||
# Save to BytesIO
|
||||
output = io.BytesIO()
|
||||
neutralize_unexpected_formulas(wb) # formula-injection guard (utils/excel_safety.py)
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
|
||||
|
||||
+14
-7
@@ -201,7 +201,8 @@ def create_user():
|
||||
flash('Error loading form. Please try again.', 'error')
|
||||
return redirect(url_for('users.users'))
|
||||
|
||||
@bp.route('/users/<int:user_id>/delete', methods=['GET', 'POST'], endpoint='delete_user')
|
||||
# POST only: a state change must carry a CSRF token — a GET link could be forged
|
||||
@bp.route('/users/<int:user_id>/delete', methods=['POST'], endpoint='delete_user')
|
||||
@admin_required
|
||||
def delete_user(user_id):
|
||||
"""Deactivate user (Admin only) - Fixed with proper validation"""
|
||||
@@ -242,7 +243,8 @@ def delete_user(user_id):
|
||||
flash('Error deactivating user. Please try again.', 'error')
|
||||
return redirect(url_for('users.users'))
|
||||
|
||||
@bp.route('/users/<int:user_id>/reactivate', methods=['GET', 'POST'], endpoint='reactivate_user')
|
||||
# POST only: a state change must carry a CSRF token — a GET link could be forged
|
||||
@bp.route('/users/<int:user_id>/reactivate', methods=['POST'], endpoint='reactivate_user')
|
||||
@admin_required
|
||||
def reactivate_user(user_id):
|
||||
"""Reactivate a deactivated user (Admin only)"""
|
||||
@@ -272,7 +274,8 @@ def reactivate_user(user_id):
|
||||
flash('Error reactivating user. Please try again.', 'error')
|
||||
return redirect(url_for('users.users'))
|
||||
|
||||
@bp.route('/users/<int:user_id>/promote', methods=['GET', 'POST'], endpoint='promote_user')
|
||||
# POST only: a state change must carry a CSRF token — a GET link could be forged
|
||||
@bp.route('/users/<int:user_id>/promote', methods=['POST'], endpoint='promote_user')
|
||||
@admin_required
|
||||
def promote_user(user_id):
|
||||
"""Promote a staff user to admin (Admin only)"""
|
||||
@@ -302,7 +305,8 @@ def promote_user(user_id):
|
||||
flash('Error promoting user. Please try again.', 'error')
|
||||
return redirect(url_for('users.users'))
|
||||
|
||||
@bp.route('/users/<int:user_id>/demote', methods=['GET', 'POST'], endpoint='demote_user')
|
||||
# POST only: a state change must carry a CSRF token — a GET link could be forged
|
||||
@bp.route('/users/<int:user_id>/demote', methods=['POST'], endpoint='demote_user')
|
||||
@admin_required
|
||||
def demote_user(user_id):
|
||||
"""Demote an admin user to staff (Admin only)"""
|
||||
@@ -616,7 +620,8 @@ def toggle_user_status(user_id):
|
||||
'message': 'Error updating user status. Please try again.'
|
||||
}), 500
|
||||
|
||||
@bp.route('/users/<int:user_id>/activate', methods=['GET', 'POST'], endpoint='activate_user')
|
||||
# POST only: a state change must carry a CSRF token — a GET link could be forged
|
||||
@bp.route('/users/<int:user_id>/activate', methods=['POST'], endpoint='activate_user')
|
||||
@admin_required
|
||||
def activate_user(user_id):
|
||||
"""Activate a user (Admin only) - Alternative route"""
|
||||
@@ -649,7 +654,8 @@ def activate_user(user_id):
|
||||
flash('Error activating user. Please try again.', 'error')
|
||||
return redirect(url_for('users.users'))
|
||||
|
||||
@bp.route('/users/<int:user_id>/deactivate', methods=['GET', 'POST'], endpoint='deactivate_user')
|
||||
# POST only: a state change must carry a CSRF token — a GET link could be forged
|
||||
@bp.route('/users/<int:user_id>/deactivate', methods=['POST'], endpoint='deactivate_user')
|
||||
@admin_required
|
||||
def deactivate_user(user_id):
|
||||
"""Deactivate a user (Admin only) - Alternative route"""
|
||||
@@ -900,7 +906,8 @@ def reverse_geocode_api():
|
||||
'message': 'Internal server error during reverse geocoding. Please try again.'
|
||||
}), 500
|
||||
|
||||
@bp.route('/users/<int:user_id>/permanently-delete', methods=['GET', 'POST'], endpoint='permanently_delete_user')
|
||||
# POST only: a state change must carry a CSRF token — a GET link could be forged
|
||||
@bp.route('/users/<int:user_id>/permanently-delete', methods=['POST'], endpoint='permanently_delete_user')
|
||||
@admin_required
|
||||
def permanently_delete_user(user_id):
|
||||
"""Permanently delete user but preserve associated QR codes (Admin only)"""
|
||||
|
||||
Reference in New Issue
Block a user