diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 82561de..d1a09c7 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -334,6 +334,8 @@ function DashboardPage({ user, setUser }) {
) : activeTab === 'settings' ? (
+ ) : activeTab === 'api-tokens' ? (
+
) : activeTab === 'users' ? (
馃懃
@@ -597,6 +599,310 @@ function SettingsTab({ user }) {
)
}
+function APITokensTab({ user }) {
+ const [tokens, setTokens] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [showCreateForm, setShowCreateForm] = useState(false)
+ const [newTokenDescription, setNewTokenDescription] = useState('')
+ const [createdToken, setCreatedToken] = useState(null)
+ const [creating, setCreating] = useState(false)
+
+ useEffect(() => {
+ loadTokens()
+ }, [])
+
+ const loadTokens = async () => {
+ try {
+ const token = localStorage.getItem('token')
+ const API_URL = import.meta.env.VITE_API_URL || ''
+
+ const response = await fetch(`${API_URL}/api/users/me/tokens`, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ })
+
+ if (response.ok) {
+ const data = await response.json()
+ setTokens(data)
+ }
+
+ setLoading(false)
+ } catch (error) {
+ console.error('Error loading tokens:', error)
+ setLoading(false)
+ }
+ }
+
+ const handleCreateToken = async (e) => {
+ e.preventDefault()
+ setCreating(true)
+
+ try {
+ const token = localStorage.getItem('token')
+ const API_URL = import.meta.env.VITE_API_URL || ''
+
+ const response = await fetch(`${API_URL}/api/users/me/tokens`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ description: newTokenDescription || null }),
+ })
+
+ if (response.ok) {
+ const data = await response.json()
+ setCreatedToken(data.token)
+ setShowCreateForm(false)
+ setNewTokenDescription('')
+ loadTokens()
+ } else {
+ alert('Error al crear token')
+ }
+ } catch (error) {
+ console.error('Error:', error)
+ alert('Error al crear token')
+ } finally {
+ setCreating(false)
+ }
+ }
+
+ const handleRevokeToken = async (tokenId) => {
+ if (!confirm('驴Est谩s seguro de revocar este token? Esta acci贸n no se puede deshacer.')) {
+ return
+ }
+
+ try {
+ const token = localStorage.getItem('token')
+ const API_URL = import.meta.env.VITE_API_URL || ''
+
+ const response = await fetch(`${API_URL}/api/users/me/tokens/${tokenId}`, {
+ method: 'DELETE',
+ headers: { 'Authorization': `Bearer ${token}` }
+ })
+
+ if (response.ok) {
+ alert('Token revocado correctamente')
+ loadTokens()
+ } else {
+ alert('Error al revocar token')
+ }
+ } catch (error) {
+ console.error('Error:', error)
+ alert('Error al revocar token')
+ }
+ }
+
+ const copyToClipboard = (text) => {
+ navigator.clipboard.writeText(text)
+ alert('Token copiado al portapapeles')
+ }
+
+ return (
+
+
+
+
Mis API Tokens
+
+ Genera tokens para acceder a la API sin necesidad de login
+
+
+
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-all transform hover:scale-105 shadow-lg"
+ >
+ + Generar Nuevo Token
+
+
+
+ {/* Modal de Token Creado */}
+ {createdToken && (
+
+
+
+
Token Creado Exitosamente
+
+ 鈿狅笍 Guarda este token ahora. No podr谩s verlo de nuevo.
+
+
+
+
+
+ {createdToken}
+ copyToClipboard(createdToken)}
+ className="px-3 py-1 bg-indigo-600 text-white rounded hover:bg-indigo-700 transition flex-shrink-0"
+ >
+ 馃搵 Copiar
+
+
+
+
+
+
Ejemplo de uso:
+
+ curl -H "Authorization: Bearer {createdToken}" http://tu-api.com/api/inspections
+
+
+
+
setCreatedToken(null)}
+ className="w-full px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition"
+ >
+ Cerrar
+
+
+
+ )}
+
+ {/* Formulario de Crear Token */}
+ {showCreateForm && (
+
+
+
Generar Nuevo Token
+
+
+
+
+ )}
+
+ {/* Lista de Tokens */}
+ {loading ? (
+
+ ) : tokens.length === 0 ? (
+
+
馃攽
+
No tienes tokens API creados
+
Genera uno para acceder a la API sin login
+
+ ) : (
+
+ {tokens.map((token) => (
+
+
+
+
+ 馃攽
+
+ {token.description || 'Token sin descripci贸n'}
+
+
+ {token.is_active ? 'Activo' : 'Revocado'}
+
+
+
+
+
+ Creado: {' '}
+ {new Date(token.created_at).toLocaleDateString('es-ES', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })}
+
+ {token.last_used_at && (
+
+ 脷ltimo uso: {' '}
+ {new Date(token.last_used_at).toLocaleDateString('es-ES', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })}
+
+ )}
+ {!token.last_used_at && (
+
+ 鈿狅笍 Nunca usado
+
+ )}
+
+
+
+ {token.is_active && (
+
handleRevokeToken(token.id)}
+ className="ml-4 px-3 py-1 text-sm bg-red-100 text-red-700 rounded hover:bg-red-200 transition"
+ >
+ Revocar
+
+ )}
+
+
+ ))}
+
+ )}
+
+ {/* Informaci贸n de Ayuda */}
+
+
+
鈩癸笍
+
+
驴C贸mo usar los tokens API?
+
+ Los tokens API te permiten acceder a todos los endpoints sin necesidad de hacer login.
+ Son perfectos para integraciones, scripts automatizados o aplicaciones externas.
+
+
+ Incluye el token en el header Authorization de tus requests:
+
+
+ Authorization: Bearer syntria_tu_token_aqui
+
+
+
+
+
+ )
+}
+
function QuestionsManagerModal({ checklist, onClose }) {
const [questions, setQuestions] = useState([])
const [loading, setLoading] = useState(true)
diff --git a/frontend/src/Sidebar.jsx b/frontend/src/Sidebar.jsx
index 7d6151c..c7350c2 100644
--- a/frontend/src/Sidebar.jsx
+++ b/frontend/src/Sidebar.jsx
@@ -81,6 +81,20 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
{sidebarOpen &&
Reportes }
+
+ setActiveTab('api-tokens')}
+ className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
+ activeTab === 'api-tokens'
+ ? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
+ : 'text-indigo-200 hover:bg-indigo-900/50'
+ }`}
+ title={!sidebarOpen ? 'API Tokens' : ''}
+ >
+ 馃攽
+ {sidebarOpen && API Tokens }
+
+
setActiveTab('settings')}