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
6 changes: 4 additions & 2 deletions src/auth/decorators/current-user.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
export const CurrentUser = createParamDecorator((data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
const user = request.user;

return data ? user?.[data] : user;
});
79 changes: 79 additions & 0 deletions src/notifications/dto/notification.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
IsBoolean,
IsObject,
} from 'class-validator';
import { NotificationType, NotificationPriority } from '../entities/notification.entity';

export class CreateNotificationDto {
@ApiProperty({ description: 'The ID of the user the notification is for' })
@IsUUID()
@IsNotEmpty()
userId: string;

@ApiProperty({ description: 'The title of the notification' })
@IsString()
@IsNotEmpty()
title: string;

@ApiProperty({ description: 'The content of the notification' })
@IsString()
@IsNotEmpty()
content: string;

@ApiProperty({ enum: NotificationType, description: 'The type of the notification' })
@IsEnum(NotificationType)
@IsOptional()
type?: NotificationType;

@ApiProperty({ enum: NotificationPriority, description: 'The priority of the notification' })
@IsEnum(NotificationPriority)
@IsOptional()
priority?: NotificationPriority;

@ApiPropertyOptional({ description: 'Additional metadata for the notification' })
@IsObject()
@IsOptional()
metadata?: Record<string, any>;
}

export class UpdateNotificationDto {
@ApiPropertyOptional({ description: 'Mark as read or unread' })
@IsBoolean()
@IsOptional()
isRead?: boolean;
}

export class NotificationResponseDto {
@ApiProperty()
id: string;

@ApiProperty()
userId: string;

@ApiProperty()
title: string;

@ApiProperty()
content: string;

@ApiProperty({ enum: NotificationType })
type: NotificationType;

@ApiProperty({ enum: NotificationPriority })
priority: NotificationPriority;

@ApiProperty()
isRead: boolean;

@ApiPropertyOptional()
readAt?: Date;

@ApiProperty()
createdAt: Date;
}
50 changes: 50 additions & 0 deletions src/notifications/entities/notification-preferences.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
JoinColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';

@Entity('notification_preferences')
export class NotificationPreferences {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
userId: string;

@OneToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn()
user: User;

@Column({ default: true })
emailEnabled: boolean;

@Column({ default: true })
pushEnabled: boolean;

@Column({ default: true })
inAppEnabled: boolean;

@Column({ default: false })
smsEnabled: boolean;

@Column({ type: 'jsonb', nullable: true })
topicSubscriptions: Record<string, boolean>;

@Column({ type: 'varchar', default: '09:00' })
quietTimeStart: string;

@Column({ type: 'varchar', default: '21:00' })
quietTimeEnd: string;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
73 changes: 73 additions & 0 deletions src/notifications/entities/notification.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
Index,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';

export enum NotificationType {
EMAIL = 'email',
PUSH = 'push',
IN_APP = 'in_app',
SMS = 'sms',
}

export enum NotificationPriority {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
URGENT = 'urgent',
}

@Entity('notifications')
export class Notification {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
@Index()
userId: string;

@ManyToOne(() => User, { onDelete: 'CASCADE' })
user: User;

@Column()
title: string;

@Column('text')
content: string;

@Column({
type: 'enum',
enum: NotificationType,
default: NotificationType.IN_APP,
})
type: NotificationType;

@Column({
type: 'enum',
enum: NotificationPriority,
default: NotificationPriority.MEDIUM,
})
priority: NotificationPriority;

@Column({ default: false })
isRead: boolean;

@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;

@Column({ nullable: true })
readAt: Date;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
@Index()
updatedAt: Date;
}
67 changes: 67 additions & 0 deletions src/notifications/notification-templates.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Injectable, Logger } from '@nestjs/common';
import { NotificationType } from './entities/notification.entity';

export interface NotificationTemplate {
title: string;
content: string;
}

@Injectable()
export class NotificationTemplatesService {
private readonly logger = new Logger(NotificationTemplatesService.name);
private readonly templates: Map<string, (data: any) => NotificationTemplate> = new Map();

constructor() {
this.registerTemplates();
}

private registerTemplates() {
this.templates.set('COURSE_ENROLLMENT', (data: any) => ({
title: 'Course Enrollment',
content: `Welcome to the course ${data.courseName}! You have been successfully enrolled.`,
}));

this.templates.set('PAYMENT_RECEIVED', (data: any) => ({
title: 'Payment Confirmed',
content: `A payment of ${data.amount} ${data.currency} for the course ${data.courseName} was successfully processed.`,
}));

this.templates.set('NEW_MESSAGE', (data: any) => ({
title: 'New Message',
content: `You have received a new message from ${data.senderName}.`,
}));

this.templates.set('COURSE_REPLY', (data: any) => ({
title: 'Course Discussion Reply',
content: `${data.senderName} replied to your comment in ${data.courseName}.`,
}));
}

renderTemplate(type: string, data: any): NotificationTemplate {
const templateFn = this.templates.get(type);
if (!templateFn) {
this.logger.warn(`Template callback not found for type: ${type}`);
return {
title: 'Notification',
content: JSON.stringify(data),
};
}

return templateFn(data);
}

formatForType(n: NotificationTemplate, type: NotificationType): string {
switch (type) {
case NotificationType.EMAIL:
return `<h1>${n.title}</h1><p>${n.content}</p>`;
case NotificationType.PUSH:
return n.content;
case NotificationType.IN_APP:
return n.content;
case NotificationType.SMS:
return `${n.title}: ${n.content}`;
default:
return n.content;
}
}
}
81 changes: 81 additions & 0 deletions src/notifications/notifications.controller.ts
Original file line number Diff line number Diff line change
@@ -1,0 +1,81 @@
import {
Controller,
Get,
Post,

Check warning on line 4 in src/notifications/notifications.controller.ts

View workflow job for this annotation

GitHub Actions / ESLint

'Post' is defined but never used. Allowed unused vars must match /^_/u
Patch,
Delete,
Param,
Body,
UseGuards,
Query,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiResponse } from '@nestjs/swagger';
import { NotificationsService } from './notifications.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { NotificationResponseDto, UpdateNotificationDto } from './dto/notification.dto';

Check warning on line 18 in src/notifications/notifications.controller.ts

View workflow job for this annotation

GitHub Actions / ESLint

'UpdateNotificationDto' is defined but never used. Allowed unused vars must match /^_/u
import { NotificationPreferences } from './entities/notification-preferences.entity';

@ApiTags('Notifications')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('notifications')
export class NotificationsController {
constructor(private readonly notificationsService: NotificationsService) {}

@Get()
@ApiOperation({ summary: 'Get all notifications for current user' })
@ApiResponse({ status: 200, type: [NotificationResponseDto] })
async getMyNotifications(
@CurrentUser('id') userId: string,
@Query('isRead') isRead?: boolean,
@Query('limit') limit?: number,
@Query('offset') offset?: number,
) {
const [data, total] = await this.notificationsService.findAllForUser(userId, {
isRead,
limit,
offset,
});
return { data, total };
}

@Patch(':id/read')
@ApiOperation({ summary: 'Mark a notification as read' })
@ApiParam({ name: 'id', description: 'Notification ID' })
async markAsRead(@Param('id') id: string, @CurrentUser('id') userId: string) {
return this.notificationsService.markAsRead(id, userId);
}

@Patch('read-all')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Mark all notifications as read' })
async markAllAsRead(@CurrentUser('id') userId: string) {
await this.notificationsService.markAllAsRead(userId);
}

@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete a notification' })
@ApiParam({ name: 'id', description: 'Notification ID' })
async deleteNotification(@Param('id') id: string, @CurrentUser('id') userId: string) {
await this.notificationsService.remove(id, userId);
}

@Get('preferences')
@ApiOperation({ summary: 'Get current user notification preferences' })
async getPreferences(@CurrentUser('id') userId: string) {
return this.notificationsService.getPreferences(userId);
}

@Patch('preferences')
@ApiOperation({ summary: 'Update notification preferences' })
async updatePreferences(
@CurrentUser('id') userId: string,
@Body() preferencesDto: Partial<NotificationPreferences>,
) {
return this.notificationsService.updatePreferences(userId, preferencesDto);
}
}
Loading
Loading