first commit
This commit is contained in:
163
app/__init__.py
Normal file
163
app/__init__.py
Normal file
@@ -0,0 +1,163 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, jsonify, redirect, render_template, request, session, url_for, Response
|
||||
from flask_login import current_user
|
||||
from werkzeug.exceptions import HTTPException, RequestEntityTooLarge
|
||||
|
||||
from .config import Config
|
||||
from .extensions import db, limiter, login_manager, migrate
|
||||
from .models import AppSetting, User, seed_categories, seed_default_settings
|
||||
from .services.assets import asset_url
|
||||
from .services.i18n import inject_i18n, translate as _
|
||||
from .services.settings import get_int_setting
|
||||
from .services.scheduler import start_scheduler
|
||||
|
||||
|
||||
def create_app(config_object=Config):
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
app.config.from_object(config_object)
|
||||
Path(app.instance_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
configure_logging(app)
|
||||
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
login_manager.init_app(app)
|
||||
limiter.init_app(app)
|
||||
|
||||
from .auth.routes import auth_bp
|
||||
from .main.routes import main_bp
|
||||
from .expenses.routes import expenses_bp
|
||||
from .admin.routes import admin_bp
|
||||
from .api.routes import api_bp
|
||||
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(expenses_bp, url_prefix='/expenses')
|
||||
app.register_blueprint(admin_bp, url_prefix='/admin')
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
from .cli.commands import register_commands
|
||||
register_commands(app)
|
||||
|
||||
app.context_processor(inject_i18n)
|
||||
app.jinja_env.globals['asset_url'] = asset_url
|
||||
app.jinja_env.globals['now'] = datetime.utcnow
|
||||
|
||||
@app.before_request
|
||||
def apply_runtime_settings():
|
||||
if not current_user.is_authenticated and request.args.get('lang') in app.config['LANGUAGES']:
|
||||
session['language'] = request.args['lang']
|
||||
app.config['MAX_CONTENT_LENGTH'] = get_int_setting('max_upload_mb', app.config['DEFAULT_MAX_UPLOAD_MB']) * 1024 * 1024
|
||||
|
||||
@app.after_request
|
||||
def security_headers(response):
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||
response.headers['Content-Security-Policy'] = (
|
||||
"default-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
|
||||
"img-src 'self' data:; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
|
||||
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;"
|
||||
)
|
||||
return response
|
||||
|
||||
register_error_handlers(app)
|
||||
|
||||
|
||||
@app.route('/manifest.json')
|
||||
def manifest():
|
||||
return jsonify({
|
||||
'name': 'Expense Monitor',
|
||||
'short_name': 'Expenses',
|
||||
'start_url': url_for('main.dashboard' if current_user.is_authenticated else 'auth.login'),
|
||||
'display': 'standalone',
|
||||
'background_color': '#0f172a',
|
||||
'theme_color': '#111827',
|
||||
'icons': [],
|
||||
})
|
||||
|
||||
@app.route('/service-worker.js')
|
||||
def service_worker():
|
||||
content = "self.addEventListener('install',()=>self.skipWaiting());self.addEventListener('fetch',()=>{});"
|
||||
return Response(content, mimetype='application/javascript')
|
||||
|
||||
@app.route('/health')
|
||||
def health():
|
||||
return {'status': 'ok'}
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
seed_categories()
|
||||
seed_default_settings()
|
||||
ensure_admin(app)
|
||||
start_scheduler(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def configure_logging(app: Flask) -> None:
|
||||
app.logger.setLevel(logging.INFO)
|
||||
if not app.logger.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s in %(module)s: %(message)s'))
|
||||
app.logger.addHandler(handler)
|
||||
|
||||
|
||||
def register_error_handlers(app: Flask) -> None:
|
||||
def render_error(status_code: int, title: str, message: str):
|
||||
payload = {'error': title, 'message': message, 'status_code': status_code}
|
||||
if request.path.startswith('/analytics') or request.accept_mimetypes.best == 'application/json':
|
||||
return jsonify(payload), status_code
|
||||
return render_template('errors/error.html', status_code=status_code, title=title, message=message), status_code
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request(_error):
|
||||
return render_error(400, _('error.400_title'), _('error.400_message'))
|
||||
|
||||
@app.errorhandler(401)
|
||||
def unauthorized(_error):
|
||||
return render_error(401, _('error.401_title'), _('error.401_message'))
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden(_error):
|
||||
return render_error(403, _('error.403_title'), _('error.403_message'))
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(_error):
|
||||
return render_error(404, _('error.404_title'), _('error.404_message'))
|
||||
|
||||
@app.errorhandler(413)
|
||||
@app.errorhandler(RequestEntityTooLarge)
|
||||
def too_large(_error):
|
||||
return render_error(413, _('error.413_title'), _('error.413_message'))
|
||||
|
||||
@app.errorhandler(429)
|
||||
def rate_limited(_error):
|
||||
return render_error(429, _('error.429_title'), _('error.429_message'))
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def internal_error(error):
|
||||
if isinstance(error, HTTPException):
|
||||
return render_error(error.code or 500, error.name, error.description)
|
||||
app.logger.exception('Unhandled exception: %s', error)
|
||||
return render_error(500, _('error.500_title'), _('error.500_message'))
|
||||
|
||||
|
||||
def ensure_admin(app: Flask) -> None:
|
||||
admin_email = 'admin@example.com'
|
||||
if not User.query.filter_by(email=admin_email).first():
|
||||
admin = User(
|
||||
email=admin_email,
|
||||
full_name='System Admin',
|
||||
role='admin',
|
||||
must_change_password=True,
|
||||
language=app.config['DEFAULT_LANGUAGE'],
|
||||
)
|
||||
admin.set_password('Admin123!')
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
Reference in New Issue
Block a user