diff --git a/App.tsx b/App.tsx index ba60bc5..5d75010 100644 --- a/App.tsx +++ b/App.tsx @@ -5,6 +5,7 @@ import { Dashboard } from './pages/Dashboard'; import { UserDetail } from './pages/UserDetail'; import { AttendanceDetail } from './pages/AttendanceDetail'; import { SuperAdmin } from './pages/SuperAdmin'; +import { ApiKeys } from './pages/ApiKeys'; import { TeamManagement } from './pages/TeamManagement'; import { Teams } from './pages/Teams'; import { Funnels } from './pages/Funnels'; @@ -81,6 +82,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> diff --git a/components/Layout.tsx b/components/Layout.tsx index 1400522..ca1fed3 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -3,7 +3,7 @@ import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { LayoutDashboard, Users, UserCircle, Bell, Search, Menu, X, LogOut, Hexagon, Settings, Building2, Sun, Moon, Loader2, Layers, - ChevronLeft, ChevronRight + ChevronLeft, ChevronRight, Key } from 'lucide-react'; import { getAttendances, getUsers, getUserById, logout, searchGlobal, @@ -249,14 +249,14 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => <> {!isSidebarCollapsed && (
- Super Admin + Super Admin
)} + - )} - + )} {/* User Profile Mini - Now Clickable to Profile */}
diff --git a/pages/ApiKeys.tsx b/pages/ApiKeys.tsx new file mode 100644 index 0000000..711f30a --- /dev/null +++ b/pages/ApiKeys.tsx @@ -0,0 +1,198 @@ +import React, { useState, useEffect } from 'react'; +import { Key, Loader2, Plus, Trash2, Copy, CheckCircle2, Building2 } from 'lucide-react'; +import { getApiKeys, createApiKey, deleteApiKey, getTenants } from '../services/dataService'; +import { Tenant } from '../types'; + +export const ApiKeys: React.FC = () => { + const [tenants, setTenants] = useState([]); + const [selectedTenantId, setSelectedTenantId] = useState(''); + const [apiKeys, setApiKeys] = useState([]); + const [loading, setLoading] = useState(true); + + const [newKeyName, setNewKeyName] = useState(''); + const [isGeneratingKey, setIsGeneratingKey] = useState(false); + const [generatedKey, setGeneratedKey] = useState(null); + + useEffect(() => { + const fetchInitialData = async () => { + try { + const fetchedTenants = await getTenants(); + setTenants(fetchedTenants); + if (fetchedTenants.length > 0) { + const defaultTenant = fetchedTenants.find(t => t.id !== 'system') || fetchedTenants[0]; + setSelectedTenantId(defaultTenant.id); + } + } catch (err) { + console.error("Failed to load tenants", err); + } finally { + setLoading(false); + } + }; + fetchInitialData(); + }, []); + + useEffect(() => { + if (selectedTenantId) { + loadApiKeys(selectedTenantId); + setGeneratedKey(null); + } + }, [selectedTenantId]); + + const loadApiKeys = async (tenantId: string) => { + setLoading(true); + try { + const keys = await getApiKeys(tenantId); + setApiKeys(keys); + } catch (e) { + console.error("Failed to load API keys", e); + } finally { + setLoading(false); + } + }; + + const handleGenerateApiKey = async () => { + if (!newKeyName.trim() || !selectedTenantId) return; + setIsGeneratingKey(true); + try { + const res = await createApiKey({ name: newKeyName, tenantId: selectedTenantId }); + setGeneratedKey(res.secret_key); + setNewKeyName(''); + loadApiKeys(selectedTenantId); + } catch (err: any) { + alert(err.message || 'Erro ao gerar chave.'); + } finally { + setIsGeneratingKey(false); + } + }; + + const handleRevokeApiKey = async (id: string) => { + if (!selectedTenantId) return; + if (confirm('Tem certeza? Todas as integrações usando esta chave pararão de funcionar imediatamente.')) { + const success = await deleteApiKey(id); + if (success) { + loadApiKeys(selectedTenantId); + } + } + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + alert('Chave copiada para a área de transferência!'); + }; + + return ( +
+
+
+

Integrações via API

+

Gerencie chaves de API para permitir que sistemas externos como o n8n se conectem a organizações específicas.

+
+
+ +
+
+
+ +
+ +
+ +
+
+
+
+ + {generatedKey && ( +
+

+ Chave Gerada com Sucesso! +

+

+ Copie esta chave agora. Por motivos de segurança, ela não será exibida novamente. +

+
+ + {generatedKey} + + +
+
+ )} + +
+ setNewKeyName(e.target.value)} + className="flex-1 p-3 border border-zinc-200 dark:border-dark-border rounded-xl bg-zinc-50 dark:bg-dark-input text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-600 focus:outline-none focus:ring-2 focus:ring-brand-yellow/20 focus:border-brand-yellow sm:text-sm transition-all" + /> + +
+ + {loading ? ( +
+ ) : apiKeys.length > 0 ? ( +
+ + + + + + + + + + + {apiKeys.map(key => ( + + + + + + + ))} + +
Nome da IntegraçãoChave (Mascarada)Último UsoAções
+ + {key.name} + {key.masked_key} + {key.last_used_at ? new Date(key.last_used_at).toLocaleString() : 'Nunca'} + + +
+
+ ) : ( +
+ +

Nenhuma chave de API gerada

+

Crie uma nova chave para conectar sistemas externos a esta organização.

+
+ )} +
+
+ ); +}; diff --git a/pages/UserProfile.tsx b/pages/UserProfile.tsx index c341773..515fb0a 100644 --- a/pages/UserProfile.tsx +++ b/pages/UserProfile.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Camera, Save, Mail, User as UserIcon, Building, Shield, Loader2, CheckCircle2, Bell, Key, Trash2, Copy, Plus } from 'lucide-react'; -import { getUserById, getTenants, getTeams, updateUser, uploadAvatar, getApiKeys, createApiKey, deleteApiKey } from '../services/dataService'; +import { Camera, Save, Mail, User as UserIcon, Building, Shield, Loader2, CheckCircle2, Bell } from 'lucide-react'; +import { getUserById, getTenants, getTeams, updateUser, uploadAvatar } from '../services/dataService'; import { User, Tenant } from '../types'; export const UserProfile: React.FC = () => { @@ -16,25 +16,9 @@ export const UserProfile: React.FC = () => { const [bio, setBio] = useState(''); const [email, setEmail] = useState(''); - // API Keys state - const [apiKeys, setApiKeys] = useState([]); - const [newKeyName, setNewKeyName] = useState(''); - const [isGeneratingKey, setIsGeneratingKey] = useState(false); - const [generatedKey, setGeneratedKey] = useState(null); - - const fetchApiKeys = async (tenantId: string) => { - try { - const keys = await getApiKeys(tenantId); - setApiKeys(keys); - } catch (e) { - console.error("Failed to load API keys", e); - } - }; - useEffect(() => { const fetchUserAndTenant = async () => { const storedUserId = localStorage.getItem('ctms_user_id'); - const storedTenantId = localStorage.getItem('ctms_tenant_id'); if (storedUserId) { try { @@ -45,12 +29,6 @@ export const UserProfile: React.FC = () => { setBio(fetchedUser.bio || ''); setEmail(fetchedUser.email); - if (fetchedUser.role === 'admin' || fetchedUser.role === 'super_admin') { - if (storedTenantId) { - fetchApiKeys(storedTenantId); - } - } - // Fetch tenant info const tenants = await getTenants(); const userTenant = tenants.find(t => t.id === fetchedUser.tenant_id); @@ -72,36 +50,6 @@ export const UserProfile: React.FC = () => { fetchUserAndTenant(); }, []); - const handleGenerateApiKey = async () => { - if (!newKeyName.trim() || !tenant) return; - setIsGeneratingKey(true); - try { - const res = await createApiKey({ name: newKeyName, tenantId: tenant.id }); - setGeneratedKey(res.secret_key); - setNewKeyName(''); - fetchApiKeys(tenant.id); - } catch (err: any) { - alert(err.message || 'Erro ao gerar chave.'); - } finally { - setIsGeneratingKey(false); - } - }; - - const handleRevokeApiKey = async (id: string) => { - if (!tenant) return; - if (confirm('Tem certeza? Todas as integrações usando esta chave pararão de funcionar imediatamente.')) { - const success = await deleteApiKey(id); - if (success) { - fetchApiKeys(tenant.id); - } - } - }; - - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); - alert('Chave copiada para a área de transferência!'); - }; - const handleAvatarClick = () => { fileInputRef.current?.click(); }; @@ -373,104 +321,6 @@ export const UserProfile: React.FC = () => {
- - {/* API Keys Section (Only for Admins/Super Admins) */} - {(user.role === 'admin' || user.role === 'super_admin') && ( -
-
-
-
-
- -
-
-

Integrações via API (n8n, etc)

-

- Gerencie as chaves de acesso para que sistemas externos possam enviar dados (como atendimentos) para sua organização. -

-
-
- - {generatedKey && ( -
-

- Chave Gerada com Sucesso! -

-

- Copie esta chave agora. Por motivos de segurança, ela não será exibida novamente. -

-
- - {generatedKey} - - -
-
- )} - -
- setNewKeyName(e.target.value)} - className="flex-1 p-3 border border-zinc-200 dark:border-zinc-800 rounded-lg bg-zinc-50 dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-600 focus:outline-none focus:ring-2 focus:ring-brand-yellow/20 focus:border-brand-yellow sm:text-sm transition-all" - /> - -
- - {apiKeys.length > 0 ? ( -
- - - - - - - - - - - {apiKeys.map(key => ( - - - - - - - ))} - -
NomeChaveÚltimo UsoAções
{key.name}{key.masked_key} - {key.last_used_at ? new Date(key.last_used_at).toLocaleDateString() : 'Nunca'} - - -
-
- ) : ( -
- - Nenhuma chave de API gerada. -
- )} -
-
-
- )} );