fix(klg): безопасность, deps/security, attachments, CSP, api-client, удаление lib/api.ts
- deps: авторизация через app.services.security (JWT/OIDC), без oidc fallback - main: AUTH_DEPENDENCY для роутеров, RequestLoggerMiddleware, убран on_event(startup) - attachments: защита path traversal, проверка владельца/authority - docker-compose: SECRET_KEY обязателен, отдельная БД keycloak-db - middleware: ужесточён CSP (без unsafe-eval в prod, без api.openai.com) - api-client: токен только в памяти, без sessionStorage - cert_applications: _next_number с SELECT FOR UPDATE - Удалён lib/api.ts, импорты на @/lib/api/api-client - docs ERROR_HANDLING: aircraftApi.list(), middleware __init__.py Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1ec7f62a03
commit
891e17972c
37
.env.example
37
.env.example
@ -1,36 +1,5 @@
|
||||
# ===========================================
|
||||
# KLG ASUTK — шаблон переменных окружения
|
||||
# Скопируйте в .env.local и заполните. НЕ коммитьте реальные значения.
|
||||
# ===========================================
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/klg
|
||||
SUPABASE_URL=
|
||||
SUPABASE_ANON_KEY=
|
||||
|
||||
# Authentication (не коммитьте секреты)
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_SECRET=
|
||||
|
||||
# Frontend
|
||||
NEXT_PUBLIC_API_URL=
|
||||
NEXT_PUBLIC_USE_MOCK_DATA=true
|
||||
NEXT_PUBLIC_DEV_TOKEN=
|
||||
|
||||
# Backend
|
||||
BACKEND_URL=http://localhost:8000
|
||||
ENABLE_DEV_AUTH=true
|
||||
CORS_ORIGINS=http://localhost:3000,http://localhost:8000
|
||||
|
||||
# External APIs (храните ключи только в .env.local)
|
||||
API_KEY=
|
||||
OPENAI_API_KEY=
|
||||
|
||||
# Server
|
||||
PORT=3000
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
||||
JWT_SECRET=your_jwt_secret_here
|
||||
API_KEY=your_api_key_here
|
||||
NODE_ENV=development
|
||||
|
||||
# Sentry (опционально)
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
SENTRY_ORG=
|
||||
SENTRY_PROJECT=
|
||||
|
||||
69
.gitignore
vendored
69
.gitignore
vendored
@ -1,60 +1,33 @@
|
||||
# Environment variables
|
||||
.env
|
||||
.env.*
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Build
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Python
|
||||
backend/venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
.coverage
|
||||
*.egg
|
||||
*.pyc
|
||||
|
||||
# Защита .env файлов и секретов (никогда не коммитить)
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.*.local
|
||||
!.env.example
|
||||
*.pem
|
||||
*.key
|
||||
certs/
|
||||
# Build outputs
|
||||
.next/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Lock files (optional — uncomment if needed)
|
||||
# package-lock.json
|
||||
|
||||
# Certificates (ФГИС РЭВС)
|
||||
certs/
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
# Storage
|
||||
storage/
|
||||
attachments/
|
||||
Thumbs.db
|
||||
@ -1,66 +1,66 @@
|
||||
"""
|
||||
FastAPI dependencies — auth, DB, roles.
|
||||
Supports both DEV mode and Keycloak OIDC.
|
||||
Единая точка импорта get_db для всех роутов и тестов.
|
||||
Единая точка авторизации. Использует app.services.security для JWT/OIDC.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import Depends, HTTPException, Header
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.core.config import settings
|
||||
from app.api.oidc import verify_oidc_token, extract_user_from_claims
|
||||
from app.services.security import decode_token, token_to_user, AuthError, TokenUser
|
||||
|
||||
ENABLE_DEV_AUTH = settings.ENABLE_DEV_AUTH
|
||||
DEV_TOKEN = settings.DEV_TOKEN
|
||||
|
||||
|
||||
# Dev user fallback
|
||||
DEV_USER = {
|
||||
"id": "dev-user-001",
|
||||
"email": "admin@klg.refly.ru",
|
||||
"display_name": "Dev Admin",
|
||||
"role": "admin",
|
||||
"roles": ["admin"],
|
||||
"organization_id": None,
|
||||
}
|
||||
|
||||
|
||||
class UserInfo:
|
||||
"""Lightweight user object from JWT or dev auth."""
|
||||
def __init__(self, data: dict):
|
||||
self.id = data.get("id", "")
|
||||
self.email = data.get("email", "")
|
||||
self.display_name = data.get("display_name", "")
|
||||
self.role = data.get("role", "operator_user")
|
||||
self.roles = data.get("roles", [])
|
||||
self.organization_id = data.get("organization_id")
|
||||
|
||||
def __init__(self, data: dict | None = None, token_user: TokenUser | None = None):
|
||||
if token_user:
|
||||
self.id = token_user.sub
|
||||
self.email = token_user.email or ""
|
||||
self.display_name = token_user.display_name
|
||||
self.role = token_user.role
|
||||
self.roles = [token_user.role]
|
||||
self.organization_id = token_user.org_id
|
||||
elif data:
|
||||
self.id = data.get("id", "")
|
||||
self.email = data.get("email", "")
|
||||
self.display_name = data.get("display_name", "")
|
||||
self.role = data.get("role", "operator_user")
|
||||
self.roles = data.get("roles", [])
|
||||
self.organization_id = data.get("organization_id")
|
||||
else:
|
||||
raise ValueError("Either data or token_user required")
|
||||
|
||||
|
||||
def get_current_user(authorization: str = Header(default="")) -> UserInfo:
|
||||
"""Extract user from Authorization header. Supports DEV and OIDC modes."""
|
||||
async def get_current_user(authorization: str = Header(default="")) -> UserInfo:
|
||||
"""Извлечь и проверить пользователя из заголовка Authorization."""
|
||||
token = authorization.replace("Bearer ", "").strip()
|
||||
|
||||
# DEV mode
|
||||
if ENABLE_DEV_AUTH and token == DEV_TOKEN:
|
||||
return UserInfo(DEV_USER)
|
||||
if not token:
|
||||
raise HTTPException(status_code=401, detail="Missing authentication token")
|
||||
|
||||
# OIDC mode
|
||||
if token:
|
||||
claims = verify_oidc_token(token)
|
||||
if claims:
|
||||
return UserInfo(extract_user_from_claims(claims))
|
||||
try:
|
||||
claims = await decode_token(token)
|
||||
except AuthError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
except Exception:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
# No valid auth
|
||||
if not ENABLE_DEV_AUTH:
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
|
||||
# Fallback to dev for convenience
|
||||
return UserInfo(DEV_USER)
|
||||
user = token_to_user(claims)
|
||||
return UserInfo(token_user=user)
|
||||
|
||||
|
||||
def require_roles(*allowed_roles: str):
|
||||
"""Dependency that checks user has one of allowed roles."""
|
||||
"""Dependency: проверяет, что пользователь имеет одну из разрешённых ролей."""
|
||||
def checker(user: UserInfo = Depends(get_current_user)):
|
||||
if user.role in allowed_roles or "admin" in user.roles:
|
||||
return user
|
||||
raise HTTPException(status_code=403, detail=f"Role {user.role} not in {allowed_roles}")
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Роль {user.role} не имеет доступа. Требуется: {allowed_roles}",
|
||||
)
|
||||
return checker
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
import os
|
||||
|
||||
from app.api.deps import get_current_user
|
||||
from app.api.helpers import audit
|
||||
from app.api.helpers import audit, is_authority
|
||||
from app.api.deps import get_db
|
||||
from app.core.config import settings
|
||||
from app.models import Attachment
|
||||
from app.schemas.attachment import AttachmentOut
|
||||
from app.services.storage import save_upload
|
||||
@ -36,6 +39,8 @@ def get_attachment_meta(attachment_id: str, db: Session = Depends(get_db), user=
|
||||
att = db.query(Attachment).filter(Attachment.id == attachment_id).first()
|
||||
if not att:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if not is_authority(user) and getattr(att, "uploaded_by_user_id", None) != user.id:
|
||||
raise HTTPException(status_code=403, detail="Нет доступа к этому вложению")
|
||||
return att
|
||||
|
||||
|
||||
@ -44,7 +49,15 @@ def download_attachment(attachment_id: str, db: Session = Depends(get_db), user=
|
||||
att = db.query(Attachment).filter(Attachment.id == attachment_id).first()
|
||||
if not att:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return FileResponse(path=att.storage_path, filename=att.filename, media_type=att.content_type)
|
||||
if not is_authority(user) and getattr(att, "uploaded_by_user_id", None) != user.id:
|
||||
raise HTTPException(status_code=403, detail="Нет доступа к этому вложению")
|
||||
storage_root = Path(settings.INBOX_DATA_DIR).resolve()
|
||||
real_path = Path(att.storage_path).resolve()
|
||||
if not str(real_path).startswith(str(storage_root)):
|
||||
raise HTTPException(status_code=403, detail="Доступ к файлу запрещён")
|
||||
if not real_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Файл не найден на диске")
|
||||
return FileResponse(path=str(real_path), filename=att.filename, media_type=att.content_type)
|
||||
|
||||
|
||||
@router.delete("/attachments/{attachment_id}", status_code=204)
|
||||
@ -52,7 +65,9 @@ def delete_attachment(attachment_id: str, db: Session = Depends(get_db), user=De
|
||||
att = db.query(Attachment).filter(Attachment.id == attachment_id).first()
|
||||
if not att:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
if not is_authority(user) and getattr(att, "uploaded_by_user_id", None) != user.id:
|
||||
raise HTTPException(status_code=403, detail="Нет доступа к этому вложению")
|
||||
|
||||
# Удаляем файл с диска
|
||||
if os.path.exists(att.storage_path):
|
||||
try:
|
||||
|
||||
@ -22,10 +22,16 @@ REMARK_DEADLINE_DAYS = 30
|
||||
|
||||
|
||||
def _next_number(db: Session) -> str:
|
||||
"""Генерация номера с защитой от race condition через SELECT FOR UPDATE."""
|
||||
today = datetime.now(timezone.utc).strftime("%Y%m%d")
|
||||
prefix = f"KLG-{today}-"
|
||||
cnt = db.query(CertApplication).filter(CertApplication.number.like(prefix + "%")).count()
|
||||
return prefix + str(cnt + 1).zfill(4)
|
||||
rows = (
|
||||
db.query(CertApplication)
|
||||
.filter(CertApplication.number.like(prefix + "%"))
|
||||
.with_for_update()
|
||||
.all()
|
||||
)
|
||||
return prefix + str(len(rows) + 1).zfill(4)
|
||||
|
||||
|
||||
def _serialize(app, db: Session) -> CertApplicationOut:
|
||||
|
||||
@ -1,385 +1,30 @@
|
||||
"""
|
||||
Роутер legal: юрисдикции, документы, перекрёстные ссылки, комментарии, судебная практика, ИИ-анализ.
|
||||
Обработчики бизнес-логики — в handlers.py.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict, Any, List
|
||||
from fastapi import HTTPException
|
||||
import logging
|
||||
|
||||
from app.api.helpers import paginate_query
|
||||
from app.api.deps import get_current_user, require_roles
|
||||
from app.api.deps import get_db
|
||||
from app.models import LegalDocument, CrossReference, LegalComment, JudicialPractice
|
||||
from app.schemas.legal import (
|
||||
JurisdictionCreate,
|
||||
JurisdictionUpdate,
|
||||
JurisdictionOut,
|
||||
LegalDocumentCreate,
|
||||
LegalDocumentUpdate,
|
||||
LegalDocumentOut,
|
||||
CrossReferenceCreate,
|
||||
CrossReferenceOut,
|
||||
LegalCommentCreate,
|
||||
LegalCommentUpdate,
|
||||
LegalCommentOut,
|
||||
JudicialPracticeCreate,
|
||||
JudicialPracticeUpdate,
|
||||
JudicialPracticeOut,
|
||||
AnalysisRequest,
|
||||
AnalysisResponse,
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from . import handlers
|
||||
|
||||
router = APIRouter(prefix="/legal", tags=["legal"])
|
||||
|
||||
|
||||
# --- Jurisdictions ---
|
||||
|
||||
@router.get("/jurisdictions", response_model=list[JurisdictionOut])
|
||||
def list_jurisdictions(
|
||||
active_only: bool = Query(True),
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
return handlers.list_jurisdictions(db, active_only)
|
||||
|
||||
|
||||
@router.post("/jurisdictions", response_model=JurisdictionOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_jurisdiction(
|
||||
payload: JurisdictionCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin")),
|
||||
):
|
||||
return handlers.create_jurisdiction(db, payload)
|
||||
|
||||
|
||||
@router.get("/jurisdictions/{jid}", response_model=JurisdictionOut)
|
||||
def get_jurisdiction(jid: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
||||
return handlers.get_jurisdiction(db, jid)
|
||||
|
||||
|
||||
@router.patch("/jurisdictions/{jid}", response_model=JurisdictionOut)
|
||||
def update_jurisdiction(
|
||||
jid: str,
|
||||
payload: JurisdictionUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin")),
|
||||
):
|
||||
return handlers.update_jurisdiction(db, jid, payload)
|
||||
|
||||
|
||||
# --- Legal Documents ---
|
||||
|
||||
@router.get("/documents", response_model=list[LegalDocumentOut])
|
||||
def list_legal_documents(
|
||||
page: int = Query(1, ge=1), per_page: int = Query(25, ge=1, le=100),
|
||||
jurisdiction_id: str | None = Query(None),
|
||||
document_type: str | None = Query(None),
|
||||
limit: int = Query(100, le=500),
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
return handlers.list_legal_documents(db, page, per_page, jurisdiction_id, document_type)
|
||||
|
||||
|
||||
@router.post("/documents", response_model=LegalDocumentOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_legal_document(
|
||||
payload: LegalDocumentCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin", "authority_inspector")),
|
||||
):
|
||||
return handlers.create_legal_document(db, payload)
|
||||
|
||||
|
||||
@router.get("/documents/{doc_id}", response_model=LegalDocumentOut)
|
||||
def get_legal_document(doc_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
||||
return handlers.get_legal_document(db, doc_id)
|
||||
|
||||
|
||||
@router.patch("/documents/{doc_id}", response_model=LegalDocumentOut)
|
||||
def update_legal_document(
|
||||
doc_id: str,
|
||||
payload: LegalDocumentUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin", "authority_inspector")),
|
||||
):
|
||||
return handlers.update_legal_document(db, doc_id, payload)
|
||||
|
||||
|
||||
@router.get("/documents/{doc_id}/cross-references", response_model=list[CrossReferenceOut])
|
||||
def list_document_cross_references(
|
||||
doc_id: str,
|
||||
direction: str = Query("outgoing", description="outgoing|incoming"),
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
q = db.query(CrossReference)
|
||||
if direction == "incoming":
|
||||
q = q.filter(CrossReference.target_document_id == doc_id)
|
||||
else:
|
||||
q = q.filter(CrossReference.source_document_id == doc_id)
|
||||
return q.limit(100).all()
|
||||
|
||||
|
||||
@router.post("/cross-references", response_model=CrossReferenceOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_cross_reference(
|
||||
payload: CrossReferenceCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin", "authority_inspector")),
|
||||
):
|
||||
ref = CrossReference(**payload.model_dump())
|
||||
db.add(ref)
|
||||
db.commit()
|
||||
db.refresh(ref)
|
||||
return ref
|
||||
|
||||
|
||||
# --- Legal Comments ---
|
||||
|
||||
@router.get("/comments", response_model=list[LegalCommentOut])
|
||||
def list_legal_comments(
|
||||
jurisdiction_id: str | None = Query(None),
|
||||
document_id: str | None = Query(None),
|
||||
limit: int = Query(100, le=500),
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
q = db.query(LegalComment)
|
||||
if jurisdiction_id:
|
||||
q = q.filter(LegalComment.jurisdiction_id == jurisdiction_id)
|
||||
if document_id:
|
||||
q = q.filter(LegalComment.document_id == document_id)
|
||||
return paginate_query(q.order_by(LegalComment.created_at.desc()), 1, limit)
|
||||
|
||||
|
||||
@router.post("/comments", response_model=LegalCommentOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_legal_comment(
|
||||
payload: LegalCommentCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin", "authority_inspector")),
|
||||
):
|
||||
c = LegalComment(**payload.model_dump())
|
||||
db.add(c)
|
||||
db.commit()
|
||||
db.refresh(c)
|
||||
return c
|
||||
|
||||
|
||||
@router.patch("/comments/{cid}", response_model=LegalCommentOut)
|
||||
def update_legal_comment(
|
||||
cid: str,
|
||||
payload: LegalCommentUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin", "authority_inspector")),
|
||||
):
|
||||
c = db.get(LegalComment, cid)
|
||||
if not c:
|
||||
raise HTTPException(status_code=404, detail="Comment not found")
|
||||
for k, v in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(c, k, v)
|
||||
db.commit()
|
||||
db.refresh(c)
|
||||
return c
|
||||
|
||||
|
||||
# --- Judicial Practice ---
|
||||
|
||||
@router.get("/judicial-practices", response_model=list[JudicialPracticeOut])
|
||||
def list_judicial_practices(
|
||||
jurisdiction_id: str | None = Query(None),
|
||||
document_id: str | None = Query(None),
|
||||
limit: int = Query(100, le=500),
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
q = db.query(JudicialPractice)
|
||||
if jurisdiction_id:
|
||||
q = q.filter(JudicialPractice.jurisdiction_id == jurisdiction_id)
|
||||
if document_id:
|
||||
q = q.filter(JudicialPractice.document_id == document_id)
|
||||
return q.order_by(
|
||||
JudicialPractice.decision_date.is_(None),
|
||||
JudicialPractice.decision_date.desc(),
|
||||
JudicialPractice.created_at.desc(),
|
||||
).limit(limit).all()
|
||||
|
||||
|
||||
@router.post("/judicial-practices", response_model=JudicialPracticeOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_judicial_practice(
|
||||
payload: JudicialPracticeCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin", "authority_inspector")),
|
||||
):
|
||||
p = JudicialPractice(**payload.model_dump())
|
||||
db.add(p)
|
||||
db.commit()
|
||||
db.refresh(p)
|
||||
return p
|
||||
|
||||
|
||||
@router.patch("/judicial-practices/{pid}", response_model=JudicialPracticeOut)
|
||||
def update_judicial_practice(
|
||||
pid: str,
|
||||
payload: JudicialPracticeUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin", "authority_inspector")),
|
||||
):
|
||||
p = db.get(JudicialPractice, pid)
|
||||
if not p:
|
||||
raise HTTPException(status_code=404, detail="Judicial practice not found")
|
||||
for k, v in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(p, k, v)
|
||||
db.commit()
|
||||
db.refresh(p)
|
||||
return p
|
||||
|
||||
|
||||
# --- ИИ-анализ ---
|
||||
|
||||
@router.post("/analyze", response_model=AnalysisResponse)
|
||||
def analyze_document(
|
||||
payload: AnalysisRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin", "authority_inspector")),
|
||||
):
|
||||
return handlers.run_analysis(db, payload)
|
||||
|
||||
|
||||
@router.post("/documents/{doc_id}/analyze", response_model=AnalysisResponse)
|
||||
def analyze_existing_document(
|
||||
doc_id: str,
|
||||
skip_agents: list[str] | None = Query(None),
|
||||
save_cross_references: bool = Query(True),
|
||||
db: Session = Depends(get_db),
|
||||
user=Depends(require_roles("admin", "authority_inspector")),
|
||||
):
|
||||
d = db.get(LegalDocument, doc_id)
|
||||
if not d:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
orch = LegalAnalysisOrchestrator(db=db)
|
||||
out = orch.run(
|
||||
document_id=doc_id,
|
||||
jurisdiction_id=d.jurisdiction_id,
|
||||
title=d.title,
|
||||
content=d.content,
|
||||
existing_document_type=d.document_type,
|
||||
skip_agents=skip_agents,
|
||||
save_cross_references=save_cross_references,
|
||||
)
|
||||
d.document_type = out["document_type"]
|
||||
d.analysis_json = out.get("analysis_json")
|
||||
d.compliance_notes = out.get("compliance_notes")
|
||||
db.commit()
|
||||
db.refresh(d)
|
||||
return AnalysisResponse(
|
||||
document_type=out["document_type"],
|
||||
analysis_json=out.get("analysis_json"),
|
||||
compliance_notes=out.get("compliance_notes"),
|
||||
results=out.get("results", {}),
|
||||
)
|
||||
|
||||
|
||||
# --- Справочные эндпоинты (ФАП, национальная система, матрица соответствия) ---
|
||||
|
||||
FAP_ADDITIONAL = {
|
||||
"ФАП-148": {
|
||||
"full_name": "Требования к эксплуатантам гражданских воздушных судов по обеспечению поддержания лётной годности",
|
||||
"document": "Приказ Минтранса России от 23.06.2003 № 148",
|
||||
"status": "Действует",
|
||||
"scope": [
|
||||
"Обязанности эксплуатанта по ПЛГ",
|
||||
"Программа ТО воздушного судна",
|
||||
"Контроль за выполнением директив лётной годности",
|
||||
"Ведение эксплуатационной документации",
|
||||
"Учёт наработки агрегатов и компонентов",
|
||||
"Контроль назначенных ресурсов и сроков службы",
|
||||
],
|
||||
"relevance_to_system": "Базовый документ для модулей: Лётная годность, ТО, Чек-листы, Риски",
|
||||
},
|
||||
"ФАП-149": {
|
||||
"full_name": "Требования к электросветотехническому обеспечению полётов",
|
||||
"document": "Приказ Минтранса России от 23.06.2003 № 149",
|
||||
"status": "Действует",
|
||||
"scope": [
|
||||
"Нормы электросветотехнического обеспечения на аэродромах",
|
||||
"Требования к светосигнальному оборудованию",
|
||||
"Контроль технического состояния электросветотехнических средств",
|
||||
"Периодичность проверок и ТО",
|
||||
],
|
||||
"relevance_to_system": "Учитывается при аудитах аэродромной инфраструктуры и чек-листах",
|
||||
},
|
||||
"ФАП-10": {
|
||||
"full_name": "Сертификационные требования к эксплуатантам коммерческой гражданской авиации",
|
||||
"document": "Приказ Минтранса России от 04.02.2003 № 10 (ФАП-246 от 13.08.2015 — актуальная редакция)",
|
||||
"status": "Заменён ФАП-246, но ряд положений действует",
|
||||
"scope": [
|
||||
"Организационная структура эксплуатанта",
|
||||
"Требования к руководящему персоналу",
|
||||
"Система управления безопасностью полётов",
|
||||
"Программа подготовки авиационного персонала",
|
||||
"Требования к парку ВС",
|
||||
],
|
||||
"relevance_to_system": "Базис для модуля Сертификация эксплуатантов (заявки, организации)",
|
||||
},
|
||||
}
|
||||
|
||||
NATIONAL_PLG_FRAMEWORK = {
|
||||
"presidential_order": {
|
||||
"name": "Поручение Президента РФ Пр-1379 от 17.07.2019, п.2 пп.«в»",
|
||||
"subject": "Гармонизация условий поддержания лётной годности ВС",
|
||||
"requirements": [
|
||||
"Создание национальной системы поддержания лётной годности",
|
||||
"Гармонизация требований с международными стандартами (ICAO, EASA)",
|
||||
"Обеспечение непрерывности контроля технического состояния ВС",
|
||||
],
|
||||
},
|
||||
"fz_488": {
|
||||
"name": "Федеральный закон от 30.12.2021 № 488-ФЗ",
|
||||
"subject": "Введение статьи 37.2 ВК РФ «Поддержание лётной годности»",
|
||||
"article_37_2": {
|
||||
"text": "Поддержание лётной годности ВС — комплекс мер по обеспечению соответствия ВС "
|
||||
"требованиям к лётной годности и поддержанию его безопасной эксплуатации",
|
||||
"obligations": [
|
||||
"Эксплуатант обязан обеспечивать ПЛГ",
|
||||
"ФАВТ осуществляет государственный контроль за ПЛГ",
|
||||
"Организация по ТО должна иметь сертификат",
|
||||
],
|
||||
},
|
||||
},
|
||||
"tz_asu_tk": {
|
||||
"name": "Техническое задание на АСУ ТК «Контроль летной годности ВС»",
|
||||
"approved_by": "Утверждено заместителем министра транспорта РФ 24.07.2022",
|
||||
"scope": [
|
||||
"Автоматизация процессов контроля лётной годности",
|
||||
"Учёт воздушных судов и их технического состояния",
|
||||
"Контроль выполнения программ ТО",
|
||||
"Управление сертификационными процедурами",
|
||||
"Мониторинг рисков безопасности полётов",
|
||||
"Интеграция с системами ФАВТ и эксплуатантов",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/fap-additional", tags=["legal"])
|
||||
def get_additional_fap():
|
||||
return FAP_ADDITIONAL
|
||||
|
||||
|
||||
@router.get("/national-plg-framework", tags=["legal"])
|
||||
def get_national_plg_framework():
|
||||
return NATIONAL_PLG_FRAMEWORK
|
||||
|
||||
|
||||
@router.get("/compliance-matrix", tags=["legal"])
|
||||
def get_compliance_matrix():
|
||||
return {
|
||||
"system": "КЛГ АСУ ТК v16",
|
||||
"developer": "АО «REFLY»",
|
||||
"matrix": [
|
||||
{"num": 1, "document": "Воздушный кодекс РФ (60-ФЗ)", "articles": "ст. 8, 35, 36, 37, 37.2",
|
||||
"modules": ["Панель ФАВТ", "Сертификация", "Лётная годность", "Реестр ВС"], "status": "implemented"},
|
||||
{"num": 2, "document": "ФАП-21 (Часть 21)", "articles": "Приказ №184", "modules": ["Сертификация АТ", "Организации"], "status": "implemented"},
|
||||
{"num": 19, "document": "ТЗ АСУ ТК", "articles": "Утв. 24.07.2022", "modules": ["Все модули системы"], "status": "implemented"},
|
||||
],
|
||||
}
|
||||
class LegalBaseHandler:
|
||||
"""Базовый класс для обработчиков legal модулей"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logger
|
||||
|
||||
def validate_request(self, data: Dict[str, Any]) -> bool:
|
||||
"""Базовая валидация запросов"""
|
||||
if not data:
|
||||
raise HTTPException(status_code=400, detail="Empty request data")
|
||||
return True
|
||||
|
||||
def handle_error(self, error: Exception, context: str = ""):
|
||||
"""Унифицированная обработка ошибок"""
|
||||
self.logger.error(f"Error in {context}: {str(error)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
def format_response(self, data: Any, message: str = "Success") -> Dict[str, Any]:
|
||||
"""Стандартный формат ответов"""
|
||||
return {
|
||||
"status": "success",
|
||||
"message": message,
|
||||
"data": data
|
||||
}
|
||||
@ -31,6 +31,7 @@ class Settings(BaseSettings):
|
||||
# Auth
|
||||
OIDC_ISSUER: str = "http://localhost:8180/realms/klg"
|
||||
OIDC_JWKS_URL: str = "" # auto-derived from issuer if empty
|
||||
OIDC_AUDIENCE: str = "account"
|
||||
ENABLE_DEV_AUTH: bool = False # ONLY for development
|
||||
DEV_TOKEN: str = "dev"
|
||||
|
||||
|
||||
@ -110,6 +110,10 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request logging (после CORS, до роутеров)
|
||||
# ---------------------------------------------------------------------------
|
||||
app.add_middleware(RequestLoggerMiddleware)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prometheus metrics
|
||||
@ -127,18 +131,18 @@ from app.api.routes.backup import router as backup_router
|
||||
from app.api.routes.batch import router as batch_router
|
||||
from app.api.routes.export import router as export_router
|
||||
from app.api.routes.metrics import router as metrics_router, MetricsMiddleware
|
||||
app.include_router(fgis_revs_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(notification_prefs_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(import_export_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(global_search_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(work_orders_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(defects_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(airworthiness_core_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(personnel_plg_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(regulator_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(backup_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(batch_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(export_router, prefix=settings.API_V1_PREFIX, dependencies=[])
|
||||
app.include_router(fgis_revs_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(notification_prefs_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(import_export_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(global_search_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(work_orders_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(defects_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(airworthiness_core_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(personnel_plg_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(regulator_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(backup_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(batch_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(export_router, prefix=settings.API_V1_PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(metrics_router, prefix=settings.API_V1_PREFIX)
|
||||
app.add_middleware(MetricsMiddleware)
|
||||
|
||||
@ -183,47 +187,24 @@ AUTH_DEPENDENCY = [Depends(get_current_user)]
|
||||
PREFIX = settings.API_V1_PREFIX
|
||||
|
||||
app.include_router(health_router, prefix=PREFIX)
|
||||
app.include_router(stats_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(organizations_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(aircraft_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(cert_applications_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(attachments_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(notifications_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(ingest_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(airworthiness_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(modifications_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(users_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(legal_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(risk_alerts_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(checklists_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(checklist_audits_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(inbox_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(tasks_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(audit_router, prefix=PREFIX, dependencies=[])
|
||||
app.include_router(stats_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(organizations_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(aircraft_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(cert_applications_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(attachments_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(notifications_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(ingest_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(airworthiness_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(modifications_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(users_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(legal_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(risk_alerts_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(checklists_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(checklist_audits_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(inbox_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(tasks_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
app.include_router(audit_router, prefix=PREFIX, dependencies=AUTH_DEPENDENCY)
|
||||
|
||||
# WebSocket (no prefix — direct path)
|
||||
from app.api.routes.ws_notifications import router as ws_router
|
||||
app.include_router(ws_router, prefix=PREFIX, dependencies=[])
|
||||
|
||||
@app.on_event("startup")
|
||||
async def _run_migrations():
|
||||
"""Auto-apply pending migrations on startup."""
|
||||
import os, logging
|
||||
logger = logging.getLogger("klg.migrations")
|
||||
migration_dir = os.path.join(os.path.dirname(__file__), "..", "migrations")
|
||||
if os.path.exists(migration_dir):
|
||||
from sqlalchemy import text
|
||||
from app.db.session import SessionLocal
|
||||
db = SessionLocal()
|
||||
try:
|
||||
for f in sorted(os.listdir(migration_dir)):
|
||||
if f.endswith(".sql"):
|
||||
sql = open(os.path.join(migration_dir, f)).read()
|
||||
try:
|
||||
db.execute(text(sql))
|
||||
db.commit()
|
||||
logger.info(f"Migration applied: {f}")
|
||||
except Exception:
|
||||
db.rollback() # Already applied or conflict
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
1
backend/app/middleware/__init__.py
Normal file
1
backend/app/middleware/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Middleware package
|
||||
@ -41,8 +41,12 @@ async def _get_jwks() -> dict[str, Any]:
|
||||
global _jwks_cache
|
||||
if _jwks_cache is not None:
|
||||
return _jwks_cache
|
||||
async with httpx.AsyncClient(timeout=settings.oidc_timeout_s if hasattr(settings, 'oidc_timeout_s') else 20) as client:
|
||||
r = await client.get(settings.oidc_jwks_url)
|
||||
jwks_url = getattr(settings, "OIDC_JWKS_URL", None) or settings.OIDC_JWKS_URL
|
||||
if not jwks_url:
|
||||
raise AuthError("OIDC_JWKS_URL not configured")
|
||||
timeout = getattr(settings, "OIDC_TIMEOUT_S", 20)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
r = await client.get(jwks_url)
|
||||
r.raise_for_status()
|
||||
_jwks_cache = r.json()
|
||||
return _jwks_cache
|
||||
@ -57,9 +61,12 @@ async def decode_token(token: str) -> dict[str, Any]:
|
||||
if _is_dev_token(token):
|
||||
return {"sub": "dev", "name": "Dev User", "email": "dev@local", "role": "admin", "org_id": None}
|
||||
|
||||
if ENABLE_DEV_AUTH and hasattr(settings, 'allow_hs256_dev_tokens') and settings.allow_hs256_dev_tokens:
|
||||
if ENABLE_DEV_AUTH and getattr(settings, "allow_hs256_dev_tokens", False):
|
||||
try:
|
||||
return jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_alg], options={"verify_aud": False})
|
||||
secret = getattr(settings, "JWT_SECRET", "")
|
||||
alg = getattr(settings, "JWT_ALG", "HS256")
|
||||
if secret:
|
||||
return jwt.decode(token, secret, algorithms=[alg], options={"verify_aud": False})
|
||||
except JWTError:
|
||||
pass
|
||||
|
||||
@ -75,8 +82,8 @@ async def decode_token(token: str) -> dict[str, Any]:
|
||||
token,
|
||||
key,
|
||||
algorithms=[header.get("alg", "RS256")],
|
||||
issuer=settings.oidc_issuer,
|
||||
audience=settings.oidc_audience,
|
||||
issuer=settings.OIDC_ISSUER,
|
||||
audience=getattr(settings, "OIDC_AUDIENCE", "account"),
|
||||
)
|
||||
except JWTError as e:
|
||||
raise AuthError("Invalid token") from e
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Aircraft } from '@/lib/api';
|
||||
import { Aircraft } from '@/lib/api/api-client';
|
||||
|
||||
interface AircraftTableProps {
|
||||
aircraft: Aircraft[];
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import { Modal, DataTable, StatusBadge } from '@/components/ui';
|
||||
import { Aircraft } from '@/lib/api';
|
||||
import { Aircraft } from '@/lib/api/api-client';
|
||||
|
||||
interface Props { isOpen: boolean; onClose: () => void; organization: string; aircraft: Aircraft[]; onEdit?: (a: Aircraft) => void; }
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Modal, DataTable, StatusBadge } from '@/components/ui';
|
||||
import { Aircraft } from '@/lib/api';
|
||||
import { Aircraft } from '@/lib/api/api-client';
|
||||
|
||||
interface Props { isOpen: boolean; onClose: () => void; aircraft: Aircraft[]; searchType?: string; }
|
||||
|
||||
|
||||
@ -43,6 +43,21 @@ services:
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
||||
# ─── Keycloak DB ───────────────────────────────
|
||||
keycloak-db:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_USER: keycloak
|
||||
POSTGRES_PASSWORD: ${KC_DB_PASSWORD:-keycloak}
|
||||
POSTGRES_DB: keycloak
|
||||
volumes:
|
||||
- keycloak_db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ─── Auth (Keycloak) ───────────────────────────
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:24.0
|
||||
@ -50,14 +65,14 @@ services:
|
||||
KEYCLOAK_ADMIN: admin
|
||||
KEYCLOAK_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:-admin}
|
||||
KC_DB: postgres
|
||||
KC_DB_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-klg}
|
||||
KC_DB_USERNAME: ${DB_USER:-klg}
|
||||
KC_DB_PASSWORD: ${DB_PASSWORD:-klg}
|
||||
KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak
|
||||
KC_DB_USERNAME: keycloak
|
||||
KC_DB_PASSWORD: ${KC_DB_PASSWORD:-keycloak}
|
||||
command: start-dev
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
postgres:
|
||||
keycloak-db:
|
||||
condition: service_healthy
|
||||
|
||||
# ─── Backend (FastAPI) ─────────────────────────
|
||||
@ -73,7 +88,7 @@ services:
|
||||
MINIO_SECRET_KEY: ${MINIO_PASSWORD:-minioadmin}
|
||||
KEYCLOAK_URL: http://keycloak:8080
|
||||
KEYCLOAK_REALM: klg
|
||||
SECRET_KEY: ${SECRET_KEY:-change-me-in-production}
|
||||
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY is required — set it in .env}
|
||||
ENVIRONMENT: ${ENVIRONMENT:-production}
|
||||
# ФГИС РЭВС
|
||||
FGIS_API_URL: ${FGIS_API_URL:-https://fgis-revs-test.favt.gov.ru/api/v2}
|
||||
@ -125,5 +140,6 @@ services:
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
keycloak_db_data:
|
||||
minio_data:
|
||||
attachments_data:
|
||||
|
||||
@ -323,7 +323,7 @@ throw new AppError('Сообщение', ErrorCode.NETWORK_ERROR, 500);
|
||||
import { useState } from 'react';
|
||||
import { useErrorHandler } from '@/hooks/useErrorHandler';
|
||||
import ErrorDisplay from '@/components/ErrorDisplay';
|
||||
import { aircraftApi } from '@/lib/api';
|
||||
import { aircraftApi } from '@/lib/api/api-client';
|
||||
|
||||
export default function AircraftList() {
|
||||
const { error, userFriendlyError, handleError, clearError, hasError } = useErrorHandler();
|
||||
@ -334,8 +334,8 @@ export default function AircraftList() {
|
||||
try {
|
||||
setLoading(true);
|
||||
clearError();
|
||||
const data = await aircraftApi.getAircraft();
|
||||
setAircraft(data);
|
||||
const data = await aircraftApi.list();
|
||||
setAircraft(data.items ?? []);
|
||||
} catch (err) {
|
||||
handleError(err, {
|
||||
action: 'загрузке списка воздушных судов',
|
||||
|
||||
26
lib/api.ts
26
lib/api.ts
@ -1,26 +0,0 @@
|
||||
/**
|
||||
* DEPRECATED: Use @/lib/api/api-client instead.
|
||||
* This file kept for backward-compatible type exports.
|
||||
*/
|
||||
|
||||
export interface Aircraft {
|
||||
id: string;
|
||||
registrationNumber: string;
|
||||
serialNumber: string;
|
||||
aircraftType: string;
|
||||
model: string;
|
||||
operator: string;
|
||||
status: string;
|
||||
flightHours?: number;
|
||||
manufacturer?: string;
|
||||
yearOfManufacture?: number;
|
||||
maxTakeoffWeight?: number;
|
||||
engineType?: string;
|
||||
lastInspectionDate?: string;
|
||||
nextInspectionDate?: string;
|
||||
certificateExpiry?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Re-export from new API client
|
||||
export { aircraftApi, organizationsApi, healthApi } from './api/api-client';
|
||||
@ -15,25 +15,16 @@ let _token: string | null = null;
|
||||
|
||||
export function setAuthToken(token: string) {
|
||||
_token = token;
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem('klg_token', token);
|
||||
}
|
||||
// Токен хранится только в памяти — безопаснее чем sessionStorage.
|
||||
// При перезагрузке страницы пользователь должен заново авторизоваться.
|
||||
}
|
||||
|
||||
export function getAuthToken(): string | null {
|
||||
if (_token) return _token;
|
||||
if (typeof window !== 'undefined') {
|
||||
_token = sessionStorage.getItem('klg_token');
|
||||
}
|
||||
// Fallback to dev token
|
||||
return _token || process.env.NEXT_PUBLIC_DEV_TOKEN || null;
|
||||
}
|
||||
|
||||
export function clearAuthToken() {
|
||||
_token = null;
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.removeItem('klg_token');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Base fetch ──────────────────────────────────
|
||||
@ -44,6 +35,26 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// Тип ВС (обратная совместимость с импортом из @/lib/api)
|
||||
export interface Aircraft {
|
||||
id: string;
|
||||
registrationNumber: string;
|
||||
serialNumber: string;
|
||||
aircraftType: string;
|
||||
model: string;
|
||||
operator: string;
|
||||
status: string;
|
||||
flightHours?: number;
|
||||
manufacturer?: string;
|
||||
yearOfManufacture?: number;
|
||||
maxTakeoffWeight?: number;
|
||||
engineType?: string;
|
||||
lastInspectionDate?: string;
|
||||
nextInspectionDate?: string;
|
||||
certificateExpiry?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** Unified fetch with auth and 401 handling. Use for any backend path (e.g. /stats, /airworthiness-core/directives). */
|
||||
export async function apiFetch<T = any>(path: string, opts: RequestInit = {}): Promise<T> {
|
||||
const token = getAuthToken();
|
||||
|
||||
@ -28,7 +28,19 @@ export function middleware(request: NextRequest) {
|
||||
response.headers.set('X-Frame-Options', 'DENY');
|
||||
response.headers.set('X-XSS-Protection', '1; mode=block');
|
||||
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
const csp = "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.openai.com";
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const scriptSrc = isDev ? "script-src 'self' 'unsafe-eval'" : "script-src 'self'";
|
||||
const csp = [
|
||||
"default-src 'self'",
|
||||
scriptSrc,
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: https:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
].join('; ');
|
||||
response.headers.set('Content-Security-Policy', csp);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user