Spaces:
Sleeping
Sleeping
Adzacam commited on
Commit ·
330e958
1
Parent(s): 0377542
feat: implement JWT-based authentication and role-based access control, and add LogAuditoriaNlp table for MLOps feedback tracking
Browse files- app.py +137 -2
- database.py +12 -0
- requirements.txt +3 -1
app.py
CHANGED
|
@@ -16,10 +16,57 @@ from database import (
|
|
| 16 |
Users,
|
| 17 |
FactRendimientoAcademico,
|
| 18 |
FactSituacionFinanciera,
|
|
|
|
| 19 |
)
|
| 20 |
from ner_engine import ner_engine
|
| 21 |
from similarity import find_best_match
|
| 22 |
from typing import List, Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
logging.basicConfig(level=logging.INFO)
|
| 25 |
logger = logging.getLogger(__name__)
|
|
@@ -97,6 +144,45 @@ def read_root():
|
|
| 97 |
"ner_initialized": ner_engine._initialized or ner_engine.pipeline is not None
|
| 98 |
}
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
@app.get("/api/v1/diagnostico")
|
| 101 |
def diagnostico_db(
|
| 102 |
secret: str = Query(default=""),
|
|
@@ -309,11 +395,14 @@ def nlp_quality_check(payload: ProcessSheetPayloadRaw):
|
|
| 309 |
from typing import List
|
| 310 |
|
| 311 |
@app.post("/api/v1/ingesta/bulk", status_code=status.HTTP_201_CREATED)
|
| 312 |
-
def procesar_lote_tabular(payloads: List[ProcessSheetPayload], db: Session = Depends(get_db)):
|
| 313 |
"""
|
| 314 |
Fase 3: Recibe una lista de registros (ya confirmados/editados por el usuario
|
| 315 |
en la Fase 2) y los inserta masivamente en una sola transacción.
|
| 316 |
"""
|
|
|
|
|
|
|
|
|
|
| 317 |
try:
|
| 318 |
for payload in payloads:
|
| 319 |
# Re-evaluamos rápidamente para obtener la misma métrica
|
|
@@ -439,7 +528,9 @@ class FinancePayload(BaseModel):
|
|
| 439 |
tipo_alerta: str
|
| 440 |
|
| 441 |
@app.post("/api/v1/ingesta/financiera", status_code=status.HTTP_201_CREATED)
|
| 442 |
-
def procesar_registro_financiero(payload: FinancePayload, db: Session = Depends(get_db)):
|
|
|
|
|
|
|
| 443 |
try:
|
| 444 |
nuevo_hecho = FactSituacionFinanciera(
|
| 445 |
id_estudiante=payload.id_estudiante,
|
|
@@ -456,6 +547,50 @@ def procesar_registro_financiero(payload: FinancePayload, db: Session = Depends(
|
|
| 456 |
db.rollback()
|
| 457 |
raise HTTPException(status_code=500, detail=str(e))
|
| 458 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
@app.get("/api/v1/dashboard/kpis")
|
| 460 |
def get_dashboard_kpis(db: Session = Depends(get_db)):
|
| 461 |
try:
|
|
|
|
| 16 |
Users,
|
| 17 |
FactRendimientoAcademico,
|
| 18 |
FactSituacionFinanciera,
|
| 19 |
+
LogAuditoriaNlp,
|
| 20 |
)
|
| 21 |
from ner_engine import ner_engine
|
| 22 |
from similarity import find_best_match
|
| 23 |
from typing import List, Optional
|
| 24 |
+
import jwt
|
| 25 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
| 26 |
+
from passlib.context import CryptContext
|
| 27 |
+
|
| 28 |
+
SECRET_KEY = os.getenv("JWT_SECRET", "super-secret-local-key")
|
| 29 |
+
ALGORITHM = "HS256"
|
| 30 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24
|
| 31 |
+
|
| 32 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 33 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
| 34 |
+
|
| 35 |
+
def verify_password(plain_password, hashed_password):
|
| 36 |
+
if hashed_password == "$placeholder$":
|
| 37 |
+
return plain_password == "admin123"
|
| 38 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 39 |
+
|
| 40 |
+
def get_password_hash(password):
|
| 41 |
+
return pwd_context.hash(password)
|
| 42 |
+
|
| 43 |
+
def create_access_token(data: dict, expires_delta: Optional[datetime.timedelta] = None):
|
| 44 |
+
to_encode = data.copy()
|
| 45 |
+
if expires_delta:
|
| 46 |
+
expire = datetime.datetime.utcnow() + expires_delta
|
| 47 |
+
else:
|
| 48 |
+
expire = datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
|
| 49 |
+
to_encode.update({"exp": expire})
|
| 50 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 51 |
+
return encoded_jwt
|
| 52 |
+
|
| 53 |
+
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
| 54 |
+
credentials_exception = HTTPException(
|
| 55 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 56 |
+
detail="Could not validate credentials",
|
| 57 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 58 |
+
)
|
| 59 |
+
try:
|
| 60 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 61 |
+
username: str = payload.get("sub")
|
| 62 |
+
if username is None:
|
| 63 |
+
raise credentials_exception
|
| 64 |
+
except jwt.PyJWTError:
|
| 65 |
+
raise credentials_exception
|
| 66 |
+
user = db.query(Users).filter(Users.username == username).first()
|
| 67 |
+
if user is None:
|
| 68 |
+
raise credentials_exception
|
| 69 |
+
return user
|
| 70 |
|
| 71 |
logging.basicConfig(level=logging.INFO)
|
| 72 |
logger = logging.getLogger(__name__)
|
|
|
|
| 144 |
"ner_initialized": ner_engine._initialized or ner_engine.pipeline is not None
|
| 145 |
}
|
| 146 |
|
| 147 |
+
class Token(BaseModel):
|
| 148 |
+
access_token: str
|
| 149 |
+
token_type: str
|
| 150 |
+
role: str
|
| 151 |
+
|
| 152 |
+
@app.post("/api/v1/auth/login", response_model=Token)
|
| 153 |
+
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
| 154 |
+
user = db.query(Users).filter(Users.username == form_data.username).first()
|
| 155 |
+
if not user or not verify_password(form_data.password, user.hashed_password):
|
| 156 |
+
# Auto-seed the user if it's one of the test users and doesn't exist
|
| 157 |
+
test_users = {
|
| 158 |
+
"directivo@giragroup.com": {"password": "Directivo@123", "role": "comite_directivo"},
|
| 159 |
+
"academico@giragroup.com": {"password": "Academico@123", "role": "coordinador_academico"},
|
| 160 |
+
"datos@giragroup.com": {"password": "Datos@123", "role": "analista_datos"},
|
| 161 |
+
"admin@giragroup.com": {"password": "Admin@123", "role": "admin"}
|
| 162 |
+
}
|
| 163 |
+
if form_data.username in test_users and form_data.password == test_users[form_data.username]["password"]:
|
| 164 |
+
if not user:
|
| 165 |
+
user = Users(
|
| 166 |
+
username=form_data.username,
|
| 167 |
+
hashed_password=get_password_hash(form_data.password),
|
| 168 |
+
role=test_users[form_data.username]["role"]
|
| 169 |
+
)
|
| 170 |
+
db.add(user)
|
| 171 |
+
db.commit()
|
| 172 |
+
db.refresh(user)
|
| 173 |
+
else:
|
| 174 |
+
raise HTTPException(
|
| 175 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 176 |
+
detail="Incorrect username or password",
|
| 177 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
access_token_expires = datetime.timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 181 |
+
access_token = create_access_token(
|
| 182 |
+
data={"sub": user.username, "role": user.role}, expires_delta=access_token_expires
|
| 183 |
+
)
|
| 184 |
+
return {"access_token": access_token, "token_type": "bearer", "role": user.role}
|
| 185 |
+
|
| 186 |
@app.get("/api/v1/diagnostico")
|
| 187 |
def diagnostico_db(
|
| 188 |
secret: str = Query(default=""),
|
|
|
|
| 395 |
from typing import List
|
| 396 |
|
| 397 |
@app.post("/api/v1/ingesta/bulk", status_code=status.HTTP_201_CREATED)
|
| 398 |
+
def procesar_lote_tabular(payloads: List[ProcessSheetPayload], db: Session = Depends(get_db), current_user: Users = Depends(get_current_user)):
|
| 399 |
"""
|
| 400 |
Fase 3: Recibe una lista de registros (ya confirmados/editados por el usuario
|
| 401 |
en la Fase 2) y los inserta masivamente en una sola transacción.
|
| 402 |
"""
|
| 403 |
+
if current_user.role not in ["coordinador_academico", "admin"]:
|
| 404 |
+
raise HTTPException(status_code=403, detail="Acceso denegado: Se requiere rol de Coordinador Académico.")
|
| 405 |
+
|
| 406 |
try:
|
| 407 |
for payload in payloads:
|
| 408 |
# Re-evaluamos rápidamente para obtener la misma métrica
|
|
|
|
| 528 |
tipo_alerta: str
|
| 529 |
|
| 530 |
@app.post("/api/v1/ingesta/financiera", status_code=status.HTTP_201_CREATED)
|
| 531 |
+
def procesar_registro_financiero(payload: FinancePayload, db: Session = Depends(get_db), current_user: Users = Depends(get_current_user)):
|
| 532 |
+
if current_user.role not in ["analista_datos", "admin"]:
|
| 533 |
+
raise HTTPException(status_code=403, detail="Acceso denegado: Se requiere rol de Analista de Datos.")
|
| 534 |
try:
|
| 535 |
nuevo_hecho = FactSituacionFinanciera(
|
| 536 |
id_estudiante=payload.id_estudiante,
|
|
|
|
| 547 |
db.rollback()
|
| 548 |
raise HTTPException(status_code=500, detail=str(e))
|
| 549 |
|
| 550 |
+
@app.post("/api/v1/ingesta/financiera/bulk", status_code=status.HTTP_201_CREATED)
|
| 551 |
+
def procesar_lote_financiero(payloads: List[FinancePayload], db: Session = Depends(get_db), current_user: Users = Depends(get_current_user)):
|
| 552 |
+
if current_user.role not in ["analista_datos", "admin"]:
|
| 553 |
+
raise HTTPException(status_code=403, detail="Acceso denegado: Se requiere rol de Analista de Datos.")
|
| 554 |
+
try:
|
| 555 |
+
for payload in payloads:
|
| 556 |
+
nuevo_hecho = FactSituacionFinanciera(
|
| 557 |
+
id_estudiante=payload.id_estudiante,
|
| 558 |
+
id_tiempo=payload.id_tiempo,
|
| 559 |
+
monto_deuda=payload.monto_deuda,
|
| 560 |
+
cuotas_impagas=payload.cuotas_impagas,
|
| 561 |
+
estado_cartera=payload.estado_cartera,
|
| 562 |
+
tipo_alerta=payload.tipo_alerta
|
| 563 |
+
)
|
| 564 |
+
db.add(nuevo_hecho)
|
| 565 |
+
db.commit()
|
| 566 |
+
return {"status": "success", "inserted_count": len(payloads)}
|
| 567 |
+
except Exception as e:
|
| 568 |
+
db.rollback()
|
| 569 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 570 |
+
|
| 571 |
+
class MLOpsFeedbackPayload(BaseModel):
|
| 572 |
+
texto_erroneo: str
|
| 573 |
+
prediccion_beto: str
|
| 574 |
+
confianza_ia: float
|
| 575 |
+
texto_corregido: str
|
| 576 |
+
|
| 577 |
+
@app.post("/api/v1/mlops/feedback", status_code=status.HTTP_201_CREATED)
|
| 578 |
+
def log_mlops_feedback(payload: MLOpsFeedbackPayload, db: Session = Depends(get_db), current_user: Users = Depends(get_current_user)):
|
| 579 |
+
try:
|
| 580 |
+
nuevo_log = LogAuditoriaNlp(
|
| 581 |
+
texto_original=payload.texto_erroneo,
|
| 582 |
+
prediccion_beto=payload.prediccion_beto,
|
| 583 |
+
confianza_ia=payload.confianza_ia,
|
| 584 |
+
correccion_humana=payload.texto_corregido,
|
| 585 |
+
usuario_auditor=current_user.id
|
| 586 |
+
)
|
| 587 |
+
db.add(nuevo_log)
|
| 588 |
+
db.commit()
|
| 589 |
+
return {"status": "success", "message": "Feedback logged successfully"}
|
| 590 |
+
except Exception as e:
|
| 591 |
+
db.rollback()
|
| 592 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 593 |
+
|
| 594 |
@app.get("/api/v1/dashboard/kpis")
|
| 595 |
def get_dashboard_kpis(db: Session = Depends(get_db)):
|
| 596 |
try:
|
database.py
CHANGED
|
@@ -36,6 +36,7 @@ engine = create_engine(
|
|
| 36 |
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 37 |
|
| 38 |
Base = declarative_base()
|
|
|
|
| 39 |
|
| 40 |
|
| 41 |
# =============================================================================
|
|
@@ -90,6 +91,17 @@ class Users(Base):
|
|
| 90 |
role = Column(String(20), default="admin")
|
| 91 |
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
# =============================================================================
|
| 95 |
# TABLAS DE HECHOS
|
|
|
|
| 36 |
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 37 |
|
| 38 |
Base = declarative_base()
|
| 39 |
+
Base.metadata.create_all(bind=engine)
|
| 40 |
|
| 41 |
|
| 42 |
# =============================================================================
|
|
|
|
| 91 |
role = Column(String(20), default="admin")
|
| 92 |
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
| 93 |
|
| 94 |
+
class LogAuditoriaNlp(Base):
|
| 95 |
+
"""Tabla para registrar retroalimentación de correcciones MLOps."""
|
| 96 |
+
__tablename__ = "log_auditoria_nlp"
|
| 97 |
+
id_log = Column(Integer, primary_key=True, index=True)
|
| 98 |
+
texto_original = Column(String(500), nullable=False)
|
| 99 |
+
prediccion_beto = Column(String(200))
|
| 100 |
+
confianza_ia = Column(Numeric(5, 4))
|
| 101 |
+
correccion_humana = Column(String(200), nullable=False)
|
| 102 |
+
usuario_auditor = Column(Integer, ForeignKey("users.id"))
|
| 103 |
+
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
| 104 |
+
|
| 105 |
|
| 106 |
# =============================================================================
|
| 107 |
# TABLAS DE HECHOS
|
requirements.txt
CHANGED
|
@@ -9,4 +9,6 @@ pandas>=2.2.1
|
|
| 9 |
openpyxl==3.1.2
|
| 10 |
gspread==6.1.0
|
| 11 |
rapidfuzz==3.6.1
|
| 12 |
-
pydantic>=2.0.0
|
|
|
|
|
|
|
|
|
| 9 |
openpyxl==3.1.2
|
| 10 |
gspread==6.1.0
|
| 11 |
rapidfuzz==3.6.1
|
| 12 |
+
pydantic>=2.0.0
|
| 13 |
+
PyJWT>=2.8.0
|
| 14 |
+
passlib[bcrypt]>=1.7.4
|