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"}