First commit

This commit is contained in:
unknown
2026-06-09 21:18:13 -03:00
commit 5bff6b938b
66 changed files with 10922 additions and 0 deletions
+333
View File
@@ -0,0 +1,333 @@
from sqlalchemy.orm import Session
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, status
from datetime import datetime, timedelta
import os
from typing import Optional
from models import User, UserAudit
from schemas import UserCreate, UserUpdate
# Configuración de seguridad
SECRET_KEY = os.getenv("SECRET_KEY")
if not SECRET_KEY:
raise RuntimeError("SECRET_KEY no configurada. Abortando inicio.")
if len(SECRET_KEY) < 20:
raise RuntimeError("SECRET_KEY demasiado corta")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class AuthService:
@staticmethod
def hash_password(password: str) -> str:
return pwd_context.hash(password)
@staticmethod
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
@staticmethod
def authenticate_user(db: Session, email: str, password: str) -> Optional[User]:
user = db.query(User).filter(User.email == email, User.active == True).first()
if not user:
return None
if not AuthService.verify_password(password, user.password):
return None
return user
@staticmethod
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@staticmethod
def verify_token(token: str):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: int = payload.get("user_id")
if user_id is None:
raise credentials_exception
return user_id
except JWTError:
raise credentials_exception
class UserService:
def __init__(self, db: Session):
self.db = db
def get_user_by_id(self, user_id: int) -> Optional[User]:
return self.db.query(User).filter(User.id == user_id).first()
def get_user_by_email(self, email: str) -> Optional[User]:
return self.db.query(User).filter(User.email == email).first()
def get_all_users(self, skip: int = 0, limit: int = 100):
return self.db.query(User).offset(skip).limit(limit).all()
def get_pending_users(self, skip: int = 0, limit: int = 100):
return self.db.query(User).filter(User.active == False).offset(skip).limit(limit).all()
def count_active_admins(self) -> int:
"""Helper function to count the number of active admin users."""
return self.db.query(User).filter(User.rol == 'admin', User.active == True).count()
def create_user(self, user_data: UserCreate, updater_id: Optional[int] = None, ip_address: str = None) -> User:
if self.get_user_by_email(user_data.email):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to create user"
)
if user_data.rol not in ['admin', 'operator']:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid role specified"
)
if user_data.password and len(user_data.password) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must be at least 8 characters long"
)
hashed_password = AuthService.hash_password(user_data.password)
user = User(
name=user_data.name,
email=user_data.email,
password=hashed_password,
rol=user_data.rol,
active=user_data.active
)
self.db.add(user)
self.db.flush()
# If no updater_id provided (unauthenticated creation), mark the user as the updater (self-created)
audit_updater = updater_id if updater_id is not None else user.id
audit = UserAudit(
user_id=user.id,
updater_id=audit_updater,
action="create",
after_value=f"User created with role: {user.rol}",
snapshot={
"name": user.name,
"email": user.email,
"rol": user.rol,
"active": user.active
},
ip_address=ip_address
)
self.db.add(audit)
self.db.commit()
self.db.refresh(user)
return user
def update_user(self, user_id: int, user_data: UserUpdate, updater_id: int, ip_address: str = None) -> User:
user = self.get_user_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
if user_data.rol not in ['admin', 'operator']:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid role specified"
)
if user_data.password and len(user_data.password) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must be at least 8 characters long"
)
changes = []
snapshot = {
"name": user.name,
"email": user.email,
"rol": user.rol,
"active": user.active
}
for field, value in user_data.dict(exclude_unset=True).items():
if hasattr(user, field):
old_value = getattr(user, field)
if field == "password":
value = AuthService.hash_password(value)
old_value = "***"
if old_value != value:
setattr(user, field, value)
audit = UserAudit(
user_id=user_id,
updater_id=updater_id,
action="update",
updated_attribute=field,
before_value=str(old_value),
after_value=str(value) if field != "password" else "***",
snapshot=snapshot,
ip_address=ip_address
)
self.db.add(audit)
changes.append(field)
if changes:
self.db.commit()
self.db.refresh(user)
return user
def change_user_role(self, user_id: int, new_role: str, updater_id: int, ip_address: str = None) -> User:
user = self.get_user_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
old_role = user.rol
snapshot = {
"name": user.name,
"email": user.email,
"rol": user.rol,
"active": user.active
}
user.rol = new_role
audit = UserAudit(
user_id=user_id,
updater_id=updater_id,
action="role_change",
updated_attribute="rol",
before_value=old_role,
after_value=new_role,
snapshot=snapshot,
ip_address=ip_address
)
self.db.add(audit)
self.db.commit()
self.db.refresh(user)
return user
def deactivate_user(self, user_id: int, updater_id: int, ip_address: str = None) -> User:
user = self.get_user_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
user.active = False
audit = UserAudit(
user_id=user_id,
updater_id=updater_id,
action="deactivate",
updated_attribute="active",
before_value="True",
after_value="False",
snapshot={
"name": user.name,
"email": user.email,
"rol": user.rol,
"active": True
},
ip_address=ip_address
)
self.db.add(audit)
self.db.commit()
self.db.refresh(user)
return user
def activate_user(self, user_id: int, updater_id: int, ip_address: str = None) -> User:
user = self.get_user_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
user.active = True
audit = UserAudit(
user_id=user_id,
updater_id=updater_id,
action="activate",
updated_attribute="active",
before_value="False",
after_value="True",
snapshot={
"name": user.name,
"email": user.email,
"rol": user.rol,
"active": False
},
ip_address=ip_address
)
self.db.add(audit)
self.db.commit()
self.db.refresh(user)
return user
def get_user_modifications(self, user_id: int, skip: int = 0, limit: int = 50):
user = self.get_user_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return (self.db.query(UserAudit)
.filter(UserAudit.user_id == user_id)
.order_by(UserAudit.modification_date.desc())
.offset(skip)
.limit(limit)
.all())
def get_users_modifications(self, skip: int = 0, limit: int = 100):
return (self.db.query(UserAudit)
.order_by(UserAudit.modification_date.desc())
.offset(skip)
.limit(limit)
.all())
def delete_user(self, user_id: int, updater_id: int, ip_address: str = None):
user = self.get_user_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
snapshot = {
"name": user.name,
"email": user.email,
"rol": user.rol,
"active": user.active
}
self.db.delete(user)
audit = UserAudit(
user_id=user_id,
updater_id=updater_id,
action="delete",
after_value="User deleted",
snapshot=snapshot,
ip_address=ip_address
)
self.db.add(audit)
self.db.commit()