import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
import { AxiosRequestConfig, AxiosError, AxiosInstance } from 'axios';
import { TimesheetConfigService } from '../../config/config.service';
import * as https from 'https';
export interface ApiResponse<T = any> {
success?: boolean;
message?: string;
data?: T;
errors?: any[];
}
@Injectable()
export class ApiClientService {
private readonly baseUrl: string;
private readonly axiosInstance: AxiosInstance;
private isRefreshing = false;
private refreshQueue: Array<{
resolve: (token: string) => void;
reject: (error: any) => void;
}> = [];
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
private readonly timesheetConfigService: TimesheetConfigService,
) {
this.baseUrl =
this.configService.get<string>('API_BASE_URL') ||
'https://timesheet.pinnacle.in:3000/api';
this.axiosInstance = this.httpService.axiosRef;
// Configure axios to bypass SSL certificate verification for self-signed certificates
this.axiosInstance.defaults.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
this.setupInterceptors();
}
/**
* Setup Axios interceptors for automatic token refresh
*/
private setupInterceptors(): void {
// Response interceptor to handle 401 errors
this.axiosInstance.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
// Check if error is 401/403 and we haven't retried yet
if (
error.response &&
(error.response.status === 401 || error.response.status === 403) &&
!originalRequest._retry &&
!originalRequest.url?.includes('/auth/login') &&
!originalRequest.url?.includes('/auth/refresh')
) {
if (this.isRefreshing) {
// If already refreshing, queue this request
return new Promise((resolve, reject) => {
this.refreshQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers['Authorization'] = `Bearer ${token}`;
return this.axiosInstance.request(originalRequest);
})
.catch((err) => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
this.isRefreshing = true;
try {
// Get current token and refresh it
const currentToken = this.timesheetConfigService.getToken();
if (!currentToken) {
throw new Error('No token available for refresh');
}
// Call refresh endpoint directly (avoid circular dependency)
const refreshResponse = await this.axiosInstance.post(
`${this.baseUrl}/auth/refresh`,
{ token: currentToken },
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${currentToken}`,
},
},
);
const newToken = refreshResponse.data.data?.token || refreshResponse.data.token;
const user = refreshResponse.data.data?.user || refreshResponse.data.user;
if (!newToken) {
throw new Error('No token received from refresh');
}
// Save new token and user
this.timesheetConfigService.setToken(newToken);
if (user) {
this.timesheetConfigService.setUser(user);
}
// Process queued requests
this.refreshQueue.forEach(({ resolve }) => resolve(newToken));
this.refreshQueue = [];
// Retry original request with new token
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
return this.axiosInstance.request(originalRequest);
} catch (refreshError) {
// Refresh failed - clear config and reject all queued requests
this.timesheetConfigService.clearConfig();
this.refreshQueue.forEach(({ reject }) =>
reject(new Error('Session expired. Please login again.')),
);
this.refreshQueue = [];
return Promise.reject(refreshError);
} finally {
this.isRefreshing = false;
}
}
return Promise.reject(error);
},
);
}
/**
* Make an authenticated API call to the backend
*/
async makeRequest<T = any>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
endpoint: string,
data?: any,
token?: string,
): Promise<ApiResponse<T>> {
try {
const config: AxiosRequestConfig = {
method,
url: `${this.baseUrl}${endpoint}`,
headers: {
'Content-Type': 'application/json',
},
};
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
if (data && (method === 'POST' || method === 'PUT')) {
config.data = data;
} else if (data && method === 'GET') {
config.params = data;
}
const response = await firstValueFrom(this.httpService.request(config));
return {
success: true,
data: response.data.data || response.data,
message: response.data.message,
};
} catch (error) {
this.handleError(error);
}
}
/**
* Login to get JWT token
*/
async login(username: string, password: string): Promise<ApiResponse> {
return this.makeRequest('POST', '/auth/login', { username, password });
}
/**
* Refresh JWT token
*/
async refreshToken(token: string): Promise<ApiResponse> {
return this.makeRequest('POST', '/auth/refresh', { token }, token);
}
/**
* Get all projects
*/
async getProjects(token: string): Promise<ApiResponse> {
return this.makeRequest('GET', '/allprojects', null, token);
}
/**
* Get projects assigned to a specific user
*/
async getUserProjects(userId: number, token: string): Promise<ApiResponse> {
return this.makeRequest('GET', `/projects/user/${userId}`, null, token);
}
/**
* Get modules for a project
*/
async getModules(projectId: number, token: string): Promise<ApiResponse> {
return this.makeRequest(
'GET',
`/modules/by-project?projectId=${projectId}`,
null,
token,
);
}
/**
* Get all activities
*/
async getActivities(token: string): Promise<ApiResponse> {
return this.makeRequest('GET', '/activities', null, token);
}
/**
* Create a timesheet entry
*/
async createTimesheet(data: any, token: string): Promise<ApiResponse> {
return this.makeRequest('POST', '/timesheets/create', data, token);
}
/**
* Get timesheet entries for a user
*/
async getTimesheets(
userId: number,
page: number = 1,
limit: number = 50,
token: string,
): Promise<ApiResponse> {
return this.makeRequest(
'GET',
`/timesheets/list/${userId}?page=${page}&limit=${limit}`,
null,
token,
);
}
/**
* Create daily scrum update
*/
async createDailyUpdate(data: any, token: string): Promise<ApiResponse> {
return this.makeRequest('POST', '/daily-updates/create', data, token);
}
/**
* Get daily scrum updates
*/
async getDailyUpdates(
userId: number,
page: number = 1,
limit: number = 7,
token: string,
): Promise<ApiResponse> {
return this.makeRequest(
'GET',
`/daily-updates?page=${page}&limit=${limit}&userid=${userId}`,
null,
token,
);
}
/**
* Get team timesheets for approval (managers only)
*/
async getTeamTimesheets(
managerId: number,
page: number = 1,
limit: number = 100,
token: string,
): Promise<ApiResponse> {
return this.makeRequest(
'GET',
`/timesheet/manager/${managerId}?page=${page}&limit=${limit}`,
null,
token,
);
}
/**
* Bulk approve/reject timesheets
*/
async bulkUpdateTimesheetStatus(
timesheetIds: number[],
status: number,
approverId: number,
name: string,
token: string,
): Promise<ApiResponse> {
return this.makeRequest(
'POST',
'/timesheets/bulk-update-status',
{ timesheetIds, status, approverId, name },
token,
);
}
/**
* Handle API errors
*/
private handleError(error: any): never {
if (error.response) {
const status = error.response.status;
const message =
error.response.data?.message || error.response.statusText;
throw new HttpException(
{
success: false,
message,
errors: error.response.data?.errors,
},
status,
);
} else if (error.request) {
throw new HttpException(
{
success: false,
message: 'No response from server. Check your connection.',
},
HttpStatus.SERVICE_UNAVAILABLE,
);
} else {
throw new HttpException(
{
success: false,
message: error.message || 'Unknown error occurred',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}