import os import logging from datetime import datetime from flask import Flask, jsonify, send_from_directory, send_file, request from werkzeug.middleware.proxy_fix import ProxyFix from flask_cors import CORS from flask_jwt_extended import JWTManager from jwt.exceptions import InvalidTokenError from flask_migrate import Migrate from flask_mail import Mail from config import config from models import db from utils.logger import setup_logger # Import blueprints from routes.auth import auth_bp from routes.todos import todos_bp from routes.users import users_bp from routes.admin import admin_bp from routes.health import health_bp from routes.reports import reports_bp from routes.excel import excel_bp from routes.notifications import notifications_bp from routes.scheduler import scheduler_bp migrate = Migrate() mail = Mail() jwt = JWTManager() def setup_jwt_error_handlers(jwt): @jwt.expired_token_loader def expired_token_callback(jwt_header, jwt_payload): return jsonify({'msg': 'Token has expired'}), 401 @jwt.invalid_token_loader def invalid_token_callback(error): return jsonify({'msg': 'Invalid token'}), 401 @jwt.unauthorized_loader def missing_token_callback(error): return jsonify({'msg': 'Missing Authorization Header'}), 401 def create_app(config_name=None): if config_name is None: config_name = os.environ.get('FLASK_ENV', 'development') app = Flask(__name__, static_folder='./frontend/out', static_url_path='') app.config.from_object(config[config_name]) # Initialize extensions db.init_app(app) migrate.init_app(app, db) mail.init_app(app) jwt.init_app(app) # Setup CORS CORS(app, origins=app.config['CORS_ORIGINS'], methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allow_headers=['Content-Type', 'Authorization'], supports_credentials=True, expose_headers=['Content-Type', 'Authorization']) # Setup logging setup_logger(app) # Setup JWT error handlers setup_jwt_error_handlers(jwt) # Register blueprints app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(todos_bp, url_prefix='/api/todos') app.register_blueprint(users_bp, url_prefix='/api/users') app.register_blueprint(admin_bp, url_prefix='/api/admin') app.register_blueprint(health_bp, url_prefix='/api/health') app.register_blueprint(reports_bp, url_prefix='/api/reports') app.register_blueprint(excel_bp, url_prefix='/api/excel') app.register_blueprint(notifications_bp, url_prefix='/api/notifications') app.register_blueprint(scheduler_bp, url_prefix='/api/scheduler') # Add static file serving routes @app.route('/') def serve_index(): return send_from_directory(app.static_folder, 'index.html') @app.route('/') def serve_static(path): # For SPA routing, return index.html for non-API routes first if not path.startswith('api/'): # Check if it's a static file first if path and os.path.exists(os.path.join(app.static_folder, path)) and os.path.isfile(os.path.join(app.static_folder, path)): return send_from_directory(app.static_folder, path) else: # Return index.html for SPA routing return send_from_directory(app.static_folder, 'index.html') else: return jsonify({'error': 'Not Found'}), 404 # Trust proxy headers (for nginx) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1) # Register error handlers register_error_handlers(app) # Create tables with app.app_context(): db.create_all() return app def register_error_handlers(app): @app.errorhandler(400) def bad_request(error): return jsonify({'error': 'Bad Request', 'message': str(error)}), 400 @app.errorhandler(401) def unauthorized(error): return jsonify({'error': 'Unauthorized', 'message': 'Authentication required'}), 401 @app.errorhandler(403) def forbidden(error): return jsonify({'error': 'Forbidden', 'message': 'Access denied'}), 403 @app.errorhandler(404) def not_found(error): # Only return JSON error for API routes if request.path.startswith('/api/'): return jsonify({'error': 'Not Found', 'message': 'Resource not found'}), 404 # For non-API routes, let Flask handle it normally (should be handled by our SPA route) return app.send_static_file('index.html') @app.errorhandler(500) def internal_error(error): db.session.rollback() app.logger.error(f"Internal error: {error}") return jsonify({'error': 'Internal Server Error', 'message': 'An error occurred'}), 500 # Database connection error handlers from sqlalchemy.exc import OperationalError, DisconnectionError, TimeoutError from pymysql.err import OperationalError as PyMySQLOperationalError, Error as PyMySQLError @app.errorhandler(OperationalError) def handle_db_operational_error(error): db.session.rollback() app.logger.error(f"Database operational error: {error}") # Check if it's a connection timeout or server unavailable error error_str = str(error) if 'timed out' in error_str or 'Lost connection' in error_str or "Can't connect" in error_str: return jsonify({ 'error': 'Database Connection Error', 'message': '資料庫連線暫時不穩定,請稍後再試' }), 503 return jsonify({ 'error': 'Database Error', 'message': '資料庫操作失敗,請稍後再試' }), 500 @app.errorhandler(DisconnectionError) def handle_db_disconnection_error(error): db.session.rollback() app.logger.error(f"Database disconnection error: {error}") return jsonify({ 'error': 'Database Connection Lost', 'message': '資料庫連線中斷,正在重新連線,請稍後再試' }), 503 @app.errorhandler(TimeoutError) def handle_db_timeout_error(error): db.session.rollback() app.logger.error(f"Database timeout error: {error}") return jsonify({ 'error': 'Database Timeout', 'message': '資料庫操作超時,請稍後再試' }), 504 @app.errorhandler(Exception) def handle_exception(error): db.session.rollback() app.logger.error(f"Unhandled exception: {error}", exc_info=True) return jsonify({'error': 'Server Error', 'message': 'An unexpected error occurred'}), 500 # Create app instance for Gunicorn app = create_app() if __name__ == '__main__': debug_mode = os.environ.get('FLASK_ENV') == 'development' app.run(host='0.0.0.0', port=12011, debug=debug_mode)