fix telegram

This commit is contained in:
Claude Agent
2026-02-23 15:12:33 +00:00
parent 6c78fec8a7
commit 8bc567a9c5
426 changed files with 112478 additions and 1 deletions

View 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",
]

View 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

View 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

View 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