All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m43s
- Refactored POST /auth/login to issue a 15-minute Access Token and a 30-day Refresh Token. - Added POST /auth/refresh endpoint to automatically issue new Access Tokens and extend the Refresh Token's lifespan by 30 days upon use (Sliding Expiration). - Built an HTTP interceptor wrapper (apiFetch) in dataService.ts that automatically catches 401 Unauthorized errors, calls the refresh endpoint, updates localStorage, and silently retries the original request without logging the user out.
852 lines
26 KiB
TypeScript
852 lines
26 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 = (customToken?: string) => {
|
|
const token = customToken || 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}`
|
|
};
|
|
};
|
|
|
|
// Global flag to prevent multiple simultaneous refresh attempts
|
|
let isRefreshing = false;
|
|
let refreshSubscribers: ((token: string) => void)[] = [];
|
|
|
|
const onRefreshed = (token: string) => {
|
|
refreshSubscribers.forEach(cb => cb(token));
|
|
refreshSubscribers = [];
|
|
};
|
|
|
|
const addRefreshSubscriber = (cb: (token: string) => void) => {
|
|
refreshSubscribers.push(cb);
|
|
};
|
|
|
|
export const apiFetch = async (url: string, options: RequestInit = {}): Promise<Response> => {
|
|
let response = await fetch(url, options);
|
|
|
|
// If unauthorized, attempt to refresh the token
|
|
if (response.status === 401 && !url.includes('/auth/login') && !url.includes('/auth/refresh')) {
|
|
const refreshToken = localStorage.getItem('ctms_refresh_token');
|
|
|
|
if (!refreshToken) {
|
|
logout();
|
|
return response;
|
|
}
|
|
|
|
if (isRefreshing) {
|
|
// If a refresh is already in progress, wait for it to finish and retry
|
|
return new Promise(resolve => {
|
|
addRefreshSubscriber((newToken) => {
|
|
options.headers = getHeaders(newToken);
|
|
resolve(fetch(url, options));
|
|
});
|
|
});
|
|
}
|
|
|
|
isRefreshing = true;
|
|
|
|
try {
|
|
const refreshResponse = await fetch(`${API_URL}/auth/refresh`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refreshToken })
|
|
});
|
|
|
|
if (!refreshResponse.ok) {
|
|
throw new Error('Refresh token invalid');
|
|
}
|
|
|
|
const data = await refreshResponse.json();
|
|
localStorage.setItem('ctms_token', data.token);
|
|
|
|
// Retry the original request
|
|
options.headers = getHeaders(data.token);
|
|
response = await fetch(url, options);
|
|
|
|
onRefreshed(data.token);
|
|
} catch (err) {
|
|
console.error("Session expired or refresh failed:", err);
|
|
logout();
|
|
} finally {
|
|
isRefreshing = false;
|
|
}
|
|
}
|
|
|
|
return response;
|
|
};
|
|
|
|
export const getNotifications = async (): Promise<any[]> => {
|
|
try {
|
|
const response = await apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${API_URL}/notifications/clear-all`, {
|
|
method: 'DELETE',
|
|
headers: getHeaders()
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (clearAllNotifications):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// --- Funnels Functions ---
|
|
export const getFunnels = async (tenantId: string): Promise<any[]> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/funnels?tenantId=${tenantId}`, {
|
|
headers: getHeaders()
|
|
});
|
|
if (!response.ok) throw new Error('Falha ao buscar funis');
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error("API Error (getFunnels):", error);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
export const createFunnel = async (data: { name: string, tenantId: string }): Promise<any> => {
|
|
const response = await apiFetch(`${API_URL}/funnels`, {
|
|
method: 'POST',
|
|
headers: getHeaders(),
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Erro ao criar funil');
|
|
}
|
|
return await response.json();
|
|
};
|
|
|
|
export const updateFunnel = async (id: string, data: { name?: string, teamIds?: string[] }): Promise<boolean> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/funnels/${id}`, {
|
|
method: 'PUT',
|
|
headers: getHeaders(),
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (updateFunnel):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const deleteFunnel = async (id: string): Promise<boolean> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/funnels/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getHeaders()
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (deleteFunnel):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const createFunnelStage = async (funnelId: string, data: any): Promise<any> => {
|
|
const response = await apiFetch(`${API_URL}/funnels/${funnelId}/stages`, {
|
|
method: 'POST',
|
|
headers: getHeaders(),
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Erro ao criar etapa');
|
|
}
|
|
return await response.json();
|
|
};
|
|
|
|
export const updateFunnelStage = async (id: string, data: any): Promise<boolean> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/funnel_stages/${id}`, {
|
|
method: 'PUT',
|
|
headers: getHeaders(),
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (updateFunnelStage):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const deleteFunnelStage = async (id: string): Promise<boolean> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/funnel_stages/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getHeaders()
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (deleteFunnelStage):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// --- Origins Functions ---
|
|
export const getOrigins = async (tenantId: string): Promise<any[]> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/origins?tenantId=${tenantId}`, {
|
|
headers: getHeaders()
|
|
});
|
|
if (!response.ok) throw new Error('Falha ao buscar origens');
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error("API Error (getOrigins):", error);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
export const createOriginGroup = async (data: { name: string, tenantId: string }): Promise<any> => {
|
|
const response = await apiFetch(`${API_URL}/origins`, {
|
|
method: 'POST',
|
|
headers: getHeaders(),
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Erro ao criar grupo de origens');
|
|
}
|
|
return await response.json();
|
|
};
|
|
|
|
export const updateOriginGroup = async (id: string, data: { name?: string, teamIds?: string[] }): Promise<boolean> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/origins/${id}`, {
|
|
method: 'PUT',
|
|
headers: getHeaders(),
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (updateOriginGroup):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const deleteOriginGroup = async (id: string): Promise<boolean> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/origins/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getHeaders()
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (deleteOriginGroup):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const createOriginItem = async (groupId: string, data: { name: string, color_class?: string }): Promise<any> => {
|
|
const response = await apiFetch(`${API_URL}/origins/${groupId}/items`, {
|
|
method: 'POST',
|
|
headers: getHeaders(),
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Erro ao criar item de origem');
|
|
}
|
|
return await response.json();
|
|
};
|
|
|
|
export const updateOriginItem = async (id: string, data: { name: string, color_class?: string }): Promise<boolean> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/origin_items/${id}`, {
|
|
method: 'PUT',
|
|
headers: getHeaders(),
|
|
body: JSON.stringify(data)
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (updateOriginItem):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const deleteOriginItem = async (id: string): Promise<boolean> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/origin_items/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getHeaders()
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (deleteOriginItem):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// --- API Keys Functions ---
|
|
export const getApiKeys = async (tenantId: string): Promise<any[]> => {
|
|
try {
|
|
const response = await apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${API_URL}/users/${id}`, {
|
|
headers: getHeaders()
|
|
});
|
|
if (!response.ok) {
|
|
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
return undefined; // Invalid user or token
|
|
}
|
|
throw new Error(`Server error: ${response.status}`);
|
|
}
|
|
|
|
const contentType = response.headers.get("content-type");
|
|
if (contentType && contentType.indexOf("application/json") !== -1) {
|
|
return await response.json();
|
|
}
|
|
} catch (error) {
|
|
console.error("API Error (getUserById):", error);
|
|
throw error; // Rethrow so AuthGuard catches it and doesn't wipe tokens
|
|
}
|
|
};
|
|
|
|
export const updateUser = async (id: string, userData: any): Promise<boolean> => {
|
|
try {
|
|
const response = await apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 deleteTeam = async (id: string): Promise<boolean> => {
|
|
try {
|
|
const response = await apiFetch(`${API_URL}/teams/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getHeaders()
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (deleteTeam):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const createTenant = async (tenantData: any): Promise<{ success: boolean; message?: string }> => {
|
|
try {
|
|
const response = await apiFetch(`${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 apiFetch(`${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 apiFetch(`${API_URL}/tenants/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getHeaders()
|
|
});
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error("API Error (deleteTenant):", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// --- Auth Functions ---
|
|
|
|
// Flag to prevent background fetches from throwing 401 and logging out during impersonation handoffs
|
|
export let isReloadingForImpersonation = false;
|
|
|
|
export const logout = () => {
|
|
if (isReloadingForImpersonation) return; // Prevent logout if we are just switching tokens
|
|
|
|
const refreshToken = localStorage.getItem('ctms_refresh_token');
|
|
|
|
// Clear local storage synchronously for instant UI update
|
|
localStorage.removeItem('ctms_token');
|
|
localStorage.removeItem('ctms_refresh_token');
|
|
localStorage.removeItem('ctms_user_id');
|
|
localStorage.removeItem('ctms_tenant_id');
|
|
|
|
// Attempt to revoke in background
|
|
if (refreshToken) {
|
|
fetch(`${API_URL}/auth/logout`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refreshToken })
|
|
}).catch(e => console.error("Failed to revoke refresh token", e));
|
|
}
|
|
};
|
|
|
|
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);
|
|
if (data.refreshToken) localStorage.setItem('ctms_refresh_token', data.refreshToken);
|
|
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 apiFetch(`${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');
|
|
}
|
|
|
|
isReloadingForImpersonation = true; // Block logouts
|
|
|
|
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 || '');
|
|
|
|
window.location.hash = '#/';
|
|
window.location.reload();
|
|
|
|
return data;
|
|
};
|
|
|
|
export const returnToSuperAdmin = (): boolean => {
|
|
const superAdminToken = localStorage.getItem('ctms_super_admin_token');
|
|
if (superAdminToken) {
|
|
try {
|
|
isReloadingForImpersonation = true; // Block logouts
|
|
|
|
// 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');
|
|
|
|
window.location.hash = '#/super-admin';
|
|
window.location.reload();
|
|
return true;
|
|
} catch (e) {
|
|
isReloadingForImpersonation = false;
|
|
console.error("Failed to restore super admin token", e);
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
export const register = async (userData: any): Promise<boolean> => {
|
|
const response = await apiFetch(`${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 apiFetch(`${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 apiFetch(`${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 apiFetch(`${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;
|
|
};
|