Files
fasto/services/dataService.ts
Cauê Faleiros 7cb78f13c0 fix: pad base64 string when parsing jwt during impersonation exit
- Prevented the browser's atob() function from throwing a 'String contains an invalid character' exception by adding proper Base64 padding to the JWT payload before decoding.
2026-03-11 15:13:19 -03:00

509 lines
16 KiB
TypeScript

import { Attendance, DashboardFilter, User } from '../types';
// URL do Backend
// Em produção (import.meta.env.PROD), usa caminho relativo '/api' pois o backend serve o frontend
// Em desenvolvimento, aponta para o localhost:3001
const API_URL = import.meta.env.PROD ? '/api' : 'http://localhost:3001/api';
const getHeaders = () => {
const token = localStorage.getItem('ctms_token');
// Evitar enviar "undefined" ou "null" como strings se o localStorage estiver corrompido
if (!token || token === 'undefined' || token === 'null') return { 'Content-Type': 'application/json' };
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
};
};
export const getNotifications = async (): Promise<any[]> => {
try {
const response = await fetch(`${API_URL}/notifications`, {
headers: getHeaders()
});
if (!response.ok) throw new Error('Failed to fetch notifications');
return await response.json();
} catch (error) {
console.error("API Error (getNotifications):", error);
return [];
}
};
export const markNotificationAsRead = async (id: string): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/notifications/${id}`, {
method: 'PUT',
headers: getHeaders()
});
return response.ok;
} catch (error) {
console.error("API Error (markNotificationAsRead):", error);
return false;
}
};
export const markAllNotificationsAsRead = async (): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/notifications/read-all`, {
method: 'PUT',
headers: getHeaders()
});
return response.ok;
} catch (error) {
console.error("API Error (markAllNotificationsAsRead):", error);
return false;
}
};
export const deleteNotification = async (id: string): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/notifications/${id}`, {
method: 'DELETE',
headers: getHeaders()
});
return response.ok;
} catch (error) {
console.error("API Error (deleteNotification):", error);
return false;
}
};
export const clearAllNotifications = async (): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/notifications`, {
method: 'DELETE',
headers: getHeaders()
});
return response.ok;
} catch (error) {
console.error("API Error (clearAllNotifications):", 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)}`, {
headers: getHeaders()
});
if (!response.ok) throw new Error('Search failed');
return await response.json();
} catch (error) {
console.error("API Error (searchGlobal):", error);
return { members: [], teams: [], attendances: [], organizations: [] };
}
};
export const getAttendances = async (tenantId: string, filter: DashboardFilter): Promise<Attendance[]> => {
try {
const params = new URLSearchParams();
params.append('tenantId', tenantId);
if (filter.dateRange.start) params.append('startDate', filter.dateRange.start.toISOString());
if (filter.dateRange.end) params.append('endDate', filter.dateRange.end.toISOString());
if (filter.userId && filter.userId !== 'all') params.append('userId', filter.userId);
if (filter.teamId && filter.teamId !== 'all') params.append('teamId', filter.teamId);
if (filter.funnelStage && filter.funnelStage !== 'all') params.append('funnelStage', filter.funnelStage);
if (filter.origin && filter.origin !== 'all') params.append('origin', filter.origin);
const response = await fetch(`${API_URL}/attendances?${params.toString()}`, {
headers: getHeaders()
});
if (!response.ok) {
throw new Error('Falha ao buscar atendimentos do servidor');
}
return await response.json();
} catch (error) {
console.error("API Error (getAttendances):", error);
// Fallback vazio ou lançar erro para a UI tratar
return [];
}
};
export const getUsers = async (tenantId: string): Promise<User[]> => {
try {
const params = new URLSearchParams();
if (tenantId !== 'all') params.append('tenantId', tenantId);
const response = await fetch(`${API_URL}/users?${params.toString()}`, {
headers: getHeaders()
});
if (!response.ok) throw new Error('Falha ao buscar usuários');
return await response.json();
} catch (error) {
console.error("API Error (getUsers):", error);
return [];
}
};
export const getUserById = async (id: string): Promise<User | undefined> => {
try {
const response = await fetch(`${API_URL}/users/${id}`, {
headers: getHeaders()
});
if (!response.ok) return undefined;
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
return await response.json();
}
return undefined;
} catch (error) {
console.error("API Error (getUserById):", error);
return undefined;
}
};
export const updateUser = async (id: string, userData: any): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/users/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(userData)
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(errorData?.error || 'Erro ao atualizar usuário no servidor');
}
return true;
} catch (error) {
console.error("API Error (updateUser):", error);
throw error;
}
};
export const uploadAvatar = async (id: string, file: File): Promise<string | null> => {
try {
const formData = new FormData();
formData.append('avatar', file);
const token = localStorage.getItem('ctms_token');
const response = await fetch(`${API_URL}/users/${id}/avatar`, {
method: 'POST',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: formData
});
if (!response.ok) throw new Error('Falha no upload');
const data = await response.json();
return data.avatarUrl;
} catch (error) {
console.error("API Error (uploadAvatar):", error);
return null;
}
};
export const createMember = async (userData: any): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/users`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(userData)
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(errorData?.error || 'Erro ao criar membro no servidor');
}
return true;
} catch (error) {
console.error("API Error (createMember):", error);
throw error;
}
};
export const deleteUser = async (id: string): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/users/${id}`, {
method: 'DELETE',
headers: getHeaders()
});
return response.ok;
} catch (error) {
console.error("API Error (deleteUser):", error);
return false;
}
};
export const getAttendanceById = async (id: string): Promise<Attendance | undefined> => {
try {
const response = await fetch(`${API_URL}/attendances/${id}`, {
headers: getHeaders()
});
if (!response.ok) return undefined;
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
return await response.json();
}
return undefined;
} catch (error) {
console.error("API Error (getAttendanceById):", error);
return undefined;
}
};
export const getTenants = async (): Promise<any[]> => {
try {
const response = await fetch(`${API_URL}/tenants`, {
headers: getHeaders()
});
if (!response.ok) throw new Error('Falha ao buscar tenants');
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
return await response.json();
}
return [];
} catch (error) {
console.error("API Error (getTenants):", error);
return [];
}
};
export const getTeams = async (tenantId: string): Promise<any[]> => {
try {
const response = await fetch(`${API_URL}/teams?tenantId=${tenantId}`, {
headers: getHeaders()
});
if (!response.ok) throw new Error('Falha ao buscar equipes');
return await response.json();
} catch (error) {
console.error("API Error (getTeams):", error);
return [];
}
};
export const createTeam = async (teamData: any): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/teams`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(teamData)
});
return response.ok;
} catch (error) {
console.error("API Error (createTeam):", error);
return false;
}
};
export const updateTeam = async (id: string, teamData: any): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/teams/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(teamData)
});
return response.ok;
} catch (error) {
console.error("API Error (updateTeam):", error);
return false;
}
};
export const createTenant = async (tenantData: any): Promise<{ success: boolean; message?: string }> => {
try {
const response = await fetch(`${API_URL}/tenants`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(tenantData)
});
const data = await response.json().catch(() => null);
if (!response.ok) throw new Error(data?.error || 'Erro ao criar organização');
return { success: true, message: data?.message || 'Organização criada!' };
} catch (error: any) {
console.error("API Error (createTenant):", error);
return { success: false, message: error.message };
}
};
export const updateTenant = async (id: string, tenantData: any): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/tenants/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(tenantData)
});
return response.ok;
} catch (error) {
console.error("API Error (updateTenant):", error);
return false;
}
};
export const deleteTenant = async (id: string): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/tenants/${id}`, {
method: 'DELETE',
headers: getHeaders()
});
return response.ok;
} catch (error) {
console.error("API Error (deleteTenant):", error);
return false;
}
};
// --- Auth Functions ---
export const logout = () => {
localStorage.removeItem('ctms_token');
localStorage.removeItem('ctms_user_id');
localStorage.removeItem('ctms_tenant_id');
};
export const login = async (credentials: any): Promise<any> => {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const contentType = response.headers.get("content-type");
const isJson = contentType && contentType.indexOf("application/json") !== -1;
if (!response.ok) {
const error = isJson ? await response.json() : { error: 'Erro no servidor' };
throw new Error(error.error || 'Erro no login');
}
const data = isJson ? await response.json() : null;
if (data && data.token) {
localStorage.setItem('ctms_token', data.token);
localStorage.setItem('ctms_user_id', data.user.id);
localStorage.setItem('ctms_tenant_id', data.user.tenant_id || '');
}
return data;
};
export const impersonateTenant = async (tenantId: string): Promise<any> => {
const response = await fetch(`${API_URL}/impersonate/${tenantId}`, {
method: 'POST',
headers: getHeaders()
});
const contentType = response.headers.get("content-type");
const isJson = contentType && contentType.indexOf("application/json") !== -1;
if (!response.ok) {
const errorData = isJson ? await response.json() : { error: 'Erro no servidor' };
throw new Error(errorData.error || 'Erro ao assumir identidade');
}
const data = await response.json();
const oldToken = localStorage.getItem('ctms_token');
if (oldToken) {
localStorage.setItem('ctms_super_admin_token', oldToken);
}
localStorage.setItem('ctms_token', data.token);
localStorage.setItem('ctms_user_id', data.user.id);
localStorage.setItem('ctms_tenant_id', data.user.tenant_id || '');
return data;
};
export const returnToSuperAdmin = (): boolean => {
const superAdminToken = localStorage.getItem('ctms_super_admin_token');
if (superAdminToken) {
try {
// Correctly decode Base64Url JWT payload with proper padding
const base64Url = superAdminToken.split('.')[1];
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const pad = base64.length % 4;
if (pad) {
base64 += '='.repeat(4 - pad);
}
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
const payload = JSON.parse(jsonPayload);
localStorage.setItem('ctms_token', superAdminToken);
localStorage.setItem('ctms_user_id', payload.id);
localStorage.setItem('ctms_tenant_id', payload.tenant_id || 'system');
localStorage.removeItem('ctms_super_admin_token');
return true;
} catch (e) {
console.error("Failed to restore super admin token", e);
return false;
}
}
return false;
};
export const register = async (userData: any): Promise<boolean> => {
const response = await fetch(`${API_URL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Erro no registro');
}
return true;
};
export const verifyCode = async (data: any): Promise<boolean> => {
const response = await fetch(`${API_URL}/auth/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Código inválido ou expirado');
}
return true;
};
export const forgotPassword = async (email: string): Promise<string> => {
const response = await fetch(`${API_URL}/auth/forgot-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const contentType = response.headers.get("content-type");
const isJson = contentType && contentType.indexOf("application/json") !== -1;
if (!response.ok) {
const error = isJson ? await response.json() : { error: 'Erro no servidor' };
throw new Error(error.error || 'Erro ao processar solicitação');
}
const data = isJson ? await response.json() : { message: 'Solicitação processada' };
return data.message;
};
export const resetPassword = async (password: string, token: string, name?: string): Promise<string> => {
const response = await fetch(`${API_URL}/auth/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password, token, name })
});
const contentType = response.headers.get("content-type");
const isJson = contentType && contentType.indexOf("application/json") !== -1;
if (!response.ok) {
const error = isJson ? await response.json() : { error: 'Erro no servidor' };
throw new Error(error.error || 'Erro ao resetar senha');
}
const data = isJson ? await response.json() : { message: 'Senha redefinida' };
return data.message;
};