Skip to main content
Glama
api-client.service.ts9.51 kB
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, ); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/arshad-khan1/Timesheet-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server