Files
IT_Ticket_System/app/__init__.py
T

169 lines
7.3 KiB
Python

import os
import logging
from logging.handlers import RotatingFileHandler
from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_mail import Mail
from flask_migrate import Migrate
from flask_socketio import SocketIO
from flask_wtf.csrf import CSRFProtect
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from config.config import config
db = SQLAlchemy()
login_manager= LoginManager()
mail = Mail()
migrate = Migrate()
socketio = SocketIO()
csrf = CSRFProtect()
# limiter is a module-level name so blueprints can do `from app import limiter`,
# but the actual Limiter object is constructed inside create_app() — AFTER
# gunicorn has called eventlet.monkey_patch() in run.py. Constructing it here
# (at import time) creates a threading.RLock before the patch runs, which
# triggers: "1 RLock(s) were not greened".
limiter: Limiter = None # type: ignore[assignment]
def create_app(config_name=None):
if config_name is None:
config_name = os.environ.get('FLASK_ENV', 'production')
app = Flask(__name__, template_folder='templates', static_folder='static')
app.config.from_object(config.get(config_name, config['default']))
# ── ProxyFix: trust one nginx proxy hop for real client IPs ───────────────
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
# ── Extensions ────────────────────────────────────────────────────────────
db.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
migrate.init_app(app, db)
csrf.init_app(app)
# Limiter is constructed here — NOT at module level — so that
# eventlet.monkey_patch() (called in run.py before any imports) has already
# replaced threading.RLock with a green-thread-safe version. Constructing
# Limiter at module import time creates a real OS RLock before the patch
# runs, which produces: "1 RLock(s) were not greened".
global limiter
limiter = Limiter(
key_func = get_remote_address,
default_limits = [],
storage_uri = app.config.get('RATELIMIT_STORAGE_URI'),
)
limiter.init_app(app)
socketio.init_app(
app,
async_mode = 'eventlet',
cors_allowed_origins = app.config.get('APP_BASE_URL', ''),
# Ping settings: server sends a ping every 25s, client has 60s to respond.
# This ensures dead connections are detected and closed cleanly rather than
# being torn down by nginx timeouts, which causes [Errno 9] Bad file descriptor.
ping_interval = 25,
ping_timeout = 60,
# Suppress eventlet's low-level socket error messages from flooding the log.
# The errors still occur occasionally (it's an eventlet limitation) but are
# benign — connections recover automatically via Socket.IO's reconnect logic.
logger = False,
engineio_logger = False,
)
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'info'
# ── Upload directory ──────────────────────────────────────────────────────
upload_dir = app.config['UPLOAD_FOLDER'] # always absolute — set in config.py
os.makedirs(upload_dir, exist_ok=True)
# ── Logging ───────────────────────────────────────────────────────────────
if not app.debug:
os.makedirs('logs', exist_ok=True)
file_handler = RotatingFileHandler(
'logs/it_tickets.log', maxBytes=10_485_760, backupCount=10
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('IT Ticket System startup')
# ── Blueprints ────────────────────────────────────────────────────────────
from app.routes.auth import auth_bp
from app.routes.tickets import tickets_bp
from app.routes.admin import admin_bp
from app.routes.api import api_bp
from app.routes.chatbot import chatbot_bp
app.register_blueprint(auth_bp)
app.register_blueprint(tickets_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(api_bp)
app.register_blueprint(chatbot_bp)
# ── User loader ───────────────────────────────────────────────────────────
from app.models import User
@login_manager.user_loader
def load_user(user_id):
# db.session.get() is the SQLAlchemy 2.x successor to the deprecated
# Model.query.get(). Both hit the identity map first (no SQL if the
# object is already in session), then fall back to a SELECT by PK.
return db.session.get(User, int(user_id))
# ── Context processors ────────────────────────────────────────────────────
@app.context_processor
def inject_globals():
from flask_login import current_user
unread = 0
if current_user.is_authenticated:
from app.models import Notification
unread = Notification.query.filter_by(
user_id=current_user.id, is_read=False
).count()
return dict(unread_notifications=unread)
# ── DB initialisation (first run) ─────────────────────────────────────────
with app.app_context():
db.create_all()
_seed_admin(app)
_seed_settings()
return app
def _seed_settings():
"""Ensure all required system settings exist with safe defaults."""
from app.models import SystemSetting
defaults = [
('registration_enabled', 'true', 'Allow new users to self-register via /auth/register'),
]
for key, value, description in defaults:
if SystemSetting.get(key) is None:
SystemSetting.set(key, value, description)
db.session.commit()
def _seed_admin(app):
"""Create the default admin account if none exists."""
from app.models import User, UserRole
if User.query.filter_by(role=UserRole.ADMIN).first():
return
admin = User(
email = app.config['ADMIN_EMAIL'],
username = 'admin',
full_name = 'System Administrator',
role = UserRole.ADMIN,
department= 'IT',
is_active = True,
)
admin.set_password(app.config['ADMIN_PASSWORD'])
db.session.add(admin)
db.session.commit()
app.logger.info(f'[SEED] Default admin account created: {admin.email}')