Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot
Modern ERP Reports Application with microservices architecture Tech Stack: - Backend: FastAPI + python-oracledb (Oracle DB integration) - Frontend: Vue.js 3 + PrimeVue + Vite - Telegram Bot: python-telegram-bot + SQLite - Infrastructure: Shared database pool, JWT authentication, SSH tunnel Features: - FastAPI backend with async Oracle connection pool - Vue.js 3 responsive frontend with PrimeVue components - Telegram bot alternative interface - Microservices architecture with shared components - Complete deployment support (Linux Docker + Windows IIS) - Comprehensive testing (Playwright E2E + pytest) Repository Structure: - reports-app/ - Main application (backend, frontend, telegram-bot) - shared/ - Shared components (database pool, auth, utils) - deployment/ - Deployment scripts (Linux & Windows) - docs/ - Project documentation - security/ - Security scanning and git hooks
This commit is contained in:
0
shared/__init__.py
Normal file
0
shared/__init__.py
Normal file
124
shared/database/README.md
Normal file
124
shared/database/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# ROA2WEB Shared Database Pool
|
||||
|
||||
Sistem de pool de conexiuni Oracle partajat între toate microserviciile ROA2WEB.
|
||||
|
||||
## Componente
|
||||
|
||||
### 📦 oracle_pool.py
|
||||
Clasa singleton `OraclePool` pentru gestionarea pool-ului de conexiuni Oracle.
|
||||
|
||||
### 📋 models.py
|
||||
Modele Pydantic comune:
|
||||
- `User` - Model pentru utilizatori
|
||||
- `Company` - Model pentru firme/scheme Oracle
|
||||
- `DatabaseConfig` - Configurare conexiune database
|
||||
|
||||
### ⚙️ config.py (în utils/)
|
||||
Configurări partajate prin environment variables.
|
||||
|
||||
### ❌ exceptions.py (în utils/)
|
||||
Exception handlers personalizate pentru ROA2WEB.
|
||||
|
||||
## Utilizare
|
||||
|
||||
### Inițializare în aplicații FastAPI
|
||||
|
||||
```python
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Import shared pool
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../shared'))
|
||||
from database.oracle_pool import oracle_pool
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup - inițializare pool
|
||||
await oracle_pool.initialize()
|
||||
print("📊 Oracle pool initialized")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown - închidere pool
|
||||
await oracle_pool.close_pool()
|
||||
print("📊 Oracle pool closed")
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
```
|
||||
|
||||
### Utilizare conexiune în endpoint-uri
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from database.oracle_pool import oracle_pool
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/companies")
|
||||
async def get_companies():
|
||||
try:
|
||||
async with oracle_pool.get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("SELECT schema, firma FROM vdef_util_grup WHERE id_firma <> 0")
|
||||
results = cursor.fetchall()
|
||||
|
||||
companies = []
|
||||
for row in results:
|
||||
companies.append({
|
||||
"code": row[0],
|
||||
"name": row[1]
|
||||
})
|
||||
|
||||
return companies
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||
```
|
||||
|
||||
### Configurare Environment Variables
|
||||
|
||||
```bash
|
||||
# Oracle Database
|
||||
ORACLE_USER=your_oracle_username
|
||||
ORACLE_PASSWORD=your_oracle_password
|
||||
ORACLE_DSN=your_oracle_dsn
|
||||
|
||||
# Pool Settings
|
||||
DB_MIN_CONNECTIONS=2
|
||||
DB_MAX_CONNECTIONS=10
|
||||
DB_CONNECTION_INCREMENT=1
|
||||
|
||||
# JWT (pentru autentificare)
|
||||
JWT_SECRET_KEY=your-super-secret-key
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
```
|
||||
|
||||
## Testare
|
||||
|
||||
Pentru a testa pool-ul de conexiuni:
|
||||
|
||||
```bash
|
||||
cd roa2web/shared/database
|
||||
python test_pool.py
|
||||
```
|
||||
|
||||
**Notă**: Testul necesită configurarea variabilelor de environment pentru Oracle.
|
||||
|
||||
## Caracteristici
|
||||
|
||||
✅ **Singleton Pattern** - O singură instanță de pool pentru toată aplicația
|
||||
✅ **Async Context Manager** - Gestionare automată a conexiunilor
|
||||
✅ **Connection Pooling** - Performanță optimizată prin reutilizarea conexiunilor
|
||||
✅ **Configurabil** - Setări flexibile prin environment variables
|
||||
✅ **Logging** - Urmărirea operațiilor de pool
|
||||
✅ **Error Handling** - Excepții personalizate pentru debugging
|
||||
|
||||
## Următorii Pași
|
||||
|
||||
👉 **ZIUA 3**: Implementarea sistemului JWT partajat (`shared/auth/`)
|
||||
|
||||
---
|
||||
|
||||
*Documentație generată pentru ROA2WEB Shared Database Pool - ZIUA 2* 🚀
|
||||
0
shared/database/__init__.py
Normal file
0
shared/database/__init__.py
Normal file
30
shared/database/models.py
Normal file
30
shared/database/models.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Modele comune pentru toate aplicațiile ROA2WEB
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict
|
||||
from datetime import datetime
|
||||
|
||||
class Company(BaseModel):
|
||||
"""Model pentru firma/schema Oracle"""
|
||||
code: str = Field(description="Codul firmei (schema Oracle)")
|
||||
name: str = Field(description="Numele firmei")
|
||||
fiscal_code: Optional[str] = Field(description="Codul fiscal")
|
||||
is_active: bool = Field(default=True, description="Firma activă")
|
||||
|
||||
class User(BaseModel):
|
||||
"""Model pentru utilizator"""
|
||||
username: str = Field(description="Numele utilizatorului")
|
||||
email: Optional[str] = Field(description="Email utilizator")
|
||||
companies: List[str] = Field(description="Lista codurilor firmelor la care are acces")
|
||||
is_active: bool = Field(default=True, description="Utilizator activ")
|
||||
last_login: Optional[datetime] = Field(description="Ultima autentificare")
|
||||
|
||||
class DatabaseConfig(BaseModel):
|
||||
"""Configurare conexiune bază de date"""
|
||||
user: str
|
||||
password: str
|
||||
dsn: str
|
||||
min_connections: int = 2
|
||||
max_connections: int = 10
|
||||
increment: int = 1
|
||||
119
shared/database/oracle_pool.py
Normal file
119
shared/database/oracle_pool.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Oracle Database Connection Pool - Shared între toate aplicațiile ROA2WEB
|
||||
Folosește oracledb cu connection pooling pentru performance optimă
|
||||
"""
|
||||
import oracledb
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OraclePool:
|
||||
"""
|
||||
Singleton class pentru Oracle connection pool
|
||||
Partajat între toate microservicele ROA2WEB
|
||||
"""
|
||||
_instance: Optional['OraclePool'] = None
|
||||
_pool: Optional[oracledb.ConnectionPool] = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(OraclePool, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
async def initialize(self, **config):
|
||||
"""Inițializează pool-ul de conexiuni"""
|
||||
if self._pool is None:
|
||||
# Check if we have DSN or individual parameters
|
||||
dsn = config.get('dsn', os.getenv('ORACLE_DSN'))
|
||||
if dsn:
|
||||
# Use DSN connection
|
||||
self._pool = oracledb.create_pool(
|
||||
user=config.get('user', os.getenv('ORACLE_USER')),
|
||||
password=config.get('password', os.getenv('ORACLE_PASSWORD')),
|
||||
dsn=dsn,
|
||||
min=config.get('min_connections', 2),
|
||||
max=config.get('max_connections', 10),
|
||||
increment=config.get('increment', 1),
|
||||
getmode=oracledb.POOL_GETMODE_WAIT
|
||||
)
|
||||
else:
|
||||
# Use individual parameters (host, port, sid)
|
||||
self._pool = oracledb.create_pool(
|
||||
user=config.get('user', os.getenv('ORACLE_USER')),
|
||||
password=config.get('password', os.getenv('ORACLE_PASSWORD')),
|
||||
host=config.get('host', os.getenv('ORACLE_HOST', 'localhost')),
|
||||
port=config.get('port', int(os.getenv('ORACLE_PORT', '1526'))),
|
||||
sid=config.get('sid', os.getenv('ORACLE_SID', 'ROA')),
|
||||
min=config.get('min_connections', 2),
|
||||
max=config.get('max_connections', 10),
|
||||
increment=config.get('increment', 1),
|
||||
getmode=oracledb.POOL_GETMODE_WAIT
|
||||
)
|
||||
logger.info(f"Oracle pool created with {self._pool.opened} connections")
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_connection(self):
|
||||
"""Context manager pentru obținerea unei conexiuni din pool"""
|
||||
if self._pool is None:
|
||||
raise RuntimeError("Pool not initialized. Call initialize() first.")
|
||||
|
||||
connection = None
|
||||
try:
|
||||
connection = self._pool.acquire()
|
||||
logger.debug("Connection acquired from pool")
|
||||
yield connection
|
||||
finally:
|
||||
if connection is not None:
|
||||
connection.close()
|
||||
logger.debug("Connection returned to pool")
|
||||
|
||||
|
||||
async def execute_query(self, query: str, parameters=None):
|
||||
"""
|
||||
Execute a SQL query and return all results
|
||||
Based on official Oracle python-oracledb patterns
|
||||
"""
|
||||
if self._pool is None:
|
||||
raise RuntimeError("Pool not initialized. Call initialize() first.")
|
||||
|
||||
connection = None
|
||||
try:
|
||||
connection = self._pool.acquire()
|
||||
logger.debug(f"Executing query: {query[:100]}...")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
if parameters:
|
||||
cursor.execute(query, parameters)
|
||||
else:
|
||||
cursor.execute(query)
|
||||
|
||||
# Check if this is a SELECT statement
|
||||
if query.strip().upper().startswith('SELECT') or query.strip().upper().startswith('WITH'):
|
||||
return cursor.fetchall()
|
||||
else:
|
||||
# For DML statements, return affected row count
|
||||
connection.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
except Exception as e:
|
||||
if connection:
|
||||
connection.rollback()
|
||||
logger.error(f"Query execution failed: {str(e)}")
|
||||
raise
|
||||
finally:
|
||||
if connection is not None:
|
||||
connection.close()
|
||||
logger.debug("Connection returned to pool")
|
||||
|
||||
async def close_pool(self):
|
||||
"""Închide pool-ul de conexiuni"""
|
||||
if self._pool is not None:
|
||||
self._pool.close()
|
||||
self._pool = None
|
||||
logger.info("Oracle pool closed")
|
||||
|
||||
# Instance globală pentru folosire în toate aplicațiile
|
||||
oracle_pool = OraclePool()
|
||||
0
shared/utils/__init__.py
Normal file
0
shared/utils/__init__.py
Normal file
39
shared/utils/config.py
Normal file
39
shared/utils/config.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Configurări comune pentru toate aplicațiile ROA2WEB
|
||||
"""
|
||||
import os
|
||||
from typing import List, Dict, Any
|
||||
from pydantic import BaseSettings
|
||||
|
||||
class SharedConfig(BaseSettings):
|
||||
"""Configurări partajate între microservicii"""
|
||||
|
||||
# Database
|
||||
oracle_user: str = os.getenv('ORACLE_USER', '')
|
||||
oracle_password: str = os.getenv('ORACLE_PASSWORD', '')
|
||||
oracle_dsn: str = os.getenv('ORACLE_DSN', '')
|
||||
|
||||
# Database Pool
|
||||
db_min_connections: int = int(os.getenv('DB_MIN_CONNECTIONS', 2))
|
||||
db_max_connections: int = int(os.getenv('DB_MAX_CONNECTIONS', 10))
|
||||
db_connection_increment: int = int(os.getenv('DB_CONNECTION_INCREMENT', 1))
|
||||
|
||||
# JWT Authentication
|
||||
jwt_secret_key: str = os.getenv('JWT_SECRET_KEY', 'your-super-secret-jwt-key-change-in-production')
|
||||
jwt_algorithm: str = os.getenv('JWT_ALGORITHM', 'HS256')
|
||||
access_token_expire_minutes: int = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES', 30))
|
||||
refresh_token_expire_days: int = int(os.getenv('REFRESH_TOKEN_EXPIRE_DAYS', 7))
|
||||
|
||||
# Authentication Settings
|
||||
auth_cache_ttl_minutes: int = int(os.getenv('AUTH_CACHE_TTL_MINUTES', 15))
|
||||
rate_limit_max_requests: int = int(os.getenv('RATE_LIMIT_MAX_REQUESTS', 5))
|
||||
rate_limit_time_window: int = int(os.getenv('RATE_LIMIT_TIME_WINDOW', 300))
|
||||
|
||||
# Logging
|
||||
log_level: str = os.getenv('LOG_LEVEL', 'INFO')
|
||||
|
||||
class Config:
|
||||
env_file = '.env'
|
||||
|
||||
# Instance globală
|
||||
shared_config = SharedConfig()
|
||||
27
shared/utils/exceptions.py
Normal file
27
shared/utils/exceptions.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Exception handlers comune pentru ROA2WEB
|
||||
"""
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
class ROAException(Exception):
|
||||
"""Exception de bază pentru aplicațiile ROA"""
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
self.message = message
|
||||
self.details = details or {}
|
||||
super().__init__(self.message)
|
||||
|
||||
class DatabaseException(ROAException):
|
||||
"""Excepții legate de baza de date"""
|
||||
pass
|
||||
|
||||
class AuthenticationException(ROAException):
|
||||
"""Excepții legate de autentificare"""
|
||||
pass
|
||||
|
||||
class AuthorizationException(ROAException):
|
||||
"""Excepții legate de autorizare"""
|
||||
pass
|
||||
|
||||
class ValidationException(ROAException):
|
||||
"""Excepții legate de validare date"""
|
||||
pass
|
||||
Reference in New Issue
Block a user