import { createReadStream, existsSync } from "node:fs"; import { join } from "node:path"; import { HttpException, HttpStatus, Injectable, OnModuleInit, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import axios, { AxiosRequestConfig } from "axios"; import * as FormData from "form-data"; @Injectable() export class AiService implements OnModuleInit { private apiKey: string; private accessToken: string = null; // These configurations are for authentication and getting the API key. private readonly loginOptions: AxiosRequestConfig = { method: "POST", headers: { "Content-Type": "application/json" }, url: `${this.configService.get("AI_URL_V2")}/auth/login`, data: { username: this.configService.get("AI_USERNAME"), password: this.configService.get("AI_PASSWORD"), }, timeout: 1000, // TODO: Make this ENV }; constructor(private readonly configService: ConfigService) {} private get profileOptions(): AxiosRequestConfig { return { method: "GET", url: `${this.configService.get("AI_URL_V2")}/auth/profile`, headers: { Authorization: `Bearer ${this.accessToken}`, }, }; } // This getter dynamically creates the base options for the image processing request. private get imageProcessOptions(): AxiosRequestConfig { return { method: "POST", url: `${process.env.AI_URL_V2}/services/car-damage/detector?version=ai-v7`, headers: { Authorization: `Bearer ${this.accessToken}`, "gateway-api-key": `${this.apiKey}`, }, }; } async onModuleInit() { try { const res = await this.login(); if (res?.accessToken) { this.accessToken = res.accessToken; await this.getApiKey(); } } catch (error) { // Don't prevent app startup if AI service is temporarily unavailable // The service will attempt to re-authenticate when aiRequestImage is called // Reset tokens so re-authentication will be attempted this.accessToken = null; this.apiKey = null; } } private async login() { const loginResponse = await axios.request(this.loginOptions); return loginResponse.data; } private async getApiKey() { const profileResponse = await axios.request(this.profileOptions); this.apiKey = profileResponse.data.apiKey.key; return this.apiKey; } public async aiRequestImage(file: { path: string; fileName?: string; }): Promise { // Ensure authentication is set up if (!this.accessToken || !this.apiKey) { try { const res = await this.login(); if (res?.accessToken) { this.accessToken = res.accessToken; await this.getApiKey(); } else { throw new HttpException( "AI Service authentication failed", HttpStatus.UNAUTHORIZED, ); } } catch (error) { throw new HttpException( "AI Service authentication failed", HttpStatus.UNAUTHORIZED, ); } } // Resolve relative paths to absolute paths const filePath = file.path.startsWith("/") ? file.path : join(process.cwd(), file.path.replace(/^\.\//, "")); // Check if file exists if (!existsSync(filePath)) { throw new HttpException( `File not found: ${file.path}`, HttpStatus.NOT_FOUND, ); } const form = new FormData(); const fileStream = createReadStream(filePath); // Append file with filename if available if (file.fileName) { form.append("images", fileStream, file.fileName); } else { // Extract filename from path if not provided const pathParts = filePath.split("/"); const extractedFileName = pathParts[pathParts.length - 1]; form.append("images", fileStream, extractedFileName); } try { const requestHeaders = { ...this.imageProcessOptions.headers, ...form.getHeaders(), }; const fs = require("fs"); const stats = fs.statSync(filePath); const response = await axios.request({ ...this.imageProcessOptions, headers: requestHeaders, data: form, maxContentLength: Infinity, maxBodyLength: Infinity, }); // Validate response structure if (!response.data) { throw new HttpException( "AI Service returned empty response", HttpStatus.BAD_GATEWAY, ); } // Check for error in response first (AI service returns 201 with error in body) if (response.data.error) { throw new HttpException( `AI Service error: ${response.data.error}`, HttpStatus.BAD_GATEWAY, ); } // Check for processed image (downloadLink) if (!response.data.downloadLink) { throw new HttpException( "AI Service did not return processed image (downloadLink missing)", HttpStatus.BAD_GATEWAY, ); } return response.data; } catch (er) { // Determine error source let errorSource = "UNKNOWN"; let errorMessage = er.message; let errorDetails = "No error details available"; if (er.response) { errorSource = "AI_SERVICE_RESPONSE"; errorMessage = er.response?.data?.message || er.message || `HTTP ${er.response.status}`; errorDetails = er.response?.data ? JSON.stringify(er.response.data, null, 2) : `Status: ${er.response.status}, StatusText: ${er.response.statusText}`; } else if (er.request) { errorSource = "NETWORK_ERROR"; errorMessage = "Network error - AI service did not respond"; errorDetails = "Request was made but no response received"; } else { errorSource = "REQUEST_SETUP_ERROR"; errorMessage = er.message || "Error setting up request"; } // Re-throw with detailed error information throw new HttpException( `[${errorSource}] ${errorMessage}`, er.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } } }