393 lines
15 KiB
Python
393 lines
15 KiB
Python
"""
|
|
app.py
|
|
======
|
|
Application entry point and factory.
|
|
|
|
This file is intentionally lean (~130 lines). All route logic lives in
|
|
the Blueprint modules under routes/. All shared utilities live in utils/.
|
|
The db and logger_handler singletons live in extensions.py.
|
|
|
|
Blueprint registration order matches the original route-definition order
|
|
so that url_for() resolution is identical to the original monolithic app.py.
|
|
"""
|
|
|
|
from flask import Flask, render_template, request, redirect, url_for, flash, session, g
|
|
from datetime import datetime, timedelta
|
|
from dotenv import load_dotenv
|
|
import os
|
|
import time as _time
|
|
|
|
# Load .env BEFORE importing anything that reads env vars
|
|
load_dotenv()
|
|
|
|
from extensions import db, init_logger
|
|
from config import get_config
|
|
from utils.template_helpers import register_template_helpers
|
|
from logger_handler import log_database_operations
|
|
from models import set_db
|
|
from turnstile_utils import turnstile_utils
|
|
from db_performance_optimization import initialize_performance_optimizations
|
|
from app_performance_middleware import PerformanceMonitor
|
|
from utils.helpers import has_admin_privileges
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Application factory
|
|
# ---------------------------------------------------------------------------
|
|
from location_logging import * # noqa: F401,F403 — registers location hooks at module level
|
|
|
|
|
|
|
|
|
|
def create_app() -> Flask:
|
|
app = Flask(__name__)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Configuration
|
|
# ------------------------------------------------------------------
|
|
# Load configuration from config.py (single source of truth for env vars)
|
|
cfg = get_config()
|
|
app.config.from_object(cfg)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Database initialization
|
|
# ------------------------------------------------------------------
|
|
db.init_app(app)
|
|
|
|
with app.app_context():
|
|
# Unpack model classes and store on app for shared access
|
|
(User, QRCode, QRCodeStyle, Project, AttendanceData,
|
|
Employee, TimeAttendance, UserProjectPermission,
|
|
UserLocationPermission) = set_db(db)
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Logger initialization
|
|
# ------------------------------------------------------------------
|
|
init_logger(app, db)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Blueprint registration (url_prefix='' preserves all original URLs)
|
|
# ------------------------------------------------------------------
|
|
from routes.auth import bp as auth_bp
|
|
from routes.dashboard import bp as dashboard_bp
|
|
from routes.users import bp as users_bp
|
|
from routes.admin import bp as admin_bp
|
|
from routes.projects import bp as projects_bp
|
|
from routes.qr_codes import bp as qr_codes_bp
|
|
from routes.attendance import bp as attendance_bp
|
|
from routes.payroll import bp as payroll_bp
|
|
from routes.statistics import bp as statistics_bp
|
|
from routes.employees import bp as employees_bp
|
|
from routes.time_attendance import bp as time_attendance_bp
|
|
|
|
for bp in (auth_bp, dashboard_bp, users_bp, admin_bp, projects_bp,
|
|
qr_codes_bp, attendance_bp, payroll_bp, statistics_bp,
|
|
employees_bp, time_attendance_bp):
|
|
app.register_blueprint(bp)
|
|
|
|
# Register location-logging routes (from location_logging.py)
|
|
# Must be called after app is created; uses app, db, logger_handler directly.
|
|
from extensions import logger_handler as _lh
|
|
create_location_logging_routes(app, db, _lh)
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Template filters (global — must be on app, not blueprints)
|
|
# ------------------------------------------------------------------
|
|
|
|
@app.context_processor
|
|
def inject_company_name():
|
|
"""Make COMPANY_NAME available to all templates"""
|
|
return {'COMPANY_NAME': os.environ.get('COMPANY_NAME', 'QR Code Management System')}
|
|
|
|
@app.context_processor
|
|
def inject_logging_status():
|
|
"""Inject logging status into all templates"""
|
|
return {
|
|
'logging_enabled': True,
|
|
'is_admin': has_admin_privileges(session.get('role', ''))
|
|
}
|
|
|
|
@app.context_processor
|
|
def inject_turnstile():
|
|
"""Inject Turnstile settings into all templates"""
|
|
return {
|
|
'turnstile_enabled': turnstile_utils.is_enabled(),
|
|
'turnstile_site_key': turnstile_utils.get_site_key()
|
|
}
|
|
|
|
# Register template helper context processors (from utils/template_helpers.py)
|
|
register_template_helpers(app)
|
|
|
|
@app.template_filter('strftime')
|
|
def strftime_filter(value, format='%m/%d/%Y'):
|
|
"""Format datetime/date/string as strftime"""
|
|
if isinstance(value, str):
|
|
if value.lower() == 'now':
|
|
return datetime.now().strftime(format)
|
|
try:
|
|
dt = datetime.fromisoformat(value)
|
|
return dt.strftime(format)
|
|
except (ValueError, TypeError):
|
|
return value
|
|
if hasattr(value, 'strftime'):
|
|
return value.strftime(format)
|
|
return str(value)
|
|
|
|
@app.template_filter('days_since')
|
|
def days_since_filter(value):
|
|
"""Calculate days since a given date"""
|
|
if not value:
|
|
return 0
|
|
now = datetime.utcnow()
|
|
return (now - value).days
|
|
|
|
@app.template_filter('time_ago')
|
|
def time_ago_filter(value):
|
|
"""Human readable time ago"""
|
|
if not value:
|
|
return 'Never'
|
|
now = datetime.utcnow()
|
|
diff = now - value
|
|
if diff.days > 365:
|
|
years = diff.days // 365
|
|
return f"{years} year{'s' if years != 1 else ''} ago"
|
|
elif diff.days > 30:
|
|
months = diff.days // 30
|
|
return f"{months} month{'s' if months != 1 else ''} ago"
|
|
elif diff.days > 0:
|
|
return f"{diff.days} day{'s' if diff.days != 1 else ''} ago"
|
|
elif diff.seconds > 3600:
|
|
hours = diff.seconds // 3600
|
|
return f"{hours} hour{'s' if hours != 1 else ''} ago"
|
|
elif diff.seconds > 60:
|
|
minutes = diff.seconds // 60
|
|
return f"{minutes} minute{'s' if minutes != 1 else ''} ago"
|
|
else:
|
|
return "Just now"
|
|
|
|
# ------------------------------------------------------------------
|
|
# Request / response hooks
|
|
# ------------------------------------------------------------------
|
|
|
|
@app.before_request
|
|
def log_request_info():
|
|
"""Log request information for security monitoring"""
|
|
if (request.endpoint and
|
|
(request.endpoint.startswith('static') or
|
|
request.path.startswith('/api/logs'))):
|
|
return
|
|
|
|
from extensions import logger_handler as lh
|
|
user_agent = request.headers.get('User-Agent', '')
|
|
ip_address = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
|
|
suspicious_patterns = [
|
|
'sqlmap', 'nikto', 'nmap', 'dirb', 'dirbuster',
|
|
'wget', 'curl.*bot', 'scanner', 'exploit'
|
|
]
|
|
if any(pattern in user_agent.lower() for pattern in suspicious_patterns):
|
|
lh.log_security_event(
|
|
event_type="suspicious_user_agent",
|
|
description=f"Suspicious user agent detected: {user_agent[:200]}",
|
|
severity="HIGH",
|
|
additional_data={'user_agent': user_agent, 'ip_address': ip_address}
|
|
)
|
|
|
|
@app.after_request
|
|
def log_response_info(response):
|
|
"""Log response information for performance monitoring"""
|
|
from extensions import logger_handler as lh
|
|
if request.endpoint and request.endpoint.startswith('static'):
|
|
return response
|
|
if hasattr(request, 'start_time'):
|
|
duration = _time.time() - request.start_time
|
|
if duration > 5.0:
|
|
lh.logger.warning(f"Slow request: {request.path} took {duration:.2f} seconds")
|
|
if response.status_code >= 400:
|
|
lh.logger.warning(
|
|
f"Error response: {response.status_code} for {request.path} "
|
|
f"by user {session.get('username', 'anonymous')}"
|
|
)
|
|
return response
|
|
|
|
# ------------------------------------------------------------------
|
|
# Error handlers
|
|
# ------------------------------------------------------------------
|
|
|
|
@app.errorhandler(500)
|
|
def internal_error(error):
|
|
"""Handle internal server errors with user-friendly page"""
|
|
if app.debug:
|
|
return None
|
|
return '''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head><title>Server Error</title></head>
|
|
<body style="font-family: Arial; text-align: center; margin-top: 100px;">
|
|
<h1>🔧 Something went wrong</h1>
|
|
<p>We're working to fix this issue. Please try again later.</p>
|
|
<a href="/" style="color: #2563eb;">← Back to Home</a>
|
|
</body>
|
|
</html>
|
|
''', 500
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
"""Handle page not found errors"""
|
|
return '''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head><title>Page Not Found</title></head>
|
|
<body style="font-family: Arial; text-align: center; margin-top: 100px;">
|
|
<h1>🔍 Page Not Found</h1>
|
|
<p>The page you're looking for doesn't exist.</p>
|
|
<a href="/" style="color: #2563eb;">← Back to Home</a>
|
|
</body>
|
|
</html>
|
|
''', 404
|
|
|
|
return app
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Database initialization helpers (called at startup)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@log_database_operations('database_initialization')
|
|
def create_tables():
|
|
"""Create database tables and default admin user with logging"""
|
|
from extensions import db as _db, logger_handler as lh
|
|
try:
|
|
_db.create_all()
|
|
from flask import current_app
|
|
from models.user import User
|
|
admin = User.query.filter_by(username='admin').first()
|
|
if not admin:
|
|
from config import Config as _Cfg
|
|
default_password = _Cfg.DEFAULT_ADMIN_PASSWORD
|
|
admin = User(
|
|
full_name='System Administrator',
|
|
email='admin@example.com',
|
|
username='admin',
|
|
role='admin'
|
|
)
|
|
admin.set_password(default_password)
|
|
_db.session.add(admin)
|
|
_db.session.commit()
|
|
if default_password == 'admin123':
|
|
print("⚠️ WARNING: Default admin password 'admin123' is in use. "
|
|
"Set DEFAULT_ADMIN_PASSWORD in your .env file before going to production.")
|
|
lh.logger.warning(
|
|
"Default admin user created with insecure default password. "
|
|
"Set DEFAULT_ADMIN_PASSWORD environment variable."
|
|
)
|
|
else:
|
|
lh.logger.info("Default admin user created during initialization")
|
|
lh._create_log_table()
|
|
except Exception as e:
|
|
lh.log_database_error('database_initialization', e)
|
|
raise
|
|
|
|
|
|
def update_existing_qr_codes():
|
|
"""Update existing QR codes with URLs and regenerate QR images with logging"""
|
|
from extensions import db as _db, logger_handler as lh
|
|
from utils.helpers import generate_qr_code, get_qr_styling, generate_qr_url
|
|
from flask import current_app, request
|
|
try:
|
|
from models.qrcode import QRCode
|
|
qr_codes = QRCode.query.filter_by(active_status=True).all()
|
|
updated_count = 0
|
|
for qr_code in qr_codes:
|
|
if not qr_code.qr_url or not qr_code.qr_code_image:
|
|
try:
|
|
if not qr_code.qr_url:
|
|
qr_code.qr_url = generate_qr_url(qr_code.name, qr_code.id)
|
|
if not qr_code.qr_code_image:
|
|
qr_data = f"{request.url_root}qr/{qr_code.qr_url}"
|
|
styling = get_qr_styling(qr_code)
|
|
qr_code.qr_code_image = generate_qr_code(
|
|
data=qr_data,
|
|
fill_color=styling['fill_color'],
|
|
back_color=styling['back_color'],
|
|
box_size=styling['box_size'],
|
|
border=styling['border'],
|
|
error_correction=styling['error_correction']
|
|
)
|
|
updated_count += 1
|
|
except Exception as e:
|
|
lh.log_flask_error('qr_code_update_error', f"Failed to update QR code {qr_code.id}: {str(e)}")
|
|
continue
|
|
if updated_count > 0:
|
|
_db.session.commit()
|
|
lh.logger.info(f"Updated {updated_count} existing QR codes with missing URLs/images")
|
|
except Exception as e:
|
|
lh.log_database_error('update_existing_qr_codes', e)
|
|
|
|
|
|
def log_slow_query_performance(app_instance):
|
|
"""Register slow-query monitoring hooks on the given app instance"""
|
|
from extensions import logger_handler as lh
|
|
|
|
@app_instance.before_request
|
|
def before_request():
|
|
g.start_time = _time.time()
|
|
|
|
@app_instance.after_request
|
|
def after_request(response):
|
|
if hasattr(g, 'start_time'):
|
|
duration = _time.time() - g.start_time
|
|
if duration > 2.0:
|
|
lh.log_system_event(
|
|
event_type="slow_query_detected",
|
|
description=f"Slow request: {request.endpoint} took {duration:.2f}s",
|
|
severity="WARNING",
|
|
additional_data={
|
|
'duration': duration,
|
|
'endpoint': request.endpoint,
|
|
'method': request.method,
|
|
'user': session.get('username', 'anonymous')
|
|
}
|
|
)
|
|
return response
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
app = create_app()
|
|
|
|
if __name__ == '__main__':
|
|
with app.app_context():
|
|
try:
|
|
create_tables()
|
|
log_slow_query_performance(app)
|
|
|
|
from extensions import logger_handler
|
|
logger_handler.logger.info("Initializing performance optimizations")
|
|
cached_query = initialize_performance_optimizations(app, db, logger_handler)
|
|
performance_monitor = PerformanceMonitor(app, db, logger_handler)
|
|
|
|
if cached_query:
|
|
logger_handler.logger.info("Performance optimizations completed successfully")
|
|
else:
|
|
logger_handler.logger.warning("Performance optimizations completed with warnings")
|
|
|
|
logger_handler.logger.info("QR Attendance Management System started successfully")
|
|
|
|
except Exception as e:
|
|
print(f"❌ Application startup failed: {e}")
|
|
raise
|
|
|
|
from config import Config as _Cfg
|
|
app.run(
|
|
debug=_Cfg.DEBUG,
|
|
host=_Cfg.FLASK_HOST,
|
|
port=_Cfg.FLASK_PORT,
|
|
threaded=_Cfg.THREADED
|
|
)
|