diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 3c48a3f..ead1bab 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -116,7 +116,7 @@ Lista todos los usuarios del sistema. **Query Params:** - `skip` (int, default: 0) - Paginación - `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:** ``` @@ -234,6 +234,7 @@ Authorization: Bearer {token} **Body:** ```json { + "username": "nuevo_username", "email": "nuevo_email@syntria.com", "full_name": "Nombre Actualizado", "role": "admin" @@ -244,7 +245,7 @@ Authorization: Bearer {token} ```json { "id": 2, - "username": "mecanico", + "username": "nuevo_username", "email": "nuevo_email@syntria.com", "full_name": "Nombre Actualizado", "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:** -- `400 Bad Request` - Email ya en uso +- `400 Bad Request` - Email o username ya en uso - `403 Forbidden` - No tienes permisos - `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 ### 1. Listar Checklists @@ -771,7 +926,9 @@ Content-Type: application/json ## 🔐 Roles y Permisos ### 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 - ✅ Ver todas las inspecciones - ✅ Crear inspecciones @@ -784,8 +941,11 @@ Content-Type: application/json - ✅ Responder preguntas de inspección - ✅ Usar análisis con IA - ✅ Actualizar su propio perfil +- ✅ Cambiar su propia contraseña +- ✅ Gestionar sus propios tokens API - ❌ No puede gestionar usuarios - ❌ No puede gestionar checklists +- ❌ No puede ver tokens de otros usuarios --- diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d1a09c7..a27d3f1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -337,10 +337,7 @@ function DashboardPage({ user, setUser }) { ) : activeTab === 'api-tokens' ? ( ) : activeTab === 'users' ? ( -
-
👥
-
Módulo de Usuarios en desarrollo...
-
+ ) : activeTab === 'reports' ? (
📊
@@ -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
Cargando usuarios...
+ } + + const filteredUsers = showInactive ? users : users.filter(u => u.is_active) + + return ( +
+
+

Gestión de Usuarios

+
+ + +
+
+ + {/* Lista de usuarios */} +
+ {filteredUsers.map(u => ( +
+
+
+
+
+ {u.username.charAt(0).toUpperCase()} +
+
+

{u.username}

+

{u.email}

+
+ + {u.role === 'admin' ? '👑 Admin' : '🔧 Mecánico'} + + + {u.is_active ? 'Activo' : 'Inactivo'} + +
+
+
+
+ + {user.role === 'admin' && ( +
+ + {u.id !== user.id && ( + <> + {u.is_active ? ( + + ) : ( + + )} + + )} +
+ )} +
+
+ ))} +
+ + {/* Modal de creación */} + {showCreateForm && ( +
+
+

Nuevo Usuario

+
+
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + +
+ +
+ + +
+
+
+
+ )} + + {/* Modal de edición */} + {editingUser && ( +
+
+

Editar Usuario

+
{ + e.preventDefault() + const updates = { + username: editingUser.username, + email: editingUser.email, + role: editingUser.role + } + handleUpdateUser(editingUser.id, updates) + }} className="space-y-4"> +
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + +
+ +
+ +
+ +
+ + +
+
+
+
+ )} +
+ ) +} + export default App - diff --git a/init-db.sh b/init-db.sh index b5b7fe6..596cbff 100644 --- a/init-db.sh +++ b/init-db.sh @@ -10,3 +10,4 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E EOSQL echo "Database initialization completed successfully!" +