188 lines
7.1 KiB
Python
188 lines
7.1 KiB
Python
import os
|
|
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.services.docs_permissions import discover_docs
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
async with async_session() as db:
|
|
from app.calculations.engine import reload_coefficients
|
|
await reload_coefficients(db)
|
|
discover_docs()
|
|
yield
|
|
|
|
|
|
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"
|
|
|
|
|
|
class RoleGuardMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
path = request.url.path
|
|
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)
|
|
response = await call_next(request)
|
|
return response
|
|
|
|
|
|
app.add_middleware(RoleGuardMiddleware)
|
|
|
|
|
|
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("/service/login")
|
|
async def login_page(request: Request, db: AsyncSession = Depends(get_db)):
|
|
if request.session.get("user"):
|
|
return RedirectResponse(url="/service/dashboard", status_code=302)
|
|
return templates.TemplateResponse("login.html", {"request": request, "error": ""})
|
|
|
|
|
|
@app.post("/service/login")
|
|
async def login_submit(request: Request, db: AsyncSession = Depends(get_db)):
|
|
form = await request.form()
|
|
login = form.get("login", "").strip()
|
|
password = form.get("password", "")
|
|
if not login or not password:
|
|
return templates.TemplateResponse("login.html", {"request": request, "error": "Заполните все поля."})
|
|
|
|
from app.auth import authenticate
|
|
ip = request.client.host if request.client else ""
|
|
result = await authenticate(db, login, password, ip)
|
|
|
|
if result["success"]:
|
|
request.session["user"] = result["user"]
|
|
request.session["csrf_token"] = __import__("secrets").token_hex(32)
|
|
redirect = request.query_params.get("redirect", "/service/dashboard")
|
|
return RedirectResponse(url=redirect, status_code=302)
|
|
return templates.TemplateResponse("login.html", {"request": request, "error": result["error"]})
|
|
|
|
|
|
@app.get("/service/logout")
|
|
async def logout(request: Request, db: AsyncSession = Depends(get_db)):
|
|
user = request.session.get("user")
|
|
if user:
|
|
await audit_log(db, user["id"], "logout", "Выход из системы", request.client.host if request.client else "")
|
|
request.session.clear()
|
|
return RedirectResponse(url="/service/login", status_code=302)
|
|
|
|
|
|
@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 app.models.models import Object, Task, Incident, SLAContract, SHSRecord
|
|
|
|
stats = {}
|
|
result = await db.execute(select(func.count(Object.id)).where(Object.status == "active"))
|
|
stats["objects"] = result.scalar() or 0
|
|
|
|
result = await db.execute(select(func.count(Task.id)).where(Task.status.in_(["open", "in_progress"])))
|
|
stats["tasks"] = result.scalar() or 0
|
|
|
|
result = await db.execute(select(func.count(Incident.id)).where(Incident.status.in_(["open", "in_progress"])))
|
|
stats["incidents"] = result.scalar() or 0
|
|
|
|
result = await db.execute(select(func.count(SLAContract.id)).where(SLAContract.status == "active"))
|
|
stats["sla_count"] = result.scalar() or 0
|
|
|
|
result = await db.execute(select(SHSRecord.shs_score, SHSRecord.shs_status).order_by(SHSRecord.id.desc()).limit(1))
|
|
shs_row = result.first()
|
|
stats["shs"] = {"shs_score": shs_row[0], "shs_status": shs_row[1]} if shs_row else None
|
|
|
|
if user["role"] in ("engineer", "technician"):
|
|
result = await db.execute(
|
|
select(func.count(Task.id)).where(
|
|
Task.assigned_to == user["id"],
|
|
Task.status.in_(["open", "in_progress"]),
|
|
)
|
|
)
|
|
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))
|
|
)
|
|
row = result.first()
|
|
total = row[0] or 0
|
|
compliant = row[1] or 0
|
|
stats["sla_compliance"] = round((compliant / total) * 100, 1) if total > 0 else 0
|
|
|
|
shs_alert = None
|
|
if stats["shs"] and stats["shs"]["shs_score"] < 70:
|
|
shs_alert = f"SHS ниже 70 ({stats['shs']['shs_score']}) — {stats['shs']['shs_status']}"
|
|
|
|
now = __import__("datetime").datetime.now().strftime("%d.%m.%Y %H:%M")
|
|
return templates.TemplateResponse("dashboard.html", {
|
|
"request": request, "stats": stats, "shs_alert": shs_alert, "now": now, "user": user,
|
|
})
|
|
|
|
|
|
@app.get("/service/api/shs")
|
|
async def api_shs(db: AsyncSession = Depends(get_db)):
|
|
from sqlalchemy import select
|
|
from app.models.models import SHSRecord
|
|
result = await db.execute(select(SHSRecord).order_by(SHSRecord.id.desc()).limit(1))
|
|
record = result.scalar_one_or_none()
|
|
if not record:
|
|
return JSONResponse({"error": "No data"})
|
|
return JSONResponse({
|
|
"shs_score": float(record.shs_score),
|
|
"shs_status": record.shs_status,
|
|
"period_start": record.period_start.isoformat(),
|
|
"period_end": record.period_end.isoformat(),
|
|
})
|
|
|
|
|
|
@app.get("/service/api/tasks")
|
|
async def api_tasks(db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
|
|
from sqlalchemy import select
|
|
from app.models.models import Task
|
|
query = select(Task).where(Task.status.in_(["open", "in_progress"]))
|
|
if user["role"] == "technician":
|
|
query = query.where(Task.assigned_to == user["id"])
|
|
result = await db.execute(query.order_by(Task.created_at.desc()))
|
|
tasks = result.scalars().all()
|
|
return JSONResponse([
|
|
{
|
|
"id": t.id, "title": t.title, "priority": t.priority,
|
|
"status": t.status, "object_id": t.object_id,
|
|
}
|
|
for t in tasks
|
|
])
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "ok"}
|