Compare commits

...

2 Commits

Author SHA1 Message Date
4670366ffc Se termina con Tokens y Modulo Usuarios 2025-11-19 10:09:46 -03:00
3a905a4d02 Se termina con Tokens y Modulo Usuarios 2025-11-19 09:52:51 -03:00
7 changed files with 786 additions and 10 deletions

View File

@@ -116,7 +116,7 @@ Lista todos los usuarios del sistema.
**Query Params:** **Query Params:**
- `skip` (int, default: 0) - Paginación - `skip` (int, default: 0) - Paginación
- `limit` (int, default: 100) - Límite de resultados - `limit` (int, default: 100) - Límite de resultados
- `active_only` (bool, default: true) - Solo usuarios activos - `active_only` (bool, default: false) - Solo usuarios activos
**Headers:** **Headers:**
``` ```
@@ -234,6 +234,7 @@ Authorization: Bearer {token}
**Body:** **Body:**
```json ```json
{ {
"username": "nuevo_username",
"email": "nuevo_email@syntria.com", "email": "nuevo_email@syntria.com",
"full_name": "Nombre Actualizado", "full_name": "Nombre Actualizado",
"role": "admin" "role": "admin"
@@ -244,7 +245,7 @@ Authorization: Bearer {token}
```json ```json
{ {
"id": 2, "id": 2,
"username": "mecanico", "username": "nuevo_username",
"email": "nuevo_email@syntria.com", "email": "nuevo_email@syntria.com",
"full_name": "Nombre Actualizado", "full_name": "Nombre Actualizado",
"role": "admin", "role": "admin",
@@ -253,8 +254,12 @@ Authorization: Bearer {token}
} }
``` ```
**Notas:**
- El campo `is_active` NO puede ser modificado por este endpoint
- Para cambiar el estado usar `/deactivate` o `/activate`
**Errores:** **Errores:**
- `400 Bad Request` - Email ya en uso - `400 Bad Request` - Email o username ya en uso
- `403 Forbidden` - No tienes permisos - `403 Forbidden` - No tienes permisos
- `404 Not Found` - Usuario no encontrado - `404 Not Found` - Usuario no encontrado
@@ -405,6 +410,156 @@ Authorization: Bearer {token}
--- ---
## 🔑 API TOKENS - Tokens de Acceso API
### Autenticación con API Tokens
Además de JWT tokens, puedes usar API tokens persistentes para autenticación. Los API tokens:
- Tienen el prefijo `syntria_`
- No expiran (hasta que sean revocados)
- Se rastrean por último uso
- Pueden ser gestionados desde el panel de usuario
**Uso:**
```bash
curl -H "Authorization: Bearer syntria_xxxxxxxxxxxxx" http://api/inspections
```
### 1. Listar Mis Tokens
**GET** `/api/users/me/tokens`
Lista todos tus API tokens.
**Headers:**
```
Authorization: Bearer {token}
```
**Response 200:**
```json
[
{
"id": 1,
"description": "Token para integración con sistema externo",
"is_active": true,
"last_used_at": "2025-11-19T14:30:00Z",
"created_at": "2025-11-15T10:00:00Z"
}
]
```
**Nota:** El valor del token no se muestra por seguridad.
---
### 2. Generar Nuevo Token
**POST** `/api/users/me/tokens`
Genera un nuevo API token.
**Headers:**
```
Authorization: Bearer {token}
```
**Body:**
```json
{
"description": "Token para integración con sistema externo"
}
```
**Response 200:**
```json
{
"id": 1,
"token": "syntria_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"description": "Token para integración con sistema externo",
"is_active": true,
"last_used_at": null,
"created_at": "2025-11-19T15:00:00Z"
}
```
**⚠️ IMPORTANTE:** El token completo solo se muestra una vez. Guárdalo de forma segura.
---
### 3. Revocar Mi Token
**DELETE** `/api/users/me/tokens/{token_id}`
Revoca (desactiva) uno de tus tokens.
**Headers:**
```
Authorization: Bearer {token}
```
**Response 200:**
```json
{
"message": "Token revocado correctamente",
"token_id": 1
}
```
**Errores:**
- `404 Not Found` - Token no encontrado
---
### 4. Listar Tokens de Usuario (Admin)
**GET** `/api/users/{user_id}/tokens`
Lista todos los tokens de un usuario específico.
**Headers:**
```
Authorization: Bearer {token}
```
**Response 200:**
```json
[
{
"id": 1,
"description": "Token de producción",
"is_active": true,
"last_used_at": "2025-11-19T14:30:00Z",
"created_at": "2025-11-15T10:00:00Z"
}
]
```
**Errores:**
- `403 Forbidden` - No tienes permisos (solo admin)
---
### 5. Revocar Token de Usuario (Admin)
**DELETE** `/api/users/{user_id}/tokens/{token_id}`
Revoca un token de cualquier usuario.
**Headers:**
```
Authorization: Bearer {token}
```
**Response 200:**
```json
{
"message": "Token revocado correctamente",
"token_id": 1
}
```
**Errores:**
- `403 Forbidden` - No tienes permisos (solo admin)
- `404 Not Found` - Token no encontrado
---
## 📋 CHECKLISTS - Plantillas de Inspección ## 📋 CHECKLISTS - Plantillas de Inspección
### 1. Listar Checklists ### 1. Listar Checklists
@@ -771,7 +926,9 @@ Content-Type: application/json
## 🔐 Roles y Permisos ## 🔐 Roles y Permisos
### Admin ### Admin
- ✅ Gestionar usuarios (crear, editar, inactivar) - ✅ Gestionar usuarios (crear, editar, inactivar, activar)
- ✅ Cambiar contraseñas de cualquier usuario
- ✅ Ver y revocar tokens API de cualquier usuario
- ✅ Gestionar checklists y preguntas - ✅ Gestionar checklists y preguntas
- ✅ Ver todas las inspecciones - ✅ Ver todas las inspecciones
- ✅ Crear inspecciones - ✅ Crear inspecciones
@@ -784,8 +941,11 @@ Content-Type: application/json
- ✅ Responder preguntas de inspección - ✅ Responder preguntas de inspección
- ✅ Usar análisis con IA - ✅ Usar análisis con IA
- ✅ Actualizar su propio perfil - ✅ Actualizar su propio perfil
- ✅ Cambiar su propia contraseña
- ✅ Gestionar sus propios tokens API
- ❌ No puede gestionar usuarios - ❌ No puede gestionar usuarios
- ❌ No puede gestionar checklists - ❌ No puede gestionar checklists
- ❌ No puede ver tokens de otros usuarios
--- ---

View File

@@ -3,6 +3,7 @@ from typing import Optional
from jose import JWTError, jwt from jose import JWTError, jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from app.core.config import settings from app.core.config import settings
import secrets
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@@ -33,3 +34,7 @@ def decode_access_token(token: str):
except JWTError as e: except JWTError as e:
print(f"JWT decode error: {e}") # Debug print(f"JWT decode error: {e}") # Debug
return None return None
def generate_api_token() -> str:
"""Genera un token API aleatorio seguro"""
return f"syntria_{secrets.token_urlsafe(32)}"

View File

@@ -33,6 +33,35 @@ def get_current_user(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
token = credentials.credentials token = credentials.credentials
# Verificar si es un API token (comienza con "syntria_")
if token.startswith("syntria_"):
api_token = db.query(models.APIToken).filter(
models.APIToken.token == token,
models.APIToken.is_active == True
).first()
if not api_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="API Token inválido o inactivo"
)
# Actualizar último uso
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()
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Usuario inválido o inactivo"
)
return user
# Si no es API token, es JWT token
payload = decode_access_token(token) payload = decode_access_token(token)
print(f"Token payload: {payload}") # Debug print(f"Token payload: {payload}") # Debug
if payload is None: if payload is None:
@@ -101,7 +130,7 @@ def get_me(current_user: models.User = Depends(get_current_user)):
def get_users( def get_users(
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
active_only: bool = True, active_only: bool = False,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user) current_user: models.User = Depends(get_current_user)
): ):
@@ -186,6 +215,16 @@ def update_user(
raise HTTPException(status_code=404, detail="Usuario no encontrado") raise HTTPException(status_code=404, detail="Usuario no encontrado")
# Actualizar campos # Actualizar campos
if user_update.username is not None:
# Verificar si username está en uso
existing = db.query(models.User).filter(
models.User.username == user_update.username,
models.User.id != user_id
).first()
if existing:
raise HTTPException(status_code=400, detail="Nombre de usuario ya está en uso")
db_user.username = user_update.username
if user_update.email is not None: if user_update.email is not None:
# Verificar si email está en uso # Verificar si email está en uso
existing = db.query(models.User).filter( existing = db.query(models.User).filter(
@@ -320,6 +359,116 @@ def update_my_profile(
return current_user return current_user
# ============= API TOKENS ENDPOINTS =============
@app.get("/api/users/me/tokens", response_model=List[schemas.APIToken])
def get_my_tokens(
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Obtener todos mis API tokens"""
tokens = db.query(models.APIToken).filter(
models.APIToken.user_id == current_user.id
).all()
return tokens
@app.post("/api/users/me/tokens", response_model=schemas.APITokenWithValue)
def create_my_token(
token_create: schemas.APITokenCreate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Generar un nuevo API token"""
from app.core.security import generate_api_token
# Generar token único
token_value = generate_api_token()
# Crear registro
api_token = models.APIToken(
user_id=current_user.id,
token=token_value,
description=token_create.description,
is_active=True
)
db.add(api_token)
db.commit()
db.refresh(api_token)
# Retornar con el token completo (solo esta vez)
return schemas.APITokenWithValue(
id=api_token.id,
token=api_token.token,
description=api_token.description,
is_active=api_token.is_active,
last_used_at=api_token.last_used_at,
created_at=api_token.created_at
)
@app.delete("/api/users/me/tokens/{token_id}")
def delete_my_token(
token_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Revocar uno de mis API tokens"""
api_token = db.query(models.APIToken).filter(
models.APIToken.id == token_id,
models.APIToken.user_id == current_user.id
).first()
if not api_token:
raise HTTPException(status_code=404, detail="Token no encontrado")
api_token.is_active = False
db.commit()
return {"message": "Token revocado correctamente", "token_id": token_id}
@app.get("/api/users/{user_id}/tokens", response_model=List[schemas.APIToken])
def get_user_tokens(
user_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Obtener tokens de un usuario (solo admin)"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="No tienes permisos")
tokens = db.query(models.APIToken).filter(
models.APIToken.user_id == user_id
).all()
return tokens
@app.delete("/api/users/{user_id}/tokens/{token_id}")
def delete_user_token(
user_id: int,
token_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Revocar token de un usuario (solo admin)"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="No tienes permisos")
api_token = db.query(models.APIToken).filter(
models.APIToken.id == token_id,
models.APIToken.user_id == user_id
).first()
if not api_token:
raise HTTPException(status_code=404, detail="Token no encontrado")
api_token.is_active = False
db.commit()
return {"message": "Token revocado correctamente", "token_id": token_id}
# ============= CHECKLIST ENDPOINTS ============= # ============= CHECKLIST ENDPOINTS =============
@app.get("/api/checklists", response_model=List[schemas.Checklist]) @app.get("/api/checklists", response_model=List[schemas.Checklist])
def get_checklists( def get_checklists(

View File

@@ -18,6 +18,22 @@ class User(Base):
# Relationships # Relationships
checklists_created = relationship("Checklist", back_populates="creator") checklists_created = relationship("Checklist", back_populates="creator")
inspections = relationship("Inspection", back_populates="mechanic") inspections = relationship("Inspection", back_populates="mechanic")
api_tokens = relationship("APIToken", back_populates="user", cascade="all, delete-orphan")
class APIToken(Base):
__tablename__ = "api_tokens"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
token = Column(String(100), unique=True, index=True, nullable=False)
description = Column(String(200))
is_active = Column(Boolean, default=True)
last_used_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationship
user = relationship("User", back_populates="api_tokens")
class Checklist(Base): class Checklist(Base):

View File

@@ -13,6 +13,7 @@ class UserCreate(UserBase):
password: str password: str
class UserUpdate(BaseModel): class UserUpdate(BaseModel):
username: Optional[str] = None
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
full_name: Optional[str] = None full_name: Optional[str] = None
role: Optional[str] = None role: Optional[str] = None
@@ -42,6 +43,24 @@ class Token(BaseModel):
user: User user: User
# API Token Schemas
class APITokenCreate(BaseModel):
description: Optional[str] = None
class APIToken(BaseModel):
id: int
description: Optional[str] = None
is_active: bool
last_used_at: Optional[datetime] = None
created_at: datetime
class Config:
from_attributes = True
class APITokenWithValue(APIToken):
token: str
# Checklist Schemas # Checklist Schemas
class ChecklistBase(BaseModel): class ChecklistBase(BaseModel):
name: str name: str

View File

@@ -337,10 +337,7 @@ function DashboardPage({ user, setUser }) {
) : activeTab === 'api-tokens' ? ( ) : activeTab === 'api-tokens' ? (
<APITokensTab user={user} /> <APITokensTab user={user} />
) : activeTab === 'users' ? ( ) : activeTab === 'users' ? (
<div className="text-center py-12"> <UsersTab user={user} />
<div className="text-xl text-gray-400">👥</div>
<div className="text-gray-500 mt-2">Módulo de Usuarios en desarrollo...</div>
</div>
) : activeTab === 'reports' ? ( ) : activeTab === 'reports' ? (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="text-xl text-gray-400">📊</div> <div className="text-xl text-gray-400">📊</div>
@@ -2715,6 +2712,435 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
) )
} }
// Componente de Gestión de Usuarios (Admin)
function UsersTab({ user }) {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [showCreateForm, setShowCreateForm] = useState(false)
const [editingUser, setEditingUser] = useState(null)
const [showInactive, setShowInactive] = useState(false)
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
role: 'mechanic'
})
useEffect(() => {
loadUsers()
}, [])
const loadUsers = async () => {
try {
const token = localStorage.getItem('token')
const response = await fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
const data = await response.json()
setUsers(data)
}
} catch (error) {
console.error('Error loading users:', error)
} finally {
setLoading(false)
}
}
const handleCreateUser = async (e) => {
e.preventDefault()
try {
const token = localStorage.getItem('token')
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
if (response.ok) {
setShowCreateForm(false)
setFormData({ username: '', email: '', password: '', role: 'mechanic' })
loadUsers()
} else {
const error = await response.json()
alert(error.detail || 'Error al crear usuario')
}
} catch (error) {
console.error('Error creating user:', error)
alert('Error al crear usuario')
}
}
const handleUpdateUser = async (userId, updates) => {
try {
const token = localStorage.getItem('token')
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
})
if (response.ok) {
setEditingUser(null)
loadUsers()
} else {
const error = await response.json()
alert(error.detail || 'Error al actualizar usuario')
}
} catch (error) {
console.error('Error updating user:', error)
alert('Error al actualizar usuario')
}
}
const handleDeactivateUser = async (userId) => {
if (!confirm('¿Está seguro que desea desactivar este usuario?')) return
try {
const token = localStorage.getItem('token')
const response = await fetch(`/api/users/${userId}/deactivate`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
loadUsers()
} else {
const error = await response.json()
alert(error.detail || 'Error al desactivar usuario')
}
} catch (error) {
console.error('Error deactivating user:', error)
alert('Error al desactivar usuario')
}
}
const handleActivateUser = async (userId) => {
try {
const token = localStorage.getItem('token')
const response = await fetch(`/api/users/${userId}/activate`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
loadUsers()
} else {
const error = await response.json()
alert(error.detail || 'Error al activar usuario')
}
} catch (error) {
console.error('Error activating user:', error)
alert('Error al activar usuario')
}
}
if (loading) {
return <div className="text-center py-12">Cargando usuarios...</div>
}
const filteredUsers = showInactive ? users : users.filter(u => u.is_active)
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-gray-800">Gestión de Usuarios</h2>
<div className="flex gap-2">
<button
onClick={() => setShowInactive(!showInactive)}
className={`px-4 py-2 rounded-lg transition ${
showInactive
? 'bg-gray-200 text-gray-700 hover:bg-gray-300'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{showInactive ? '👁️ Ocultar Inactivos' : '👁️‍🗨️ Mostrar Inactivos'}
</button>
<button
onClick={() => setShowCreateForm(true)}
className="px-4 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition"
>
+ Nuevo Usuario
</button>
</div>
</div>
{/* Lista de usuarios */}
<div className="grid gap-4">
{filteredUsers.map(u => (
<div key={u.id} className="bg-white rounded-lg shadow-md p-4">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-full flex items-center justify-center text-white font-bold">
{u.username.charAt(0).toUpperCase()}
</div>
<div>
<h3 className="font-semibold text-gray-800">{u.username}</h3>
<p className="text-sm text-gray-500">{u.email}</p>
<div className="flex gap-2 mt-1">
<span className={`text-xs px-2 py-1 rounded ${
u.role === 'admin'
? 'bg-purple-100 text-purple-700'
: 'bg-blue-100 text-blue-700'
}`}>
{u.role === 'admin' ? '👑 Admin' : '🔧 Mecánico'}
</span>
<span className={`text-xs px-2 py-1 rounded ${
u.is_active
? 'bg-green-100 text-green-700'
: 'bg-red-100 text-red-700'
}`}>
{u.is_active ? 'Activo' : 'Inactivo'}
</span>
</div>
</div>
</div>
</div>
{user.role === 'admin' && (
<div className="flex gap-2">
<button
onClick={() => setEditingUser(u)}
className="px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition"
>
Editar
</button>
{u.id !== user.id && (
<>
{u.is_active ? (
<button
onClick={() => handleDeactivateUser(u.id)}
className="px-3 py-1 text-sm bg-red-100 text-red-700 rounded hover:bg-red-200 transition"
>
Desactivar
</button>
) : (
<button
onClick={() => handleActivateUser(u.id)}
className="px-3 py-1 text-sm bg-green-100 text-green-700 rounded hover:bg-green-200 transition"
>
Activar
</button>
)}
</>
)}
</div>
)}
</div>
</div>
))}
</div>
{/* Modal de creación */}
{showCreateForm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<h3 className="text-xl font-bold mb-4">Nuevo Usuario</h3>
<form onSubmit={handleCreateUser} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nombre de usuario
</label>
<input
type="text"
required
value={formData.username}
onChange={(e) => setFormData({...formData, username: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
type="email"
required
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Contraseña
</label>
<input
type="password"
required
value={formData.password}
onChange={(e) => setFormData({...formData, password: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Rol
</label>
<select
value={formData.role}
onChange={(e) => setFormData({...formData, role: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
>
<option value="mechanic">Mecánico</option>
<option value="admin">Administrador</option>
</select>
</div>
<div className="flex gap-2 pt-4">
<button
type="button"
onClick={() => {
setShowCreateForm(false)
setFormData({ username: '', email: '', password: '', role: 'mechanic' })
}}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
>
Cancelar
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition"
>
Crear Usuario
</button>
</div>
</form>
</div>
</div>
)}
{/* Modal de edición */}
{editingUser && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<h3 className="text-xl font-bold mb-4">Editar Usuario</h3>
<form onSubmit={(e) => {
e.preventDefault()
const updates = {
username: editingUser.username,
email: editingUser.email,
role: editingUser.role
}
handleUpdateUser(editingUser.id, updates)
}} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nombre de usuario
</label>
<input
type="text"
required
value={editingUser.username}
onChange={(e) => setEditingUser({...editingUser, username: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
type="email"
required
value={editingUser.email}
onChange={(e) => setEditingUser({...editingUser, email: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Rol
</label>
<select
value={editingUser.role}
onChange={(e) => setEditingUser({...editingUser, role: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
>
<option value="mechanic">Mecánico</option>
<option value="admin">Administrador</option>
</select>
</div>
<div className="border-t pt-4">
<button
type="button"
onClick={async () => {
const newPassword = prompt('Ingrese la nueva contraseña:')
if (newPassword && newPassword.length >= 6) {
try {
const token = localStorage.getItem('token')
const response = await fetch(`/api/users/${editingUser.id}/password`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ new_password: newPassword })
})
if (response.ok) {
alert('Contraseña actualizada correctamente')
} else {
const error = await response.json()
alert(error.detail || 'Error al actualizar contraseña')
}
} catch (error) {
console.error('Error:', error)
alert('Error al actualizar contraseña')
}
} else if (newPassword !== null) {
alert('La contraseña debe tener al menos 6 caracteres')
}
}}
className="w-full px-4 py-2 bg-yellow-100 text-yellow-700 rounded-lg hover:bg-yellow-200 transition"
>
🔑 Resetear Contraseña
</button>
</div>
<div className="flex gap-2 pt-2">
<button
type="button"
onClick={() => setEditingUser(null)}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
>
Cancelar
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition"
>
Guardar Cambios
</button>
</div>
</form>
</div>
</div>
)}
</div>
)
}
export default App export default App

View File

@@ -10,3 +10,4 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
EOSQL EOSQL
echo "Database initialization completed successfully!" echo "Database initialization completed successfully!"