fix telegram
This commit is contained in:
21
deploy-package-20260223-151231/shared/routes/__init__.py
Normal file
21
deploy-package-20260223-151231/shared/routes/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Shared Routes for ROA2WEB Applications
|
||||
|
||||
This module provides factory functions for creating common API routers
|
||||
that can be mounted in both the unified monolith backend.
|
||||
|
||||
Usage:
|
||||
from shared.routes import create_companies_router, create_calendar_router
|
||||
|
||||
# In main.py
|
||||
companies_router = create_companies_router(oracle_pool)
|
||||
app.include_router(companies_router, prefix="/api/companies")
|
||||
"""
|
||||
|
||||
from .companies import create_companies_router
|
||||
from .calendar import create_calendar_router
|
||||
|
||||
__all__ = [
|
||||
"create_companies_router",
|
||||
"create_calendar_router",
|
||||
]
|
||||
151
deploy-package-20260223-151231/shared/routes/calendar.py
Normal file
151
deploy-package-20260223-151231/shared/routes/calendar.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
Shared Calendar Router Factory for ROA2WEB Applications
|
||||
|
||||
Creates a FastAPI router for /api/calendar endpoints that can be used
|
||||
by both the unified monolith backend.
|
||||
|
||||
Usage:
|
||||
from shared.routes.calendar import create_calendar_router
|
||||
|
||||
calendar_router = create_calendar_router(oracle_pool, cache_decorator=cached)
|
||||
app.include_router(calendar_router, prefix="/api/calendar")
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Callable, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
|
||||
from auth.dependencies import get_current_user
|
||||
from auth.models import CurrentUser
|
||||
from models.calendar import CalendarPeriod, CalendarPeriodsResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Romanian month names
|
||||
MONTH_NAMES_RO = [
|
||||
"Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie",
|
||||
"Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie"
|
||||
]
|
||||
|
||||
|
||||
def create_calendar_router(
|
||||
oracle_pool,
|
||||
cache_decorator: Optional[Callable] = None,
|
||||
tags: Optional[List[str]] = None
|
||||
) -> APIRouter:
|
||||
"""
|
||||
Factory function to create a calendar router.
|
||||
|
||||
Args:
|
||||
oracle_pool: The Oracle connection pool instance
|
||||
cache_decorator: Optional caching decorator (e.g., @cached)
|
||||
tags: OpenAPI tags for the router
|
||||
|
||||
Returns:
|
||||
Configured FastAPI router for calendar endpoints
|
||||
"""
|
||||
router = APIRouter(
|
||||
redirect_slashes=False,
|
||||
tags=tags or ["calendar"]
|
||||
)
|
||||
|
||||
# Helper to get schema for company
|
||||
async def _get_schema_for_company(company_id: int, server_id: Optional[str] = None) -> Optional[str]:
|
||||
"""Get Oracle schema for company ID.
|
||||
|
||||
Args:
|
||||
company_id: The company ID to get schema for
|
||||
server_id: The Oracle server ID (for multi-server mode)
|
||||
"""
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT SCHEMA FROM CONTAFIN_ORACLE.V_NOM_FIRME
|
||||
WHERE ID_FIRMA = :company_id
|
||||
""", {'company_id': company_id})
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
|
||||
# Apply cache to schema lookup if decorator provided
|
||||
# Include server_id in cache key for multi-server mode
|
||||
if cache_decorator:
|
||||
_get_schema_for_company = cache_decorator(
|
||||
cache_type='schema',
|
||||
key_params=['company_id', 'server_id']
|
||||
)(_get_schema_for_company)
|
||||
|
||||
# Helper to get periods - can be cached
|
||||
async def _get_available_periods(company_id: int, server_id: Optional[str] = None) -> CalendarPeriodsResponse:
|
||||
"""Get available accounting periods for a company.
|
||||
|
||||
Args:
|
||||
company_id: The company ID to get periods for
|
||||
server_id: The Oracle server ID (for multi-server mode)
|
||||
"""
|
||||
schema = await _get_schema_for_company(company_id, server_id)
|
||||
if not schema:
|
||||
logger.warning(f"Schema not found for company {company_id}")
|
||||
return CalendarPeriodsResponse(periods=[], current_period=None, total_count=0)
|
||||
|
||||
try:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"""
|
||||
SELECT ANUL, LUNA
|
||||
FROM {schema}.CALENDAR
|
||||
ORDER BY ANUL DESC, LUNA DESC
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
periods = []
|
||||
for row in rows:
|
||||
an, luna = row[0], row[1]
|
||||
month_name = MONTH_NAMES_RO[luna - 1]
|
||||
periods.append(CalendarPeriod(
|
||||
an=an,
|
||||
luna=luna,
|
||||
display_name=f"{month_name} {an}"
|
||||
))
|
||||
|
||||
current_period = periods[0] if periods else None
|
||||
|
||||
logger.info(f"Loaded {len(periods)} periods for company {company_id}")
|
||||
|
||||
return CalendarPeriodsResponse(
|
||||
periods=periods,
|
||||
current_period=current_period,
|
||||
total_count=len(periods)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching periods for company {company_id}: {e}")
|
||||
return CalendarPeriodsResponse(periods=[], current_period=None, total_count=0)
|
||||
|
||||
# Apply cache decorator if provided
|
||||
# Include server_id in cache key for multi-server mode
|
||||
if cache_decorator:
|
||||
_get_available_periods = cache_decorator(
|
||||
cache_type='calendar_periods',
|
||||
key_params=['company_id', 'server_id']
|
||||
)(_get_available_periods)
|
||||
|
||||
@router.get("/periods", response_model=CalendarPeriodsResponse)
|
||||
async def get_calendar_periods(
|
||||
request: Request,
|
||||
company: int = Query(..., description="Company ID"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> CalendarPeriodsResponse:
|
||||
"""
|
||||
Get available accounting periods for a company.
|
||||
Returns periods ordered by year DESC, month DESC with Romanian month names.
|
||||
"""
|
||||
# Validate company access
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(403, f"Nu aveți acces la firma {company}")
|
||||
|
||||
# Get server_id from request state (injected by auth middleware from JWT)
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
return await _get_available_periods(company, server_id)
|
||||
|
||||
return router
|
||||
185
deploy-package-20260223-151231/shared/routes/companies.py
Normal file
185
deploy-package-20260223-151231/shared/routes/companies.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Shared Companies Router Factory for ROA2WEB Applications
|
||||
|
||||
Creates a FastAPI router for /api/companies endpoints that can be used
|
||||
by both the unified monolith backend.
|
||||
|
||||
Usage:
|
||||
from shared.routes.companies import create_companies_router
|
||||
|
||||
companies_router = create_companies_router(oracle_pool, cache_decorator=cached)
|
||||
app.include_router(companies_router, prefix="/api/companies")
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Callable, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from auth.dependencies import get_current_user
|
||||
from auth.models import CurrentUser
|
||||
from models.company import Company, CompanyListResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_companies_router(
|
||||
oracle_pool,
|
||||
cache_decorator: Optional[Callable] = None,
|
||||
tags: Optional[List[str]] = None
|
||||
) -> APIRouter:
|
||||
"""
|
||||
Factory function to create a companies router.
|
||||
|
||||
Args:
|
||||
oracle_pool: The Oracle connection pool instance
|
||||
cache_decorator: Optional caching decorator (e.g., @cached)
|
||||
tags: OpenAPI tags for the router
|
||||
|
||||
Returns:
|
||||
Configured FastAPI router for company endpoints
|
||||
"""
|
||||
router = APIRouter(
|
||||
redirect_slashes=False,
|
||||
tags=tags or ["companies"]
|
||||
)
|
||||
|
||||
# Helper function to get companies - can be cached
|
||||
async def _get_user_companies_data(username: str, server_id: Optional[str] = None) -> List[Company]:
|
||||
"""
|
||||
Get list of companies for a user from Oracle.
|
||||
|
||||
Args:
|
||||
username: The username to get companies for
|
||||
server_id: The Oracle server ID (for multi-server mode)
|
||||
"""
|
||||
companies = []
|
||||
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
# Get user ID
|
||||
cursor.execute("""
|
||||
SELECT ID_UTIL, UTILIZATOR
|
||||
FROM UTILIZATORI
|
||||
WHERE UPPER(UTILIZATOR) = :username
|
||||
""", {'username': username.upper()})
|
||||
|
||||
user_row = cursor.fetchone()
|
||||
if not user_row:
|
||||
logger.warning(f"User {username} not found in UTILIZATORI")
|
||||
return []
|
||||
|
||||
user_id = user_row[0]
|
||||
|
||||
# Get companies for user (program 2 = data entry/reports)
|
||||
cursor.execute("""
|
||||
SELECT A.ID_FIRMA, A.FIRMA, A.SCHEMA, A.COD_FISCAL
|
||||
FROM V_NOM_FIRME A
|
||||
WHERE A.ID_FIRMA IN (
|
||||
SELECT ID_FIRMA
|
||||
FROM VDEF_UTIL_FIRME
|
||||
WHERE ID_PROGRAM = 2 AND ID_UTIL = :user_id
|
||||
)
|
||||
ORDER BY A.FIRMA
|
||||
""", {'user_id': user_id})
|
||||
|
||||
for row in cursor.fetchall():
|
||||
companies.append(Company(
|
||||
id_firma=row[0],
|
||||
name=row[1],
|
||||
schema_name=row[2],
|
||||
fiscal_code=row[3],
|
||||
is_active=True
|
||||
))
|
||||
|
||||
logger.info(f"Found {len(companies)} companies for user {username}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching companies: {e}")
|
||||
|
||||
return companies
|
||||
|
||||
# Apply cache decorator if provided
|
||||
# Include server_id in cache key for multi-server mode
|
||||
if cache_decorator:
|
||||
_get_user_companies_data = cache_decorator(
|
||||
cache_type='companies',
|
||||
key_params=['username', 'server_id']
|
||||
)(_get_user_companies_data)
|
||||
|
||||
@router.get("", response_model=CompanyListResponse)
|
||||
@router.get("/", response_model=CompanyListResponse)
|
||||
async def get_user_companies(
|
||||
request: Request,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""Get list of companies the user has access to."""
|
||||
try:
|
||||
# Get server_id from request state (injected by auth middleware from JWT)
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
companies = await _get_user_companies_data(current_user.username, server_id)
|
||||
|
||||
return CompanyListResponse(
|
||||
companies=companies,
|
||||
total_count=len(companies)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_user_companies: {e}")
|
||||
raise HTTPException(500, f"Eroare la obținerea listei de firme: {str(e)}")
|
||||
|
||||
@router.get("/{company_id}", response_model=Company)
|
||||
async def get_company_details(
|
||||
company_id: str,
|
||||
request: Request,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""Get details of a specific company."""
|
||||
# Validate access
|
||||
if company_id not in current_user.companies:
|
||||
raise HTTPException(403, f"Nu aveți acces la firma {company_id}")
|
||||
|
||||
try:
|
||||
# Get server_id from request state (injected by auth middleware from JWT)
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT ID_FIRMA, FIRMA, SCHEMA, COD_FISCAL
|
||||
FROM V_NOM_FIRME
|
||||
WHERE ID_FIRMA = :company_id
|
||||
""", {'company_id': int(company_id)})
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
raise HTTPException(404, f"Firma {company_id} nu a fost găsită")
|
||||
|
||||
return Company(
|
||||
id_firma=row[0],
|
||||
name=row[1],
|
||||
schema_name=row[2],
|
||||
fiscal_code=row[3] or "",
|
||||
is_active=True
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Eroare la obținerea detaliilor firmei: {str(e)}")
|
||||
|
||||
@router.get("/{company_id}/validate")
|
||||
async def validate_company_access(
|
||||
company_id: str,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""Validate if user has access to a company."""
|
||||
has_access = company_id in current_user.companies
|
||||
|
||||
return {
|
||||
"company_id": company_id,
|
||||
"has_access": has_access,
|
||||
"user": current_user.username,
|
||||
"message": "Acces validat" if has_access else "Acces refuzat"
|
||||
}
|
||||
|
||||
return router
|
||||
192
deploy-package-20260223-151231/shared/routes/system.py
Normal file
192
deploy-package-20260223-151231/shared/routes/system.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
System routes for server monitoring and logs.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from collections import deque
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from shared.auth.dependencies import get_current_user, CurrentUser
|
||||
|
||||
|
||||
class AuthModeResponse(BaseModel):
|
||||
"""Response for auth mode endpoint."""
|
||||
mode: str # "single-server" or "multi-server"
|
||||
supports_email_login: bool # True if email-based login is available
|
||||
|
||||
|
||||
class LogEntry(BaseModel):
|
||||
"""Single log entry."""
|
||||
line: str
|
||||
level: Optional[str] = None
|
||||
|
||||
|
||||
class LogsResponse(BaseModel):
|
||||
"""Response with log entries."""
|
||||
file: str
|
||||
lines: list[str]
|
||||
total_lines: int
|
||||
showing: int
|
||||
logs_path: Optional[str] = None
|
||||
file_exists: bool = True
|
||||
file_size_kb: Optional[float] = None
|
||||
|
||||
|
||||
def create_system_router() -> APIRouter:
|
||||
"""
|
||||
Create system router for logs and monitoring.
|
||||
"""
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/auth-mode", response_model=AuthModeResponse)
|
||||
async def get_auth_mode():
|
||||
"""
|
||||
Get the authentication mode configuration.
|
||||
|
||||
This is a PUBLIC endpoint (no auth required) that tells the frontend
|
||||
whether to use the email-based multi-server login flow or the classic
|
||||
username/password single-server flow.
|
||||
|
||||
Returns:
|
||||
- mode: "single-server" for legacy config, "multi-server" for ORACLE_SERVERS
|
||||
- supports_email_login: True only in multi-server mode with email cache
|
||||
"""
|
||||
from backend.config import settings
|
||||
|
||||
servers = settings.get_oracle_servers()
|
||||
|
||||
# Multi-server mode: ANY servers configured via ORACLE_SERVERS
|
||||
# Shows server dropdown even with 1 server (explicit server selection)
|
||||
if servers and len(servers) >= 1:
|
||||
return AuthModeResponse(
|
||||
mode="multi-server",
|
||||
supports_email_login=len(servers) > 1 # Email lookup only for 2+ servers
|
||||
)
|
||||
|
||||
# Single-server mode: legacy config (no ORACLE_SERVERS, uses env vars)
|
||||
return AuthModeResponse(
|
||||
mode="single-server",
|
||||
supports_email_login=False
|
||||
)
|
||||
|
||||
def get_logs_path() -> Path:
|
||||
"""Get logs directory path based on environment."""
|
||||
# Windows production: C:\inetpub\wwwroot\roa2web\logs
|
||||
# Development: backend/logs or ./logs
|
||||
if os.name == 'nt': # Windows
|
||||
prod_path = Path(r"C:\inetpub\wwwroot\roa2web\logs")
|
||||
if prod_path.exists():
|
||||
return prod_path
|
||||
|
||||
# Development fallback
|
||||
dev_paths = [
|
||||
Path(__file__).parent.parent.parent / "backend" / "logs",
|
||||
Path(__file__).parent.parent.parent / "logs",
|
||||
Path("./logs"),
|
||||
]
|
||||
for path in dev_paths:
|
||||
if path.exists():
|
||||
return path
|
||||
|
||||
return Path("./logs")
|
||||
|
||||
@router.get("/logs", response_model=LogsResponse)
|
||||
async def get_logs(
|
||||
file: str = Query(default="backend-stderr", description="Log file: backend-stderr or backend-stdout"),
|
||||
lines: int = Query(default=100, ge=10, le=1000, description="Number of lines to return"),
|
||||
filter: Optional[str] = Query(default=None, description="Filter text (case-insensitive)"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get server log entries.
|
||||
|
||||
Args:
|
||||
file: Log file name (backend-stderr or backend-stdout)
|
||||
lines: Number of lines to return (10-1000)
|
||||
filter: Optional filter text
|
||||
|
||||
Returns:
|
||||
LogsResponse with log lines
|
||||
"""
|
||||
# Validate file name to prevent path traversal
|
||||
allowed_files = ["backend-stderr", "backend-stdout"]
|
||||
if file not in allowed_files:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid file. Allowed: {allowed_files}")
|
||||
|
||||
logs_path = get_logs_path()
|
||||
log_file = logs_path / f"{file}.log"
|
||||
logs_path_str = str(logs_path.resolve())
|
||||
|
||||
if not log_file.exists():
|
||||
return LogsResponse(
|
||||
file=file,
|
||||
lines=[f"Log file not found: {log_file}"],
|
||||
total_lines=0,
|
||||
showing=0,
|
||||
logs_path=logs_path_str,
|
||||
file_exists=False,
|
||||
file_size_kb=0
|
||||
)
|
||||
|
||||
try:
|
||||
# Get file size
|
||||
file_size_kb = round(log_file.stat().st_size / 1024, 2)
|
||||
|
||||
# Read file and get last N lines efficiently
|
||||
with open(log_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||
# Use deque for efficient tail operation
|
||||
all_lines = deque(f, maxlen=lines * 2 if filter else lines)
|
||||
|
||||
# Apply filter if provided
|
||||
if filter:
|
||||
filter_lower = filter.lower()
|
||||
filtered_lines = [line.rstrip() for line in all_lines if filter_lower in line.lower()]
|
||||
result_lines = list(filtered_lines)[-lines:]
|
||||
else:
|
||||
result_lines = [line.rstrip() for line in all_lines][-lines:]
|
||||
|
||||
return LogsResponse(
|
||||
file=file,
|
||||
lines=result_lines,
|
||||
total_lines=len(result_lines),
|
||||
showing=len(result_lines),
|
||||
logs_path=logs_path_str,
|
||||
file_exists=True,
|
||||
file_size_kb=file_size_kb
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error reading logs: {str(e)}")
|
||||
|
||||
@router.get("/logs/available")
|
||||
async def get_available_logs(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get list of available log files.
|
||||
"""
|
||||
logs_path = get_logs_path()
|
||||
|
||||
if not logs_path.exists():
|
||||
return {"logs_path": str(logs_path), "files": [], "exists": False}
|
||||
|
||||
log_files = []
|
||||
for f in logs_path.glob("*.log"):
|
||||
stat = f.stat()
|
||||
log_files.append({
|
||||
"name": f.stem,
|
||||
"size_kb": round(stat.st_size / 1024, 1),
|
||||
"modified": stat.st_mtime
|
||||
})
|
||||
|
||||
return {
|
||||
"logs_path": str(logs_path),
|
||||
"files": sorted(log_files, key=lambda x: x["name"]),
|
||||
"exists": True
|
||||
}
|
||||
|
||||
return router
|
||||
Reference in New Issue
Block a user