from __future__ import annotations import logging import os from logging.config import dictConfig import click from flask import Flask, jsonify, make_response, request, session from werkzeug.exceptions import HTTPException from app.core_settings import get_settings from app.routes import ( analytics_blueprint, auth_blueprint, dashboard_blueprint, health_blueprint, historical_blueprint, realtime_blueprint, ) from app.services.auth import get_auth_service from app.services.historical_sync import get_historical_sync_service def configure_logging(debug: bool) -> None: level = "DEBUG" if debug else "INFO" dictConfig( { "version": 1, "disable_existing_loggers": False, "formatters": { "default": {"format": "%(asctime)s | %(levelname)s | %(name)s | %(message)s"} }, "handlers": { "console": { "class": "logging.StreamHandler", "formatter": "default", "level": level, } }, "root": {"handlers": ["console"], "level": level}, } ) def create_app() -> Flask: settings = get_settings() configure_logging(settings.debug) app = Flask(__name__) app.config["JSON_SORT_KEYS"] = False get_auth_service().configure_app(app) app.register_blueprint(health_blueprint) app.register_blueprint(auth_blueprint, url_prefix=settings.api_prefix) app.register_blueprint(dashboard_blueprint, url_prefix=settings.api_prefix) app.register_blueprint(realtime_blueprint, url_prefix=settings.api_prefix) app.register_blueprint(analytics_blueprint, url_prefix=settings.api_prefix) app.register_blueprint(historical_blueprint, url_prefix=settings.api_prefix) @app.get("/") def index(): return { "app": settings.app_name, "version": settings.version, "api_prefix": settings.api_prefix, "message": "PV Insight backend is running", } @app.before_request def handle_preflight_and_auth(): if request.method == "OPTIONS": response = make_response("", 204) return _apply_cors(response) if not settings.auth["enabled"]: return None if request.path in {"/", "/health", "/favicon.ico"}: return None if request.path.startswith(f"{settings.api_prefix}/auth/"): return None public_kiosk = request.args.get("publicKiosk") == "1" public_kiosk_allowed_paths = { f"{settings.api_prefix}/dashboard/config", f"{settings.api_prefix}/dashboard/kiosk-settings", f"{settings.api_prefix}/realtime/snapshot", f"{settings.api_prefix}/realtime/history", f"{settings.api_prefix}/analytics/production", f"{settings.api_prefix}/analytics/distribution", } if public_kiosk and request.method == "GET" and request.path in public_kiosk_allowed_paths: return None if request.path.startswith(settings.api_prefix) and "auth_user" not in session: return _apply_cors(make_response(jsonify({"detail": "Authentication required"}), 401)) return None @app.after_request def append_cors_headers(response): return _apply_cors(response) @app.errorhandler(HTTPException) def handle_http_exception(exc: HTTPException): response = jsonify({"detail": exc.description}) return _apply_cors(make_response(response, exc.code or 500)) @app.errorhandler(Exception) def handle_exception(exc: Exception): logging.getLogger(__name__).exception("Unhandled application error") response = {"detail": str(exc) if settings.debug else "Internal server error"} return _apply_cors(make_response(response, 500)) _register_cli_commands(app) _bootstrap_background_services(settings.debug) return app def _register_cli_commands(app: Flask) -> None: auth_service = get_auth_service() @app.cli.command("create-admin") @click.option("--username", required=True, help="Login") @click.option("--password", required=True, hide_input=True, confirmation_prompt=True, help="Password") @click.option("--display-name", default=None, help="Name") def create_admin_command(username: str, password: str, display_name: str | None): try: user = auth_service.create_user( username=username, password=password, role="admin", display_name=display_name, ) click.echo(f"Utworzono konto admina: {user.username}") except ValueError as exc: raise click.ClickException(str(exc)) from exc except Exception as exc: # pragma: no cover raise click.ClickException(f"Cant create admin account: {exc}") from exc @app.cli.command("create-user") @click.option("--username", required=True, help="Login") @click.option("--password", required=True, hide_input=True, confirmation_prompt=True, help="Password") @click.option("--display-name", default=None, help="Name") def create_user_command(username: str, password: str, display_name: str | None): try: user = auth_service.create_user( username=username, password=password, role="user", display_name=display_name, ) click.echo(f"Admin created: {user.username}") except ValueError as exc: raise click.ClickException(str(exc)) from exc except Exception as exc: # pragma: no cover raise click.ClickException(f"Cant create user account: {exc}") from exc @app.cli.command("reset-password") @click.option("--username", required=True, help="Login") @click.option("--password", required=True, hide_input=True, confirmation_prompt=True, help="Password") def reset_password_command(username: str, password: str): try: user = auth_service.reset_password(username=username, new_password=password) click.echo(f"Passowrd reseted for: {user.username}") except ValueError as exc: raise click.ClickException(str(exc)) from exc except Exception as exc: # pragma: no cover raise click.ClickException(f"Can't password reset: {exc}") from exc def _bootstrap_background_services(debug: bool) -> None: should_run = (not debug) or os.environ.get("WERKZEUG_RUN_MAIN") == "true" if not should_run: return get_historical_sync_service().start_scheduler_if_enabled() def _apply_cors(response): settings = get_settings() origin = request.headers.get("Origin") allowed = settings.cors_origins if origin and (origin in allowed or "*" in allowed): response.headers["Access-Control-Allow-Origin"] = origin response.headers.add("Vary", "Origin") elif "*" in allowed: response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT" response.headers["Access-Control-Allow-Credentials"] = "true" return response