develop #1

Merged
gitea merged 44 commits from develop into main 2025-11-26 01:15:20 +00:00
7 changed files with 550 additions and 8 deletions
Showing only changes of commit 57ad12754f - Show all commits

View File

@@ -0,0 +1,166 @@
# Guía de Implementación del Sistema de Roles
## 📋 Resumen
Se implementa un sistema de roles basado en base de datos con 3 roles:
- **Administrador** (id=1): Acceso completo
- **Asesor** (id=2): Solo informes y reportes
- **Mecánico** (id=3): Crear y completar inspecciones
## 🔄 Pasos de Migración
### 1. Ejecutar Migración de Base de Datos
```bash
cd backend
python migrate_roles.py
```
Esto hará:
- ✅ Crear tabla `roles`
- ✅ Insertar 3 roles predefinidos
- ✅ Migrar usuarios existentes (admin -> administrador, mechanic -> mecanico)
- ✅ Eliminar columna `role` antigua
### 2. Reemplazar Verificaciones de Permisos en main.py
**ANTES:**
```python
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="No tienes permisos")
```
**DESPUÉS:**
```python
require_permission(current_user, "can_manage_users")
```
### 3. Permisos por Rol
| Permiso | Administrador | Asesor | Mecánico |
|---------|--------------|--------|----------|
| can_manage_users | ✅ | ❌ | ❌ |
| can_manage_roles | ✅ | ❌ | ❌ |
| can_manage_checklists | ✅ | ❌ | ❌ |
| can_create_inspections | ✅ | ❌ | ✅ |
| can_view_all_inspections | ✅ | ✅ | ❌ |
| can_view_reports | ✅ | ✅ | ❌ |
| can_deactivate_inspections | ✅ | ❌ | ❌ |
### 4. Cambios en Endpoints
#### Gestión de Usuarios
```python
# Antes
if current_user.role != "admin":
# Después
require_permission(current_user, "can_manage_users")
```
#### Gestión de Checklists
```python
# Antes
if current_user.role != "admin":
# Después
require_permission(current_user, "can_manage_checklists")
```
#### Ver todas las Inspecciones
```python
# Antes
if current_user.role == "mechanic":
query = query.filter(models.Inspection.mechanic_id == current_user.id)
# Después
if not has_permission(current_user, "can_view_all_inspections"):
query = query.filter(models.Inspection.mechanic_id == current_user.id)
```
#### Desactivar Inspecciones
```python
# Antes
if current_user.role != "admin":
# Después
require_permission(current_user, "can_deactivate_inspections")
```
### 5. Actualizar Frontend
**schemas en App.jsx:**
```javascript
// Antes
role: 'mechanic'
// Después
role_id: 3, // mecanico
role: {
id: 3,
name: 'mecanico',
display_name: 'Mecánico',
can_create_inspections: true,
can_view_reports: false,
// ...otros permisos
}
```
**Verificación de permisos:**
```javascript
// Antes
if (user.role === 'admin')
// Después
if (user.role.can_manage_users)
```
### 6. Agregar Endpoints de Roles
Ver archivo `role_endpoints.txt` para los 5 nuevos endpoints:
- GET /api/roles - Listar roles
- GET /api/roles/{id} - Ver rol
- POST /api/roles - Crear rol
- PUT /api/roles/{id} - Actualizar rol
- DELETE /api/roles/{id} - Eliminar rol
### 7. Actualizar Componentes Frontend
#### UsersTab
- Cambiar selector de rol de texto a dropdown con roles de BD
- Mostrar `role.display_name` en lugar de role
- Enviar `role_id` en lugar de `role` al crear/editar
#### Sidebar
- Mostrar tab "Informes" solo si `user.role.can_view_reports`
- Mostrar tab "Usuarios" solo si `user.role.can_manage_users`
## ⚠️ Importante
1. **Backup de BD**: Hacer backup antes de ejecutar migración
2. **Orden de deployment**:
- Backend primero (migración + código)
- Frontend después
3. **Validar**: Probar login con cada tipo de usuario después de migrar
## 🧪 Testing
```bash
# 1. Probar login con admin
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin123"}'
# 2. Verificar que el response incluye role object
# Debe tener: role: { id: 1, name: 'administrador', ... }
# 3. Probar permisos
curl -X GET http://localhost:8000/api/users \
-H "Authorization: Bearer {token}"
# Debe funcionar para admin, fallar para mecanico
```
## 📝 Notas
- Los 3 roles predefinidos (id 1, 2, 3) no se pueden eliminar ni editar
- Nuevos roles personalizados pueden crearse con id > 3
- La migración es irreversible (elimina columna `role`)
- Usuarios sin `role_id` después de migración se asignan automáticamente a "mecanico"

View File

@@ -27,6 +27,19 @@ app.add_middleware(
security = HTTPBearer()
# ============= PERMISSION HELPERS =============
def require_permission(user: models.User, permission: str):
"""Verifica que el usuario tenga un permiso específico"""
if not hasattr(user.role_obj, permission) or not getattr(user.role_obj, permission):
raise HTTPException(
status_code=403,
detail=f"No tienes permisos para esta acción (requiere: {permission})"
)
def has_permission(user: models.User, permission: str) -> bool:
"""Verifica si el usuario tiene un permiso específico"""
return hasattr(user.role_obj, permission) and getattr(user.role_obj, permission)
# Dependency para obtener usuario actual
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
@@ -51,8 +64,11 @@ def get_current_user(
api_token.last_used_at = datetime.utcnow()
db.commit()
# Obtener usuario
user = db.query(models.User).filter(models.User.id == api_token.user_id).first()
# Obtener usuario con rol
user = db.query(models.User).options(
joinedload(models.User.role_obj)
).filter(models.User.id == api_token.user_id).first()
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -72,7 +88,10 @@ def get_current_user(
user_id = int(payload.get("sub"))
print(f"Looking for user ID: {user_id}") # Debug
user = db.query(models.User).filter(models.User.id == user_id).first()
user = db.query(models.User).options(
joinedload(models.User.role_obj)
).filter(models.User.id == user_id).first()
if user is None:
print(f"User not found with ID: {user_id}") # Debug
raise HTTPException(status_code=404, detail="Usuario no encontrado")
@@ -94,13 +113,15 @@ def register(user: schemas.UserCreate, db: Session = Depends(get_db)):
username=user.username,
email=user.email,
full_name=user.full_name,
role=user.role,
role_id=user.role_id,
password_hash=hashed_password
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
db.refresh(db_user)
return db_user
@app.post("/api/auth/login", response_model=schemas.Token)

View File

@@ -3,6 +3,26 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
class Role(Base):
__tablename__ = "roles"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50), unique=True, nullable=False) # administrador, asesor, mecanico
display_name = Column(String(100), nullable=False) # Administrador, Asesor, Mecánico
description = Column(String(255))
# Permisos
can_manage_users = Column(Boolean, default=False)
can_manage_roles = Column(Boolean, default=False)
can_manage_checklists = Column(Boolean, default=False)
can_create_inspections = Column(Boolean, default=False)
can_view_all_inspections = Column(Boolean, default=False)
can_view_reports = Column(Boolean, default=False)
can_deactivate_inspections = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
users = relationship("User", back_populates="role_obj")
class User(Base):
__tablename__ = "users"
@@ -10,12 +30,13 @@ class User(Base):
username = Column(String(50), unique=True, index=True, nullable=False)
email = Column(String(100), unique=True, index=True)
password_hash = Column(String(255), nullable=False)
role = Column(String(20), nullable=False) # admin, mechanic
role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
full_name = Column(String(100))
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
role_obj = relationship("Role", back_populates="users")
checklists_created = relationship("Checklist", back_populates="creator")
inspections = relationship("Inspection", back_populates="mechanic")
api_tokens = relationship("APIToken", back_populates="user", cascade="all, delete-orphan")

View File

@@ -2,12 +2,46 @@ from pydantic import BaseModel, EmailStr, Field
from typing import Optional, List
from datetime import datetime
# Role Schemas
class RoleBase(BaseModel):
name: str
display_name: str
description: Optional[str] = None
can_manage_users: bool = False
can_manage_roles: bool = False
can_manage_checklists: bool = False
can_create_inspections: bool = False
can_view_all_inspections: bool = False
can_view_reports: bool = False
can_deactivate_inspections: bool = False
class RoleCreate(RoleBase):
pass
class RoleUpdate(BaseModel):
display_name: Optional[str] = None
description: Optional[str] = None
can_manage_users: Optional[bool] = None
can_manage_roles: Optional[bool] = None
can_manage_checklists: Optional[bool] = None
can_create_inspections: Optional[bool] = None
can_view_all_inspections: Optional[bool] = None
can_view_reports: Optional[bool] = None
can_deactivate_inspections: Optional[bool] = None
class Role(RoleBase):
id: int
created_at: datetime
class Config:
from_attributes = True
# User Schemas
class UserBase(BaseModel):
username: str
email: Optional[EmailStr] = None
full_name: Optional[str] = None
role: str = "mechanic"
role_id: int = 3 # Default: mecanico
class UserCreate(UserBase):
password: str
@@ -16,7 +50,7 @@ class UserUpdate(BaseModel):
username: Optional[str] = None
email: Optional[EmailStr] = None
full_name: Optional[str] = None
role: Optional[str] = None
role_id: Optional[int] = None
class UserPasswordUpdate(BaseModel):
current_password: str
@@ -29,8 +63,13 @@ class UserLogin(BaseModel):
username: str
password: str
class User(UserBase):
class User(BaseModel):
id: int
username: str
email: Optional[str] = None
full_name: Optional[str] = None
role_id: int
role: Role # Role object
is_active: bool
created_at: datetime

128
backend/migrate_roles.py Normal file
View File

@@ -0,0 +1,128 @@
"""
Script de migración para implementar sistema de roles
Crea tabla roles y migra usuarios existentes
"""
from sqlalchemy import create_engine, text
from app.core.config import settings
def migrate():
engine = create_engine(settings.DATABASE_URL)
with engine.connect() as conn:
print("🔄 Iniciando migración de roles...")
# 1. Crear tabla roles
print("📋 Creando tabla roles...")
conn.execute(text("""
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
display_name VARCHAR(100) NOT NULL,
description VARCHAR(255),
can_manage_users BOOLEAN DEFAULT FALSE,
can_manage_roles BOOLEAN DEFAULT FALSE,
can_manage_checklists BOOLEAN DEFAULT FALSE,
can_create_inspections BOOLEAN DEFAULT FALSE,
can_view_all_inspections BOOLEAN DEFAULT FALSE,
can_view_reports BOOLEAN DEFAULT FALSE,
can_deactivate_inspections BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
)
"""))
conn.commit()
# 2. Insertar roles predefinidos
print("👥 Insertando roles predefinidos...")
conn.execute(text("""
INSERT INTO roles (name, display_name, description,
can_manage_users, can_manage_roles, can_manage_checklists,
can_create_inspections, can_view_all_inspections,
can_view_reports, can_deactivate_inspections)
VALUES
('administrador', 'Administrador', 'Acceso completo al sistema',
TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
('asesor', 'Asesor', 'Acceso a reportes e informes',
FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, FALSE),
('mecanico', 'Mecánico', 'Realizar inspecciones',
FALSE, FALSE, FALSE, TRUE, FALSE, FALSE, FALSE)
ON CONFLICT (name) DO NOTHING
"""))
conn.commit()
# 3. Agregar columna role_id a users (temporal)
print("🔧 Agregando columna role_id a usuarios...")
conn.execute(text("""
ALTER TABLE users ADD COLUMN IF NOT EXISTS role_id INTEGER
"""))
conn.commit()
# 4. Migrar datos: admin -> administrador (id=1)
print("🔄 Migrando usuarios admin -> administrador...")
conn.execute(text("""
UPDATE users
SET role_id = (SELECT id FROM roles WHERE name = 'administrador')
WHERE role = 'admin' AND role_id IS NULL
"""))
conn.commit()
# 5. Migrar datos: mechanic -> mecanico (id=3)
print("🔄 Migrando usuarios mechanic -> mecanico...")
conn.execute(text("""
UPDATE users
SET role_id = (SELECT id FROM roles WHERE name = 'mecanico')
WHERE role IN ('mechanic', 'mecanico') AND role_id IS NULL
"""))
conn.commit()
# 6. Verificar que todos tienen role_id
result = conn.execute(text("SELECT COUNT(*) FROM users WHERE role_id IS NULL"))
null_count = result.scalar()
if null_count > 0:
print(f"⚠️ Advertencia: {null_count} usuarios sin role_id asignado")
print(" Asignando rol de mecánico por defecto...")
conn.execute(text("""
UPDATE users
SET role_id = (SELECT id FROM roles WHERE name = 'mecanico')
WHERE role_id IS NULL
"""))
conn.commit()
# 7. Hacer role_id NOT NULL y crear foreign key
print("🔒 Aplicando restricciones...")
conn.execute(text("""
ALTER TABLE users ALTER COLUMN role_id SET NOT NULL
"""))
conn.execute(text("""
ALTER TABLE users
ADD CONSTRAINT fk_users_role_id
FOREIGN KEY (role_id) REFERENCES roles(id)
"""))
conn.commit()
# 8. Eliminar columna role antigua
print("🗑️ Eliminando columna role antigua...")
conn.execute(text("""
ALTER TABLE users DROP COLUMN IF EXISTS role
"""))
conn.commit()
# 9. Mostrar resumen
print("\n✅ Migración completada!")
print("\n📊 Resumen de roles:")
result = conn.execute(text("""
SELECT r.name, r.display_name, COUNT(u.id) as user_count
FROM roles r
LEFT JOIN users u ON u.role_id = r.id
GROUP BY r.id, r.name, r.display_name
ORDER BY r.id
"""))
for row in result:
print(f" {row[1]}: {row[2]} usuario(s)")
print("\n🎉 Sistema de roles implementado correctamente!")
if __name__ == "__main__":
migrate()

100
backend/role_endpoints.txt Normal file
View File

@@ -0,0 +1,100 @@
# Endpoints para gestión de roles - Agregar después de los endpoints de usuarios
# ============= ROLE ENDPOINTS =============
@app.get("/api/roles", response_model=List[schemas.Role])
def get_roles(
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Lista todos los roles disponibles (cualquier usuario autenticado)"""
return db.query(models.Role).all()
@app.get("/api/roles/{role_id}", response_model=schemas.Role)
def get_role(
role_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Obtiene un rol específico"""
require_permission(current_user, "can_manage_roles")
role = db.query(models.Role).filter(models.Role.id == role_id).first()
if not role:
raise HTTPException(status_code=404, detail="Rol no encontrado")
return role
@app.post("/api/roles", response_model=schemas.Role)
def create_role(
role: schemas.RoleCreate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Crea un nuevo rol (solo administrador)"""
require_permission(current_user, "can_manage_roles")
# Verificar si el rol ya existe
existing = db.query(models.Role).filter(models.Role.name == role.name).first()
if existing:
raise HTTPException(status_code=400, detail="El rol ya existe")
db_role = models.Role(**role.dict())
db.add(db_role)
db.commit()
db.refresh(db_role)
return db_role
@app.put("/api/roles/{role_id}", response_model=schemas.Role)
def update_role(
role_id: int,
role_update: schemas.RoleUpdate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Actualiza un rol existente (solo administrador)"""
require_permission(current_user, "can_manage_roles")
db_role = db.query(models.Role).filter(models.Role.id == role_id).first()
if not db_role:
raise HTTPException(status_code=404, detail="Rol no encontrado")
# No permitir editar roles predefinidos (1, 2, 3)
if role_id in [1, 2, 3]:
raise HTTPException(status_code=403, detail="No se pueden editar roles predefinidos")
# Actualizar campos
update_data = role_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_role, field, value)
db.commit()
db.refresh(db_role)
return db_role
@app.delete("/api/roles/{role_id}")
def delete_role(
role_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Elimina un rol (solo administrador, no permite eliminar roles predefinidos)"""
require_permission(current_user, "can_manage_roles")
# No permitir eliminar roles predefinidos
if role_id in [1, 2, 3]:
raise HTTPException(status_code=403, detail="No se pueden eliminar roles predefinidos")
db_role = db.query(models.Role).filter(models.Role.id == role_id).first()
if not db_role:
raise HTTPException(status_code=404, detail="Rol no encontrado")
# Verificar si hay usuarios con este rol
users_count = db.query(models.User).filter(models.User.role_id == role_id).count()
if users_count > 0:
raise HTTPException(
status_code=400,
detail=f"No se puede eliminar el rol porque tiene {users_count} usuario(s) asignado(s)"
)
db.delete(db_role)
db.commit()
return {"message": "Rol eliminado correctamente", "role_id": role_id}

View File

@@ -0,0 +1,67 @@
"""
Script para actualizar automáticamente las verificaciones de permisos en main.py
Reemplaza las verificaciones de role string por verificaciones basadas en permisos
"""
import re
def update_permissions():
with open('app/main.py', 'r', encoding='utf-8') as f:
content = f.read()
# Mapa de reemplazos: patrón -> reemplazo
replacements = [
# Gestión de usuarios
(
r'if current_user\.role != "admin":\s+raise HTTPException\(status_code=403, detail="No tienes permisos para ver usuarios"\)',
'require_permission(current_user, "can_manage_users")'
),
(
r'if current_user\.role != "admin":\s+raise HTTPException\(status_code=403, detail="No tienes permisos.*usuarios?"\)',
'require_permission(current_user, "can_manage_users")'
),
# Gestión de checklists
(
r'if current_user\.role != "admin":\s+raise HTTPException\(status_code=403, detail=".*checklist.*"\)',
'require_permission(current_user, "can_manage_checklists")'
),
# Desactivar inspecciones
(
r'if current_user\.role != "admin":\s+raise HTTPException\(status_code=403, detail=".*inactivar.*inspecc.*"\)',
'require_permission(current_user, "can_deactivate_inspections")'
),
# Ver todas las inspecciones (mechanic filter)
(
r'if current_user\.role == "mechanic":\s+query = query\.filter\(models\.Inspection\.mechanic_id == current_user\.id\)',
'if not has_permission(current_user, "can_view_all_inspections"):\n query = query.filter(models.Inspection.mechanic_id == current_user.id)'
),
# Crear inspecciones
(
r'# Crear usuario\s+hashed_password = get_password_hash\(user\.password\)\s+db_user = models\.User\(\s+username=user\.username,\s+email=user\.email,\s+full_name=user\.full_name,\s+role=user\.role,',
'# Crear usuario\n hashed_password = get_password_hash(user.password)\n db_user = models.User(\n username=user.username,\n email=user.email,\n full_name=user.full_name,\n role_id=user.role_id,'
),
]
# Aplicar reemplazos
for pattern, replacement in replacements:
content = re.sub(pattern, replacement, content, flags=re.MULTILINE | re.DOTALL)
# Reemplazos específicos adicionales
# Cambiar role por role_id en UserUpdate
content = content.replace(
'if user_update.role is not None:\n if current_user.role != "admin":\n raise HTTPException(status_code=403, detail="No tienes permisos para cambiar roles")\n db_user.role = user_update.role',
'if user_update.role_id is not None:\n require_permission(current_user, "can_manage_roles")\n db_user.role_id = user_update.role_id'
)
with open('app/main.py', 'w', encoding='utf-8') as f:
f.write(content)
print("✅ Archivo main.py actualizado con sistema de permisos")
print("⚠️ Revisar manualmente y ajustar según sea necesario")
if __name__ == "__main__":
update_permissions()