190 lines
6.8 KiB
Python
190 lines
6.8 KiB
Python
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('/<path:path>')
|
|
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)
|