Continuation of the previous commit (gdrive_backup.py removal landed separately by accident) — adds the new module itself, the admin routes/template using it, generalized Setting keys, updated docs/ provisioning notes, and tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
115 lines
4.1 KiB
Python
115 lines
4.1 KiB
Python
"""Production entrypoint for the Flask backend (API + admin + webapp static).
|
||
|
||
Dev: python bober_bbq/app.py (Flask debug server, auto-reload)
|
||
Prod: python run_web.py (waitress, no reload, multi-threaded)
|
||
"""
|
||
|
||
import logging
|
||
import os
|
||
|
||
from apscheduler.schedulers.background import BackgroundScheduler
|
||
from waitress import serve
|
||
|
||
from bober_bbq.app import create_app
|
||
from bober_bbq.config import config
|
||
from bober_bbq.extensions import db
|
||
from bober_bbq.migrate import migrate
|
||
from bober_bbq.models import Setting
|
||
from bober_bbq.seed import seed
|
||
from bober_bbq.utils import error_alerts
|
||
from bober_bbq.utils.backup_remote import run_scheduled_backup
|
||
from bober_bbq.utils.checkbox_prro import maybe_close_shift
|
||
from bober_bbq.utils.reconciliation import (
|
||
check_liqpay_token_health,
|
||
check_monobank_token_health,
|
||
reconcile_pending_payments,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
app = create_app()
|
||
|
||
with app.app_context():
|
||
db.create_all()
|
||
migrate()
|
||
seed()
|
||
|
||
|
||
def _warn_on_insecure_defaults() -> None:
|
||
# config.py silently falls back to these if .env is missing a line or a
|
||
# client's provisioning skipped provision_client.sh (which generates a
|
||
# real SECRET_KEY and asks for a real admin password). Left unnoticed,
|
||
# SECRET_KEY="dev-secret-change-me" lets anyone forge session cookies,
|
||
# and admin/admin is guessable in one try. Don't refuse to start (that
|
||
# could brick a legitimate first boot before the owner sets a real
|
||
# password) — just make it impossible to miss.
|
||
problems = []
|
||
if config.SECRET_KEY == "dev-secret-change-me":
|
||
problems.append("SECRET_KEY все ще стандартний (dev-secret-change-me)")
|
||
if config.FLASK_ADMIN_PASSWORD == "admin":
|
||
problems.append("пароль адміна все ще стандартний (admin/admin)")
|
||
if not problems:
|
||
return
|
||
message = "; ".join(problems)
|
||
logger.error("INSECURE DEFAULTS IN PRODUCTION: %s — виправте .env і перезапустіть сервіс", message)
|
||
error_alerts.notify(
|
||
"перевірка налаштувань безпеки при старті",
|
||
RuntimeError(f"Використовуються стандартні небезпечні значення: {message}"),
|
||
)
|
||
|
||
|
||
_warn_on_insecure_defaults()
|
||
|
||
|
||
def _run_in_app_context(fn, job_name):
|
||
# APScheduler's default executor only logs a job's exception itself —
|
||
# nothing here previously caught one, so a crashing job (like
|
||
# reconcile_payments hitting something other than MonobankError) failed
|
||
# silently from the owner's perspective, visible only in server logs.
|
||
with app.app_context():
|
||
try:
|
||
fn()
|
||
except Exception as e:
|
||
logger.exception("Scheduled job %s crashed", job_name)
|
||
error_alerts.notify(f"фонове завдання {job_name}", e)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
scheduler = BackgroundScheduler(timezone=config.TIMEZONE)
|
||
scheduler.add_job(
|
||
lambda: _run_in_app_context(reconcile_pending_payments, "reconcile_payments"),
|
||
"interval",
|
||
minutes=10,
|
||
id="reconcile_payments",
|
||
)
|
||
scheduler.add_job(
|
||
lambda: _run_in_app_context(check_monobank_token_health, "monobank_token_health"),
|
||
"interval",
|
||
hours=1,
|
||
id="monobank_token_health",
|
||
)
|
||
scheduler.add_job(
|
||
lambda: _run_in_app_context(check_liqpay_token_health, "liqpay_token_health"),
|
||
"interval",
|
||
hours=1,
|
||
id="liqpay_token_health",
|
||
)
|
||
scheduler.add_job(
|
||
lambda: _run_in_app_context(run_scheduled_backup, "backup_remote"),
|
||
"interval",
|
||
hours=1,
|
||
id="backup_remote",
|
||
)
|
||
scheduler.add_job(
|
||
lambda: _run_in_app_context(maybe_close_shift, "checkbox_auto_close_shift"),
|
||
"interval",
|
||
minutes=10,
|
||
id="checkbox_auto_close_shift",
|
||
)
|
||
scheduler.start()
|
||
|
||
port = int(os.getenv("PORT", "8000"))
|
||
with app.app_context():
|
||
cafe_name = Setting.get("cafe_name", "Bober BBQ")
|
||
print(f"{cafe_name} backend listening on http://0.0.0.0:{port}")
|
||
serve(app, host="0.0.0.0", port=port)
|