feat: implement secure 2-token authentication with rolling sessions
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m43s
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.
This commit is contained in:
@@ -236,8 +236,78 @@ apiRouter.post('/auth/login', async (req, res) => {
|
||||
const valid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!valid) return res.status(401).json({ error: 'Credenciais inválidas.' });
|
||||
|
||||
const token = jwt.sign({ id: user.id, tenant_id: user.tenant_id, role: user.role, team_id: user.team_id, slug: user.slug }, JWT_SECRET, { expiresIn: '24h' });
|
||||
res.json({ token, user: { id: user.id, name: user.name, email: user.email, role: user.role, tenant_id: user.tenant_id, team_id: user.team_id, slug: user.slug } });
|
||||
// Generate Access Token (short-lived)
|
||||
const token = jwt.sign({ id: user.id, tenant_id: user.tenant_id, role: user.role, team_id: user.team_id, slug: user.slug }, JWT_SECRET, { expiresIn: '15m' });
|
||||
|
||||
// Generate Refresh Token (long-lived)
|
||||
const refreshToken = crypto.randomBytes(40).toString('hex');
|
||||
const refreshId = `rt_${crypto.randomUUID().split('-')[0]}`;
|
||||
|
||||
// Store Refresh Token in database (expires in 30 days)
|
||||
await pool.query(
|
||||
'INSERT INTO refresh_tokens (id, user_id, token, expires_at) VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))',
|
||||
[refreshId, user.id, refreshToken]
|
||||
);
|
||||
|
||||
res.json({
|
||||
token,
|
||||
refreshToken,
|
||||
user: { id: user.id, name: user.name, email: user.email, role: user.role, tenant_id: user.tenant_id, team_id: user.team_id, slug: user.slug }
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh Token
|
||||
apiRouter.post('/auth/refresh', async (req, res) => {
|
||||
const { refreshToken } = req.body;
|
||||
if (!refreshToken) return res.status(400).json({ error: 'Refresh token não fornecido.' });
|
||||
|
||||
try {
|
||||
// Verifies if the token exists and hasn't expired
|
||||
const [tokens] = await pool.query(
|
||||
'SELECT r.user_id, u.tenant_id, u.role, u.team_id, u.slug, u.status FROM refresh_tokens r JOIN users u ON r.user_id = u.id WHERE r.token = ? AND r.expires_at > NOW()',
|
||||
[refreshToken]
|
||||
);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
// If invalid, optionally delete the bad token if it's just expired
|
||||
await pool.query('DELETE FROM refresh_tokens WHERE token = ?', [refreshToken]);
|
||||
return res.status(401).json({ error: 'Sessão expirada. Faça login novamente.' });
|
||||
}
|
||||
|
||||
const user = tokens[0];
|
||||
|
||||
if (user.status !== 'active') {
|
||||
await pool.query('DELETE FROM refresh_tokens WHERE token = ?', [refreshToken]);
|
||||
return res.status(403).json({ error: 'Sua conta está inativa.' });
|
||||
}
|
||||
|
||||
// Sliding Expiration: Extend the refresh token's life by another 30 days
|
||||
await pool.query('UPDATE refresh_tokens SET expires_at = DATE_ADD(NOW(), INTERVAL 30 DAY) WHERE token = ?', [refreshToken]);
|
||||
|
||||
// Issue a new short-lived access token
|
||||
const newAccessToken = jwt.sign(
|
||||
{ id: user.user_id, tenant_id: user.tenant_id, role: user.role, team_id: user.team_id, slug: user.slug },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '15m' }
|
||||
);
|
||||
|
||||
res.json({ token: newAccessToken });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Logout (Revoke Refresh Token)
|
||||
apiRouter.post('/auth/logout', async (req, res) => {
|
||||
const { refreshToken } = req.body;
|
||||
try {
|
||||
if (refreshToken) {
|
||||
await pool.query('DELETE FROM refresh_tokens WHERE token = ?', [refreshToken]);
|
||||
}
|
||||
res.json({ message: 'Logout bem-sucedido.' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
@@ -1562,6 +1632,20 @@ const provisionSuperAdmin = async (retries = 10, delay = 10000) => {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// Create refresh_tokens table for persistent sessions
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id varchar(36) NOT NULL,
|
||||
user_id varchar(36) NOT NULL,
|
||||
token varchar(255) NOT NULL,
|
||||
expires_at timestamp NOT NULL,
|
||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY token (token),
|
||||
KEY user_id (user_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");
|
||||
|
||||
@@ -6,8 +6,8 @@ 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');
|
||||
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' };
|
||||
|
||||
@@ -17,9 +17,76 @@ const getHeaders = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// 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 fetch(`${API_URL}/notifications`, {
|
||||
const response = await apiFetch(`${API_URL}/notifications`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to fetch notifications');
|
||||
@@ -32,7 +99,7 @@ export const getNotifications = async (): Promise<any[]> => {
|
||||
|
||||
export const markNotificationAsRead = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/notifications/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/notifications/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -45,7 +112,7 @@ export const markNotificationAsRead = async (id: string): Promise<boolean> => {
|
||||
|
||||
export const markAllNotificationsAsRead = async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/notifications/read-all`, {
|
||||
const response = await apiFetch(`${API_URL}/notifications/read-all`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -58,7 +125,7 @@ export const markAllNotificationsAsRead = async (): Promise<boolean> => {
|
||||
|
||||
export const deleteNotification = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/notifications/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/notifications/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -71,7 +138,7 @@ export const deleteNotification = async (id: string): Promise<boolean> => {
|
||||
|
||||
export const clearAllNotifications = async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/notifications/clear-all`, {
|
||||
const response = await apiFetch(`${API_URL}/notifications/clear-all`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -85,7 +152,7 @@ export const clearAllNotifications = async (): Promise<boolean> => {
|
||||
// --- Funnels Functions ---
|
||||
export const getFunnels = async (tenantId: string): Promise<any[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/funnels?tenantId=${tenantId}`, {
|
||||
const response = await apiFetch(`${API_URL}/funnels?tenantId=${tenantId}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao buscar funis');
|
||||
@@ -97,7 +164,7 @@ export const getFunnels = async (tenantId: string): Promise<any[]> => {
|
||||
};
|
||||
|
||||
export const createFunnel = async (data: { name: string, tenantId: string }): Promise<any> => {
|
||||
const response = await fetch(`${API_URL}/funnels`, {
|
||||
const response = await apiFetch(`${API_URL}/funnels`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
@@ -111,7 +178,7 @@ export const createFunnel = async (data: { name: string, tenantId: string }): Pr
|
||||
|
||||
export const updateFunnel = async (id: string, data: { name?: string, teamIds?: string[] }): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/funnels/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/funnels/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
@@ -125,7 +192,7 @@ export const updateFunnel = async (id: string, data: { name?: string, teamIds?:
|
||||
|
||||
export const deleteFunnel = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/funnels/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/funnels/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -137,7 +204,7 @@ export const deleteFunnel = async (id: string): Promise<boolean> => {
|
||||
};
|
||||
|
||||
export const createFunnelStage = async (funnelId: string, data: any): Promise<any> => {
|
||||
const response = await fetch(`${API_URL}/funnels/${funnelId}/stages`, {
|
||||
const response = await apiFetch(`${API_URL}/funnels/${funnelId}/stages`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
@@ -151,7 +218,7 @@ export const createFunnelStage = async (funnelId: string, data: any): Promise<an
|
||||
|
||||
export const updateFunnelStage = async (id: string, data: any): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/funnel_stages/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/funnel_stages/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
@@ -165,7 +232,7 @@ export const updateFunnelStage = async (id: string, data: any): Promise<boolean>
|
||||
|
||||
export const deleteFunnelStage = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/funnel_stages/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/funnel_stages/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -179,7 +246,7 @@ export const deleteFunnelStage = async (id: string): Promise<boolean> => {
|
||||
// --- Origins Functions ---
|
||||
export const getOrigins = async (tenantId: string): Promise<any[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/origins?tenantId=${tenantId}`, {
|
||||
const response = await apiFetch(`${API_URL}/origins?tenantId=${tenantId}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao buscar origens');
|
||||
@@ -191,7 +258,7 @@ export const getOrigins = async (tenantId: string): Promise<any[]> => {
|
||||
};
|
||||
|
||||
export const createOriginGroup = async (data: { name: string, tenantId: string }): Promise<any> => {
|
||||
const response = await fetch(`${API_URL}/origins`, {
|
||||
const response = await apiFetch(`${API_URL}/origins`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
@@ -205,7 +272,7 @@ export const createOriginGroup = async (data: { name: string, tenantId: string }
|
||||
|
||||
export const updateOriginGroup = async (id: string, data: { name?: string, teamIds?: string[] }): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/origins/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/origins/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
@@ -219,7 +286,7 @@ export const updateOriginGroup = async (id: string, data: { name?: string, teamI
|
||||
|
||||
export const deleteOriginGroup = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/origins/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/origins/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -231,7 +298,7 @@ export const deleteOriginGroup = async (id: string): Promise<boolean> => {
|
||||
};
|
||||
|
||||
export const createOriginItem = async (groupId: string, data: { name: string, color_class?: string }): Promise<any> => {
|
||||
const response = await fetch(`${API_URL}/origins/${groupId}/items`, {
|
||||
const response = await apiFetch(`${API_URL}/origins/${groupId}/items`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
@@ -245,7 +312,7 @@ export const createOriginItem = async (groupId: string, data: { name: string, co
|
||||
|
||||
export const updateOriginItem = async (id: string, data: { name: string, color_class?: string }): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/origin_items/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/origin_items/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
@@ -259,7 +326,7 @@ export const updateOriginItem = async (id: string, data: { name: string, color_c
|
||||
|
||||
export const deleteOriginItem = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/origin_items/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/origin_items/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -273,7 +340,7 @@ export const deleteOriginItem = 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}`, {
|
||||
const response = await apiFetch(`${API_URL}/api-keys?tenantId=${tenantId}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao buscar chaves');
|
||||
@@ -285,7 +352,7 @@ export const getApiKeys = async (tenantId: string): Promise<any[]> => {
|
||||
};
|
||||
|
||||
export const createApiKey = async (data: { name: string, tenantId: string }): Promise<any> => {
|
||||
const response = await fetch(`${API_URL}/api-keys`, {
|
||||
const response = await apiFetch(`${API_URL}/api-keys`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
@@ -299,7 +366,7 @@ export const createApiKey = async (data: { name: string, tenantId: string }): Pr
|
||||
|
||||
export const deleteApiKey = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api-keys/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/api-keys/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -312,7 +379,7 @@ export const deleteApiKey = async (id: string): Promise<boolean> => {
|
||||
|
||||
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)}`, {
|
||||
const response = await apiFetch(`${API_URL}/search?q=${encodeURIComponent(query)}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Search failed');
|
||||
@@ -336,7 +403,7 @@ export const getAttendances = async (tenantId: string, filter: DashboardFilter):
|
||||
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 apiFetch(`${API_URL}/attendances?${params.toString()}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
|
||||
@@ -357,7 +424,7 @@ 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 apiFetch(`${API_URL}/users?${params.toString()}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao buscar usuários');
|
||||
@@ -371,7 +438,7 @@ 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 apiFetch(`${API_URL}/users/${id}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -393,7 +460,7 @@ export const getUserById = async (id: string): Promise<User | undefined> => {
|
||||
|
||||
export const updateUser = async (id: string, userData: any): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/users/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/users/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(userData)
|
||||
@@ -417,7 +484,7 @@ export const uploadAvatar = async (id: string, file: File): Promise<string | nul
|
||||
formData.append('avatar', file);
|
||||
|
||||
const token = localStorage.getItem('ctms_token');
|
||||
const response = await fetch(`${API_URL}/users/${id}/avatar`, {
|
||||
const response = await apiFetch(`${API_URL}/users/${id}/avatar`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
@@ -436,7 +503,7 @@ export const uploadAvatar = async (id: string, file: File): Promise<string | nul
|
||||
|
||||
export const createMember = async (userData: any): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/users`, {
|
||||
const response = await apiFetch(`${API_URL}/users`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(userData)
|
||||
@@ -456,7 +523,7 @@ 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}`, {
|
||||
const response = await apiFetch(`${API_URL}/users/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -469,7 +536,7 @@ 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 apiFetch(`${API_URL}/attendances/${id}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) return undefined;
|
||||
@@ -487,7 +554,7 @@ 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 apiFetch(`${API_URL}/tenants`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao buscar tenants');
|
||||
@@ -505,7 +572,7 @@ 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 apiFetch(`${API_URL}/teams?tenantId=${tenantId}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao buscar equipes');
|
||||
@@ -518,7 +585,7 @@ export const getTeams = async (tenantId: string): Promise<any[]> => {
|
||||
|
||||
export const createTeam = async (teamData: any): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/teams`, {
|
||||
const response = await apiFetch(`${API_URL}/teams`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(teamData)
|
||||
@@ -532,7 +599,7 @@ export const createTeam = async (teamData: any): Promise<boolean> => {
|
||||
|
||||
export const updateTeam = async (id: string, teamData: any): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/teams/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/teams/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(teamData)
|
||||
@@ -546,7 +613,7 @@ export const updateTeam = async (id: string, teamData: any): Promise<boolean> =>
|
||||
|
||||
export const deleteTeam = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/teams/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/teams/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -559,7 +626,7 @@ export const deleteTeam = async (id: string): Promise<boolean> => {
|
||||
|
||||
export const createTenant = async (tenantData: any): Promise<{ success: boolean; message?: string }> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tenants`, {
|
||||
const response = await apiFetch(`${API_URL}/tenants`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(tenantData)
|
||||
@@ -575,7 +642,7 @@ export const createTenant = async (tenantData: any): Promise<{ success: boolean;
|
||||
|
||||
export const updateTenant = async (id: string, tenantData: any): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tenants/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/tenants/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify(tenantData)
|
||||
@@ -589,7 +656,7 @@ export const updateTenant = async (id: string, tenantData: any): Promise<boolean
|
||||
|
||||
export const deleteTenant = async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tenants/${id}`, {
|
||||
const response = await apiFetch(`${API_URL}/tenants/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -607,9 +674,23 @@ 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> => {
|
||||
@@ -630,6 +711,7 @@ export const login = async (credentials: any): Promise<any> => {
|
||||
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 || '');
|
||||
}
|
||||
@@ -637,7 +719,7 @@ export const login = async (credentials: any): Promise<any> => {
|
||||
};
|
||||
|
||||
export const impersonateTenant = async (tenantId: string): Promise<any> => {
|
||||
const response = await fetch(`${API_URL}/impersonate/${tenantId}`, {
|
||||
const response = await apiFetch(`${API_URL}/impersonate/${tenantId}`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders()
|
||||
});
|
||||
@@ -705,7 +787,7 @@ export const returnToSuperAdmin = (): boolean => {
|
||||
};
|
||||
|
||||
export const register = async (userData: any): Promise<boolean> => {
|
||||
const response = await fetch(`${API_URL}/auth/register`, {
|
||||
const response = await apiFetch(`${API_URL}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(userData)
|
||||
@@ -718,7 +800,7 @@ export const register = async (userData: any): Promise<boolean> => {
|
||||
};
|
||||
|
||||
export const verifyCode = async (data: any): Promise<boolean> => {
|
||||
const response = await fetch(`${API_URL}/auth/verify`, {
|
||||
const response = await apiFetch(`${API_URL}/auth/verify`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
@@ -731,7 +813,7 @@ export const verifyCode = async (data: any): Promise<boolean> => {
|
||||
};
|
||||
|
||||
export const forgotPassword = async (email: string): Promise<string> => {
|
||||
const response = await fetch(`${API_URL}/auth/forgot-password`, {
|
||||
const response = await apiFetch(`${API_URL}/auth/forgot-password`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email })
|
||||
@@ -750,7 +832,7 @@ export const forgotPassword = async (email: string): Promise<string> => {
|
||||
};
|
||||
|
||||
export const resetPassword = async (password: string, token: string, name?: string): Promise<string> => {
|
||||
const response = await fetch(`${API_URL}/auth/reset-password`, {
|
||||
const response = await apiFetch(`${API_URL}/auth/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password, token, name })
|
||||
|
||||
Reference in New Issue
Block a user