deploy: change method to get status from device
This commit is contained in:
@@ -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<string, unknown>
|
|
||||||
config?: AxiosRequestConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HttpResponse<T = unknown> {
|
|
||||||
data: T
|
|
||||||
status: number
|
|
||||||
statusText: string
|
|
||||||
headers: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
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<T = unknown>(options: HttpRequestOptions): Promise<HttpResponse<T>> {
|
|
||||||
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<T> = await instance.request(axiosConfig)
|
|
||||||
return {
|
|
||||||
data: response.data,
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
headers: response.headers as Record<string, string>,
|
|
||||||
}
|
|
||||||
} 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 }
|
|
||||||
@@ -123,19 +123,6 @@ export function sign(data: unknown, secretKey?: string): string {
|
|||||||
return hmac.digest('hex')
|
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.
|
* Verifies the signature of the data using HMAC-SHA256 with constant-time comparison.
|
||||||
* @param data The data to verify.
|
* @param data The data to verify.
|
||||||
|
|||||||
@@ -5,12 +5,6 @@ export const deviceGuard = () => ({
|
|||||||
beforeHandle(context: unknown) {
|
beforeHandle(context: unknown) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const { headers, set } = context as 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
|
// Validate X-API-Key
|
||||||
const apiKey = headers['x-api-key']
|
const apiKey = headers['x-api-key']
|
||||||
if (!apiKey || typeof apiKey !== 'string' || apiKey.trim() === '') {
|
if (!apiKey || typeof apiKey !== 'string' || apiKey.trim() === '') {
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { httpRequest } from '~/helpers/http/axios'
|
import { httpConfig } from '~/config'
|
||||||
import logger from '~/plugins/logger'
|
import logger from '~/plugins/logger'
|
||||||
|
|
||||||
export async function sendCommandToThirdParty(topicData: string, payload: string): Promise<void> {
|
export async function sendCommandToThirdParty(topicData: string, payload: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await httpRequest({
|
const response = await fetch(`${httpConfig.baseUrl}/command`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/command',
|
headers: {
|
||||||
body: {
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
topic: topicData,
|
topic: topicData,
|
||||||
payload,
|
payload,
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info({ msg: 'Third-party command sent successfully', topic: topicData, status: response.status })
|
logger.info({ msg: 'Third-party command sent successfully', topic: topicData, status: response.status })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ msg: 'Failed to send command to third-party service', topic: topicData, error: (error as Error).message })
|
logger.error({ msg: 'Failed to send command to third-party service', topic: topicData, error: (error as Error).message })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { httpRequest } from '~/helpers/http/axios'
|
import { omit } from 'lodash-es'
|
||||||
import logger from '~/plugins/logger'
|
import { httpConfig } from '~/config'
|
||||||
|
|
||||||
export async function queryDeviceStatus(topic: string): Promise<{
|
interface DeviceStatus {
|
||||||
floor: string
|
floor: string
|
||||||
unit: string
|
unit: string
|
||||||
device: string
|
device: string
|
||||||
@@ -10,34 +10,28 @@ export async function queryDeviceStatus(topic: string): Promise<{
|
|||||||
code: string
|
code: string
|
||||||
tower: string
|
tower: string
|
||||||
payload: string
|
payload: string
|
||||||
} | null> {
|
topics?: unknown
|
||||||
try {
|
createAt?: unknown
|
||||||
const response = await httpRequest({
|
}
|
||||||
method: 'GET',
|
|
||||||
url: '/state-reply',
|
|
||||||
body: { topic },
|
|
||||||
})
|
|
||||||
|
|
||||||
if ((response.data as Record<string, unknown>) && (response.data as Record<string, unknown>).data) {
|
export async function queryDeviceStatus(topic: string): Promise<Omit<DeviceStatus, 'topics' | 'createAt'> | null> {
|
||||||
const thirdPartyData = (response.data as Record<string, unknown>).data as Record<string, unknown>
|
const response = await fetch(`${httpConfig.baseUrl}/state-reply`, {
|
||||||
logger.info({ msg: 'Device status queried successfully', topic, status: response.status })
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ topic }),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
if (!response.ok) {
|
||||||
floor: thirdPartyData.floor as string,
|
throw new Error('internal_server_error')
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
if (!json || !json.data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return omit(json.data as DeviceStatus, ['topics', 'createAt'])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,27 +25,12 @@ export const swaggerPlugin = openapi({
|
|||||||
],
|
],
|
||||||
'components': {
|
'components': {
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
basicAuth: {
|
|
||||||
type: 'http',
|
|
||||||
scheme: 'basic',
|
|
||||||
},
|
|
||||||
bearerAuth: {
|
|
||||||
type: 'http',
|
|
||||||
scheme: 'bearer',
|
|
||||||
bearerFormat: 'JWT',
|
|
||||||
},
|
|
||||||
apiKey: {
|
apiKey: {
|
||||||
type: 'apiKey',
|
type: 'apiKey',
|
||||||
in: 'header',
|
in: 'header',
|
||||||
name: 'X-API-Key',
|
name: 'X-API-Key',
|
||||||
description: 'API key for authentication',
|
description: 'API key for authentication',
|
||||||
},
|
},
|
||||||
timestamp: {
|
|
||||||
type: 'apiKey',
|
|
||||||
in: 'header',
|
|
||||||
name: 'X-Timestamp',
|
|
||||||
description: 'UTC timestamp in ISO8601 format',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as OpenApiDocumentation,
|
} as OpenApiDocumentation,
|
||||||
|
|||||||
Reference in New Issue
Block a user