Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/notifications/channels/channel.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Notification } from '../entities/notification.entity';

export interface ChannelDeliveryResult {
success: boolean;
error?: string;
deliveryTimestamp?: Date;
}

export interface NotificationChannel {
/**
* Unique identifier for the channel
*/
readonly channelType: string;

/**
* Check if this channel is enabled for a specific user
*/
isEnabled(userId: string): Promise<boolean>;

/**
* Send a notification through this channel
*/
send(notification: Notification): Promise<ChannelDeliveryResult>;

/**
* Validate user configuration for this channel
*/
validateConfig(userId: string): Promise<{ valid: boolean; errors: string[] }>;
}
105 changes: 105 additions & 0 deletions src/notifications/channels/email.channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotificationChannel as ChannelType, Notification } from '../entities/notification.entity';
import { UserNotificationPreferences } from '../entities/notification.entity';
import { NotificationChannel, ChannelDeliveryResult } from './channel.interface';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class EmailChannel implements NotificationChannel {
private readonly logger = new Logger(EmailChannel.name);
readonly channelType = ChannelType.EMAIL;

constructor(
@InjectRepository(UserNotificationPreferences)
private readonly preferencesRepository: Repository<UserNotificationPreferences>,
private readonly configService: ConfigService,
) {}

async isEnabled(userId: string): Promise<boolean> {
const preferences = await this.preferencesRepository.findOne({
where: { userId },
});

if (!preferences) {
return false; // Default to disabled if no email configured
}

return preferences.enabledChannels[this.channelType] ?? false && preferences.emailEnabled;
}

async send(notification: Notification): Promise<ChannelDeliveryResult> {
const preferences = await this.preferencesRepository.findOne({
where: { userId: notification.recipientId },
});

if (!preferences || !preferences.emailAddress) {
return {
success: false,
error: 'No email address configured for user',
};
}

this.logger.debug(
`Sending email notification ${notification.id} to ${preferences.emailAddress}`,
);

// In a real implementation, this would integrate with an email service like SendGrid, AWS SES, etc.
// For now, we'll just log that the email would be sent

const emailHtml = this.generateEmailHtml(notification);
this.logger.debug(`Email content would be: ${emailHtml.substring(0, 200)}...`);

return {
success: true,
deliveryTimestamp: new Date(),
};
}

async validateConfig(userId: string): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = [];
const preferences = await this.preferencesRepository.findOne({
where: { userId },
});

if (!preferences) {
errors.push('No user preferences found');
return { valid: false, errors };
}

if (!preferences.emailAddress) {
errors.push('No email address configured');
}

if (!preferences.emailEnabled) {
errors.push('Email notifications are disabled');
}

const smtpHost = this.configService.get('SMTP_HOST');
if (!smtpHost) {
errors.push('SMTP server not configured');
}

return {
valid: errors.length === 0,
errors,
};
}

private generateEmailHtml(notification: Notification): string {
return `
<!DOCTYPE html>
<html>
<head>
<title>${notification.title}</title>
</head>
<body>
<h1>${notification.title}</h1>
<p>${notification.message}</p>
${notification.metadata ? `<pre>${JSON.stringify(notification.metadata, null, 2)}</pre>` : ''}
</body>
</html>
`;
}
}
49 changes: 49 additions & 0 deletions src/notifications/channels/in-app.channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotificationChannel as ChannelType, Notification } from '../entities/notification.entity';
import { UserNotificationPreferences } from '../entities/notification.entity';
import { NotificationChannel, ChannelDeliveryResult } from './channel.interface';

@Injectable()
export class InAppChannel implements NotificationChannel {
private readonly logger = new Logger(InAppChannel.name);
readonly channelType = ChannelType.IN_APP;

constructor(
@InjectRepository(UserNotificationPreferences)
private readonly preferencesRepository: Repository<UserNotificationPreferences>,
) {}

async isEnabled(userId: string): Promise<boolean> {
const preferences = await this.preferencesRepository.findOne({
where: { userId },
});

if (!preferences) {
return true; // Default to enabled if no preferences set
}

return preferences.enabledChannels[this.channelType] ?? true;
}

async send(notification: Notification): Promise<ChannelDeliveryResult> {
this.logger.debug(
`Sending in-app notification ${notification.id} to user ${notification.recipientId}`,
);

// In-app notifications are just stored in the database, they're retrieved via API
// The WebSocket server will broadcast the new notification to connected clients

return {
success: true,
deliveryTimestamp: new Date(),
};
}

async validateConfig(userId: string): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = [];
// In-app channel always has a valid config since it doesn't require any user configuration
return { valid: true, errors };
}
}
97 changes: 97 additions & 0 deletions src/notifications/channels/push.channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotificationChannel as ChannelType, Notification } from '../entities/notification.entity';
import { UserNotificationPreferences } from '../entities/notification.entity';
import { NotificationChannel, ChannelDeliveryResult } from './channel.interface';

@Injectable()
export class PushChannel implements NotificationChannel {
private readonly logger = new Logger(PushChannel.name);
readonly channelType = ChannelType.PUSH;

constructor(
@InjectRepository(UserNotificationPreferences)
private readonly preferencesRepository: Repository<UserNotificationPreferences>,
) {}

async isEnabled(userId: string): Promise<boolean> {
const preferences = await this.preferencesRepository.findOne({
where: { userId },
});

if (!preferences || !preferences.pushSubscription) {
return false;
}

return preferences.enabledChannels[this.channelType] ?? false;
}

async send(notification: Notification): Promise<ChannelDeliveryResult> {
const preferences = await this.preferencesRepository.findOne({
where: { userId: notification.recipientId },
});

if (!preferences?.pushSubscription?.endpoint) {
return {
success: false,
error: 'No push subscription configured for user',
};
}

this.logger.debug(
`Sending push notification ${notification.id} to user ${notification.recipientId}`,
);

// In a real implementation, this would use a service like Firebase Cloud Messaging (FCM),
// Apple Push Notification Service (APNs), or a web push library to send the notification
// to the user's device

const pushPayload = {
title: notification.title,
body: notification.message,
data: {
notificationId: notification.id,
type: notification.type,
...notification.metadata,
},
};

this.logger.debug(`Push payload: ${JSON.stringify(pushPayload)}`);

return {
success: true,
deliveryTimestamp: new Date(),
};
}

async validateConfig(userId: string): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = [];
const preferences = await this.preferencesRepository.findOne({
where: { userId },
});

if (!preferences) {
errors.push('No user preferences found');
return { valid: false, errors };
}

if (!preferences.pushSubscription) {
errors.push('No push subscription configured');
return { valid: false, errors };
}

if (!preferences.pushSubscription.endpoint) {
errors.push('Push subscription endpoint not provided');
}

if (!preferences.pushSubscription.keys?.p256dh || !preferences.pushSubscription.keys?.auth) {
errors.push('Push subscription encryption keys missing');
}

return {
valid: errors.length === 0,
errors,
};
}
}
Loading
Loading