Browse Source

feat(api): bulk messaging feature

pull/29/head
isra el 1 year ago
parent
commit
13192aceb3
  1. 13
      api/src/gateway/gateway.controller.ts
  2. 16
      api/src/gateway/gateway.dto.ts
  3. 147
      api/src/gateway/gateway.service.ts
  4. 42
      web/app/(app)/(auth)/verify-email.tsx/page.tsx

13
api/src/gateway/gateway.controller.ts

@ -23,6 +23,7 @@ import {
ReceivedSMSDTO,
RegisterDeviceInputDTO,
RetrieveSMSResponseDTO,
SendBulkSMSInputDTO,
SendSMSInputDTO,
} from './gateway.dto'
import { GatewayService } from './gateway.service'
@ -88,6 +89,18 @@ export class GatewayController {
return { data }
}
@ApiOperation({ summary: 'Send Bulk SMS' })
@UseGuards(AuthGuard, CanModifyDevice)
@Post(['/devices/:id/send-bulk-sms'])
async sendBulkSMS(
@Param('id') deviceId: string,
@Body() body: SendBulkSMSInputDTO,
) {
const data = await this.gatewayService.sendBulkSMS(deviceId, body)
return { data }
}
@ApiOperation({ summary: 'Received SMS from a device' })
@HttpCode(HttpStatus.OK)
// deprecate receiveSMS route in favor of receive-sms

16
api/src/gateway/gateway.dto.ts

@ -78,6 +78,22 @@ export class SMSData {
}
export class SendSMSInputDTO extends SMSData {}
export class SendBulkSMSInputDTO {
@ApiProperty({
type: String,
required: true,
description: 'The template to send the SMS with',
})
messageTemplate: string
@ApiProperty({
type: [SMSData],
required: true,
description: 'The messages to send',
})
messages: SMSData[]
}
export class ReceivedSMSDTO {
@ApiProperty({
type: String,

147
api/src/gateway/gateway.service.ts

@ -7,6 +7,7 @@ import {
ReceivedSMSDTO,
RegisterDeviceInputDTO,
RetrieveSMSDTO,
SendBulkSMSInputDTO,
SendSMSInputDTO,
} from './gateway.dto'
import { User } from '../users/schemas/user.schema'
@ -14,7 +15,10 @@ import { AuthService } from 'src/auth/auth.service'
import { SMS } from './schemas/sms.schema'
import { SMSType } from './sms-type.enum'
import { SMSBatch } from './schemas/sms-batch.schema'
import { Message } from 'firebase-admin/lib/messaging/messaging-api'
import {
BatchResponse,
Message,
} from 'firebase-admin/lib/messaging/messaging-api'
@Injectable()
export class GatewayService {
constructor(
@ -35,7 +39,10 @@ export class GatewayService {
})
if (device) {
return await this.updateDevice(device._id.toString(), { ...input, enabled: true })
return await this.updateDevice(device._id.toString(), {
...input,
enabled: true,
})
} else {
return await this.deviceModel.create({ ...input, user })
}
@ -218,6 +225,142 @@ export class GatewayService {
}
}
async sendBulkSMS(deviceId: string, body: SendBulkSMSInputDTO): Promise<any> {
const device = await this.deviceModel.findById(deviceId)
if (!device?.enabled) {
throw new HttpException(
{
success: false,
error: 'Device does not exist or is not enabled',
},
HttpStatus.BAD_REQUEST,
)
}
if (
!Array.isArray(body.messages) ||
body.messages.length === 0 ||
body.messages.map((m) => m.recipients).flat().length === 0
) {
throw new HttpException(
{
success: false,
error: 'Invalid message list',
},
HttpStatus.BAD_REQUEST,
)
}
if (body.messages.map((m) => m.recipients).flat().length > 50) {
throw new HttpException(
{
success: false,
error: 'Maximum of 50 recipients per batch is allowed',
},
HttpStatus.BAD_REQUEST,
)
}
const { messageTemplate, messages } = body
const smsBatch = await this.smsBatchModel.create({
device: device._id,
message: messageTemplate,
recipientCount: messages
.map((m) => m.recipients.length)
.reduce((a, b) => a + b, 0),
recipientPreview: this.getRecipientsPreview(
messages.map((m) => m.recipients).flat(),
),
})
const fcmResponses: BatchResponse[] = []
for (const smsData of messages) {
const message = smsData.message
const recipients = smsData.recipients
if (!message) {
continue
}
if (!Array.isArray(recipients) || recipients.length === 0) {
continue
}
const fcmMessages: Message[] = []
for (const recipient of recipients) {
const sms = await this.smsModel.create({
device: device._id,
smsBatch: smsBatch._id,
message: message,
type: SMSType.SENT,
recipient,
requestedAt: new Date(),
})
const updatedSMSData = {
smsId: sms._id,
smsBatchId: smsBatch._id,
message,
recipients: [recipient],
// Legacy fields to be removed in the future
smsBody: message,
receivers: [recipient],
}
const stringifiedSMSData = JSON.stringify(updatedSMSData)
const fcmMessage: Message = {
data: {
smsData: stringifiedSMSData,
},
token: device.fcmToken,
android: {
priority: 'high',
},
}
fcmMessages.push(fcmMessage)
}
try {
const response = await firebaseAdmin.messaging().sendEach(fcmMessages)
console.log(response)
fcmResponses.push(response)
this.deviceModel
.findByIdAndUpdate(deviceId, {
$inc: { sentSMSCount: response.successCount },
})
.exec()
.catch((e) => {
console.log('Failed to update sentSMSCount')
console.log(e)
})
} catch (e) {
console.log('Failed to send SMS: FCM')
console.log(e)
}
}
const successCount = fcmResponses.reduce(
(acc, m) => acc + m.successCount,
0,
)
const failureCount = fcmResponses.reduce(
(acc, m) => acc + m.failureCount,
0,
)
const response = {
success: successCount > 0,
successCount,
failureCount,
fcmResponses,
}
return response
}
async receiveSMS(deviceId: string, dto: ReceivedSMSDTO): Promise<any> {
const device = await this.deviceModel.findById(deviceId)

42
web/app/(app)/(auth)/verify-email.tsx/page.tsx

@ -0,0 +1,42 @@
'use client'
import { useSearchParams } from 'next/navigation'
import ResetPasswordForm from '../(components)/reset-password-form'
import RequestPasswordResetForm from '../(components)/request-password-reset-form'
import { useQuery } from '@tanstack/react-query'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
import { useMutation } from '@tanstack/react-query'
import { useSession } from 'next-auth/react'
export default function ResetPasswordPage() {
const searchParams = useSearchParams()
const code = searchParams.get('code')
const email = searchParams.get('email')
const session = useSession()
const {
mutate: verifyEmail,
isPending: isVerifyingEmail,
isSuccess: isVerifyingEmailSuccess,
isError: isVerifyingEmailError,
error: verifyingEmailError,
} = useMutation({
mutationFn: () =>
httpBrowserClient.post(ApiEndpoints.auth.verifyEmail(), {
email: decodeURIComponent(email),
code,
}),
})
if (!email) {
return <div>Email is required</div>
}
if (code && email) {
verifyEmail()
}
return <RequestPasswordResetForm />
}
Loading…
Cancel
Save