Files
expense_monitor/app/__init__.py
Mateusz Gruszczyński 986ffb200a first commit
2026-03-13 15:17:32 +01:00

164 lines
5.9 KiB
Python

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()