diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index 857f587..411715a 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -154,7 +154,7 @@ func (h *Handler) Register(c *gin.Context) { HttpOnly: true, Secure: false, SameSite: http.SameSiteStrictMode, - MaxAge: 15 * 60, + MaxAge: 180 * 60, // 3 hours }) c.JSON(http.StatusCreated, gin.H{ @@ -243,7 +243,7 @@ func (h *Handler) Login(c *gin.Context) { HttpOnly: true, Secure: false, SameSite: http.SameSiteStrictMode, - MaxAge: 15 * 60, // 15 mins + MaxAge: 180 * 60, // 3 hours }) // Handle Nullable Fields @@ -362,6 +362,17 @@ func (h *Handler) Refresh(c *gin.Context) { return } + // Set new access_token cookie + http.SetCookie(c.Writer, &http.Cookie{ + Name: "access_token", + Value: accessToken, + Path: "/", + HttpOnly: true, + Secure: false, + SameSite: http.SameSiteStrictMode, + MaxAge: 180 * 60, // 3 hours + }) + c.JSON(http.StatusOK, gin.H{ "access_token": accessToken, "expires_at": accessExp, diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index c8643bf..68eacf4 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -38,7 +38,7 @@ func LoadConfig() *Config { DBDsn: getEnv("DB_DSN", ""), JwtAccessSecret: getEnv("JWT_ACCESS_SECRET", "secret"), JwtRefreshSecret: getEnv("JWT_REFRESH_SECRET", "refresh_secret"), - JwtAccessTTLMinutes: getEnvAsInt("JWT_ACCESS_TTL_MINUTES", 15), + JwtAccessTTLMinutes: getEnvAsInt("JWT_ACCESS_TTL_MINUTES", 180), JwtRefreshTTLDays: getEnvAsInt("JWT_REFRESH_TTL_DAYS", 30), CorsAllowedOrigins: getEnv("CORS_ALLOWED_ORIGINS", "*"), SwaggerHost: getEnv("SWAGGER_HOST", "localhost:8080"), diff --git a/frontend/contexts/AuthContext.tsx b/frontend/contexts/AuthContext.tsx index 9e7a0cf..2209b74 100644 --- a/frontend/contexts/AuthContext.tsx +++ b/frontend/contexts/AuthContext.tsx @@ -1,6 +1,7 @@ import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react'; import { User, UserRole } from '../types'; +import { apiFetch } from '../services/apiService'; // Mock Users Database const MOCK_USERS: User[] = [ @@ -88,7 +89,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => try { const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8080"; - const response = await fetch(`${API_URL}/api/me`, { + const response = await apiFetch(`${API_URL}/api/me`, { headers: { 'Authorization': `Bearer ${token}` } @@ -97,7 +98,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => if (response.ok) { const data = await response.json(); const backendUser = data.user; - console.log("AuthContext: restoreSession raw user:", backendUser); const mappedUser: User = { id: backendUser.id, email: backendUser.email, @@ -122,7 +122,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => professionalId: data.profissional?.id, // Map professional ID functions: data.profissional?.functions || [], }; - console.log("AuthContext: restoreSession mapped user:", mappedUser); if (!backendUser.ativo) { console.warn("User is not active, logging out."); logout(); diff --git a/frontend/contexts/DataContext.tsx b/frontend/contexts/DataContext.tsx index 4a455d2..f8b800d 100644 --- a/frontend/contexts/DataContext.tsx +++ b/frontend/contexts/DataContext.tsx @@ -651,9 +651,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ }); const result = await getAgendas(visibleToken); - console.log("Raw Agenda Data:", result.data); // Debug logging if (result.data) { - console.log("Sample event from backend:", result.data[0]); // DEBUG: Ver estrutura e status // Map backend status to frontend EventStatus const mapStatus = (backendStatus: string): EventStatus => { @@ -797,11 +795,9 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ useEffect(() => { const fetchProfs = async () => { const token = localStorage.getItem('token'); - console.log("[DEBUG] Fetching Professionals...", { token }); if (token) { try { const result = await getProfessionals(token); - console.log("[DEBUG] Fetch Professionals Result:", result); if (result.data) { const mappedProfs: Professional[] = result.data.map((p: any) => ({ id: p.id, @@ -851,8 +847,6 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ } catch (error) { console.error("Failed to fetch professionals", error); } - } else { - console.warn("[DEBUG] No token found for fetching professionals"); } }; fetchProfs(); diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 0c75b29..9f401e7 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -2,20 +2,83 @@ const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8080"; -console.log('API_BASE_URL:', API_BASE_URL); -console.log('VITE_API_URL:', import.meta.env.VITE_API_URL); - interface ApiResponse { data: T | null; error: string | null; isBackendDown: boolean; } +// Wrapper request handler for 401 token refresh +let isRefreshing = false; +let refreshSubscribers: ((token: string) => void)[] = []; + +function subscribeTokenRefresh(cb: (token: string) => void) { + refreshSubscribers.push(cb); +} + +function onRefreshed(token: string) { + refreshSubscribers.forEach((cb) => cb(token)); + refreshSubscribers = []; +} + +export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + let response = await fetch(input, init); + + if (response.status === 401) { + const originalRequest = input; + const originalInit = init || {}; + + if (!isRefreshing) { + isRefreshing = true; + try { + const refreshRes = await fetch(`${API_BASE_URL}/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', // Send the HttpOnly refresh_token cookie + }); + + if (refreshRes.ok) { + const data = await refreshRes.json(); + const newAccessToken = data.access_token; + localStorage.setItem('token', newAccessToken); + isRefreshing = false; + onRefreshed(newAccessToken); + } else { + isRefreshing = false; + // Refresh failed (token expired/invalid), logout + localStorage.removeItem('token'); + window.location.href = '/entrar'; + throw new Error('Session expired'); + } + } catch (err) { + isRefreshing = false; + localStorage.removeItem('token'); + window.location.href = '/entrar'; + throw err; + } + } + + // Wait for the token to refresh and then retry the request + return new Promise((resolve, reject) => { + subscribeTokenRefresh((newToken) => { + // Update the Authorization header with the new token + const newHeaders = new Headers(originalInit.headers); + newHeaders.set('Authorization', `Bearer ${newToken}`); + originalInit.headers = newHeaders; + + fetch(originalRequest, originalInit).then(resolve).catch(reject); + }); + }); + } + + return response; +} + // Função auxiliar para fazer requisições async function fetchFromBackend(endpoint: string): Promise> { try { const region = localStorage.getItem("photum_selected_region") || "SP"; - const response = await fetch(`${API_BASE_URL}${endpoint}`, { + const response = await apiFetch(`${API_BASE_URL}${endpoint}`, { method: "GET", headers: { "Content-Type": "application/json", @@ -96,7 +159,7 @@ export async function createProfessional(data: any, token?: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/profissionais/${id}`, { + const response = await apiFetch(`${API_BASE_URL}/api/profissionais/${id}`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -164,7 +227,7 @@ export async function updateProfessional(id: string, data: any, token: string): */ export async function updateUserProfile(data: any, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/me`, { + const response = await apiFetch(`${API_BASE_URL}/api/me`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -200,7 +263,7 @@ export async function updateUserProfile(data: any, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/profissionais/${id}`, { + const response = await apiFetch(`${API_BASE_URL}/api/profissionais/${id}`, { method: "DELETE", headers: { "Content-Type": "application/json", @@ -234,7 +297,7 @@ export async function deleteProfessional(id: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/profissionais/${id}`, { + const response = await apiFetch(`${API_BASE_URL}/api/profissionais/${id}`, { method: "GET", headers: { "Content-Type": "application/json", @@ -269,7 +332,7 @@ export async function getProfessionalById(id: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/profissionais`, { + const response = await apiFetch(`${API_BASE_URL}/api/profissionais`, { method: "GET", headers: { "Content-Type": "application/json", @@ -370,7 +433,7 @@ export async function getCadastroFot(token: string, empresaId?: string): Promise url += `&empresa_id=${empresaId}`; } - const response = await fetch(url, { + const response = await apiFetch(url, { method: "GET", headers: { "Content-Type": "application/json", @@ -404,7 +467,7 @@ export async function getCadastroFot(token: string, empresaId?: string): Promise */ export async function createCadastroFot(data: any, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/cadastro-fot`, { + const response = await apiFetch(`${API_BASE_URL}/api/cadastro-fot`, { method: "POST", headers: { "Content-Type": "application/json", @@ -440,7 +503,7 @@ export async function createCadastroFot(data: any, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/cadastro-fot/${id}`, { + const response = await apiFetch(`${API_BASE_URL}/api/cadastro-fot/${id}`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -476,7 +539,7 @@ export async function updateCadastroFot(id: string, data: any, token: string): P */ export async function checkFotHasEvents(fotId: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/cadastro-fot/${fotId}/eventos`, { + const response = await apiFetch(`${API_BASE_URL}/api/cadastro-fot/${fotId}/eventos`, { method: "GET", headers: { "Content-Type": "application/json", @@ -510,7 +573,7 @@ export async function checkFotHasEvents(fotId: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/cadastro-fot/${id}/finalize`, { + const response = await apiFetch(`${API_BASE_URL}/api/cadastro-fot/${id}/finalize`, { method: "POST", headers: { "Content-Type": "application/json", @@ -538,7 +601,7 @@ export async function finalizeFOT(id: string, finalizada: boolean, token: string */ export async function deleteCadastroFot(id: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/cadastro-fot/${id}`, { + const response = await apiFetch(`${API_BASE_URL}/api/cadastro-fot/${id}`, { method: "DELETE", headers: { "Content-Type": "application/json", @@ -598,7 +661,7 @@ export async function getUniversities(): Promise< // Agenda export const createAgenda = async (token: string, data: any) => { try { - const response = await fetch(`${API_BASE_URL}/api/agenda`, { + const response = await apiFetch(`${API_BASE_URL}/api/agenda`, { method: "POST", headers: { "Content-Type": "application/json", @@ -624,7 +687,7 @@ export const createAgenda = async (token: string, data: any) => { // Agenda export const getAgendas = async (token: string): Promise> => { try { - const response = await fetch(`${API_BASE_URL}/api/agenda`, { + const response = await apiFetch(`${API_BASE_URL}/api/agenda`, { method: "GET", headers: { "Content-Type": "application/json", @@ -648,7 +711,7 @@ export const getAgendas = async (token: string): Promise> => // Agenda - Update export const updateAgenda = async (token: string, id: string, data: any) => { try { - const response = await fetch(`${API_BASE_URL}/api/agenda/${id}`, { + const response = await apiFetch(`${API_BASE_URL}/api/agenda/${id}`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -673,7 +736,7 @@ export const updateAgenda = async (token: string, id: string, data: any) => { export const updateAssignmentStatus = async (token: string, eventId: string, professionalId: string, status: string, reason?: string) => { try { - const response = await fetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals/${professionalId}/status`, { + const response = await apiFetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals/${professionalId}/status`, { method: "PATCH", headers: { Authorization: `Bearer ${token}`, @@ -702,7 +765,7 @@ export const updateAssignmentStatus = async (token: string, eventId: string, pro */ export async function getPendingUsers(token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/admin/users/pending`, { + const response = await apiFetch(`${API_BASE_URL}/api/admin/users/pending`, { method: "GET", headers: { "Content-Type": "application/json", @@ -736,7 +799,7 @@ export async function getPendingUsers(token: string): Promise */ export async function approveUser(userId: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/admin/users/${userId}/approve`, { + const response = await apiFetch(`${API_BASE_URL}/api/admin/users/${userId}/approve`, { method: "PATCH", headers: { "Content-Type": "application/json", @@ -770,7 +833,7 @@ export async function approveUser(userId: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/admin/users/${userId}/reject`, { + const response = await apiFetch(`${API_BASE_URL}/api/admin/users/${userId}/reject`, { method: "PATCH", headers: { "Content-Type": "application/json", @@ -805,7 +868,7 @@ export async function rejectUser(userId: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/admin/users/${userId}/role`, { + const response = await apiFetch(`${API_BASE_URL}/api/admin/users/${userId}/role`, { method: "PATCH", headers: { "Content-Type": "application/json", @@ -845,7 +908,7 @@ export async function assignProfessional(token: string, eventId: string, profess body.funcao_id = funcaoId; } - const response = await fetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals`, { + const response = await apiFetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals`, { method: "POST", headers: { "Content-Type": "application/json", @@ -871,7 +934,7 @@ export async function assignProfessional(token: string, eventId: string, profess */ export async function removeProfessional(token: string, eventId: string, professionalId: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals/${professionalId}`, { + const response = await apiFetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals/${professionalId}`, { method: "DELETE", headers: { "Content-Type": "application/json", @@ -895,7 +958,7 @@ export async function removeProfessional(token: string, eventId: string, profess */ export async function getAllUsers(token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/admin/users`, { + const response = await apiFetch(`${API_BASE_URL}/api/admin/users`, { method: "GET", headers: { "Content-Type": "application/json", @@ -929,7 +992,7 @@ export async function getAllUsers(token: string): Promise> { */ export async function createAdminUser(data: any, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/admin/users`, { + const response = await apiFetch(`${API_BASE_URL}/api/admin/users`, { method: "POST", headers: { "Content-Type": "application/json", @@ -965,7 +1028,7 @@ export async function createAdminUser(data: any, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals`, { + const response = await apiFetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals`, { method: "GET", headers: { "Content-Type": "application/json", @@ -990,7 +1053,7 @@ export async function getEventProfessionals(token: string, eventId: string): Pro */ export async function updateEventStatus(token: string, eventId: string, status: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/agenda/${eventId}/status`, { + const response = await apiFetch(`${API_BASE_URL}/api/agenda/${eventId}/status`, { method: "PATCH", headers: { "Content-Type": "application/json", @@ -1016,7 +1079,7 @@ export async function updateEventStatus(token: string, eventId: string, status: */ export async function notifyLogistics(token: string, eventId: string, passengerOrders?: any): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/agenda/${eventId}/notify-logistics`, { + const response = await apiFetch(`${API_BASE_URL}/api/agenda/${eventId}/notify-logistics`, { method: "POST", headers: { "Content-Type": "application/json", @@ -1042,7 +1105,7 @@ export async function notifyLogistics(token: string, eventId: string, passengerO */ export async function adminCreateUser(data: any, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/admin/users`, { + const response = await apiFetch(`${API_BASE_URL}/api/admin/users`, { method: "POST", headers: { "Content-Type": "application/json", @@ -1078,7 +1141,7 @@ export async function adminCreateUser(data: any, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/auth/upload-url`, { + const response = await apiFetch(`${API_BASE_URL}/auth/upload-url`, { method: "POST", headers: { "Content-Type": "application/json", @@ -1122,7 +1185,7 @@ export async function deleteAccessCode(token: string, id: string) { // Helpers for unified fetch async function fetchFromBackendAuthenticated(url: string, token: string) { try { - const res = await fetch(url, { + const res = await apiFetch(url, { headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", @@ -1139,7 +1202,7 @@ async function fetchFromBackendAuthenticated(url: string, token: string) { async function mutationFetch(url: string, method: string, body: any, token: string) { try { - const res = await fetch(url, { + const res = await apiFetch(url, { method, headers: { "Authorization": `Bearer ${token}`, @@ -1164,7 +1227,7 @@ async function mutationFetch(url: string, method: string, body: any, token: stri * Realiza o upload do arquivo para a URL pré-assinada */ export async function uploadFileToSignedUrl(uploadUrl: string, file: File): Promise { - const response = await fetch(uploadUrl, { + const response = await apiFetch(uploadUrl, { method: "PUT", headers: { "Content-Type": file.type, @@ -1189,7 +1252,7 @@ export interface EscalaInput { export async function createEscala(data: EscalaInput, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/escalas`, { + const response = await apiFetch(`${API_BASE_URL}/api/escalas`, { method: "POST", headers: { "Content-Type": "application/json", @@ -1213,7 +1276,7 @@ export async function createEscala(data: EscalaInput, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/escalas?agenda_id=${agendaId}`, { + const response = await apiFetch(`${API_BASE_URL}/api/escalas?agenda_id=${agendaId}`, { headers: { "Authorization": `Bearer ${token}` }, }); @@ -1231,7 +1294,7 @@ export async function listEscalas(agendaId: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/escalas/${id}`, { + const response = await apiFetch(`${API_BASE_URL}/api/escalas/${id}`, { method: "DELETE", headers: { "Authorization": `Bearer ${token}` }, }); @@ -1249,7 +1312,7 @@ export async function deleteEscala(id: string, token: string): Promise, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/escalas/${id}`, { + const response = await apiFetch(`${API_BASE_URL}/api/escalas/${id}`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -1282,7 +1345,7 @@ export interface CarroInput { export async function createCarro(data: CarroInput, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/logistica/carros`, { + const response = await apiFetch(`${API_BASE_URL}/api/logistica/carros`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, body: JSON.stringify(data), @@ -1295,7 +1358,7 @@ export async function createCarro(data: CarroInput, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/logistica/carros?agenda_id=${agendaId}`, { + const response = await apiFetch(`${API_BASE_URL}/api/logistica/carros?agenda_id=${agendaId}`, { headers: { "Authorization": `Bearer ${token}` }, }); const resData = await response.json(); @@ -1306,7 +1369,7 @@ export async function listCarros(agendaId: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/logistica/carros/${id}`, { + const response = await apiFetch(`${API_BASE_URL}/api/logistica/carros/${id}`, { method: "DELETE", headers: { "Authorization": `Bearer ${token}` }, }); @@ -1317,7 +1380,7 @@ export async function deleteCarro(id: string, token: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/profissionais/me/financial-statement`, { + const response = await apiFetch(`${API_BASE_URL}/api/profissionais/me/financial-statement`, { method: "GET", headers: { "Content-Type": "application/json", @@ -1396,7 +1459,7 @@ export async function getProfessionalFinancialStatement(token: string): Promise< */ export async function getPrice(token: string, eventName: string, serviceName: string): Promise> { try { - const response = await fetch(`${API_BASE_URL}/api/finance/price?event=${encodeURIComponent(eventName)}&service=${encodeURIComponent(serviceName)}`, { + const response = await apiFetch(`${API_BASE_URL}/api/finance/price?event=${encodeURIComponent(eventName)}&service=${encodeURIComponent(serviceName)}`, { method: "GET", headers: { "Content-Type": "application/json", @@ -1419,7 +1482,7 @@ export async function getPrice(token: string, eventName: string, serviceName: st export const setCoordinator = async (token: string, eventId: string, professionalId: string, isCoordinator: boolean) => { try { const region = localStorage.getItem("photum_selected_region") || "SP"; - const response = await fetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals/${professionalId}/coordinator`, { + const response = await apiFetch(`${API_BASE_URL}/api/agenda/${eventId}/professionals/${professionalId}/coordinator`, { method: "PUT", headers: { Authorization: `Bearer ${token}`,