160 lines
5.5 KiB
Python
160 lines
5.5 KiB
Python
import os
|
|
import logging
|
|
from datetime import datetime
|
|
from flask import Flask, jsonify
|
|
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__)
|
|
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')
|
|
|
|
# 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):
|
|
return jsonify({'error': 'Not Found', 'message': 'Resource not found'}), 404
|
|
|
|
@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
|
|
|
|
if __name__ == '__main__':
|
|
app = create_app()
|
|
app.run(host='0.0.0.0', port=5000, debug=True) |