194 lines
7.2 KiB
Python
194 lines
7.2 KiB
Python
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
|