first commit

This commit is contained in:
2026-04-01 13:48:40 -07:00
commit 61d386a7c7
178 changed files with 3827 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,79 @@
from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Text, ForeignKey, Enum as SQLEnum
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from database import Base
import enum
class Rol(str, enum.Enum):
COMPRAS ="compras"
VENTAS ="ventas"
LOGISTICA ="logistica"
ADUANA_SOFT ="aduana_soft"
CLIENTE ="cliente"
OPERATIVO ="operativo"
class TipoUsuario(str, enum.Enum):
ROOT ="root"
ADMIN ="admin"
PRIVILEGED ="privileged"
ADMIN_LICENCIAS ="admin_licencias"
UPDATER = "updater"
USER ="user"
class Users(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, nullable=False, index=True, autoincrement=True)
#identity
name = Column(String(100), nullable=False)
middle_Name = Column(String(100), nullable=True)
last_Name = Column(String(120), nullable=False)
rfc = Column(String(60), nullable=False, unique=True)
#==== CONEXIONS ====#
email = Column(String(120), nullable=False, unique=True)
password = Column(String(255), nullable=False)
#=== credentials
rol_operativo = Column(SQLEnum(Rol), default="operativo", nullable=False)
tipo_usuario = Column(SQLEnum(TipoUsuario), default="user", nullable=False)
permite_diot = Column(Boolean, default=False, nullable=False)
#==== WHERES
enterprise_id = Column(Integer, nullable=True)
firma = Column(String(110), nullable=True, unique=True)
is_active = Column(Boolean, nullable=False, default=True)
#timestamsp
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=True)
deleted_at = Column(DateTime(timezone=True), nullable=True)
last_login = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
#trace
created_by = Column(Integer, nullable=True)
updated_by = Column(Integer, nullable=True)
deleted_by = Column(Integer, nullable=True)
#=====
# PASSWORD RESET
#=====
password_reset_token = Column(String, nullable=True)
password_reset_expires = Column(String, nullable=True)
#relationships
clients = relationship("Client", back_populates="user")
branch = relationship("Branches", back_populates="user")
interactions = relationship("Interacction", back_populates="user")
moves = relationship("Moves", back_populates="user", primaryjoin="Users.id == Moves.user_id")
licenses = relationship("License", back_populates="user")
feed = relationship("Feed", back_populates="user", primaryjoin="Users.id == Feed.user_id")
coments = relationship("Coments", back_populates="user", primaryjoin="Users.id == Coments.user_id")
affidavit_logs = relationship("AffidavitRecord", back_populates="user", primaryjoin="Users.id == AffidavitRecord.user_id")

View File

@@ -0,0 +1,212 @@
# modules/users/repositories.py
from typing import Optional, List, Dict, Any
from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from app.core.baseRepository import BaseRepository
from app.modules.users.models import Users, TipoUsuario, Rol
from app.core.security import get_password_hash, verify_password
class UserRepository(BaseRepository[Users]):
"""Repositorio específico para Users"""
def __init__(self, db: Session):
super().__init__(db, Users)
# ============================================
# MÉTODOS DE AUTENTICACIÓN
# ============================================
def get_by_email(self, email: str, include_deleted: bool = False) -> Optional[Users]:
"""Obtiene usuario por email"""
return self._base_query(include_deleted).filter(Users.email == email).first()
def get_by_firma(self, firma: str, include_deleted: bool = False) -> Optional[Users]:
"""Obtiene usuario por firma digital"""
return self._base_query(include_deleted).filter(Users.firma == firma).first()
def authenticate(self, email: str, password: str) -> Optional[Users]:
"""Autentica usuario por email y contraseña"""
user = self.get_by_email(email)
if not user:
return None
if not user.is_active or user.deleted_at is not None:
return None
if not verify_password(password, user.password):
return None
return user
def update_last_login(self, user_id: int, ip_address: str) -> Optional[Users]:
"""Actualiza timestamp e IP del último login"""
user = self.get_by_id(user_id)
if user:
user.last_login = datetime.utcnow()
user.last_ip = ip_address
self.db.commit()
self.db.refresh(user)
return user
def change_password(self, user_id: int, new_password: str, updated_by: int = None) -> bool:
"""Cambia la contraseña del usuario"""
user = self.get_by_id(user_id)
if not user:
return False
user.password = get_password_hash(new_password)
if updated_by:
user.updated_by = updated_by
self.db.commit()
return True
# ============================================
# MÉTODOS POR TIPO DE USUARIO
# ============================================
def get_by_tipo_usuario(self, tipo_usuario: str, include_deleted: bool = False) -> List[Users]:
"""Obtiene usuarios por tipo (root, admin, admin_licencias, user)"""
return self._base_query(include_deleted).filter(Users.tipo_usuario == tipo_usuario).all()
def get_root(self, include_deleted: bool = False) -> List[Users]:
"""Usuarios tipo root"""
return self.get_by_tipo_usuario(TipoUsuario.ROOT, include_deleted)
def get_admin(self, include_deleted: bool = False) -> List[Users]:
"""Usuarios tipo admin"""
return self.get_by_tipo_usuario(TipoUsuario.ADMIN, include_deleted)
def get_admin_licencias(self, include_deleted: bool = False) -> List[Users]:
"""Usuarios tipo admin_licencias"""
return self.get_by_tipo_usuario(TipoUsuario.ADMIN_LICENCIAS, include_deleted)
def get_regular_users(self, include_deleted: bool = False) -> List[Users]:
"""Usuarios tipo user (regulares)"""
return self.get_by_tipo_usuario(TipoUsuario.USER, include_deleted)
# ============================================
# MÉTODOS POR ROL OPERATIVO
# ============================================
def get_by_rol_operativo(self, rol: str, include_deleted: bool = False) -> List[Users]:
"""Obtiene usuarios por rol operativo"""
return self._base_query(include_deleted).filter(Users.rol_operativo == rol).all()
def get_by_permiso_diot(self, include_deleted: bool = False) -> List[Users]:
"""Obtiene usuarios que tienen permiso DIOT"""
return self._base_query(include_deleted).filter(Users.permite_diot == True).all()
# ============================================
# MÉTODOS POR ASOCIACIONES
# ============================================
def get_by_enterprise(self, enterprise_id: int, include_deleted: bool = False) -> List[Users]:
"""Obtiene usuarios de una empresa específica"""
return self._base_query(include_deleted).filter(Users.enterprise_id == enterprise_id).all()
def get_by_sucursal(self, sucursal_id: int, include_deleted: bool = False) -> List[Users]:
"""Obtiene usuarios de una sucursal específica"""
return self._base_query(include_deleted).filter(Users.sucursales_id == sucursal_id).all()
def get_by_licencia(self, licencia_id: int, include_deleted: bool = False) -> List[Users]:
"""Obtiene usuarios asociados a una licencia"""
return self._base_query(include_deleted).filter(Users.licencia_id == licencia_id).all()
def get_by_perfil(self, perfil_id: int, include_deleted: bool = False) -> List[Users]:
"""Obtiene usuarios por perfil"""
return self._base_query(include_deleted).filter(Users.perfil_id == perfil_id).all()
# ============================================
# MÉTODOS DE ESTADO
# ============================================
def get_active(self, skip: int = 0, limit: int = 100) -> List[Users]:
"""Obtiene usuarios activos (no eliminados)"""
return self.get_all(skip=skip, limit=limit, include_deleted=False)
def get_inactive(self, skip: int = 0, limit: int = 100) -> List[Users]:
"""Obtiene usuarios inactivos o eliminados"""
return self._base_query(include_deleted=True).filter(
or_(
Users.is_active == False,
Users.deleted_at.isnot(None)
)
).offset(skip).limit(limit).all()
def activate(self, user_id: int, updated_by: int = None) -> Optional[Users]:
"""Activa un usuario inactivo"""
user = self.get_by_id(user_id, include_deleted=True)
if not user:
return None
user.is_active = True
if user.deleted_at:
user.deleted_at = None
if updated_by:
user.updated_by = updated_by
self.db.commit()
self.db.refresh(user)
return user
def deactivate(self, user_id: int, updated_by: int = None) -> Optional[Users]:
"""Desactiva un usuario (sin soft delete)"""
user = self.get_by_id(user_id)
if not user:
return None
user.is_active = False
if updated_by:
user.updated_by = updated_by
self.db.commit()
self.db.refresh(user)
return user
# ============================================
# MÉTODOS DE BÚSQUEDA
# ============================================
def search(self, term: str, skip: int = 0, limit: int = 100) -> List[Users]:
"""Búsqueda por nombre, email o RFC"""
search_term = f"%{term}%"
return self._base_query().filter(
or_(
Users.name.ilike(search_term),
Users.last_name.ilike(search_term),
Users.email.ilike(search_term),
Users.rfc.ilike(search_term)
)
).offset(skip).limit(limit).all()
def count_by_enterprise(self, enterprise_id: int) -> int:
"""Cuenta usuarios por empresa"""
return self._base_query().filter(Users.enterprise_id == enterprise_id).count()
def count_by_tipo(self, tipo_usuario: str) -> int:
"""Cuenta usuarios por tipo"""
return self._base_query().filter(Users.tipo_usuario == tipo_usuario).count()
# ============================================
# SOBRESCRITURA DE MÉTODOS BASE CON TRAZABILIDAD
# ============================================
def create(self, data: Dict[str, Any], created_by: int = None) -> Users:
"""Crea usuario con hash de contraseña"""
create_data = data.copy()
# Hashear contraseña si existe
if "password" in create_data:
raw_password = create_data["password"]
print(f"HASHEANDO PASSWORD: {raw_password} (longitud: {len(raw_password)})")
create_data["password"] = get_password_hash(raw_password)
print(f"HASH GENERADO: {create_data['password'][:20]}...")
return super().create(create_data, created_by)
def update(self, id: int, data: Dict[str, Any], updated_by: int = None, include_deleted: bool = False) -> Optional[Users]:
"""Actualiza usuario, hashea contraseña si viene"""
if "password" in data:
data["password"] = get_password_hash(data["password"])
return super().update(id, data, updated_by, include_deleted)

188
app/modules/users/route.py Normal file
View File

@@ -0,0 +1,188 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Header, status
#
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session
from typing import Optional, List
#
from database import get_db
from app.core.auth import get_current_user
from app.modules.users.models import Users
#
from app.modules.users.schema import UserCreate, UserResponse, UserLogin, LoginResponse, DeleteResponse, UpdateUser
from app.modules.users.services import UserCrudService
from app.core.security import create_access_token, verify_password
router = APIRouter(prefix="/users", tags=["users"])
security = HTTPBearer()
#================ Login ====================
@router.post("/login", response_model=LoginResponse)
async def login(
login_data: UserLogin,
db: Session = Depends(get_db)
):
"""Authenticate user and return JWT token"""
user = db.query(Users).filter(Users.email == login_data.email).first()
#check email
if not user:
raise HTTPException(
status_code = status.HTTP_401_UNAUTHORIZED,
detail="Credentials invalid - user not found",
headers={"WWW-Authenticate": "Bearer"},
)
#check password ...
if not verify_password(login_data.password, user.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials = worng password",
headers={"WWW-Athenticate": "Bearer"}
)
#check is active
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="This user is ianctive, contact with support"
)
#update last login
from datetime import datetime
user.last_login = datetime.utcnow()
db.commit()
#create token
token_data = {
"sub": str(user.id),
"email": user.email,
"user_type": user.tipo_usuario,
"rol_operativo": user.rol_operativo
}
access_token = create_access_token(token_data)
return LoginResponse(access_token=access_token, token_type="bearer")
#================ Register ====================
@router.post("/register", response_model=UserResponse)
async def register(data: UserCreate, db: Session = Depends(get_db)):
user_count = db.query(Users).count()
if user_count == 0:
service = UserCrudService()
return UserCrudService.create_user(db = db, user_create=data, current_user=None)
else:
raise HTTPException(
status_code=403,
detail="Root user already exists, use create_users enpoint"
)
#================ Create User ====================
@router.post("/create_users", response_model=UserResponse)
async def new_user(
data: UserCreate,
db: Session = Depends(get_db),
credentials: HTTPAuthorizationCredentials = Depends(security)
):
"""Create a new user - Requeres authentication """
from app.core.auth import AuthService
token = credentials.credentials
auth_service = AuthService()
payload = auth_service.decode_token(token)
user_id = int(payload.get("sub"))
current_user = db.query(Users).filter(Users.id == user_id).first()
if not current_user:
raise HTTPException(
status_code=401,
detail="User not found - invalid token"
)
if current_user.tipo_usuario not in ["root", "admin"]:
raise HTTPException(status_code=403, detail="you dont have permission to create users")
if current_user.tipo_usuario == "admin" and data.tipo_usuario != "user":
raise HTTPException(status_code=403, detail="Admin can only create users type user")
return UserCrudService.create_user(
db = db, user_create = data, current_user = current_user
)
#================ Get Users ====================
@router.get("/get", response_model=List[UserResponse])
def obtain_users(
db: Session = Depends(get_db),
current_user: Users = Depends(get_current_user),
skip: int = Query(0, ge=0, description="Number of records to skip for pagination"),
limit: int = Query(100, ge=1, le= 1000, description="Maximun number of records to return")
):
"""Get a list of user with pagination - requeres authentication"""
return UserCrudService.get_users(db = db, skip=skip, limit=limit)
#================ Delete Users ====================
@router.delete("/delete/{user_id}", response_model=DeleteResponse)
async def delete(
db:Session = Depends(get_db),
user_id= int,
current_user: Users = Depends(get_current_user)
):
"""Delete a user by id"""
current_user = db.query(Users).filter(Users.id == user_id).first()
if current_user.tipo_usuario not in ["root", "admin"]:
raise HTTPException(status_code=403,
detail="You dont have permitions to delete ")
try:
UserCrudService.delete_user(db = db, user_id=user_id, current_user=current_user)
return {"message": "ok"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
#================ Update Users ====================
@router.patch("/update/{user_id}", response_model=UserResponse)
async def user_update(
user_id: int,
data: UpdateUser,
db: Session = Depends(get_db),
current_user: Users = Depends(get_current_user)
):
""" Update existing user"""
user = db.query(Users).filter(Users.id == user_id).first()
current_user = db.query(Users).filter(Users.id == user_id).first()
if not user:
raise ValueError("Usuario no encontrado")
if current_user.id != user_id:
if current_user.tipo_usuario not in ["root", "admin"]:
raise PermissionError("Admin only can update user ur enterprise")
try:
updated_user = await UserCrudService.Update_user(db=db, user_id=user_id, user_data=data, current_user=current_user)
print(f"TIPO DE updated_user: {type(updated_user)}")
print(f"VALOR: {updated_user}")
print(f"ID: {updated_user.id if updated_user else 'None'}")
return UserResponse.model_validate(updated_user)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except PermissionError as e:
raise HTTPException(status_code=403, detail=str(e))

View File

@@ -0,0 +1,96 @@
# modules/users/schemas.py
from pydantic import BaseModel, EmailStr, Field, validator
from datetime import datetime
from typing import Optional
#
import re
from enum import Enum
class TipoUsuario(str, Enum):
ROOT = "root"
ADMIN = "admin"
ADMIN_LICENCIAS = "admin_licencias"
USER = "user"
class RolOperativo(str, Enum):
COMPRAS ="compras"
VENTAS ="ventas"
LOGISTICA ="logistica"
ADUANA_SOFT ="aduana_soft"
CLIENTE ="cliente"
OPERATIVO ="operativo"
# ============================================
# BASE SCHEMAS
# ============================================
class UserBase(BaseModel):
name: str = Field(...)
middle_Name: Optional[str] = Field(None)
last_Name: str = Field(...)
rfc: str = Field(..., example="PORF8513JH3")
email: EmailStr
tipo_usuario: TipoUsuario
rol_operativo: RolOperativo
permite_diot: bool = False
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
enterprise_id: Optional[int] = Field(None)
firma: Optional[str] = Field(None)
@validator("password")
def validate_password(cls, v):
if len(v) < 8:
raise ValueError("La contrasena debe tener al menos 8 caracteres")
if not re.search(r"[A-Z]", v):
raise ValueError("La contrasena debe contenera la emnos una mayuscula")
return v
class UserResponse(UserBase):
id: int
is_active: bool
last_login: datetime
created_by: Optional[int]
updated_by: Optional[int]
deleted_by: Optional[int]
created_at: datetime
updated_at: Optional[datetime]
deleted_at: Optional[datetime]
class Config:
from_attributes = True
class UpdateUser(UserBase):
name: Optional[str] = None
middle_Name: Optional[str] = None
last_Name: Optional[str] = None
rfc: Optional[str] = None
email: Optional[EmailStr] = None
permite_diot: bool = None
rol_operativo: Optional[RolOperativo] = None
tipo_usuario: Optional[TipoUsuario] = None
firma: Optional[str] = Field(None, max_length=110)
brnaches_id: Optional[int] = None
license_id: Optional[int] = None
is_active: Optional[bool] = None
class Config:
from_attributes = True
#============================================
class UserLogin(BaseModel):
email: EmailStr = Field(..., examples=["User@example.com"])
password: str = Field(..., min_length=8)
class LoginResponse(BaseModel):
access_token: str
token_type: str = "bearer"
class DeleteResponse(BaseModel):
message: str

View File

@@ -0,0 +1,158 @@
from datetime import datetime
from typing import List, Optional
#
from sqlalchemy.orm import Session
import logging
from app.modules.users.models import Users, TipoUsuario, Rol
#
from app.modules.users.schema import UserCreate, UserResponse, UpdateUser
from app.modules.users.repository import UserRepository
from app.core.security import get_password_hash
#
from app.core.auth import get_current_user
from app.core.baseRepository import BaseRepository
from app.pipelines.crud import CreatePipe
class UserCrudService:
@staticmethod
def create_user(db:Session, user_create: UserCreate, current_user: Optional[Users] = None) -> UserResponse:
""" Create a new user in the database and approved if is root user"""
first = db.query(Users).count()
is_first_user = (first == 0)
user_data = user_create.dict()
#
#
model_data = {
"name": user_data.get("name"),
"middle_Name": user_data.get("middle_Name"),
"last_Name": user_data.get("last_Name"),
"rfc": user_data.get("rfc"),
"email": user_data.get("email"),
"rol_operativo": user_data.get("rol_operativo"),
"tipo_usuario": user_data.get("tipo_usuario"),
"permite_diot": user_data.get("permite_diot", False),
"enterprise_id": user_data.get("enterprise_id"),
"firma": user_data.get("firma"),
"is_active": True,
}
#Password comes raw since schema
plain_pass = user_data.get("password")
# this step is for hash the pasword
hashed_pass = get_password_hash(plain_pass)
model_data["password"] = hashed_pass
# first user or not that, this is a joke :)
if is_first_user:
# if first user is being created, it must be root and approved by default
model_data["tipo_usuario"] = TipoUsuario.ROOT.value
model_data["created_by"] = None
#this point is for make one instance
new_user = Users(**model_data)
db.add(new_user)
db.commit()
db.refresh(new_user)
else:
# Case B: aditional User
if not current_user:
raise ValueError("requeries authentication for create user")
#validate who is creating or if intent create a root user
if model_data["tipo_usuario"] == TipoUsuario.ROOT.value:
if current_user.tipo_usuario != TipoUsuario.ROOT.value:
raise ValueError(" only ROOT user can create another Root user")
model_data["created_by"] = current_user.id
new_user = Users(**model_data)
db.add(new_user)
db.commit()
db.refresh(new_user)
#to this point validate the next users if can be used the app, admin, license_admin and users
return UserResponse.model_validate(new_user)
# if first is registered allthem users only can set admin or user role.
@staticmethod
def get_users(db: Session, skip: int =0, limit: int = 100) -> List[UserResponse]:
""""Get a list of users whith pagination"""
userRes = db.query(Users).offset(skip).limit(limit).all()
return [UserResponse.model_validate(user) for user in userRes]
@staticmethod
def delete_user(db: Session, user_id: int, current_user: Users) -> None:
""" this is soft delete, for traceability"""
user = db.query(Users).filter(Users.id == user_id).first()
if not user:
raise ValueError(
"User not found")
if user.tipo_usuario == TipoUsuario.ROOT.value:
raise ValueError("Root user cannot be deleted")
user.is_active = False
user.deleted_by = current_user.id
user.deleted_at = datetime.utcnow()
db.commit()
return {
"message": f"User has been deleted"
}
#=========== UPDATE =====================
@staticmethod
async def Update_user(db: Session, user_id: int, user_data: UpdateUser, current_user: Users) -> Users:
"""Function to updates users, only personal data"""
user = db.query(Users).filter(Users.id == user_id).first()
if not user:
raise ValueError(
"User not found")
if current_user.id != user_id:
if current_user.tipo_usuario not in ["root", "admin"]:
raise PermissionError("You don't have permition to updated")
if current_user.tipo_usuario == "admin":
if user.enterprise_id != current_user.enterprise_id:
raise PermissionError(" Admin only can't update user enterprise ")
update_data = user_data.dict(exclude_unset=True)
if user.tipo_usuario == "root" and update_data["tipo_usuario"] != "root":
root_count = db.query(Users).filter(
Users.tipo_usuario == "root",
Users.deleted_at.is_(None)
).count
if root_count <= 1:
raise ValueError("Can't downgrade ROOT user")
for field, value in update_data.items():
if hasattr(user, field):
setattr(user, field, value)
user.updated_by = current_user.id
user.updated_at = datetime.utcnow()
db.commit()
db.refresh(user)
return user