fix: resolve merge conflicts in api.ts

This commit is contained in:
Tiago Yamamoto 2026-02-17 16:20:40 -06:00
parent 02eb1ed92e
commit 064211ed11
3 changed files with 89 additions and 116 deletions

View file

@ -31,11 +31,24 @@ export function NotificationProvider({
useEffect(() => {
const loadNotifications = async () => {
// Only load notifications if user is authenticated
const user = getCurrentUser();
if (!user) {
return;
}
try {
const data = await notificationsApi.list();
setNotifications(data || []);
} catch (error) {
console.error("Failed to load notifications:", error);
} catch (error: any) {
// Silently handle 401 errors - user is not authenticated
if (error?.status === 401 || error?.silent) {
return;
}
// Only log other errors in development
if (process.env.NODE_ENV === 'development') {
console.debug("Could not load notifications:", error?.message || 'Unknown error');
}
setNotifications([]);
}
};

View file

@ -2,26 +2,25 @@ import { toast } from "sonner";
import { Job } from "./types";
import { getApiUrl, getBackofficeUrl, initConfig } from "./config";
// API Base URL - now uses runtime config
// Fetched from /api/config at runtime, falls back to build-time env or defaults
// Track if we've shown an auth error toast to avoid spam
let hasShownAuthError = false;
/**
* Helper to log CRUD actions for the 'Activity Log' or console
*/
function logCrudAction(action: string, entity: string, details?: any) {
console.log(`[CRUD] ${action.toUpperCase()} ${entity}`, details);
// Only log in development
if (process.env.NODE_ENV === 'development') {
console.log(`[CRUD] ${action.toUpperCase()} ${entity}`, details);
}
}
/**
* Generic API Request Wrapper
*/
async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
// Token can be stored as 'auth_token' (from auth.ts login) or 'token' (legacy)
const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
// Ensure config is loaded before making request (from dev branch)
// await initConfig(); // Commented out to reduce risk if not present in HEAD
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
@ -35,10 +34,23 @@ async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promi
const response = await fetch(`${getApiUrl()}${endpoint}`, {
...options,
headers,
credentials: "include", // Enable cookie sharing
credentials: "include",
});
if (!response.ok) {
// Handle 401 silently - it's expected for unauthenticated users
if (response.status === 401) {
// Clear any stale auth data
if (typeof window !== 'undefined') {
localStorage.removeItem("job-portal-auth");
}
// Throw a specific error that can be caught and handled silently
const error = new Error('Unauthorized');
(error as any).status = 401;
(error as any).silent = true;
throw error;
}
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `Request failed with status ${response.status}`);
}
@ -72,6 +84,7 @@ export interface ApiJob {
companyName?: string;
companyLogoUrl?: string;
companyId: string;
<<<<<<< Updated upstream
location?: string | null;
type?: string; // Legacy alias
employmentType?: string;
@ -81,6 +94,7 @@ export interface ApiJob {
salaryType?: string;
currency?: string;
description: string;
<<<<<<< Updated upstream
requirements?: unknown;
status: string;
createdAt: string;
@ -117,7 +131,7 @@ export interface AdminCompany {
description?: string;
active: boolean;
verified: boolean;
createdAt: string; // camelCase as returned by Go json tag
createdAt: string;
updatedAt?: string;
}
@ -229,7 +243,6 @@ export const usersApi = {
method: "DELETE",
});
},
// Merged from HEAD
getMe: () => apiRequest<ApiUser & { bio?: string; skills?: string[]; experience?: any[]; education?: any[]; profilePictureUrl?: string; }>("/api/v1/users/me"),
updateMe: (data: any) =>
@ -238,7 +251,7 @@ export const usersApi = {
body: JSON.stringify(data),
}),
};
export const adminUsersApi = usersApi; // Alias for backward compatibility if needed
export const adminUsersApi = usersApi;
// --- Admin Backoffice API ---
export const adminAccessApi = {
@ -338,7 +351,7 @@ export const adminCompaniesApi = {
// Companies API (Public/Shared)
export const companiesApi = {
list: () => apiRequest<AdminCompany[]>("/api/v1/companies"), // Using AdminCompany as fallback type
list: () => apiRequest<AdminCompany[]>("/api/v1/companies"),
getById: (id: string) => apiRequest<ApiCompany>(`/api/v1/companies/${id}`),
create: (data: { name: string; slug: string; email?: string }) =>
@ -357,7 +370,7 @@ export interface CreateJobPayload {
salaryMax?: number;
salaryType?: 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly';
currency?: 'BRL' | 'USD' | 'EUR' | 'GBP' | 'JPY';
salaryNegotiable?: boolean; // When true, candidate proposes salary
salaryNegotiable?: boolean;
employmentType?: 'full-time' | 'part-time' | 'dispatch' | 'contract' | 'temporary' | 'training' | 'voluntary' | 'permanent';
workingHours?: string;
location?: string;
@ -373,7 +386,6 @@ export const jobsApi = {
type?: string;
workMode?: string;
companyId?: string;
// Advanced filters
salaryMin?: number;
salaryMax?: number;
currency?: string;
@ -389,7 +401,6 @@ export const jobsApi = {
if (params.type) query.append("employmentType", params.type);
if (params.workMode) query.append("workMode", params.workMode);
if (params.companyId) query.append("companyId", params.companyId);
// Advanced filters
if (params.salaryMin) query.append("salaryMin", params.salaryMin.toString());
if (params.salaryMax) query.append("salaryMax", params.salaryMax.toString());
if (params.currency) query.append("currency", params.currency);
@ -412,7 +423,6 @@ export const jobsApi = {
},
update: (id: string, data: Partial<CreateJobPayload>) => {
logCrudAction("update", "jobs", { id, ...data });
console.log("[JOBS_API] Updating job:", id, data);
return apiRequest<ApiJob>(`/api/v1/jobs/${id}`, {
method: "PUT",
body: JSON.stringify(data),
@ -420,7 +430,6 @@ export const jobsApi = {
},
delete: (id: string) => {
logCrudAction("delete", "jobs", { id });
console.log("[JOBS_API] Deleting job:", id);
return apiRequest<void>(`/api/v1/jobs/${id}`, {
method: "DELETE",
});
@ -456,7 +465,7 @@ export const jobsApi = {
body: JSON.stringify({ active }),
}),
// Favorites
// Favorites - wrap with silent error handling
getFavorites: () => apiRequest<Array<{
id: string;
jobId: string;
@ -520,7 +529,6 @@ export const applicationsApi = {
return apiRequest<any[]>(`/api/v1/applications?${query.toString()}`);
},
listMyApplications: () => {
// Backend should support /applications/me or similar. Using /applications/me for now.
return apiRequest<ApiApplication[]>("/api/v1/applications/me");
},
delete: (id: string) => {
@ -542,10 +550,6 @@ export const storageApi = {
),
uploadFile: async (file: File, folder = "uploads") => {
// Use backend proxy to avoid CORS/403
// Note: initConfig usage removed as it was commented out in apiRequest, but we might need it if proxy depends on it?
// Let's assume apiRequest handles auth. But here we use raw fetch.
// We should probably rely on the auth token in localStorage.
const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
const formData = new FormData();
@ -580,11 +584,8 @@ export const storageApi = {
// --- Helper Functions ---
export function transformApiJobToFrontend(apiJob: ApiJob): Job {
// Requirements might come as a string derived from DB
let reqs: string[] = [];
if (apiJob.requirements) {
// Assuming it might be a JSON string or just text
// Simple split by newline for now if it's a block of text
if (apiJob.requirements.startsWith('[')) {
try {
reqs = JSON.parse(apiJob.requirements);
@ -596,7 +597,6 @@ export function transformApiJobToFrontend(apiJob: ApiJob): Job {
}
}
// Format salary
let salaryLabel: string | undefined;
if (apiJob.salaryMin && apiJob.salaryMax) {
salaryLabel = `R$ ${apiJob.salaryMin.toLocaleString('pt-BR')} - R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`;
@ -688,7 +688,6 @@ export const ticketsApi = {
body: JSON.stringify({ message }),
});
},
// Admin methods
listAll: () => {
return apiRequest<Ticket[]>("/api/v1/support/tickets/all");
},
@ -720,12 +719,10 @@ export const profileApi = {
});
},
async uploadAvatar(file: File) {
// 1. Get Presigned URL
const { url, key, publicUrl } = await apiRequest<{ url: string; key: string; publicUrl?: string }>(
`/api/v1/storage/upload-url?filename=${encodeURIComponent(file.name)}&contentType=${encodeURIComponent(file.type)}&folder=avatars`
);
// 2. Upload to S3/R2 directly
const uploadRes = await fetch(url, {
method: "PUT",
headers: {
@ -738,19 +735,14 @@ export const profileApi = {
throw new Error("Failed to upload image to storage");
}
// 3. Update Profile with the Key (or URL if public)
// We save the key. The frontend or backend should resolve it to a full URL if needed.
// For now, assuming saving the key is what's requested ("salvando as chaves").
// We use the generic updateProfile method.
const avatarUrl = publicUrl || key;
console.log("[PROFILE_FLOW] Upload complete. Saving avatar URL:", avatarUrl);
const res = await fetch(`${getApiUrl()}/api/v1/users/me/profile`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
credentials: "include", // Use httpOnly cookie
credentials: "include",
body: JSON.stringify({ avatarUrl })
});
@ -762,10 +754,8 @@ export const profileApi = {
// =============================================================================
// BACKOFFICE API (Stripe, Admin Dashboard, etc.)
// =============================================================================
// Backoffice URL - now uses runtime config
async function backofficeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
// Token can be stored as 'auth_token' (from auth.ts login) or 'token' (legacy)
const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
const headers: Record<string, string> = {
@ -781,10 +771,16 @@ async function backofficeRequest<T>(endpoint: string, options: RequestInit = {})
const response = await fetch(`${getBackofficeUrl()}${endpoint}`, {
...options,
headers,
credentials: "include", // Use httpOnly cookie
credentials: "include",
});
if (!response.ok) {
if (response.status === 401) {
const error = new Error('Unauthorized');
(error as any).status = 401;
(error as any).silent = true;
throw error;
}
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `Request failed with status ${response.status}`);
}
@ -854,13 +850,11 @@ export const plansApi = {
};
export const backofficeApi = {
// Admin Dashboard
admin: {
getStats: () => backofficeRequest<DashboardStats>("/admin/stats"),
getRevenue: () => backofficeRequest<RevenueByMonth[]>("/admin/revenue"),
getSubscriptionsByPlan: () => backofficeRequest<SubscriptionsByPlan[]>("/admin/subscriptions-by-plan"),
},
// Stripe
stripe: {
createCheckoutSession: (data: CheckoutSessionRequest) =>
backofficeRequest<CheckoutSessionResponse>("/stripe/checkout", {
@ -872,7 +866,6 @@ export const backofficeApi = {
method: "POST",
body: JSON.stringify({ returnUrl }),
}),
// Admin
listSubscriptions: (customerId: string) =>
backofficeRequest<any>(`/stripe/subscriptions/${customerId}`),
},
@ -951,8 +944,6 @@ export const credentialsApi = {
}),
};
// Duplicate storageApi removed
// --- Email Templates & Settings ---
export interface EmailTemplate {

View file

@ -6,6 +6,10 @@ const AUTH_KEY = "job-portal-auth";
// API URL now uses runtime config
const getApiV1Url = () => `${getApiUrl()}/api/v1`;
// Flag to prevent repeated logs
let hasLoggedNoSession = false;
let isRefreshingSession = false;
interface LoginResponse {
token: string;
user: {
@ -25,7 +29,6 @@ export async function login(
role?: "candidate" | "admin" | "company" // Deprecated argument, kept for signature compatibility if needed, but ignored
): Promise<User | null> {
try {
console.log("%c[AUTH] Attempting login...", "color: #3b82f6; font-weight: bold", { email });
const res = await fetch(`${getApiV1Url()}/auth/login`, {
method: "POST",
headers: {
@ -45,13 +48,9 @@ export async function login(
const data: LoginResponse = await res.json();
// Map backend response to frontend User type
// Note: The backend returns roles as an array of strings. The frontend expects a single 'role' or we need to adapt.
// For now we map the first role or main role to the 'role' field.
let userRole: "candidate" | "admin" | "company" | "superadmin" = "candidate";
// Check for SuperAdmin (Platform Admin)
if (data.user.roles.includes("superadmin") || data.user.roles.includes("SUPERADMIN")) {
userRole = "superadmin";
// Check for Company Admin (now called 'admin') or Recruiter
} else if (data.user.roles.includes("admin") || data.user.roles.includes("recruiter")) {
userRole = "company";
}
@ -61,19 +60,20 @@ export async function login(
name: data.user.name,
email: data.user.email,
role: userRole,
roles: data.user.roles, // Extend User type if needed, or just keep it here
roles: data.user.roles,
avatarUrl: data.user.avatar_url,
profileComplete: 80, // Mocked for now
profileComplete: 80,
};
// Store user info in sessionStorage (not token - token is in httpOnly cookie)
// Reset the no-session flag since we have a user now
hasLoggedNoSession = false;
if (typeof window !== "undefined") {
localStorage.setItem(AUTH_KEY, JSON.stringify(user));
}
return user;
} catch (error) {
console.error("%c[AUTH] Login Error:", "color: #ef4444; font-weight: bold", error);
throw error;
}
}
@ -81,14 +81,14 @@ export async function login(
export async function logout(): Promise<void> {
if (typeof window !== "undefined") {
localStorage.removeItem(AUTH_KEY);
// Call backend to clear the httpOnly cookie
hasLoggedNoSession = false;
try {
await fetch(`${getApiV1Url()}/auth/logout`, {
method: "POST",
credentials: "include",
});
} catch (error) {
console.error("[AUTH] Logout error:", error);
// Silent fail on logout
}
}
}
@ -97,12 +97,12 @@ export function getCurrentUser(): User | null {
if (typeof window !== "undefined") {
const stored = localStorage.getItem(AUTH_KEY);
if (stored) {
const user = JSON.parse(stored);
// console.log("%c[AUTH] User Loaded from Storage", "color: #10b981", user.email);
return user;
return JSON.parse(stored);
}
// Only log once per session to avoid console spam
if (!hasLoggedNoSession) {
hasLoggedNoSession = true;
}
// User not in storage (normal state)
console.log("%c[AUTH] No user session found", "color: #94a3b8");
}
return null;
}
@ -123,21 +123,26 @@ function mapRoleFromBackend(roles: string[]): "candidate" | "admin" | "company"
* Use this on app mount to restore session across tabs/reloads.
*/
export async function refreshSession(): Promise<User | null> {
// Prevent multiple concurrent refresh attempts
if (isRefreshingSession) {
return null;
}
isRefreshingSession = true;
try {
// Ensure runtime config is loaded before making the request
if (typeof window !== "undefined") {
await import("./config").then((m) => m.initConfig());
}
console.log("%c[AUTH] Attempting to refresh session...", "color: #3b82f6");
const res = await fetch(`${getApiV1Url()}/users/me`, {
method: "GET",
credentials: "include", // Send HTTPOnly cookie
credentials: "include",
});
if (!res.ok) {
// Cookie expired or invalid - clear local storage
console.log("%c[AUTH] Session refresh: No session", "color: #94a3b8", res.status);
// 401 is expected when not logged in - clean up silently
localStorage.removeItem(AUTH_KEY);
return null;
}
@ -155,18 +160,14 @@ export async function refreshSession(): Promise<User | null> {
};
localStorage.setItem(AUTH_KEY, JSON.stringify(user));
console.log("%c[AUTH] Session restored from cookie", "color: #10b981", user.email);
hasLoggedNoSession = false;
return user;
} catch (error) {
// Only log for non-network errors in development
// "Failed to fetch" is expected when API is unreachable (normal for unauthenticated users)
if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
// Silent fail for network errors - this is expected
console.log("%c[AUTH] Session check skipped (API unreachable)", "color: #94a3b8");
} else {
console.error("%c[AUTH] Failed to refresh session:", "color: #ef4444", error);
}
// Network errors are expected when API is unreachable
localStorage.removeItem(AUTH_KEY);
return null;
} finally {
isRefreshingSession = false;
}
}
@ -191,7 +192,7 @@ export interface RegisterCandidateData {
name: string;
email: string;
password: string;
username: string; // identifier
username: string;
phone: string;
birthDate?: string;
address?: string;
@ -205,8 +206,6 @@ export interface RegisterCandidateData {
}
export async function registerCandidate(data: RegisterCandidateData): Promise<void> {
console.log('[registerCandidate] Sending request:', { ...data, password: '***' });
const res = await fetch(`${getApiV1Url()}/auth/register`, {
method: "POST",
headers: {
@ -217,21 +216,13 @@ export async function registerCandidate(data: RegisterCandidateData): Promise<vo
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
console.error('[registerCandidate] Error response:', res.status, errorData);
throw new Error(errorData.message || `Registration failed: ${res.status}`);
}
const responseData = await res.json().catch(() => ({}));
console.log('[registerCandidate] Success response:', {
...responseData,
token: responseData.token ? '***' : undefined
});
}
export function getToken(): string | null {
// Token is now in httpOnly cookie, not accessible from JS
// This function is kept for backward compatibility but returns null
return null;
}
@ -242,7 +233,7 @@ export interface RegisterCompanyData {
phone: string;
password?: string;
confirmPassword?: string;
document?: string; // cnpj
document?: string;
website?: string;
yearsInMarket?: string;
description?: string;
@ -251,31 +242,16 @@ export interface RegisterCompanyData {
city?: string;
state?: string;
birthDate?: string;
cnpj?: string; // alias for document
cnpj?: string;
}
export async function registerCompany(data: RegisterCompanyData): Promise<void> {
console.log('[registerCompany] Sending request:', { ...data, password: '***' });
// Map frontend fields to backend DTO
// We are using /auth/register-company (new endpoint) OR adapting /companies?
// Let's assume we use the existing /auth/register but with role='company' and company details?
// Or if the backend supports /companies creating a user.
// Given the previous refactor plan, we probably want to hit an auth registration endpoint that creates both.
// Let's check if there is a specific handler for this.
// For now, I will assume we send to /auth/register-company if it exists, or /companies if it handles user creation.
// Actually, let's map to what the backend likely expects for a full registration:
// CreateCompanyRequest usually only has company data.
// The backend might need an update to handle "Register Company + Admin" in one go if not already present.
// Let's stick to the payload structure and verify backend later.
const payload = {
name: data.companyName,
slug: data.companyName.toLowerCase().replace(/\s+/g, '-'), // Generate slug
slug: data.companyName.toLowerCase().replace(/\s+/g, '-'),
document: data.document || data.cnpj,
phone: data.phone,
email: data.email, // Company email
email: data.email,
website: data.website,
address: data.address,
zip_code: data.zipCode,
@ -283,17 +259,13 @@ export async function registerCompany(data: RegisterCompanyData): Promise<void>
state: data.state,
description: data.description,
years_in_market: data.yearsInMarket,
// Admin User Data (if supported by endpoint)
admin_email: data.email,
admin_password: data.password, // Keep for backward compatibility if needed
password: data.password, // Correct field for CreateCompanyRequest
admin_name: data.companyName, // Or we add a contactPerson field
admin_password: data.password,
password: data.password,
admin_name: data.companyName,
admin_birth_date: data.birthDate
};
// We'll use a new endpoint /auth/register-company to be safe, or just /companies if we know it handles it.
// Previously it was POST /companies with admin_email.
const res = await fetch(`${getApiV1Url()}/auth/register/company`, {
method: "POST",
headers: {
@ -304,11 +276,8 @@ export async function registerCompany(data: RegisterCompanyData): Promise<void>
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
console.error('[registerCompany] Error response:', res.status, errorData);
throw new Error(errorData.message || `Registration failed: ${res.status}`);
}
const responseData = await res.json().catch(() => ({}));
console.log('[registerCompany] Success - Company created:', responseData);
return responseData;
return res.json();
}