feat: implement n8n api integration endpoints and api key management
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m6s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m6s
- Added api_keys table to database schema. - Added API Key authentication middleware to express router. - Created GET /api/integration/users endpoint for n8n to map agents. - Created POST /api/integration/attendances endpoint to accept webhooks from n8n. - Added UI in UserProfile (for Admins/Owners) to generate, view, and revoke API keys.
This commit is contained in:
205
backend/index.js
205
backend/index.js
@@ -101,11 +101,37 @@ app.use('/uploads', express.static(uploadDir, {
|
||||
const apiRouter = express.Router();
|
||||
|
||||
// Middleware de autenticação
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const authenticateToken = async (req, res, next) => {
|
||||
// Ignorar rotas de auth
|
||||
if (req.path.startsWith('/auth/')) return next();
|
||||
|
||||
const authHeader = req.headers['authorization'];
|
||||
|
||||
// API Key Authentication for n8n/External Integrations
|
||||
if (authHeader && authHeader.startsWith('Bearer fasto_sk_')) {
|
||||
const apiKey = authHeader.split(' ')[1];
|
||||
try {
|
||||
const [keys] = await pool.query('SELECT * FROM api_keys WHERE secret_key = ?', [apiKey]);
|
||||
if (keys.length === 0) return res.status(401).json({ error: 'Chave de API inválida.' });
|
||||
|
||||
// Update last used timestamp
|
||||
await pool.query('UPDATE api_keys SET last_used_at = CURRENT_TIMESTAMP WHERE id = ?', [keys[0].id]);
|
||||
|
||||
// Attach a "system/bot" user identity to the request based on the tenant
|
||||
req.user = {
|
||||
id: 'bot_integration',
|
||||
tenant_id: keys[0].tenant_id,
|
||||
role: 'admin', // Give integration admin privileges within its tenant
|
||||
is_api_key: true
|
||||
};
|
||||
return next();
|
||||
} catch (error) {
|
||||
console.error('API Key validation error:', error);
|
||||
return res.status(500).json({ error: 'Erro ao validar chave de API.' });
|
||||
}
|
||||
}
|
||||
|
||||
// Standard JWT Authentication
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) return res.status(401).json({ error: 'Token não fornecido.' });
|
||||
@@ -305,6 +331,17 @@ apiRouter.post('/auth/reset-password', async (req, res) => {
|
||||
[crypto.randomUUID(), n.id, 'info', 'Novo Membro Ativo', `${name} concluiu o cadastro e já pode acessar o sistema.`, `/users/${user.id}`]
|
||||
);
|
||||
}
|
||||
|
||||
// If the new user is an admin, notify super_admins too
|
||||
if (user.role === 'admin') {
|
||||
const [superAdmins] = await pool.query("SELECT id FROM users WHERE role = 'super_admin'");
|
||||
for (const sa of superAdmins) {
|
||||
await pool.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[crypto.randomUUID(), sa.id, 'success', 'Admin Ativo', `O admin ${name} da organização configurou sua conta.`, `/super-admin`]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standard password reset, just update the hash
|
||||
@@ -459,6 +496,18 @@ apiRouter.put('/users/:id', async (req, res) => {
|
||||
'UPDATE users SET name = ?, bio = ?, email = ?, role = ?, team_id = ?, status = ?, sound_enabled = ? WHERE id = ?',
|
||||
[name || existing[0].name, bio !== undefined ? bio : existing[0].bio, finalEmail, finalRole, finalTeamId || null, finalStatus, finalSoundEnabled, req.params.id]
|
||||
);
|
||||
|
||||
// Trigger Notification for Team Change
|
||||
if (finalTeamId && finalTeamId !== existing[0].team_id && existing[0].status === 'active') {
|
||||
const [team] = await pool.query('SELECT name FROM teams WHERE id = ?', [finalTeamId]);
|
||||
if (team.length > 0) {
|
||||
await pool.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[crypto.randomUUID(), req.params.id, 'info', 'Novo Time', `Você foi adicionado ao time ${team[0].name}.`, '/']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ message: 'User updated successfully.' });
|
||||
} catch (error) { console.error('Update user error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -842,6 +891,145 @@ apiRouter.get('/attendances/:id', async (req, res) => {
|
||||
} catch (error) { res.status(500).json({ error: error.message }); }
|
||||
});
|
||||
|
||||
// --- API Key Management Routes ---
|
||||
apiRouter.get('/api-keys', requireRole(['admin', 'owner', 'super_admin']), async (req, res) => {
|
||||
try {
|
||||
const { tenantId } = req.query;
|
||||
const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id;
|
||||
if (!effectiveTenantId || effectiveTenantId === 'all') return res.json([]);
|
||||
|
||||
const [rows] = await pool.query('SELECT id, name, created_at, last_used_at, CONCAT(SUBSTRING(secret_key, 1, 14), "...") as masked_key FROM api_keys WHERE tenant_id = ?', [effectiveTenantId]);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.post('/api-keys', requireRole(['admin', 'owner', 'super_admin']), async (req, res) => {
|
||||
const { name, tenantId } = req.body;
|
||||
const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id;
|
||||
try {
|
||||
const id = `apk_${crypto.randomUUID().split('-')[0]}`;
|
||||
// Generate a strong, random 32-byte hex string for the secret key
|
||||
const secretKey = `fasto_sk_${crypto.randomBytes(32).toString('hex')}`;
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO api_keys (id, tenant_id, name, secret_key) VALUES (?, ?, ?, ?)',
|
||||
[id, effectiveTenantId, name || 'Nova Integração API', secretKey]
|
||||
);
|
||||
|
||||
// We only return the actual secret key ONCE during creation.
|
||||
res.status(201).json({ id, secret_key: secretKey, message: 'Chave criada. Salve-a agora, ela não será exibida novamente.' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.delete('/api-keys/:id', requireRole(['admin', 'owner', 'super_admin']), async (req, res) => {
|
||||
try {
|
||||
const [existing] = await pool.query('SELECT tenant_id FROM api_keys WHERE id = ?', [req.params.id]);
|
||||
if (existing.length === 0) return res.status(404).json({ error: 'Chave não encontrada' });
|
||||
if (req.user.role !== 'super_admin' && existing[0].tenant_id !== req.user.tenant_id) return res.status(403).json({ error: 'Acesso negado' });
|
||||
|
||||
await pool.query('DELETE FROM api_keys WHERE id = ?', [req.params.id]);
|
||||
res.json({ message: 'Chave de API revogada com sucesso.' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// --- External Integration API (n8n) ---
|
||||
apiRouter.get('/integration/users', requireRole(['admin']), async (req, res) => {
|
||||
if (!req.user.is_api_key) return res.status(403).json({ error: 'Endpoint restrito a chaves de API.' });
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT u.id, u.name, u.email, t.name as team_name FROM users u LEFT JOIN teams t ON u.team_id = t.id WHERE u.tenant_id = ? AND u.status = "active"',
|
||||
[req.user.tenant_id]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.post('/integration/attendances', requireRole(['admin']), async (req, res) => {
|
||||
if (!req.user.is_api_key) return res.status(403).json({ error: 'Endpoint restrito a chaves de API.' });
|
||||
|
||||
const {
|
||||
user_id,
|
||||
origin,
|
||||
funnel_stage,
|
||||
summary,
|
||||
score,
|
||||
first_response_time_min,
|
||||
handling_time_min,
|
||||
product_requested,
|
||||
product_sold,
|
||||
converted,
|
||||
attention_points,
|
||||
improvement_points
|
||||
} = req.body;
|
||||
|
||||
if (!user_id || !origin || !funnel_stage || !summary) {
|
||||
return res.status(400).json({ error: 'Campos obrigatórios ausentes: user_id, origin, funnel_stage, summary' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate user belongs to the API Key's tenant
|
||||
const [users] = await pool.query('SELECT id FROM users WHERE id = ? AND tenant_id = ? AND status = "active"', [user_id, req.user.tenant_id]);
|
||||
if (users.length === 0) return res.status(400).json({ error: 'user_id inválido, inativo ou não pertence a esta organização.' });
|
||||
|
||||
const attId = `att_${crypto.randomUUID().split('-')[0]}`;
|
||||
await pool.query(
|
||||
`INSERT INTO attendances (
|
||||
id, tenant_id, user_id, summary, score,
|
||||
first_response_time_min, handling_time_min,
|
||||
funnel_stage, origin, product_requested, product_sold,
|
||||
converted, attention_points, improvement_points
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
attId,
|
||||
req.user.tenant_id,
|
||||
user_id,
|
||||
summary,
|
||||
score || 0,
|
||||
first_response_time_min || 0,
|
||||
handling_time_min || 0,
|
||||
funnel_stage,
|
||||
origin,
|
||||
product_requested || null,
|
||||
product_sold || null,
|
||||
converted ? 1 : 0,
|
||||
attention_points ? JSON.stringify(attention_points) : null,
|
||||
improvement_points ? JSON.stringify(improvement_points) : null
|
||||
]
|
||||
);
|
||||
|
||||
// Automation Trigger: "Venda Fechada!" (Ganhos)
|
||||
if (converted) {
|
||||
// Find the user's manager/admin
|
||||
const [managers] = await pool.query(
|
||||
"SELECT id FROM users WHERE tenant_id = ? AND role IN ('admin', 'manager') AND id != ?",
|
||||
[req.user.tenant_id, user_id]
|
||||
);
|
||||
const [agentInfo] = await pool.query("SELECT name FROM users WHERE id = ?", [user_id]);
|
||||
const agentName = agentInfo[0]?.name || 'Um agente';
|
||||
|
||||
for (const m of managers) {
|
||||
await pool.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[crypto.randomUUID(), m.id, 'success', 'Venda Fechada!', `${agentName} converteu um lead em ${funnel_stage}.`, `/attendances/${attId}`]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({ id: attId, message: 'Atendimento registrado com sucesso.' });
|
||||
} catch (error) {
|
||||
console.error('Integration Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// --- Tenant Routes ---
|
||||
apiRouter.get('/tenants', requireRole(['super_admin']), async (req, res) => {
|
||||
try {
|
||||
@@ -1138,6 +1326,21 @@ const provisionSuperAdmin = async (retries = 10, delay = 10000) => {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// Create api_keys table for external integrations (n8n)
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id varchar(36) NOT NULL,
|
||||
tenant_id varchar(36) NOT NULL,
|
||||
name varchar(255) NOT NULL,
|
||||
secret_key varchar(255) NOT NULL,
|
||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY secret_key (secret_key),
|
||||
KEY tenant_id (tenant_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// Add funnel_id to teams
|
||||
try {
|
||||
await connection.query("ALTER TABLE teams ADD COLUMN funnel_id VARCHAR(36) DEFAULT NULL");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
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 { 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 { User, Tenant } from '../types';
|
||||
|
||||
export const UserProfile: React.FC = () => {
|
||||
@@ -16,9 +16,26 @@ export const UserProfile: React.FC = () => {
|
||||
const [bio, setBio] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
// API Keys state
|
||||
const [apiKeys, setApiKeys] = useState<any[]>([]);
|
||||
const [newKeyName, setNewKeyName] = useState('');
|
||||
const [isGeneratingKey, setIsGeneratingKey] = useState(false);
|
||||
const [generatedKey, setGeneratedKey] = useState<string | null>(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 {
|
||||
const fetchedUser = await getUserById(storedUserId);
|
||||
@@ -28,6 +45,12 @@ 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);
|
||||
@@ -49,6 +72,36 @@ 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();
|
||||
};
|
||||
@@ -320,6 +373,104 @@ export const UserProfile: React.FC = () => {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Keys Section (Only for Admins/Super Admins) */}
|
||||
{(user.role === 'admin' || user.role === 'super_admin') && (
|
||||
<div className="lg:col-span-3">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl shadow-sm border border-zinc-200 dark:border-zinc-800 overflow-hidden transition-colors">
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-xl">
|
||||
<Key size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-zinc-900 dark:text-zinc-100">Integrações via API (n8n, etc)</h2>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
|
||||
Gerencie as chaves de acesso para que sistemas externos possam enviar dados (como atendimentos) para sua organização.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{generatedKey && (
|
||||
<div className="mb-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl">
|
||||
<h4 className="font-bold text-green-800 dark:text-green-300 flex items-center gap-2 mb-2">
|
||||
<CheckCircle2 size={18} /> Chave Gerada com Sucesso!
|
||||
</h4>
|
||||
<p className="text-sm text-green-700 dark:text-green-400 mb-3">
|
||||
Copie esta chave agora. Por motivos de segurança, ela <strong>não</strong> será exibida novamente.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 bg-white dark:bg-zinc-950 p-3 rounded-lg text-sm font-mono text-zinc-800 dark:text-zinc-200 border border-green-200 dark:border-green-800 break-all">
|
||||
{generatedKey}
|
||||
</code>
|
||||
<button onClick={() => copyToClipboard(generatedKey)} className="p-3 bg-white dark:bg-zinc-950 border border-green-200 dark:border-green-800 rounded-lg hover:bg-green-100 dark:hover:bg-zinc-800 text-green-700 dark:text-green-400 transition-colors" title="Copiar">
|
||||
<Copy size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 mb-8">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nome da integração (ex: n8n WhatsApp)"
|
||||
value={newKeyName}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={handleGenerateApiKey}
|
||||
disabled={isGeneratingKey || !newKeyName.trim()}
|
||||
className="px-6 py-3 bg-zinc-900 dark:bg-brand-yellow text-white dark:text-zinc-950 rounded-lg font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-opacity disabled:opacity-50 shrink-0"
|
||||
>
|
||||
{isGeneratingKey ? <Loader2 className="animate-spin" size={18} /> : <Plus size={18} />}
|
||||
Gerar Chave
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{apiKeys.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 dark:border-zinc-800">
|
||||
<th className="py-3 px-4 text-xs font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Nome</th>
|
||||
<th className="py-3 px-4 text-xs font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Chave</th>
|
||||
<th className="py-3 px-4 text-xs font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Último Uso</th>
|
||||
<th className="py-3 px-4 text-xs font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider text-right">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-800/50">
|
||||
{apiKeys.map(key => (
|
||||
<tr key={key.id} className="hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors">
|
||||
<td className="py-3 px-4 text-sm font-medium text-zinc-900 dark:text-zinc-100">{key.name}</td>
|
||||
<td className="py-3 px-4 text-sm font-mono text-zinc-500 dark:text-zinc-400">{key.masked_key}</td>
|
||||
<td className="py-3 px-4 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{key.last_used_at ? new Date(key.last_used_at).toLocaleDateString() : 'Nunca'}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
<button
|
||||
onClick={() => handleRevokeApiKey(key.id)}
|
||||
className="p-2 text-zinc-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors inline-flex"
|
||||
title="Revogar Chave"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center p-8 border border-dashed border-zinc-200 dark:border-zinc-800 rounded-xl text-zinc-500 dark:text-zinc-400">
|
||||
<Key size={32} className="mx-auto mb-3 opacity-20" />
|
||||
Nenhuma chave de API gerada.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -176,6 +176,46 @@ export const deleteFunnelStage = async (id: string): Promise<boolean> => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- API Keys Functions ---
|
||||
export const getApiKeys = async (tenantId: string): Promise<any[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api-keys?tenantId=${tenantId}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao buscar chaves');
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("API Error (getApiKeys):", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const createApiKey = async (data: { name: string, tenantId: string }): Promise<any> => {
|
||||
const response = await fetch(`${API_URL}/api-keys`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Erro ao criar chave de API');
|
||||
}
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const deleteApiKey = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api-keys/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error("API Error (deleteApiKey):", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const searchGlobal = async (query: string): Promise<{ members: User[], teams: any[], attendances: any[], organizations?: any[] }> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/search?q=${encodeURIComponent(query)}`, {
|
||||
|
||||
Reference in New Issue
Block a user