166 lines
5.3 KiB
Python
166 lines
5.3 KiB
Python
"""Application factory."""
|
|
from flask import Flask, render_template, request
|
|
from flask_login import current_user
|
|
from app.config import get_config
|
|
from app.extensions import (db, migrate, login_manager, csrf, babel, limiter)
|
|
|
|
_MAINTENANCE_EXEMPT_ENDPOINTS = {
|
|
"static", "main.healthz", "auth.login", "auth.logout", "payments.webhook",
|
|
}
|
|
|
|
|
|
def create_app(config_object=None):
|
|
app = Flask(__name__)
|
|
app.config.from_object(config_object or get_config())
|
|
|
|
_init_extensions(app)
|
|
_init_login(app)
|
|
_init_babel(app)
|
|
_register_blueprints(app)
|
|
_register_errorhandlers(app)
|
|
_register_context(app)
|
|
_register_hooks(app)
|
|
_register_cli(app)
|
|
|
|
return app
|
|
|
|
|
|
def _init_extensions(app):
|
|
# Honor X-Forwarded-* from the Nginx reverse proxy (scheme, host, client IP).
|
|
# Correct is_secure, secure cookies, and rate-limit keying behind the proxy.
|
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
|
|
|
|
db.init_app(app)
|
|
migrate.init_app(app, db)
|
|
csrf.init_app(app)
|
|
limiter.init_app(app)
|
|
# ensure models are imported so migrations see them
|
|
from app import models # noqa: F401
|
|
|
|
|
|
def _init_login(app):
|
|
login_manager.init_app(app)
|
|
|
|
@login_manager.user_loader
|
|
def load_user(user_id):
|
|
from app.models.user import User
|
|
return User.query.get(int(user_id))
|
|
|
|
|
|
def _init_babel(app):
|
|
from app.blueprints.i18n.routes import select_locale
|
|
babel.init_app(app, locale_selector=select_locale)
|
|
|
|
|
|
def _register_blueprints(app):
|
|
from app.blueprints.main.routes import main_bp
|
|
from app.blueprints.auth.routes import auth_bp
|
|
from app.blueprints.i18n.routes import i18n_bp
|
|
from app.blueprints.listings.routes import listings_bp
|
|
from app.blueprints.messaging.routes import messaging_bp
|
|
from app.blueprints.payments.routes import payments_bp
|
|
from app.blueprints.ads.routes import ads_bp
|
|
from app.blueprints.admin.routes import admin_bp
|
|
app.register_blueprint(main_bp)
|
|
app.register_blueprint(auth_bp)
|
|
app.register_blueprint(i18n_bp)
|
|
app.register_blueprint(listings_bp)
|
|
app.register_blueprint(messaging_bp)
|
|
app.register_blueprint(payments_bp)
|
|
app.register_blueprint(ads_bp)
|
|
app.register_blueprint(admin_bp)
|
|
|
|
|
|
def _register_errorhandlers(app):
|
|
@app.errorhandler(403)
|
|
def forbidden(e):
|
|
return render_template("errors/403.html"), 403
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(e):
|
|
return render_template("errors/404.html"), 404
|
|
|
|
@app.errorhandler(500)
|
|
def server_error(e):
|
|
return render_template("errors/500.html"), 500
|
|
|
|
|
|
def _register_context(app):
|
|
from flask_babel import get_locale
|
|
from flask import request
|
|
from flask_login import current_user
|
|
from app.blueprints.ads.routes import inject_ads
|
|
|
|
def merge_query(**overrides):
|
|
merged = request.args.to_dict()
|
|
merged.update({k: v for k, v in overrides.items() if v is not None})
|
|
return merged
|
|
|
|
@app.context_processor
|
|
def inject_globals():
|
|
unread = 0
|
|
if current_user.is_authenticated:
|
|
try:
|
|
from app.services.messaging import total_unread
|
|
unread = total_unread(current_user)
|
|
except Exception:
|
|
pass
|
|
ad_context = {}
|
|
try:
|
|
ad_context = inject_ads()
|
|
except Exception:
|
|
pass
|
|
return {
|
|
"get_locale": get_locale,
|
|
"merge_query": merge_query,
|
|
"SUPPORTED_LOCALES": app.config["SUPPORTED_LOCALES"],
|
|
"TURNSTILE_SITE_KEY": app.config.get("TURNSTILE_SITE_KEY", ""),
|
|
"unread_count": unread,
|
|
**ad_context,
|
|
}
|
|
|
|
|
|
def _register_hooks(app):
|
|
@app.before_request
|
|
def check_maintenance():
|
|
from app.services.settings import get_setting
|
|
if not get_setting("maintenance_mode", False):
|
|
return None
|
|
if current_user.is_authenticated and getattr(current_user, "is_admin", False):
|
|
return None
|
|
ep = request.endpoint or ""
|
|
if ep in _MAINTENANCE_EXEMPT_ENDPOINTS or ep.startswith("admin."):
|
|
return None
|
|
return render_template("errors/maintenance.html"), 503
|
|
|
|
|
|
def _register_cli(app):
|
|
@app.cli.command("expire-listings")
|
|
def expire_listings_cmd():
|
|
"""Sweep: flip past-due active listings to expired."""
|
|
from app.services.listings import expire_due_listings
|
|
n = expire_due_listings()
|
|
print(f"Expired {n} listing(s).")
|
|
|
|
@app.cli.command("expire-boosts")
|
|
def expire_boosts_cmd():
|
|
"""Sweep: clear expired boosts, revert listing effects."""
|
|
from app.services.billing import expire_boosts
|
|
n = expire_boosts()
|
|
print(f"Cleared {n} expired boost(s).")
|
|
|
|
@app.cli.command("expire-promoted-keywords")
|
|
def expire_promoted_cmd():
|
|
"""Sweep: remove expired promoted keyword rows."""
|
|
from app.services.ads import expire_promoted_keywords
|
|
n = expire_promoted_keywords()
|
|
print(f"Removed {n} expired promoted keyword(s).")
|
|
|
|
@app.cli.command("reconcile-subscriptions")
|
|
def reconcile_cmd():
|
|
"""Nightly: sync local subscription status vs Stripe."""
|
|
from app.services.billing import reconcile_subscriptions
|
|
checked, fixed = reconcile_subscriptions()
|
|
print(f"Reconciled {checked} subscription(s), fixed {fixed}.")
|