From c167f4ec435fba76af45e913309eda20425ce125 Mon Sep 17 00:00:00 2001 From: NurAlan Date: Wed, 15 Oct 2025 10:54:05 +0700 Subject: [PATCH] deploy: change method to get status from device --- src/helpers/http/axios.ts | 191 ------------------------- src/helpers/signature.ts | 13 -- src/middlewares/deviceGuard.ts | 6 - src/modules/device/commands/command.ts | 13 +- src/modules/device/queries/query.ts | 54 ++++--- src/plugins/swagger.ts | 15 -- 6 files changed, 32 insertions(+), 260 deletions(-) delete mode 100644 src/helpers/http/axios.ts diff --git a/src/helpers/http/axios.ts b/src/helpers/http/axios.ts deleted file mode 100644 index 356e541..0000000 --- a/src/helpers/http/axios.ts +++ /dev/null @@ -1,191 +0,0 @@ -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' -import { httpConfig } from '~/config' -import logger from '~/plugins/logger' - -interface AxiosConfigWithMetadata extends AxiosRequestConfig { - metadata?: { - startTime: number - } -} - -interface HttpRequestOptions { - method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' - url: string - body?: Record - config?: AxiosRequestConfig -} - -interface HttpResponse { - data: T - status: number - statusText: string - headers: Record -} - -class HttpError extends Error { - constructor( - message: string, - public status: number, - public response?: AxiosResponse, - ) { - super(message) - this.name = 'HttpError' - } -} - -let axiosInstance: AxiosInstance | null = null - -function getAxiosInstance(): AxiosInstance { - if (!axiosInstance) { - axiosInstance = axios.create({ - baseURL: httpConfig.baseUrl, - timeout: 30000, // 30 seconds default timeout - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - }) - - // Add basic auth if configured - if (httpConfig.username && httpConfig.password) { - const auth = Buffer.from(`${httpConfig.username}:${httpConfig.password}`).toString('base64') - axiosInstance.defaults.headers.common['Authorization'] = `Basic ${auth}` - } - - // Request interceptor for logging - axiosInstance.interceptors.request.use( - (config) => { - ;(config as AxiosConfigWithMetadata).metadata = { startTime: Date.now() } - logger.info({ - msg: 'HTTP Request', - method: config.method?.toUpperCase(), - url: config.url, - baseURL: config.baseURL, - headers: config.headers, - params: config.params, - data: config.data, - }) - return config - }, - (error) => { - logger.error({ - msg: 'HTTP Request Error', - error: error.message, - config: { - method: error.config?.method, - url: error.config?.url, - baseURL: error.config?.baseURL, - }, - }) - return Promise.reject(error) - }, - ) - - // Response interceptor for logging - axiosInstance.interceptors.response.use( - (response) => { - const startTime = (response.config as AxiosConfigWithMetadata).metadata?.startTime - logger.info({ - msg: 'HTTP Response', - status: response.status, - statusText: response.statusText, - method: response.config.method?.toUpperCase(), - url: response.config.url, - baseURL: response.config.baseURL, - responseTime: startTime ? Date.now() - startTime : undefined, - }) - return response - }, - (error) => { - const startTime = (error.config as AxiosConfigWithMetadata)?.metadata?.startTime - logger.error({ - msg: 'HTTP Response Error', - status: error.response?.status, - statusText: error.response?.statusText, - method: error.config?.method?.toUpperCase(), - url: error.config?.url, - baseURL: error.config?.baseURL, - error: error.message, - responseData: error.response?.data, - responseTime: startTime ? Date.now() - startTime : undefined, - }) - return Promise.reject(error) - }, - ) - } - - return axiosInstance -} - -async function httpRequest(options: HttpRequestOptions): Promise> { - const { method, url, body, config = {} } = options - const instance = getAxiosInstance() - - const axiosConfig: AxiosRequestConfig = { - method, - url, - ...config, - } - - if (body && ['POST', 'PUT', 'PATCH'].includes(method)) { - axiosConfig.data = body - } - - if (method === 'GET' && body) { - axiosConfig.params = body - } - - const maxRetries = 3 - let lastError: Error | null = null - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - const response: AxiosResponse = await instance.request(axiosConfig) - return { - data: response.data, - status: response.status, - statusText: response.statusText, - headers: response.headers as Record, - } - } catch (error) { - lastError = error as Error - - // Check if it's a transient error worth retrying - const isRetryable = axios.isAxiosError(error) - && ( - !error.response // Network errors - || error.response.status >= 500 // Server errors - || error.response.status === 429 // Rate limiting - ) - - if (!isRetryable || attempt === maxRetries) { - break - } - - // Exponential backoff - const delay = Math.pow(2, attempt) * 1000 - logger.warn({ - msg: 'HTTP Retry', - attempt, - maxRetries, - delay, - method, - url, - error: lastError.message, - }) - await new Promise((resolve) => setTimeout(resolve, delay)) - } - } - - // If we get here, all retries failed - if (lastError && axios.isAxiosError(lastError)) { - const status = lastError.response?.status || 0 - const message = lastError.response?.data?.message || lastError.message - throw new HttpError(`HTTP ${method} ${url} failed: ${message}`, status, lastError.response) - } - - throw new HttpError(`HTTP ${method} ${url} failed: ${lastError?.message || 'Unknown error'}`, 0) -} - -export { httpRequest, HttpError } -export type { HttpRequestOptions, HttpResponse } diff --git a/src/helpers/signature.ts b/src/helpers/signature.ts index 53a34af..ba5eb46 100644 --- a/src/helpers/signature.ts +++ b/src/helpers/signature.ts @@ -123,19 +123,6 @@ export function sign(data: unknown, secretKey?: string): string { return hmac.digest('hex') } -console.log(sign({ - merchantName: 'SAVY', - floorName: 'L2', - unitNumber: '01', - deviceName: 'AC', - roomName: 'KN', - deviceType: 'A', - commandType: 'C', - towerNumber: 'T1', - timestamp: '2025-10-14T04:06:52.022Z', - nonce: '50ce476cd1c355ae552ae18be55af72c', -}, '458fd644689a456efa87d38f788511d9')) - /** * Verifies the signature of the data using HMAC-SHA256 with constant-time comparison. * @param data The data to verify. diff --git a/src/middlewares/deviceGuard.ts b/src/middlewares/deviceGuard.ts index 7313359..85e7614 100644 --- a/src/middlewares/deviceGuard.ts +++ b/src/middlewares/deviceGuard.ts @@ -5,12 +5,6 @@ export const deviceGuard = () => ({ beforeHandle(context: unknown) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const { headers, set } = context as any - // Validate Content-Type - if (headers['content-type'] !== 'application/json') { - set.status = 400 - throw new Error('invalid_request') - } - // Validate X-API-Key const apiKey = headers['x-api-key'] if (!apiKey || typeof apiKey !== 'string' || apiKey.trim() === '') { diff --git a/src/modules/device/commands/command.ts b/src/modules/device/commands/command.ts index d8b2f2e..97e6fc2 100644 --- a/src/modules/device/commands/command.ts +++ b/src/modules/device/commands/command.ts @@ -1,16 +1,19 @@ -import { httpRequest } from '~/helpers/http/axios' +import { httpConfig } from '~/config' import logger from '~/plugins/logger' export async function sendCommandToThirdParty(topicData: string, payload: string): Promise { try { - const response = await httpRequest({ + const response = await fetch(`${httpConfig.baseUrl}/command`, { method: 'POST', - url: '/command', - body: { + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ topic: topicData, payload, - }, + }), }) + logger.info({ msg: 'Third-party command sent successfully', topic: topicData, status: response.status }) } catch (error) { logger.error({ msg: 'Failed to send command to third-party service', topic: topicData, error: (error as Error).message }) diff --git a/src/modules/device/queries/query.ts b/src/modules/device/queries/query.ts index 0a18580..55570cd 100644 --- a/src/modules/device/queries/query.ts +++ b/src/modules/device/queries/query.ts @@ -1,7 +1,7 @@ -import { httpRequest } from '~/helpers/http/axios' -import logger from '~/plugins/logger' +import { omit } from 'lodash-es' +import { httpConfig } from '~/config' -export async function queryDeviceStatus(topic: string): Promise<{ +interface DeviceStatus { floor: string unit: string device: string @@ -10,34 +10,28 @@ export async function queryDeviceStatus(topic: string): Promise<{ code: string tower: string payload: string -} | null> { - try { - const response = await httpRequest({ - method: 'GET', - url: '/state-reply', - body: { topic }, - }) + topics?: unknown + createAt?: unknown +} - if ((response.data as Record) && (response.data as Record).data) { - const thirdPartyData = (response.data as Record).data as Record - logger.info({ msg: 'Device status queried successfully', topic, status: response.status }) +export async function queryDeviceStatus(topic: string): Promise | null> { + const response = await fetch(`${httpConfig.baseUrl}/state-reply`, { + method: 'POST', + body: JSON.stringify({ topic }), + headers: { + 'Content-Type': 'application/json', + }, + }) - return { - floor: thirdPartyData.floor as string, - unit: thirdPartyData.unit as string, - device: thirdPartyData.device as string, - room: thirdPartyData.room as string, - devType: thirdPartyData.devType as string, - code: thirdPartyData.code as string, - tower: thirdPartyData.tower as string, - payload: thirdPartyData.payload as string, - } - } else { - logger.info({ msg: 'Device status not found', topic, status: response.status }) - return null - } - } catch (error) { - logger.error({ msg: 'Failed to query device status', topic, error: (error as Error).message }) - throw new Error('third_party_query_failed') + if (!response.ok) { + throw new Error('internal_server_error') } + + const json = await response.json() + + if (!json || !json.data) { + return null + } + + return omit(json.data as DeviceStatus, ['topics', 'createAt']) } diff --git a/src/plugins/swagger.ts b/src/plugins/swagger.ts index c7c626e..d3ce30d 100644 --- a/src/plugins/swagger.ts +++ b/src/plugins/swagger.ts @@ -25,27 +25,12 @@ export const swaggerPlugin = openapi({ ], 'components': { securitySchemes: { - basicAuth: { - type: 'http', - scheme: 'basic', - }, - bearerAuth: { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - }, apiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key', description: 'API key for authentication', }, - timestamp: { - type: 'apiKey', - in: 'header', - name: 'X-Timestamp', - description: 'UTC timestamp in ISO8601 format', - }, }, }, } as OpenApiDocumentation,