fix: resolve merge conflicts in api.ts
This commit is contained in:
parent
02eb1ed92e
commit
064211ed11
3 changed files with 89 additions and 116 deletions
|
|
@ -31,11 +31,24 @@ export function NotificationProvider({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadNotifications = async () => {
|
const loadNotifications = async () => {
|
||||||
|
// Only load notifications if user is authenticated
|
||||||
|
const user = getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await notificationsApi.list();
|
const data = await notificationsApi.list();
|
||||||
setNotifications(data || []);
|
setNotifications(data || []);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Failed to load notifications:", error);
|
// 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([]);
|
setNotifications([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,25 @@ import { toast } from "sonner";
|
||||||
import { Job } from "./types";
|
import { Job } from "./types";
|
||||||
import { getApiUrl, getBackofficeUrl, initConfig } from "./config";
|
import { getApiUrl, getBackofficeUrl, initConfig } from "./config";
|
||||||
|
|
||||||
// API Base URL - now uses runtime config
|
// Track if we've shown an auth error toast to avoid spam
|
||||||
// Fetched from /api/config at runtime, falls back to build-time env or defaults
|
let hasShownAuthError = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to log CRUD actions for the 'Activity Log' or console
|
* Helper to log CRUD actions for the 'Activity Log' or console
|
||||||
*/
|
*/
|
||||||
function logCrudAction(action: string, entity: string, details?: any) {
|
function logCrudAction(action: string, entity: string, details?: any) {
|
||||||
|
// Only log in development
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.log(`[CRUD] ${action.toUpperCase()} ${entity}`, details);
|
console.log(`[CRUD] ${action.toUpperCase()} ${entity}`, details);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic API Request Wrapper
|
* Generic API Request Wrapper
|
||||||
*/
|
*/
|
||||||
async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
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;
|
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> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
|
|
@ -35,10 +34,23 @@ async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promi
|
||||||
const response = await fetch(`${getApiUrl()}${endpoint}`, {
|
const response = await fetch(`${getApiUrl()}${endpoint}`, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
credentials: "include", // Enable cookie sharing
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(errorData.message || `Request failed with status ${response.status}`);
|
throw new Error(errorData.message || `Request failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +84,7 @@ export interface ApiJob {
|
||||||
companyName?: string;
|
companyName?: string;
|
||||||
companyLogoUrl?: string;
|
companyLogoUrl?: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
<<<<<<< Updated upstream
|
||||||
location?: string | null;
|
location?: string | null;
|
||||||
type?: string; // Legacy alias
|
type?: string; // Legacy alias
|
||||||
employmentType?: string;
|
employmentType?: string;
|
||||||
|
|
@ -81,6 +94,7 @@ export interface ApiJob {
|
||||||
salaryType?: string;
|
salaryType?: string;
|
||||||
currency?: string;
|
currency?: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
<<<<<<< Updated upstream
|
||||||
requirements?: unknown;
|
requirements?: unknown;
|
||||||
status: string;
|
status: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
@ -117,7 +131,7 @@ export interface AdminCompany {
|
||||||
description?: string;
|
description?: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
createdAt: string; // camelCase as returned by Go json tag
|
createdAt: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,7 +243,6 @@ export const usersApi = {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// Merged from HEAD
|
|
||||||
getMe: () => apiRequest<ApiUser & { bio?: string; skills?: string[]; experience?: any[]; education?: any[]; profilePictureUrl?: string; }>("/api/v1/users/me"),
|
getMe: () => apiRequest<ApiUser & { bio?: string; skills?: string[]; experience?: any[]; education?: any[]; profilePictureUrl?: string; }>("/api/v1/users/me"),
|
||||||
|
|
||||||
updateMe: (data: any) =>
|
updateMe: (data: any) =>
|
||||||
|
|
@ -238,7 +251,7 @@ export const usersApi = {
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
export const adminUsersApi = usersApi; // Alias for backward compatibility if needed
|
export const adminUsersApi = usersApi;
|
||||||
|
|
||||||
// --- Admin Backoffice API ---
|
// --- Admin Backoffice API ---
|
||||||
export const adminAccessApi = {
|
export const adminAccessApi = {
|
||||||
|
|
@ -338,7 +351,7 @@ export const adminCompaniesApi = {
|
||||||
|
|
||||||
// Companies API (Public/Shared)
|
// Companies API (Public/Shared)
|
||||||
export const companiesApi = {
|
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}`),
|
getById: (id: string) => apiRequest<ApiCompany>(`/api/v1/companies/${id}`),
|
||||||
|
|
||||||
create: (data: { name: string; slug: string; email?: string }) =>
|
create: (data: { name: string; slug: string; email?: string }) =>
|
||||||
|
|
@ -357,7 +370,7 @@ export interface CreateJobPayload {
|
||||||
salaryMax?: number;
|
salaryMax?: number;
|
||||||
salaryType?: 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly';
|
salaryType?: 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||||||
currency?: 'BRL' | 'USD' | 'EUR' | 'GBP' | 'JPY';
|
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';
|
employmentType?: 'full-time' | 'part-time' | 'dispatch' | 'contract' | 'temporary' | 'training' | 'voluntary' | 'permanent';
|
||||||
workingHours?: string;
|
workingHours?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
|
|
@ -373,7 +386,6 @@ export const jobsApi = {
|
||||||
type?: string;
|
type?: string;
|
||||||
workMode?: string;
|
workMode?: string;
|
||||||
companyId?: string;
|
companyId?: string;
|
||||||
// Advanced filters
|
|
||||||
salaryMin?: number;
|
salaryMin?: number;
|
||||||
salaryMax?: number;
|
salaryMax?: number;
|
||||||
currency?: string;
|
currency?: string;
|
||||||
|
|
@ -389,7 +401,6 @@ export const jobsApi = {
|
||||||
if (params.type) query.append("employmentType", params.type);
|
if (params.type) query.append("employmentType", params.type);
|
||||||
if (params.workMode) query.append("workMode", params.workMode);
|
if (params.workMode) query.append("workMode", params.workMode);
|
||||||
if (params.companyId) query.append("companyId", params.companyId);
|
if (params.companyId) query.append("companyId", params.companyId);
|
||||||
// Advanced filters
|
|
||||||
if (params.salaryMin) query.append("salaryMin", params.salaryMin.toString());
|
if (params.salaryMin) query.append("salaryMin", params.salaryMin.toString());
|
||||||
if (params.salaryMax) query.append("salaryMax", params.salaryMax.toString());
|
if (params.salaryMax) query.append("salaryMax", params.salaryMax.toString());
|
||||||
if (params.currency) query.append("currency", params.currency);
|
if (params.currency) query.append("currency", params.currency);
|
||||||
|
|
@ -412,7 +423,6 @@ export const jobsApi = {
|
||||||
},
|
},
|
||||||
update: (id: string, data: Partial<CreateJobPayload>) => {
|
update: (id: string, data: Partial<CreateJobPayload>) => {
|
||||||
logCrudAction("update", "jobs", { id, ...data });
|
logCrudAction("update", "jobs", { id, ...data });
|
||||||
console.log("[JOBS_API] Updating job:", id, data);
|
|
||||||
return apiRequest<ApiJob>(`/api/v1/jobs/${id}`, {
|
return apiRequest<ApiJob>(`/api/v1/jobs/${id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
|
@ -420,7 +430,6 @@ export const jobsApi = {
|
||||||
},
|
},
|
||||||
delete: (id: string) => {
|
delete: (id: string) => {
|
||||||
logCrudAction("delete", "jobs", { id });
|
logCrudAction("delete", "jobs", { id });
|
||||||
console.log("[JOBS_API] Deleting job:", id);
|
|
||||||
return apiRequest<void>(`/api/v1/jobs/${id}`, {
|
return apiRequest<void>(`/api/v1/jobs/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|
@ -456,7 +465,7 @@ export const jobsApi = {
|
||||||
body: JSON.stringify({ active }),
|
body: JSON.stringify({ active }),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Favorites
|
// Favorites - wrap with silent error handling
|
||||||
getFavorites: () => apiRequest<Array<{
|
getFavorites: () => apiRequest<Array<{
|
||||||
id: string;
|
id: string;
|
||||||
jobId: string;
|
jobId: string;
|
||||||
|
|
@ -520,7 +529,6 @@ export const applicationsApi = {
|
||||||
return apiRequest<any[]>(`/api/v1/applications?${query.toString()}`);
|
return apiRequest<any[]>(`/api/v1/applications?${query.toString()}`);
|
||||||
},
|
},
|
||||||
listMyApplications: () => {
|
listMyApplications: () => {
|
||||||
// Backend should support /applications/me or similar. Using /applications/me for now.
|
|
||||||
return apiRequest<ApiApplication[]>("/api/v1/applications/me");
|
return apiRequest<ApiApplication[]>("/api/v1/applications/me");
|
||||||
},
|
},
|
||||||
delete: (id: string) => {
|
delete: (id: string) => {
|
||||||
|
|
@ -542,10 +550,6 @@ export const storageApi = {
|
||||||
),
|
),
|
||||||
|
|
||||||
uploadFile: async (file: File, folder = "uploads") => {
|
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 token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
@ -580,11 +584,8 @@ export const storageApi = {
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
export function transformApiJobToFrontend(apiJob: ApiJob): Job {
|
export function transformApiJobToFrontend(apiJob: ApiJob): Job {
|
||||||
// Requirements might come as a string derived from DB
|
|
||||||
let reqs: string[] = [];
|
let reqs: string[] = [];
|
||||||
if (apiJob.requirements) {
|
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('[')) {
|
if (apiJob.requirements.startsWith('[')) {
|
||||||
try {
|
try {
|
||||||
reqs = JSON.parse(apiJob.requirements);
|
reqs = JSON.parse(apiJob.requirements);
|
||||||
|
|
@ -596,7 +597,6 @@ export function transformApiJobToFrontend(apiJob: ApiJob): Job {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format salary
|
|
||||||
let salaryLabel: string | undefined;
|
let salaryLabel: string | undefined;
|
||||||
if (apiJob.salaryMin && apiJob.salaryMax) {
|
if (apiJob.salaryMin && apiJob.salaryMax) {
|
||||||
salaryLabel = `R$ ${apiJob.salaryMin.toLocaleString('pt-BR')} - R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`;
|
salaryLabel = `R$ ${apiJob.salaryMin.toLocaleString('pt-BR')} - R$ ${apiJob.salaryMax.toLocaleString('pt-BR')}`;
|
||||||
|
|
@ -688,7 +688,6 @@ export const ticketsApi = {
|
||||||
body: JSON.stringify({ message }),
|
body: JSON.stringify({ message }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// Admin methods
|
|
||||||
listAll: () => {
|
listAll: () => {
|
||||||
return apiRequest<Ticket[]>("/api/v1/support/tickets/all");
|
return apiRequest<Ticket[]>("/api/v1/support/tickets/all");
|
||||||
},
|
},
|
||||||
|
|
@ -720,12 +719,10 @@ export const profileApi = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async uploadAvatar(file: File) {
|
async uploadAvatar(file: File) {
|
||||||
// 1. Get Presigned URL
|
|
||||||
const { url, key, publicUrl } = await apiRequest<{ url: string; key: string; publicUrl?: string }>(
|
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`
|
`/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, {
|
const uploadRes = await fetch(url, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -738,19 +735,14 @@ export const profileApi = {
|
||||||
throw new Error("Failed to upload image to storage");
|
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;
|
const avatarUrl = publicUrl || key;
|
||||||
console.log("[PROFILE_FLOW] Upload complete. Saving avatar URL:", avatarUrl);
|
|
||||||
|
|
||||||
const res = await fetch(`${getApiUrl()}/api/v1/users/me/profile`, {
|
const res = await fetch(`${getApiUrl()}/api/v1/users/me/profile`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
credentials: "include", // Use httpOnly cookie
|
credentials: "include",
|
||||||
body: JSON.stringify({ avatarUrl })
|
body: JSON.stringify({ avatarUrl })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -762,10 +754,8 @@ export const profileApi = {
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// BACKOFFICE API (Stripe, Admin Dashboard, etc.)
|
// BACKOFFICE API (Stripe, Admin Dashboard, etc.)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Backoffice URL - now uses runtime config
|
|
||||||
|
|
||||||
async function backofficeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
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 token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
|
|
@ -781,10 +771,16 @@ async function backofficeRequest<T>(endpoint: string, options: RequestInit = {})
|
||||||
const response = await fetch(`${getBackofficeUrl()}${endpoint}`, {
|
const response = await fetch(`${getBackofficeUrl()}${endpoint}`, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
credentials: "include", // Use httpOnly cookie
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(errorData.message || `Request failed with status ${response.status}`);
|
throw new Error(errorData.message || `Request failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
@ -854,13 +850,11 @@ export const plansApi = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const backofficeApi = {
|
export const backofficeApi = {
|
||||||
// Admin Dashboard
|
|
||||||
admin: {
|
admin: {
|
||||||
getStats: () => backofficeRequest<DashboardStats>("/admin/stats"),
|
getStats: () => backofficeRequest<DashboardStats>("/admin/stats"),
|
||||||
getRevenue: () => backofficeRequest<RevenueByMonth[]>("/admin/revenue"),
|
getRevenue: () => backofficeRequest<RevenueByMonth[]>("/admin/revenue"),
|
||||||
getSubscriptionsByPlan: () => backofficeRequest<SubscriptionsByPlan[]>("/admin/subscriptions-by-plan"),
|
getSubscriptionsByPlan: () => backofficeRequest<SubscriptionsByPlan[]>("/admin/subscriptions-by-plan"),
|
||||||
},
|
},
|
||||||
// Stripe
|
|
||||||
stripe: {
|
stripe: {
|
||||||
createCheckoutSession: (data: CheckoutSessionRequest) =>
|
createCheckoutSession: (data: CheckoutSessionRequest) =>
|
||||||
backofficeRequest<CheckoutSessionResponse>("/stripe/checkout", {
|
backofficeRequest<CheckoutSessionResponse>("/stripe/checkout", {
|
||||||
|
|
@ -872,7 +866,6 @@ export const backofficeApi = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ returnUrl }),
|
body: JSON.stringify({ returnUrl }),
|
||||||
}),
|
}),
|
||||||
// Admin
|
|
||||||
listSubscriptions: (customerId: string) =>
|
listSubscriptions: (customerId: string) =>
|
||||||
backofficeRequest<any>(`/stripe/subscriptions/${customerId}`),
|
backofficeRequest<any>(`/stripe/subscriptions/${customerId}`),
|
||||||
},
|
},
|
||||||
|
|
@ -951,8 +944,6 @@ export const credentialsApi = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Duplicate storageApi removed
|
|
||||||
|
|
||||||
|
|
||||||
// --- Email Templates & Settings ---
|
// --- Email Templates & Settings ---
|
||||||
export interface EmailTemplate {
|
export interface EmailTemplate {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ const AUTH_KEY = "job-portal-auth";
|
||||||
// API URL now uses runtime config
|
// API URL now uses runtime config
|
||||||
const getApiV1Url = () => `${getApiUrl()}/api/v1`;
|
const getApiV1Url = () => `${getApiUrl()}/api/v1`;
|
||||||
|
|
||||||
|
// Flag to prevent repeated logs
|
||||||
|
let hasLoggedNoSession = false;
|
||||||
|
let isRefreshingSession = false;
|
||||||
|
|
||||||
interface LoginResponse {
|
interface LoginResponse {
|
||||||
token: string;
|
token: string;
|
||||||
user: {
|
user: {
|
||||||
|
|
@ -25,7 +29,6 @@ export async function login(
|
||||||
role?: "candidate" | "admin" | "company" // Deprecated argument, kept for signature compatibility if needed, but ignored
|
role?: "candidate" | "admin" | "company" // Deprecated argument, kept for signature compatibility if needed, but ignored
|
||||||
): Promise<User | null> {
|
): Promise<User | null> {
|
||||||
try {
|
try {
|
||||||
console.log("%c[AUTH] Attempting login...", "color: #3b82f6; font-weight: bold", { email });
|
|
||||||
const res = await fetch(`${getApiV1Url()}/auth/login`, {
|
const res = await fetch(`${getApiV1Url()}/auth/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -45,13 +48,9 @@ export async function login(
|
||||||
const data: LoginResponse = await res.json();
|
const data: LoginResponse = await res.json();
|
||||||
|
|
||||||
// Map backend response to frontend User type
|
// 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";
|
let userRole: "candidate" | "admin" | "company" | "superadmin" = "candidate";
|
||||||
// Check for SuperAdmin (Platform Admin)
|
|
||||||
if (data.user.roles.includes("superadmin") || data.user.roles.includes("SUPERADMIN")) {
|
if (data.user.roles.includes("superadmin") || data.user.roles.includes("SUPERADMIN")) {
|
||||||
userRole = "superadmin";
|
userRole = "superadmin";
|
||||||
// Check for Company Admin (now called 'admin') or Recruiter
|
|
||||||
} else if (data.user.roles.includes("admin") || data.user.roles.includes("recruiter")) {
|
} else if (data.user.roles.includes("admin") || data.user.roles.includes("recruiter")) {
|
||||||
userRole = "company";
|
userRole = "company";
|
||||||
}
|
}
|
||||||
|
|
@ -61,19 +60,20 @@ export async function login(
|
||||||
name: data.user.name,
|
name: data.user.name,
|
||||||
email: data.user.email,
|
email: data.user.email,
|
||||||
role: userRole,
|
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,
|
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") {
|
if (typeof window !== "undefined") {
|
||||||
localStorage.setItem(AUTH_KEY, JSON.stringify(user));
|
localStorage.setItem(AUTH_KEY, JSON.stringify(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("%c[AUTH] Login Error:", "color: #ef4444; font-weight: bold", error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,14 +81,14 @@ export async function login(
|
||||||
export async function logout(): Promise<void> {
|
export async function logout(): Promise<void> {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
localStorage.removeItem(AUTH_KEY);
|
localStorage.removeItem(AUTH_KEY);
|
||||||
// Call backend to clear the httpOnly cookie
|
hasLoggedNoSession = false;
|
||||||
try {
|
try {
|
||||||
await fetch(`${getApiV1Url()}/auth/logout`, {
|
await fetch(`${getApiV1Url()}/auth/logout`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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") {
|
if (typeof window !== "undefined") {
|
||||||
const stored = localStorage.getItem(AUTH_KEY);
|
const stored = localStorage.getItem(AUTH_KEY);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const user = JSON.parse(stored);
|
return JSON.parse(stored);
|
||||||
// console.log("%c[AUTH] User Loaded from Storage", "color: #10b981", user.email);
|
}
|
||||||
return user;
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -123,21 +123,26 @@ function mapRoleFromBackend(roles: string[]): "candidate" | "admin" | "company"
|
||||||
* Use this on app mount to restore session across tabs/reloads.
|
* Use this on app mount to restore session across tabs/reloads.
|
||||||
*/
|
*/
|
||||||
export async function refreshSession(): Promise<User | null> {
|
export async function refreshSession(): Promise<User | null> {
|
||||||
|
// Prevent multiple concurrent refresh attempts
|
||||||
|
if (isRefreshingSession) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRefreshingSession = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure runtime config is loaded before making the request
|
// Ensure runtime config is loaded before making the request
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
await import("./config").then((m) => m.initConfig());
|
await import("./config").then((m) => m.initConfig());
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("%c[AUTH] Attempting to refresh session...", "color: #3b82f6");
|
|
||||||
const res = await fetch(`${getApiV1Url()}/users/me`, {
|
const res = await fetch(`${getApiV1Url()}/users/me`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include", // Send HTTPOnly cookie
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// Cookie expired or invalid - clear local storage
|
// 401 is expected when not logged in - clean up silently
|
||||||
console.log("%c[AUTH] Session refresh: No session", "color: #94a3b8", res.status);
|
|
||||||
localStorage.removeItem(AUTH_KEY);
|
localStorage.removeItem(AUTH_KEY);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -155,18 +160,14 @@ export async function refreshSession(): Promise<User | null> {
|
||||||
};
|
};
|
||||||
|
|
||||||
localStorage.setItem(AUTH_KEY, JSON.stringify(user));
|
localStorage.setItem(AUTH_KEY, JSON.stringify(user));
|
||||||
console.log("%c[AUTH] Session restored from cookie", "color: #10b981", user.email);
|
hasLoggedNoSession = false;
|
||||||
return user;
|
return user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Only log for non-network errors in development
|
// Network errors are expected when API is unreachable
|
||||||
// "Failed to fetch" is expected when API is unreachable (normal for unauthenticated users)
|
localStorage.removeItem(AUTH_KEY);
|
||||||
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);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
isRefreshingSession = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,7 +192,7 @@ export interface RegisterCandidateData {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
username: string; // identifier
|
username: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
birthDate?: string;
|
birthDate?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
|
|
@ -205,8 +206,6 @@ export interface RegisterCandidateData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerCandidate(data: RegisterCandidateData): Promise<void> {
|
export async function registerCandidate(data: RegisterCandidateData): Promise<void> {
|
||||||
console.log('[registerCandidate] Sending request:', { ...data, password: '***' });
|
|
||||||
|
|
||||||
const res = await fetch(`${getApiV1Url()}/auth/register`, {
|
const res = await fetch(`${getApiV1Url()}/auth/register`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -217,21 +216,13 @@ export async function registerCandidate(data: RegisterCandidateData): Promise<vo
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorData = await res.json().catch(() => ({}));
|
const errorData = await res.json().catch(() => ({}));
|
||||||
console.error('[registerCandidate] Error response:', res.status, errorData);
|
|
||||||
throw new Error(errorData.message || `Registration failed: ${res.status}`);
|
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 {
|
export function getToken(): string | null {
|
||||||
// Token is now in httpOnly cookie, not accessible from JS
|
// Token is now in httpOnly cookie, not accessible from JS
|
||||||
// This function is kept for backward compatibility but returns null
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,7 +233,7 @@ export interface RegisterCompanyData {
|
||||||
phone: string;
|
phone: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
confirmPassword?: string;
|
confirmPassword?: string;
|
||||||
document?: string; // cnpj
|
document?: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
yearsInMarket?: string;
|
yearsInMarket?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
@ -251,31 +242,16 @@ export interface RegisterCompanyData {
|
||||||
city?: string;
|
city?: string;
|
||||||
state?: string;
|
state?: string;
|
||||||
birthDate?: string;
|
birthDate?: string;
|
||||||
cnpj?: string; // alias for document
|
cnpj?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerCompany(data: RegisterCompanyData): Promise<void> {
|
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 = {
|
const payload = {
|
||||||
name: data.companyName,
|
name: data.companyName,
|
||||||
slug: data.companyName.toLowerCase().replace(/\s+/g, '-'), // Generate slug
|
slug: data.companyName.toLowerCase().replace(/\s+/g, '-'),
|
||||||
document: data.document || data.cnpj,
|
document: data.document || data.cnpj,
|
||||||
phone: data.phone,
|
phone: data.phone,
|
||||||
email: data.email, // Company email
|
email: data.email,
|
||||||
website: data.website,
|
website: data.website,
|
||||||
address: data.address,
|
address: data.address,
|
||||||
zip_code: data.zipCode,
|
zip_code: data.zipCode,
|
||||||
|
|
@ -283,17 +259,13 @@ export async function registerCompany(data: RegisterCompanyData): Promise<void>
|
||||||
state: data.state,
|
state: data.state,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
years_in_market: data.yearsInMarket,
|
years_in_market: data.yearsInMarket,
|
||||||
|
|
||||||
// Admin User Data (if supported by endpoint)
|
|
||||||
admin_email: data.email,
|
admin_email: data.email,
|
||||||
admin_password: data.password, // Keep for backward compatibility if needed
|
admin_password: data.password,
|
||||||
password: data.password, // Correct field for CreateCompanyRequest
|
password: data.password,
|
||||||
admin_name: data.companyName, // Or we add a contactPerson field
|
admin_name: data.companyName,
|
||||||
admin_birth_date: data.birthDate
|
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`, {
|
const res = await fetch(`${getApiV1Url()}/auth/register/company`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -304,11 +276,8 @@ export async function registerCompany(data: RegisterCompanyData): Promise<void>
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorData = await res.json().catch(() => ({}));
|
const errorData = await res.json().catch(() => ({}));
|
||||||
console.error('[registerCompany] Error response:', res.status, errorData);
|
|
||||||
throw new Error(errorData.message || `Registration failed: ${res.status}`);
|
throw new Error(errorData.message || `Registration failed: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseData = await res.json().catch(() => ({}));
|
return res.json();
|
||||||
console.log('[registerCompany] Success - Company created:', responseData);
|
|
||||||
return responseData;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue