docs
This commit is contained in:
484
data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_UNITAR.md
Normal file
484
data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_UNITAR.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# Plan: Sincronizare Nomenclatoare Oracle + Auth SSO + OCR Furnizori
|
||||
|
||||
## Obiective
|
||||
1. **Sincronizare nomenclatoare din Oracle în SQLite** (furnizori, casa/banca)
|
||||
2. **Auth pentru data-entry-app** cu SSO (frontend-uri separate pe path)
|
||||
3. **OCR: căutare furnizor după CUI** + creare locală dacă nu există
|
||||
4. **Deploy Windows IIS** cu path routing
|
||||
|
||||
---
|
||||
|
||||
## Arhitectura Aleasă
|
||||
|
||||
```
|
||||
roa2web.romfast.ro (IIS + ARR)
|
||||
│
|
||||
├── /reports/ → reports-app/frontend/
|
||||
├── /data/ → data-entry-app/frontend/
|
||||
│
|
||||
├── /api/reports/* → reports-backend:8001
|
||||
├── /api/data/* → data-entry-backend:8003
|
||||
└── /api/auth/* → reports-backend (auth provider)
|
||||
```
|
||||
|
||||
**URL-uri compacte:**
|
||||
- `roa2web.romfast.ro/reports/` - Rapoarte
|
||||
- `roa2web.romfast.ro/data/` - Introducere date (bonuri fiscale)
|
||||
- `roa2web.romfast.ro/api/reports/` - API rapoarte
|
||||
- `roa2web.romfast.ro/api/data/` - API introducere date
|
||||
|
||||
**SSO**: Același domeniu = localStorage partajat = token JWT valid pentru ambele
|
||||
|
||||
---
|
||||
|
||||
## Faza 1: Auth pentru Data-Entry-App
|
||||
|
||||
### 1.1 Backend - Integrare shared/auth/
|
||||
|
||||
**Fișiere de modificat:**
|
||||
- `data-entry-app/backend/app/main.py`
|
||||
- `data-entry-app/backend/app/routers/receipts.py`
|
||||
- `data-entry-app/backend/app/core/config.py`
|
||||
|
||||
**Acțiuni:**
|
||||
```python
|
||||
# main.py - Adăugare middleware
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent / "shared"))
|
||||
|
||||
from auth.middleware import AuthenticationMiddleware
|
||||
from auth.dependencies import get_current_user
|
||||
|
||||
app.add_middleware(
|
||||
AuthenticationMiddleware,
|
||||
excluded_paths=["/docs", "/redoc", "/openapi.json", "/health", "/"]
|
||||
)
|
||||
```
|
||||
|
||||
```python
|
||||
# receipts.py - Înlocuire placeholder
|
||||
from auth.dependencies import get_current_user
|
||||
from auth.models import CurrentUser
|
||||
|
||||
@router.get("/")
|
||||
async def list_receipts(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
# folosește current_user.username
|
||||
```
|
||||
|
||||
### 1.2 Frontend - Auth Store + Login Page
|
||||
|
||||
**Fișiere de creat/copiat din reports-app:**
|
||||
- `data-entry-app/frontend/src/stores/auth.js` (copiat)
|
||||
- `data-entry-app/frontend/src/views/LoginView.vue` (copiat)
|
||||
- `data-entry-app/frontend/src/router/index.js` (adăugat guard)
|
||||
- `data-entry-app/frontend/src/services/api.js` (axios interceptor)
|
||||
|
||||
**Decizie SSO:**
|
||||
- Frontend data-entry folosește `/api/auth/login` de pe reports-backend
|
||||
- Sau: redirect la `/login` (reports-app) care setează token în localStorage
|
||||
- Token valid pentru ambele (același JWT_SECRET_KEY)
|
||||
|
||||
---
|
||||
|
||||
## Faza 2: Sincronizare Nomenclatoare Oracle → SQLite
|
||||
|
||||
### 2.1 Noi Modele SQLModel
|
||||
|
||||
**Fișier:** `data-entry-app/backend/app/db/models/nomenclature.py`
|
||||
|
||||
```python
|
||||
class SyncedSupplier(SQLModel, table=True):
|
||||
"""Furnizori sincronizați din Oracle"""
|
||||
__tablename__ = "synced_suppliers"
|
||||
|
||||
id: int = Field(primary_key=True) # ID din Oracle (ID_PART)
|
||||
company_id: int = Field(index=True)
|
||||
name: str = Field(max_length=200) # DEN_PART
|
||||
fiscal_code: Optional[str] = Field(max_length=20, index=True) # COD_FISCAL
|
||||
address: Optional[str] = Field(max_length=500)
|
||||
synced_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class LocalSupplier(SQLModel, table=True):
|
||||
"""Furnizori creați local din OCR (neexistenți în Oracle)"""
|
||||
__tablename__ = "local_suppliers"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
company_id: int = Field(index=True)
|
||||
name: str = Field(max_length=200)
|
||||
fiscal_code: str = Field(max_length=20, unique=True, index=True)
|
||||
address: Optional[str] = Field(max_length=500)
|
||||
created_by: str = Field(max_length=100)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
oracle_synced: bool = Field(default=False) # True când e creat în Oracle
|
||||
|
||||
class SyncedCashRegister(SQLModel, table=True):
|
||||
"""Case/Bănci sincronizate din Oracle"""
|
||||
__tablename__ = "synced_cash_registers"
|
||||
|
||||
id: int = Field(primary_key=True) # ID din Oracle
|
||||
company_id: int = Field(index=True)
|
||||
name: str = Field(max_length=100)
|
||||
account_code: str = Field(max_length=20) # 5311, 5121 etc.
|
||||
register_type: str = Field(max_length=20) # CASA sau BANCA
|
||||
synced_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
```
|
||||
|
||||
### 2.2 Alembic Migration
|
||||
|
||||
**Fișier:** `data-entry-app/backend/migrations/versions/xxx_add_nomenclature_tables.py`
|
||||
|
||||
### 2.3 Sync Service
|
||||
|
||||
**Fișier:** `data-entry-app/backend/app/services/sync_service.py`
|
||||
|
||||
```python
|
||||
class NomenclatureSyncService:
|
||||
"""Sincronizare nomenclatoare din Oracle în SQLite"""
|
||||
|
||||
@staticmethod
|
||||
async def sync_suppliers(company_id: int, schema: str) -> int:
|
||||
"""Sincronizează furnizori pentru o companie"""
|
||||
async with oracle_pool.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"""
|
||||
SELECT ID_PART, DEN_PART, COD_FISCAL, ADRESA
|
||||
FROM {schema}.NOM_PARTENERI
|
||||
WHERE TIP_PART IN ('F', 'A') -- Furnizori sau Ambele
|
||||
""")
|
||||
# Upsert în SQLite
|
||||
|
||||
@staticmethod
|
||||
async def sync_cash_registers(company_id: int, schema: str) -> int:
|
||||
"""Sincronizează case și bănci"""
|
||||
# Similar pentru NOM_CASE și NOM_BANCI
|
||||
|
||||
@staticmethod
|
||||
async def get_schema_for_company(company_id: int) -> str:
|
||||
"""Obține schema Oracle pentru o companie"""
|
||||
# Folosește cache din shared sau query V_NOM_FIRME
|
||||
```
|
||||
|
||||
### 2.4 Strategia de Sync Hibrid
|
||||
|
||||
1. **La startup app**: Sync automat (background task)
|
||||
2. **Periodic**: Task programat la 4h
|
||||
3. **On-demand**: Căutare live în Oracle când CUI nu există local
|
||||
|
||||
**Fișier:** `data-entry-app/backend/app/main.py`
|
||||
```python
|
||||
@app.on_event("startup")
|
||||
async def startup_sync():
|
||||
# Background sync pentru company-urile active
|
||||
asyncio.create_task(sync_nomenclatures_background())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Faza 3: OCR + Căutare Furnizor după CUI
|
||||
|
||||
### 3.1 Flow Căutare Furnizor
|
||||
|
||||
```
|
||||
OCR extrage CUI
|
||||
↓
|
||||
Căutare în SyncedSupplier (SQLite)
|
||||
↓ (nu găsit)
|
||||
Căutare în LocalSupplier (SQLite)
|
||||
↓ (nu găsit)
|
||||
Căutare LIVE în Oracle (NOM_PARTENERI)
|
||||
↓ (nu găsit)
|
||||
Creare LocalSupplier cu date OCR
|
||||
↓
|
||||
Utilizator poate edita înainte de submit
|
||||
```
|
||||
|
||||
### 3.2 Endpoint Căutare Furnizor
|
||||
|
||||
**Fișier:** `data-entry-app/backend/app/routers/nomenclature.py`
|
||||
|
||||
```python
|
||||
@router.get("/suppliers/search")
|
||||
async def search_supplier(
|
||||
company_id: int,
|
||||
fiscal_code: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> SupplierSearchResult:
|
||||
"""
|
||||
Caută furnizor:
|
||||
1. În SQLite (synced + local)
|
||||
2. Live în Oracle dacă nu găsit
|
||||
3. Returnează sugestie creare dacă nu există
|
||||
"""
|
||||
|
||||
@router.post("/suppliers/local")
|
||||
async def create_local_supplier(
|
||||
supplier: LocalSupplierCreate,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> LocalSupplier:
|
||||
"""Crează furnizor local din date OCR"""
|
||||
```
|
||||
|
||||
### 3.3 Modificare OCR Flow în Frontend
|
||||
|
||||
**Fișier:** `data-entry-app/frontend/src/views/ReceiptCreateView.vue`
|
||||
|
||||
```javascript
|
||||
// După OCR, caută automat furnizor
|
||||
async function handleOCRResult(ocrData) {
|
||||
if (ocrData.cui) {
|
||||
const result = await api.get('/api/data-entry/suppliers/search', {
|
||||
params: { company_id: selectedCompany.id, fiscal_code: ocrData.cui }
|
||||
});
|
||||
|
||||
if (result.found) {
|
||||
form.partner_id = result.supplier.id;
|
||||
form.partner_name = result.supplier.name;
|
||||
} else {
|
||||
// Afișează opțiune creare locală
|
||||
showCreateSupplierDialog(ocrData);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Faza 4: Deploy Windows IIS
|
||||
|
||||
### 4.1 Serviciu Windows pentru data-entry-backend
|
||||
|
||||
**Fișier:** `deployment/windows/scripts/Install-DataEntry.ps1`
|
||||
|
||||
Similar cu Install-ROA2WEB.ps1 dar:
|
||||
- ServiceName: `ROA2WEB-DataEntry`
|
||||
- Port: 8003
|
||||
- BackendPath: `C:\inetpub\wwwroot\roa2web\data-entry-app\backend`
|
||||
- FrontendPath: `C:\inetpub\wwwroot\roa2web\data-entry-app\frontend`
|
||||
|
||||
**Actualizare Install-ROA2WEB.ps1** pentru structura unitară:
|
||||
- BackendPath: `C:\inetpub\wwwroot\roa2web\reports-app\backend`
|
||||
- FrontendPath: `C:\inetpub\wwwroot\roa2web\reports-app\frontend`
|
||||
|
||||
### 4.2 Actualizare web.config
|
||||
|
||||
**Fișier:** `deployment/windows/config/web.config`
|
||||
|
||||
Reguli URL compacte (`/reports/`, `/data/`, `/api/reports/`, `/api/data/`):
|
||||
|
||||
```xml
|
||||
<!-- API Auth (comun) -->
|
||||
<rule name="Auth API" stopProcessing="true">
|
||||
<match url="^api/auth/(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:8001/api/auth/{R:1}" />
|
||||
</rule>
|
||||
|
||||
<!-- API Data Entry -->
|
||||
<rule name="Data Entry API" stopProcessing="true">
|
||||
<match url="^api/data/(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:8003/api/{R:1}" />
|
||||
</rule>
|
||||
|
||||
<!-- API Reports -->
|
||||
<rule name="Reports API" stopProcessing="true">
|
||||
<match url="^api/reports/(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:8001/api/{R:1}" />
|
||||
</rule>
|
||||
|
||||
<!-- Frontend Data Entry SPA (/data/) -->
|
||||
<rule name="Data Entry SPA" stopProcessing="true">
|
||||
<match url="^data($|/.*)" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="/data/index.html" />
|
||||
</rule>
|
||||
|
||||
<!-- Frontend Reports SPA (/reports/) -->
|
||||
<rule name="Reports SPA" stopProcessing="true">
|
||||
<match url="^reports($|/.*)" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="/reports/index.html" />
|
||||
</rule>
|
||||
|
||||
<!-- Root redirect la /reports/ -->
|
||||
<rule name="Root Redirect" stopProcessing="true">
|
||||
<match url="^$" />
|
||||
<action type="Redirect" url="/reports/" redirectType="Found" />
|
||||
</rule>
|
||||
```
|
||||
|
||||
**IIS Virtual Directories (pentru URL-uri compacte):**
|
||||
```powershell
|
||||
# /reports/ → reports-app/frontend/
|
||||
New-WebVirtualDirectory -Site "Default Web Site" -Name "reports" `
|
||||
-PhysicalPath "C:\inetpub\wwwroot\roa2web\reports-app\frontend"
|
||||
|
||||
# /data/ → data-entry-app/frontend/
|
||||
New-WebVirtualDirectory -Site "Default Web Site" -Name "data" `
|
||||
-PhysicalPath "C:\inetpub\wwwroot\roa2web\data-entry-app\frontend"
|
||||
```
|
||||
|
||||
### 4.3 Structura Foldere (UNITARĂ - identică dev/prod)
|
||||
|
||||
**În development (git repo):**
|
||||
```
|
||||
roa2web/
|
||||
├── reports-app/
|
||||
│ ├── backend/ # FastAPI port 8001
|
||||
│ ├── frontend/ # Vue.js port 3000
|
||||
│ └── telegram-bot/ # Bot Telegram
|
||||
├── data-entry-app/
|
||||
│ ├── backend/ # FastAPI port 8003
|
||||
│ └── frontend/ # Vue.js port 3010
|
||||
└── shared/ # Cod partajat (auth, database)
|
||||
```
|
||||
|
||||
**În producție (Windows IIS) - IDENTIC:**
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\
|
||||
├── reports-app/
|
||||
│ ├── backend/ # Serviciu Windows port 8001
|
||||
│ └── frontend/ # Servit de IIS pe /
|
||||
├── data-entry-app/
|
||||
│ ├── backend/ # Serviciu Windows port 8003
|
||||
│ └── frontend/ # Servit de IIS pe /data-entry/
|
||||
├── telegram-bot/ # Serviciu Windows port 8002
|
||||
└── shared/ # Cod partajat
|
||||
```
|
||||
|
||||
**Avantaje structură unitară:**
|
||||
- Deploy simplu: `xcopy /E /Y source\reports-app dest\reports-app`
|
||||
- Path-uri identice în cod (no surprises)
|
||||
- Un singur script de deploy pentru ambele medii
|
||||
|
||||
---
|
||||
|
||||
## Faza 5: Configurare Dev (identic cu prod)
|
||||
|
||||
### 5.1 Vite Config pentru URL-uri Compacte
|
||||
|
||||
**Fișier:** `data-entry-app/frontend/vite.config.js`
|
||||
```javascript
|
||||
export default defineConfig({
|
||||
base: '/data/', // URL compact în producție
|
||||
server: {
|
||||
proxy: {
|
||||
'/api/auth': 'http://localhost:8001',
|
||||
'/api/data': 'http://localhost:8003'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Fișier:** `reports-app/frontend/vite.config.js` (ACTUALIZAT)
|
||||
```javascript
|
||||
export default defineConfig({
|
||||
base: '/reports/', // URL compact în producție (era '/')
|
||||
server: {
|
||||
proxy: {
|
||||
'/api/auth': 'http://localhost:8001',
|
||||
'/api/reports': 'http://localhost:8001'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**IMPORTANT:** Actualizare API calls în frontend:
|
||||
- Reports: `/api/reports/companies`, `/api/reports/invoices`, etc.
|
||||
- Data Entry: `/api/data/receipts`, `/api/data/suppliers`, etc.
|
||||
- Auth (comun): `/api/auth/login`, `/api/auth/refresh`
|
||||
|
||||
### 5.2 Script Start Unificat
|
||||
|
||||
**Fișier:** `start-all.sh` (nou)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Pornește toate serviciile pentru dev
|
||||
|
||||
# SSH tunnel
|
||||
./ssh_tunnel.sh start
|
||||
|
||||
# Reports backend
|
||||
cd reports-app/backend && uvicorn app.main:app --port 8001 &
|
||||
|
||||
# Data entry backend
|
||||
cd data-entry-app/backend && uvicorn app.main:app --port 8003 &
|
||||
|
||||
# Reports frontend
|
||||
cd reports-app/frontend && npm run dev -- --port 3000 &
|
||||
|
||||
# Data entry frontend
|
||||
cd data-entry-app/frontend && npm run dev -- --port 3010 &
|
||||
|
||||
wait
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ordine Implementare
|
||||
|
||||
| # | Task | Efort | Dependențe |
|
||||
|---|------|-------|------------|
|
||||
| 1 | Modele SQLModel nomenclatoare | 30 min | - |
|
||||
| 2 | Alembic migration | 15 min | #1 |
|
||||
| 3 | Sync service (Oracle → SQLite) | 2h | #2 |
|
||||
| 4 | Auth middleware în data-entry-backend | 1h | - |
|
||||
| 5 | Auth store + login în data-entry-frontend | 1h | #4 |
|
||||
| 6 | Endpoint căutare furnizor | 1h | #3 |
|
||||
| 7 | Frontend OCR + furnizor flow | 1.5h | #6 |
|
||||
| 8 | web.config IIS actualizat | 30 min | - |
|
||||
| 9 | Script deploy data-entry Windows | 1h | #8 |
|
||||
| 10 | Testare end-to-end | 1h | all |
|
||||
|
||||
**Total estimat: ~10h**
|
||||
|
||||
---
|
||||
|
||||
## Fișiere Critice de Modificat/Creat
|
||||
|
||||
### Backend data-entry-app:
|
||||
- `app/main.py` - middleware auth + startup sync
|
||||
- `app/db/models/nomenclature.py` - noi modele (CREARE)
|
||||
- `app/services/sync_service.py` - sync Oracle (CREARE)
|
||||
- `app/services/nomenclature_service.py` - refactorizare
|
||||
- `app/routers/nomenclature.py` - endpoint-uri noi (CREARE)
|
||||
- `app/routers/receipts.py` - auth dependencies
|
||||
- `migrations/versions/xxx_nomenclature.py` - migrare (CREARE)
|
||||
|
||||
### Frontend data-entry-app:
|
||||
- `src/stores/auth.js` - copiat din reports-app
|
||||
- `src/views/LoginView.vue` - copiat + adaptat
|
||||
- `src/router/index.js` - auth guard
|
||||
- `src/services/api.js` - axios config
|
||||
- `src/views/ReceiptCreateView.vue` - OCR + supplier flow
|
||||
|
||||
### Deploy (structură unitară):
|
||||
- `deployment/windows/config/web.config` - reguli noi + actualizate
|
||||
- `deployment/windows/scripts/Install-ROA2WEB.ps1` - ACTUALIZAT pentru structura unitară
|
||||
- `deployment/windows/scripts/Install-DataEntry.ps1` - NOU
|
||||
- `deployment/windows/scripts/Build-ROA2WEB.ps1` - ACTUALIZAT pentru ambele apps
|
||||
- `deployment/windows/docs/WINDOWS_DEPLOYMENT.md` - ACTUALIZAT cu noua structură
|
||||
|
||||
### Shared:
|
||||
- Nu necesită modificări (refolosim exact ce există)
|
||||
|
||||
---
|
||||
|
||||
## Întrebări Rezolvate
|
||||
|
||||
| Întrebare | Răspuns |
|
||||
|-----------|---------|
|
||||
| Furnizor nou din OCR? | Creare automată în SQLite (LocalSupplier) |
|
||||
| Sync strategy? | Hibrid: startup + periodic 4h + on-demand |
|
||||
| Auth sharing? | Frontend-uri separate pe path, același token JWT (SSO via localStorage) |
|
||||
| Deployment? | IIS path routing, servicii Windows separate |
|
||||
| Structura directoare? | **UNITARĂ** - grupat pe app (`{app}/backend`, `{app}/frontend`) identic dev/prod |
|
||||
| SSO cum funcționează? | Același domeniu IIS → localStorage partajat → token valid pentru ambele API-uri |
|
||||
| URL-uri? | **COMPACTE**: `/reports/`, `/data/`, `/api/reports/`, `/api/data/` |
|
||||
| Root (/)? | Redirect automat la `/reports/` |
|
||||
Reference in New Issue
Block a user