feat(api): create heart beat endpoint

This commit is contained in:
isra el
2026-01-27 15:30:06 +03:00
parent 141b8b334a
commit 541f32406e
4 changed files with 373 additions and 0 deletions

View File

@@ -26,6 +26,8 @@ import {
SendBulkSMSInputDTO,
SendSMSInputDTO,
UpdateSMSStatusDTO,
HeartbeatInputDTO,
HeartbeatResponseDTO,
} from './gateway.dto'
import { GatewayService } from './gateway.service'
import { CanModifyDevice } from './guards/can-modify-device.guard'
@@ -70,6 +72,18 @@ export class GatewayController {
return { data }
}
@ApiOperation({ summary: 'Device heartbeat' })
@UseGuards(AuthGuard, CanModifyDevice)
@Post('/devices/:id/heartbeat')
@HttpCode(HttpStatus.OK)
async heartbeat(
@Param('id') deviceId: string,
@Body() input: HeartbeatInputDTO,
): Promise<HeartbeatResponseDTO> {
const data = await this.gatewayService.heartbeat(deviceId, input)
return data
}
@ApiOperation({ summary: 'Delete device' })
@UseGuards(AuthGuard, CanModifyDevice)
@Delete('/devices/:id')

View File

@@ -299,3 +299,134 @@ export class UpdateSMSStatusDTO {
})
errorMessage?: string
}
export class HeartbeatInputDTO {
@ApiProperty({
type: String,
required: false,
description: 'FCM token for push notifications',
})
fcmToken?: string
@ApiProperty({
type: Number,
required: false,
description: 'Battery percentage (0-100)',
})
batteryPercentage?: number
@ApiProperty({
type: Boolean,
required: false,
description: 'Whether device is currently charging',
})
isCharging?: boolean
@ApiProperty({
type: String,
required: false,
description: 'Network type',
enum: ['wifi', 'cellular', 'none'],
})
networkType?: 'wifi' | 'cellular' | 'none'
@ApiProperty({
type: String,
required: false,
description: 'App version name',
})
appVersionName?: string
@ApiProperty({
type: Number,
required: false,
description: 'App version code',
})
appVersionCode?: number
@ApiProperty({
type: Number,
required: false,
description: 'Device uptime in milliseconds since boot',
})
deviceUptimeMillis?: number
@ApiProperty({
type: Number,
required: false,
description: 'Free memory in bytes',
})
memoryFreeBytes?: number
@ApiProperty({
type: Number,
required: false,
description: 'Total memory in bytes',
})
memoryTotalBytes?: number
@ApiProperty({
type: Number,
required: false,
description: 'Max memory in bytes',
})
memoryMaxBytes?: number
@ApiProperty({
type: Number,
required: false,
description: 'Available storage in bytes',
})
storageAvailableBytes?: number
@ApiProperty({
type: Number,
required: false,
description: 'Total storage in bytes',
})
storageTotalBytes?: number
@ApiProperty({
type: String,
required: false,
description: 'Device timezone (e.g., "America/New_York")',
})
timezone?: string
@ApiProperty({
type: String,
required: false,
description: 'Device locale (e.g., "en_US")',
})
locale?: string
@ApiProperty({
type: Boolean,
required: false,
description: 'Whether receive SMS feature is enabled',
})
receiveSMSEnabled?: boolean
}
export class HeartbeatResponseDTO {
@ApiProperty({
type: Boolean,
required: true,
description: 'Whether the heartbeat was successful',
})
success: boolean
@ApiProperty({
type: Boolean,
required: true,
description: 'Whether the FCM token was updated',
})
fcmTokenUpdated: boolean
@ApiProperty({
type: Date,
required: true,
description: 'Server timestamp of the heartbeat',
})
lastHeartbeat: Date
}

View File

@@ -10,6 +10,8 @@ import {
SendBulkSMSInputDTO,
SendSMSInputDTO,
UpdateSMSStatusDTO,
HeartbeatInputDTO,
HeartbeatResponseDTO,
} from './gateway.dto'
import { User } from '../users/schemas/user.schema'
import { AuthService } from '../auth/auth.service'
@@ -917,4 +919,129 @@ const updatedSms = await this.smsModel.findByIdAndUpdate(
messages: smsMessages
};
}
async heartbeat(
deviceId: string,
input: HeartbeatInputDTO,
): Promise<HeartbeatResponseDTO> {
const device = await this.deviceModel.findById(deviceId)
if (!device) {
throw new HttpException(
{
success: false,
error: 'Device not found',
},
HttpStatus.NOT_FOUND,
)
}
const now = new Date()
const updateData: any = {
lastHeartbeat: now,
}
let fcmTokenUpdated = false
// Update FCM token if provided and different
if (input.fcmToken && input.fcmToken !== device.fcmToken) {
updateData.fcmToken = input.fcmToken
fcmTokenUpdated = true
}
// Update receiveSMSEnabled if provided and different
if (
input.receiveSMSEnabled !== undefined &&
input.receiveSMSEnabled !== device.receiveSMSEnabled
) {
updateData.receiveSMSEnabled = input.receiveSMSEnabled
}
// Update batteryInfo if provided
if (input.batteryPercentage !== undefined || input.isCharging !== undefined) {
updateData.batteryInfo = {
...(device.batteryInfo || {}),
...(input.batteryPercentage !== undefined && { percentage: input.batteryPercentage }),
...(input.isCharging !== undefined && { isCharging: input.isCharging }),
lastUpdated: now,
}
}
// Update networkInfo if provided
if (input.networkType !== undefined) {
updateData.networkInfo = {
...(device.networkInfo || {}),
type: input.networkType,
lastUpdated: now,
}
}
// Update appVersionInfo if provided
if (input.appVersionName !== undefined || input.appVersionCode !== undefined) {
updateData.appVersionInfo = {
...(device.appVersionInfo || {}),
...(input.appVersionName !== undefined && { versionName: input.appVersionName }),
...(input.appVersionCode !== undefined && { versionCode: input.appVersionCode }),
lastUpdated: now,
}
}
// Update deviceUptimeInfo if provided
if (input.deviceUptimeMillis !== undefined) {
updateData.deviceUptimeInfo = {
...(device.deviceUptimeInfo || {}),
uptimeMillis: input.deviceUptimeMillis,
lastUpdated: now,
}
}
// Update memoryInfo if any memory field provided
if (
input.memoryFreeBytes !== undefined ||
input.memoryTotalBytes !== undefined ||
input.memoryMaxBytes !== undefined
) {
updateData.memoryInfo = {
...(device.memoryInfo || {}),
...(input.memoryFreeBytes !== undefined && { freeBytes: input.memoryFreeBytes }),
...(input.memoryTotalBytes !== undefined && { totalBytes: input.memoryTotalBytes }),
...(input.memoryMaxBytes !== undefined && { maxBytes: input.memoryMaxBytes }),
lastUpdated: now,
}
}
// Update storageInfo if any storage field provided
if (
input.storageAvailableBytes !== undefined ||
input.storageTotalBytes !== undefined
) {
updateData.storageInfo = {
...(device.storageInfo || {}),
...(input.storageAvailableBytes !== undefined && { availableBytes: input.storageAvailableBytes }),
...(input.storageTotalBytes !== undefined && { totalBytes: input.storageTotalBytes }),
lastUpdated: now,
}
}
// Update systemInfo if timezone or locale provided
if (input.timezone !== undefined || input.locale !== undefined) {
updateData.systemInfo = {
...(device.systemInfo || {}),
...(input.timezone !== undefined && { timezone: input.timezone }),
...(input.locale !== undefined && { locale: input.locale }),
lastUpdated: now,
}
}
// Update device with all changes
await this.deviceModel.findByIdAndUpdate(deviceId, {
$set: updateData,
})
return {
success: true,
fcmTokenUpdated,
lastHeartbeat: now,
}
}
}

View File

@@ -49,6 +49,107 @@ export class Device {
@Prop({ type: Number, default: 0 })
receivedSMSCount: number
@Prop({ type: Boolean, default: true })
heartbeatEnabled: boolean
@Prop({ type: Number, default: 30 })
heartbeatIntervalMinutes: number
@Prop({ type: Boolean, default: false })
receiveSMSEnabled: boolean
@Prop({ type: Date })
lastHeartbeat: Date
@Prop({
type: {
percentage: Number,
isCharging: Boolean,
lastUpdated: Date,
},
})
batteryInfo: {
percentage?: number
isCharging?: boolean
lastUpdated?: Date
}
@Prop({
type: {
type: String,
lastUpdated: Date,
},
})
networkInfo: {
type?: 'wifi' | 'cellular' | 'none'
lastUpdated?: Date
}
@Prop({
type: {
versionName: String,
versionCode: Number,
lastUpdated: Date,
},
})
appVersionInfo: {
versionName?: string
versionCode?: number
lastUpdated?: Date
}
@Prop({
type: {
uptimeMillis: Number,
lastUpdated: Date,
},
})
deviceUptimeInfo: {
uptimeMillis?: number
lastUpdated?: Date
}
@Prop({
type: {
freeBytes: Number,
totalBytes: Number,
maxBytes: Number,
lastUpdated: Date,
},
})
memoryInfo: {
freeBytes?: number
totalBytes?: number
maxBytes?: number
lastUpdated?: Date
}
@Prop({
type: {
availableBytes: Number,
totalBytes: Number,
lastUpdated: Date,
},
})
storageInfo: {
availableBytes?: number
totalBytes?: number
lastUpdated?: Date
}
@Prop({
type: {
timezone: String,
locale: String,
lastUpdated: Date,
},
})
systemInfo: {
timezone?: string
locale?: string
lastUpdated?: Date
}
}
export const DeviceSchema = SchemaFactory.createForClass(Device)