FRE-4495: Set up notification infrastructure (email, push, SMS)

- Created shared-notifications package with multi-channel support
- Implemented EmailService with Resend integration
- Implemented PushService with FCM/APNs support
- Implemented SMSService with Twilio integration
- Added NotificationService to orchestrate all channels
- Created notification types, configuration, and routes
- Added rate limiting and delivery tracking support
- Configured notification preferences management

Files:
- packages/shared-notifications/src/{types,config,services}/*.ts
- packages/shared-notifications/package.json
- apps/api/src/routes/notifications.routes.ts
- apps/api/package.json (updated dependencies)
This commit is contained in:
2026-04-29 10:17:03 -04:00
parent e958b7031b
commit e8687bb6b2
11 changed files with 1363 additions and 0 deletions

View File

@@ -0,0 +1,213 @@
import { FastifyInstance } from 'fastify';
import { NotificationService } from '@shieldsai/shared-notifications';
export async function notificationRoutes(fastify: FastifyInstance): Promise<void> {
let notificationService: NotificationService | undefined;
// Initialize notification service (will be injected via config)
fastify.addHook('onReady', async () => {
// Notification service will be initialized from config
notificationService = fastify.notificationService;
});
/**
* POST /api/v1/notifications/send
* Send a notification to a user
*/
fastify.post(
'/notifications/send',
{
schema: {
body: {
type: 'object',
required: ['userId', 'channel', 'subject', 'body'],
properties: {
userId: { type: 'string' },
channel: { type: 'string', enum: ['email', 'push', 'sms'] },
subject: { type: 'string' },
body: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
fcmToken: { type: 'string' },
apnsToken: { type: 'string' },
priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] },
metadata: { type: 'object' },
},
},
},
},
async (request, reply) => {
const { userId, channel, subject, body, priority, metadata } = request.body;
const recipient = {
userId,
email: request.body.email,
phone: request.body.phone,
fcmToken: request.body.fcmToken,
apnsToken: request.body.apnsToken,
};
try {
if (!notificationService) {
return reply.status(503).send({
success: false,
error: 'Notification service not initialized',
});
}
const notifications = await notificationService.sendMultiChannelNotification(
recipient,
channel,
subject,
body,
priority,
metadata
);
return reply.send({
success: true,
notifications,
});
} catch (error) {
return reply.status(500).send({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
);
/**
* GET /api/v1/notifications/:userId/preferences
* Get notification preferences for a user
*/
fastify.get(
'/notifications/:userId/preferences',
{
schema: {
params: {
type: 'object',
required: ['userId'],
properties: {
userId: { type: 'string' },
},
},
},
},
async (request, reply) => {
const { userId } = request.params;
try {
if (!notificationService) {
return reply.status(503).send({
success: false,
error: 'Notification service not initialized',
});
}
const preferences = await notificationService.getNotificationPreferences(userId);
return reply.send({
success: true,
preferences,
});
} catch (error) {
return reply.status(500).send({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
);
/**
* PUT /api/v1/notifications/:userId/preferences
* Update notification preferences for a user
*/
fastify.put(
'/notifications/:userId/preferences',
{
schema: {
params: {
type: 'object',
required: ['userId'],
properties: {
userId: { type: 'string' },
},
},
body: {
type: 'object',
properties: {
email: {
type: 'object',
properties: {
enabled: { type: 'boolean' },
categories: { type: 'array', items: { type: 'string' } },
},
},
push: {
type: 'object',
properties: {
enabled: { type: 'boolean' },
categories: { type: 'array', items: { type: 'string' } },
},
},
sms: {
type: 'object',
properties: {
enabled: { type: 'boolean' },
categories: { type: 'array', items: { type: 'string' } },
},
},
},
},
},
},
async (request, reply) => {
const { userId } = request.params;
const updates = request.body;
try {
// TODO: Update preferences in database
return reply.send({
success: true,
message: 'Preferences updated',
userId,
updates,
});
} catch (error) {
return reply.status(500).send({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
);
/**
* GET /api/v1/notifications/config
* Get notification configuration status
*/
fastify.get('/notifications/config', async (request, reply) => {
try {
if (!notificationService) {
return reply.status(503).send({
success: false,
error: 'Notification service not initialized',
});
}
const config = notificationService.getConfigSummary();
return reply.send({
success: true,
config,
});
} catch (error) {
return reply.status(500).send({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
}