+102
-23
@@ -3,20 +3,90 @@ from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Request, Depends
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.config import get_settings
|
||||
from app.database import get_db, async_session
|
||||
from app.auth import audit_log
|
||||
from app.templating import templates
|
||||
from app.services.docs_permissions import discover_docs
|
||||
from app.documents_router import router as documents_router
|
||||
from app.dependencies import get_current_user, require_role
|
||||
from app.version import get_app_version
|
||||
|
||||
settings = get_settings()
|
||||
APP_VERSION = get_app_version()
|
||||
|
||||
|
||||
SEED_COEFFICIENTS = [
|
||||
("risk_score.no_archive", 25, "Нет архива видеонаблюдения", "calc_risk_score()"),
|
||||
("risk_score.no_power_backup", 20, "Нет резервного питания", "calc_risk_score()"),
|
||||
("risk_score.no_regulations", 15, "Нет регламента обслуживания", "calc_risk_score()"),
|
||||
("risk_score.system_failures", 20, "Частые сбои систем", "calc_risk_score()"),
|
||||
("risk_score.no_documentation", 10, "Нет документации", "calc_risk_score()"),
|
||||
("risk_score.bound_low", 20, "Граница низкого риска", "risk_label()"),
|
||||
("risk_score.bound_medium", 50, "Граница среднего риска", "risk_label()"),
|
||||
("risk_score.bound_high", 75, "Граница высокого риска", "risk_label()"),
|
||||
("risk_multiplier.low", 1.0, "Множитель низкого риска (0-20)", "risk_multiplier()"),
|
||||
("risk_multiplier.medium", 1.3, "Множитель среднего риска (21-50)", "risk_multiplier()"),
|
||||
("risk_multiplier.high", 1.6, "Множитель высокого риска (51-75)", "risk_multiplier()"),
|
||||
("risk_multiplier.critical", 2.0, "Множитель критического риска (76-100)", "risk_multiplier()"),
|
||||
("complexity.video_0_20", 10, "Видеонаблюдение до 20 камер", "calc_complexity()"),
|
||||
("complexity.video_20_100", 20, "Видеонаблюдение 20-100 камер", "calc_complexity()"),
|
||||
("complexity.video_100plus", 35, "Видеонаблюдение 100+ камер", "calc_complexity()"),
|
||||
("complexity.access_0_5", 10, "СКУД до 5 точек", "calc_complexity()"),
|
||||
("complexity.access_5_20", 20, "СКУД 5-20 точек", "calc_complexity()"),
|
||||
("complexity.access_20plus", 30, "СКУД 20+ точек", "calc_complexity()"),
|
||||
("complexity.fire_simple", 15, "Пожарная сигнализация простая", "calc_complexity()"),
|
||||
("complexity.fire_medium", 25, "Пожарная сигнализация средняя", "calc_complexity()"),
|
||||
("complexity.fire_complex", 40, "Пожарная сигнализация сложная (>5000 м²)", "calc_complexity()"),
|
||||
("complexity.it", 10, "IT-инфраструктура", "calc_complexity()"),
|
||||
("infra.server_none", 15, "Нет сервера", "calc_infrastructure_load()"),
|
||||
("infra.server_weak", 10, "Слабый сервер", "calc_infrastructure_load()"),
|
||||
("infra.network_unstable", 20, "Нестабильная сеть", "calc_infrastructure_load()"),
|
||||
("infra.network_partial", 10, "Частичная сеть", "calc_infrastructure_load()"),
|
||||
("infra.power_none", 20, "Нет UPS", "calc_infrastructure_load()"),
|
||||
("infra.power_weak", 10, "Слабый UPS", "calc_infrastructure_load()"),
|
||||
("history.none", 30, "Нет обслуживания", "calc_service_history()"),
|
||||
("history.irregular", 20, "Нерегулярное обслуживание", "calc_service_history()"),
|
||||
("history.formal", 10, "Формальный подрядчик", "calc_service_history()"),
|
||||
("history.sla", 0, "Есть SLA", "calc_service_history()"),
|
||||
("object_index.risk_weight", 0.4, "Вес Risk Score", "calc_object_index()"),
|
||||
("object_index.complexity_weight", 0.3, "Вес Complexity", "calc_object_index()"),
|
||||
("object_index.infra_weight", 0.2, "Вес Infra Load", "calc_object_index()"),
|
||||
("object_index.history_weight", 0.1, "Вес Service History", "calc_object_index()"),
|
||||
("sla_price.base_cost", 15000, "Базовая стоимость инженера", "calc_sla_price()"),
|
||||
("sla_price.region_default", 1.0, "Региональный коэффициент по умолчанию", "calc_sla_price()"),
|
||||
]
|
||||
|
||||
|
||||
async def seed_initial_data():
|
||||
from sqlalchemy import text, select, func
|
||||
from app.models.models import FormulaCoefficient, User
|
||||
|
||||
async with async_session() as db:
|
||||
try:
|
||||
count = await db.execute(select(func.count(FormulaCoefficient.id)))
|
||||
if count.scalar() == 0:
|
||||
from app.models.models import FormulaCoefficient as FC
|
||||
for key, value, desc, ref in SEED_COEFFICIENTS:
|
||||
db.add(FC(key=key, value=value, description=desc, formula_ref=ref))
|
||||
await db.commit()
|
||||
|
||||
await db.execute(text("DELETE FROM users WHERE login = 'admin'"))
|
||||
await db.commit()
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
from app.models.models import Base
|
||||
from app.database import engine
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await seed_initial_data()
|
||||
async with async_session() as db:
|
||||
from app.calculations.engine import reload_coefficients
|
||||
await reload_coefficients(db)
|
||||
@@ -26,10 +96,7 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
app = FastAPI(title="AegisOne Service Portal", lifespan=lifespan)
|
||||
|
||||
app.add_middleware(SessionMiddleware, secret_key=settings.SECRET_KEY, max_age=settings.SESSION_TTL)
|
||||
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
templates.env.globals["SITE_NAME"] = "AegisOne Engineering"
|
||||
app.mount("/service/static", StaticFiles(directory="app/static"), name="static")
|
||||
|
||||
|
||||
class RoleGuardMiddleware(BaseHTTPMiddleware):
|
||||
@@ -38,28 +105,28 @@ class RoleGuardMiddleware(BaseHTTPMiddleware):
|
||||
if path.startswith("/service/") and not any(
|
||||
path.startswith(p) for p in ["/service/login", "/service/static", "/service/api/"]
|
||||
):
|
||||
user = request.session.get("user")
|
||||
if not user:
|
||||
return RedirectResponse(url="/service/login", status_code=302)
|
||||
if "session" in request.scope:
|
||||
user = request.session.get("user")
|
||||
if not user:
|
||||
return RedirectResponse(url="/service/login", status_code=302)
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
app.add_middleware(RoleGuardMiddleware)
|
||||
app.add_middleware(SessionMiddleware, secret_key=settings.SECRET_KEY, max_age=settings.SESSION_TTL)
|
||||
|
||||
app.include_router(documents_router)
|
||||
|
||||
from app.routers.service_pages import router as service_pages_router
|
||||
app.include_router(service_pages_router)
|
||||
|
||||
|
||||
def get_current_user(request: Request) -> dict | None:
|
||||
return request.session.get("user")
|
||||
|
||||
|
||||
def require_role(*roles: str):
|
||||
def dependency(request: Request, user: dict = Depends(get_current_user)):
|
||||
if not user:
|
||||
return RedirectResponse(url="/service/login", status_code=302)
|
||||
if roles and user["role"] not in roles:
|
||||
return HTMLResponse(content="Доступ запрещён", status_code=403)
|
||||
return user
|
||||
return dependency
|
||||
@app.get("/")
|
||||
async def root(request: Request):
|
||||
if request.session.get("user"):
|
||||
return RedirectResponse(url="/service/dashboard", status_code=302)
|
||||
return RedirectResponse(url="/service/login", status_code=302)
|
||||
|
||||
|
||||
@app.get("/service/login")
|
||||
@@ -100,7 +167,7 @@ async def logout(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
@app.get("/service/dashboard")
|
||||
async def dashboard(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy import select, func, Integer
|
||||
from app.models.models import Object, Task, Incident, SLAContract, SHSRecord
|
||||
|
||||
stats = {}
|
||||
@@ -130,8 +197,8 @@ async def dashboard(request: Request, db: AsyncSession = Depends(get_db), user:
|
||||
stats["my_tasks"] = result.scalar() or 0
|
||||
|
||||
result = await db.execute(
|
||||
select(func.count(Task.id), func.sum(Task.sla_compliant.cast(int)))
|
||||
.where(Task.closed_at.isnot(None))
|
||||
select(func.count(Task.id), func.sum(Task.sla_compliant.cast(Integer)))
|
||||
.where(Task.closed_at != None)
|
||||
)
|
||||
row = result.first()
|
||||
total = row[0] or 0
|
||||
@@ -182,6 +249,18 @@ async def api_tasks(db: AsyncSession = Depends(get_db), user: dict = Depends(get
|
||||
])
|
||||
|
||||
|
||||
@app.get("/service/api/changelog")
|
||||
async def api_changelog():
|
||||
import os, markdown
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
changelog_path = os.path.join(base, "..", "CHANGELOG.md")
|
||||
if not os.path.isfile(changelog_path):
|
||||
return JSONResponse({"html": "<p>Файл изменений не найден.</p>"})
|
||||
with open(changelog_path, encoding="utf-8") as f:
|
||||
html = markdown.markdown(f.read(), extensions=["fenced_code", "tables"])
|
||||
return JSONResponse({"html": html})
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
Reference in New Issue
Block a user