mockup API device
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { verify } from '~/helpers/signature'
|
||||
import { DeviceCommandBody } from '../schema'
|
||||
|
||||
export default abstract class CommandService {
|
||||
static async sendCommand(body: DeviceCommandBody, headers: Record<string, string | undefined>): Promise<{
|
||||
status: 'success'
|
||||
message: 'Command accepted'
|
||||
receivedAt: string
|
||||
}> {
|
||||
const apiKey = headers['x-api-key']
|
||||
if (!apiKey) {
|
||||
throw new Error('invalid_api_key')
|
||||
}
|
||||
|
||||
// For now, use apiKey as secretKey since DB validation is skipped
|
||||
const secretKey = apiKey
|
||||
|
||||
// Remove signature from body for verification
|
||||
const { signature, ...bodyWithoutSignature } = body
|
||||
|
||||
const isValid = verify(bodyWithoutSignature, signature, secretKey)
|
||||
if (!isValid) {
|
||||
throw new Error('invalid_signature')
|
||||
}
|
||||
|
||||
// TODO: Check nonce for replay attack when DB is implemented
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
message: 'Command accepted',
|
||||
receivedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Elysia } from 'elysia'
|
||||
import { deviceGuard } from '~/middlewares/deviceGuard'
|
||||
import { deviceCommandBody, deviceCommandResponseSchema } from './schema'
|
||||
import CommandService from './commands/service'
|
||||
import { deviceErrorResponseMap } from '~/helpers/errors'
|
||||
|
||||
export const router = new Elysia({
|
||||
name: 'modules.device',
|
||||
detail: { tags: ['Device'] },
|
||||
prefix: '/device/v1',
|
||||
})
|
||||
.guard(deviceGuard())
|
||||
.post('/command', async ({ body, headers, set }) => {
|
||||
try {
|
||||
const result = await CommandService.sendCommand(body, headers)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message in deviceErrorResponseMap) {
|
||||
const { status, response } = deviceErrorResponseMap[error.message]
|
||||
set.status = status
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return response as any
|
||||
}
|
||||
set.status = 500
|
||||
return { status: 'internal_error', message: 'Server error' }
|
||||
}
|
||||
}, {
|
||||
body: deviceCommandBody,
|
||||
response: deviceCommandResponseSchema,
|
||||
detail: {
|
||||
security: [
|
||||
{ apiKey: [] },
|
||||
{ timestamp: [] },
|
||||
{ signature: [] },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,52 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// Request body schema
|
||||
export const deviceCommandBody = z.object({
|
||||
merchantName: z.string(),
|
||||
floorName: z.string(),
|
||||
unitNumber: z.string(),
|
||||
deviceName: z.string(),
|
||||
roomName: z.string(),
|
||||
deviceType: z.string(),
|
||||
commandType: z.string(),
|
||||
towerNumber: z.string(),
|
||||
timestamp: z.string().datetime(), // ISO8601
|
||||
nonce: z.string(),
|
||||
payload: z.object({
|
||||
AC: z.string(),
|
||||
Temp: z.number(),
|
||||
}),
|
||||
signature: z.string(),
|
||||
})
|
||||
export type DeviceCommandBody = z.infer<typeof deviceCommandBody>
|
||||
|
||||
// Response schemas
|
||||
export const deviceCommandResponseSchema = {
|
||||
200: z.object({
|
||||
status: z.literal('success'),
|
||||
message: z.literal('Command accepted'),
|
||||
receivedAt: z.string().datetime(),
|
||||
}),
|
||||
400: z.object({
|
||||
status: z.literal('invalid_request'),
|
||||
message: z.literal('Invalid request format'),
|
||||
}),
|
||||
401: z.union([
|
||||
z.object({
|
||||
status: z.literal('invalid_signature'),
|
||||
message: z.literal('Signature mismatch'),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal('invalid_api_key'),
|
||||
message: z.literal('API Key is not valid'),
|
||||
}),
|
||||
]),
|
||||
403: z.object({
|
||||
status: z.literal('replay_attack_detected'),
|
||||
message: z.literal('Timestamp or nonce is invalid/has been reused'),
|
||||
}),
|
||||
500: z.object({
|
||||
status: z.literal('internal_error'),
|
||||
message: z.literal('Server error'),
|
||||
}),
|
||||
}
|
||||
Reference in New Issue
Block a user