feat: implement secure multi-tenancy, RBAC, and premium dark mode
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m54s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m54s
- Enforced tenant isolation and Role-Based Access Control across all API routes - Implemented secure profile avatar upload using multer and UUIDs - Redesigned UI with a premium "Onyx & Gold" Charcoal dark mode - Added Funnel Stage and Origin filters to Dashboard and User Detail pages - Replaced "Referral" with "Indicação" across the platform and database - Optimized Dockerfile and local environment setup for reliable deployments - Fixed frontend syntax errors and improved KPI/Chart visualizations
This commit is contained in:
@@ -6,6 +6,17 @@ import { Attendance, DashboardFilter, User } from '../types';
|
||||
// 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 getAttendances = async (tenantId: string, filter: DashboardFilter): Promise<Attendance[]> => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
@@ -16,8 +27,12 @@ export const getAttendances = async (tenantId: string, filter: DashboardFilter):
|
||||
|
||||
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()}`);
|
||||
const response = await fetch(`${API_URL}/attendances?${params.toString()}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Falha ao buscar atendimentos do servidor');
|
||||
@@ -36,7 +51,9 @@ export const getUsers = async (tenantId: string): Promise<User[]> => {
|
||||
const params = new URLSearchParams();
|
||||
if (tenantId !== 'all') params.append('tenantId', tenantId);
|
||||
|
||||
const response = await fetch(`${API_URL}/users?${params.toString()}`);
|
||||
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();
|
||||
@@ -48,7 +65,9 @@ export const getUsers = async (tenantId: string): Promise<User[]> => {
|
||||
|
||||
export const getUserById = async (id: string): Promise<User | undefined> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/users/${id}`);
|
||||
const response = await fetch(`${API_URL}/users/${id}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) return undefined;
|
||||
|
||||
const contentType = response.headers.get("content-type");
|
||||
@@ -66,7 +85,7 @@ export const updateUser = async (id: string, userData: any): Promise<boolean> =>
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/users/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
return response.ok;
|
||||
@@ -76,11 +95,34 @@ export const updateUser = async (id: string, userData: any): Promise<boolean> =>
|
||||
}
|
||||
};
|
||||
|
||||
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: { 'Content-Type': 'application/json' },
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
return response.ok;
|
||||
@@ -93,7 +135,8 @@ export const createMember = async (userData: any): Promise<boolean> => {
|
||||
export const deleteUser = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/users/${id}`, {
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
@@ -104,7 +147,9 @@ export const deleteUser = async (id: string): Promise<boolean> => {
|
||||
|
||||
export const getAttendanceById = async (id: string): Promise<Attendance | undefined> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/attendances/${id}`);
|
||||
const response = await fetch(`${API_URL}/attendances/${id}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) return undefined;
|
||||
|
||||
const contentType = response.headers.get("content-type");
|
||||
@@ -120,7 +165,9 @@ export const getAttendanceById = async (id: string): Promise<Attendance | undefi
|
||||
|
||||
export const getTenants = async (): Promise<any[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tenants`);
|
||||
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");
|
||||
@@ -136,7 +183,9 @@ export const getTenants = async (): Promise<any[]> => {
|
||||
|
||||
export const getTeams = async (tenantId: string): Promise<any[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/teams?tenantId=${tenantId}`);
|
||||
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) {
|
||||
@@ -149,7 +198,7 @@ export const createTeam = async (teamData: any): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/teams`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(teamData)
|
||||
});
|
||||
return response.ok;
|
||||
@@ -163,7 +212,7 @@ export const updateTeam = async (id: string, teamData: any): Promise<boolean> =>
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/teams/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(teamData)
|
||||
});
|
||||
return response.ok;
|
||||
@@ -177,7 +226,7 @@ export const createTenant = async (tenantData: any): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tenants`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(tenantData)
|
||||
});
|
||||
return response.ok;
|
||||
@@ -189,6 +238,12 @@ export const createTenant = async (tenantData: any): Promise<boolean> => {
|
||||
|
||||
// --- 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',
|
||||
@@ -204,7 +259,13 @@ export const login = async (credentials: any): Promise<any> => {
|
||||
throw new Error(error.error || 'Erro no login');
|
||||
}
|
||||
|
||||
return isJson ? await response.json() : null;
|
||||
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 register = async (userData: any): Promise<boolean> => {
|
||||
|
||||
Reference in New Issue
Block a user