feat: implement secure multi-tenancy, RBAC, and premium dark mode
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:
Cauê Faleiros
2026-03-03 17:16:55 -03:00
parent b7e73fce3d
commit 20bdf510fd
32 changed files with 2810 additions and 1140 deletions

View File

@@ -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> => {