first commit
This commit is contained in:
193
backend/app/app_factory.py
Normal file
193
backend/app/app_factory.py
Normal file
@@ -0,0 +1,193 @@
|
||||
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
|
||||
Reference in New Issue
Block a user