diff --git a/description.md b/description.md new file mode 100644 index 0000000..a81d43d --- /dev/null +++ b/description.md @@ -0,0 +1,39 @@ +# Notification Preferences API + +This pull request implements the Notification Preferences API, enabling users to control their notification preferences independently. It includes the database schema, entity mappings, REST API endpoints, user signup hooks, comprehensive unit testing, and API documentation. + +## Proposed Changes + +### 1. Database Schema & Migration +- **Entity**: Created `NotificationPreference` (`notification_preferences` table) with the following fields: + - `id`: Auto-generated primary key (identity). + - `userId`: `int` linked to the `User` entity via a one-to-one relationship (`onDelete: 'CASCADE'`). Indexed for query performance. + - `newSubscriber`: `boolean`, default `true`. + - `postFromSubscribedCreator`: `boolean`, default `true`. + - `securityAlerts`: `boolean`, default `true`. + - `marketing`: `boolean`, default `false`. + - `created_at` / `updated_at`: Timestamps. +- **Migration**: Added database migration script `1769050000000-CreateNotificationPreferences.ts` with correct defaults and constraints. + +### 2. REST Endpoints (`/users/me/notification-preferences`) +- **GET**: Retrieves the authenticated user's notification preferences. If the preferences do not exist yet, they are automatically created with default values (lazy-creation). +- **PATCH**: Supports partial updates to the notification preferences. The endpoint identifies and rejects requests containing invalid keys with a `400 Bad Request` exception. +- Both endpoints are secured under `JwtAuthGuard` and utilize the `AuthenticatedRequest` context. + +### 3. User Signup Integration Hook +- Hooks into `UsersService.createUser` to automatically create default notification preferences immediately upon successful signup. + +### 4. Service Helper (`shouldNotify`) +- Implemented `NotificationsService.shouldNotify(userId, eventType)` for other system modules to query if they should deliver notifications for events like: + - `newSubscriber` + - `postFromSubscribedCreator` + - `securityAlerts` + - `marketing` +- Ensures invalid event types are rejected with a `BadRequestException`. + +### 5. Code Quality & Testing +- Added comprehensive unit tests in `notifications.service.spec.ts` and `notifications.controller.spec.ts`. +- Cleaned up unused imports/variables and corrected TypeScript/ESLint warnings (e.g., resolving `unbound-method` errors and adding type assertions for mock requests/arguments). +- Formatted the codebase utilizing Prettier. + +closes #29 diff --git a/docs/notifications.md b/docs/notifications.md new file mode 100644 index 0000000..3d97eea --- /dev/null +++ b/docs/notifications.md @@ -0,0 +1,76 @@ +# Notification Preferences API + +## Overview + +The Notification Preferences API allows users to control which types of events they receive notifications for across various channels (e.g., email, in-app, push). + +## Entity Structure + +Each user has a 1:1 relation with the `NotificationPreference` entity. The following boolean preferences are available: + +- `newSubscriber`: Alerts when a new user subscribes to the user. Default: `true` +- `postFromSubscribedCreator`: Alerts when a creator the user is subscribed to posts new content. Default: `true` +- `securityAlerts`: Critical security alerts (e.g., login from new device, password change). Default: `true` +- `marketing`: Promotional and marketing communications. Default: `false` + +## Endpoints + +### 1. Get Preferences + +Retrieves the authenticated user's notification preferences. If the preferences do not exist yet, they are automatically created with default values. + +**Request:** +`GET /users/me/notification-preferences` +Headers: `Authorization: Bearer ` + +**Response:** +```json +{ + "newSubscriber": true, + "postFromSubscribedCreator": true, + "securityAlerts": true, + "marketing": false +} +``` + +### 2. Update Preferences + +Partially update the authenticated user's notification preferences. Invalid keys are rejected with a 400 Bad Request. + +**Request:** +`PATCH /users/me/notification-preferences` +Headers: `Authorization: Bearer ` +Body: +```json +{ + "marketing": true, + "newSubscriber": false +} +``` + +**Response:** +```json +{ + "newSubscriber": false, + "postFromSubscribedCreator": true, + "securityAlerts": true, + "marketing": true +} +``` + +## Internal Usage (for other modules) + +To check if a notification should be delivered to a user for a specific event, inject `NotificationsService` and use the `shouldNotify` method: + +```typescript +import { NotificationsService, NotificationEventType } from '../notifications/notifications.service'; + +constructor(private notificationsService: NotificationsService) {} + +async processEvent(userId: number, eventType: NotificationEventType) { + const shouldNotify = await this.notificationsService.shouldNotify(userId, eventType); + if (shouldNotify) { + // Deliver the notification + } +} +``` diff --git a/src/app.module.ts b/src/app.module.ts index f1ddde6..ee6b342 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { LoggerModule } from './logger/logger.module'; import { MonitoringModule } from './monitoring/monitoring.module'; import { HealthModule } from './monitoring/health.module'; import { RateLimitService } from './common/services/rate-limit.service'; +import { NotificationsModule } from './notifications/notifications.module'; @Module({ imports: [ @@ -33,6 +34,7 @@ import { RateLimitService } from './common/services/rate-limit.service'; HealthModule, AuthModule, SubscriptionsModule, + NotificationsModule, ], controllers: [AppController], providers: [AppService, RateLimitService], diff --git a/src/migrations/1769050000000-CreateNotificationPreferences.ts b/src/migrations/1769050000000-CreateNotificationPreferences.ts new file mode 100644 index 0000000..e06663e --- /dev/null +++ b/src/migrations/1769050000000-CreateNotificationPreferences.ts @@ -0,0 +1,105 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableIndex, + TableForeignKey, +} from 'typeorm'; + +export class CreateNotificationPreferences1769050000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'notification_preferences', + columns: [ + { + name: 'id', + type: 'int', + isPrimary: true, + isGenerated: true, + generationStrategy: 'identity', + }, + { + name: 'userId', + type: 'int', + isUnique: true, + }, + { + name: 'newSubscriber', + type: 'boolean', + default: true, + }, + { + name: 'postFromSubscribedCreator', + type: 'boolean', + default: true, + }, + { + name: 'securityAlerts', + type: 'boolean', + default: true, + }, + { + name: 'marketing', + type: 'boolean', + default: false, + }, + { + name: 'created_at', + type: 'timestamp', + default: 'CURRENT_TIMESTAMP', + }, + { + name: 'updated_at', + type: 'timestamp', + default: 'CURRENT_TIMESTAMP', + onUpdate: 'CURRENT_TIMESTAMP', + }, + ], + }), + true, + ); + + await queryRunner.createForeignKey( + 'notification_preferences', + new TableForeignKey({ + columnNames: ['userId'], + referencedColumnNames: ['id'], + referencedTableName: 'users', + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createIndex( + 'notification_preferences', + new TableIndex({ + name: 'IDX_NOTIFICATION_PREF_USER_ID', + columnNames: ['userId'], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + const table = await queryRunner.getTable('notification_preferences'); + if (table) { + const foreignKey = table.foreignKeys.find( + (fk) => fk.columnNames.indexOf('userId') !== -1, + ); + if (foreignKey) { + await queryRunner.dropForeignKey( + 'notification_preferences', + foreignKey, + ); + } + + const index = table.indices.find( + (idx) => idx.name === 'IDX_NOTIFICATION_PREF_USER_ID', + ); + if (index) { + await queryRunner.dropIndex('notification_preferences', index); + } + + await queryRunner.dropTable('notification_preferences'); + } + } +} diff --git a/src/notifications/dtos/notification-preferences.dto.ts b/src/notifications/dtos/notification-preferences.dto.ts new file mode 100644 index 0000000..66258d9 --- /dev/null +++ b/src/notifications/dtos/notification-preferences.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; + +export class NotificationPreferencesDto { + @ApiProperty({ description: 'Preference for new subscriber notifications' }) + @Expose() + newSubscriber: boolean; + + @ApiProperty({ + description: 'Preference for post from subscribed creator notifications', + }) + @Expose() + postFromSubscribedCreator: boolean; + + @ApiProperty({ description: 'Preference for security alerts' }) + @Expose() + securityAlerts: boolean; + + @ApiProperty({ description: 'Preference for marketing communications' }) + @Expose() + marketing: boolean; +} diff --git a/src/notifications/dtos/update-notification-preferences.dto.ts b/src/notifications/dtos/update-notification-preferences.dto.ts new file mode 100644 index 0000000..387f125 --- /dev/null +++ b/src/notifications/dtos/update-notification-preferences.dto.ts @@ -0,0 +1,23 @@ +import { PartialType } from '@nestjs/swagger'; +import { NotificationPreferencesDto } from './notification-preferences.dto'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export class UpdateNotificationPreferencesDto extends PartialType( + NotificationPreferencesDto, +) { + @IsOptional() + @IsBoolean() + newSubscriber?: boolean; + + @IsOptional() + @IsBoolean() + postFromSubscribedCreator?: boolean; + + @IsOptional() + @IsBoolean() + securityAlerts?: boolean; + + @IsOptional() + @IsBoolean() + marketing?: boolean; +} diff --git a/src/notifications/notification-preference.entity.ts b/src/notifications/notification-preference.entity.ts new file mode 100644 index 0000000..4f33c58 --- /dev/null +++ b/src/notifications/notification-preference.entity.ts @@ -0,0 +1,43 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + OneToOne, + JoinColumn, +} from 'typeorm'; +import { User } from '../users/user.entity'; + +@Entity('notification_preferences') +export class NotificationPreference { + @PrimaryGeneratedColumn('identity') + id: number; + + @Index() + @Column({ type: 'int', unique: true }) + userId: number; + + @OneToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user: User; + + @Column({ type: 'boolean', default: true }) + newSubscriber: boolean; + + @Column({ type: 'boolean', default: true }) + postFromSubscribedCreator: boolean; + + @Column({ type: 'boolean', default: true }) + securityAlerts: boolean; + + @Column({ type: 'boolean', default: false }) + marketing: boolean; + + @CreateDateColumn({ type: 'timestamp' }) + created_at: Date; + + @UpdateDateColumn({ type: 'timestamp' }) + updated_at: Date; +} diff --git a/src/notifications/notifications.controller.spec.ts b/src/notifications/notifications.controller.spec.ts new file mode 100644 index 0000000..0d9755a --- /dev/null +++ b/src/notifications/notifications.controller.spec.ts @@ -0,0 +1,77 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { + NotificationsController, + AuthenticatedRequest, +} from './notifications.controller'; +import { NotificationsService } from './notifications.service'; +import { NotificationPreferencesDto } from './dtos/notification-preferences.dto'; + +describe('NotificationsController', () => { + let controller: NotificationsController; + + const mockService = { + getPreferences: jest.fn(), + updatePreferences: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [NotificationsController], + providers: [ + { + provide: NotificationsService, + useValue: mockService, + }, + ], + }).compile(); + + controller = module.get(NotificationsController); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const mockRequest = { + user: { + userId: 1, + email: 'test@example.com', + username: 'test', + }, + } as unknown as AuthenticatedRequest; + + describe('getPreferences', () => { + it('should call service.getPreferences with userId', async () => { + const mockResult: NotificationPreferencesDto = { + newSubscriber: true, + postFromSubscribedCreator: true, + securityAlerts: true, + marketing: false, + }; + mockService.getPreferences.mockResolvedValue(mockResult); + + const result = await controller.getPreferences(mockRequest); + + expect(mockService.getPreferences).toHaveBeenCalledWith(1); + expect(result).toEqual(mockResult); + }); + }); + + describe('updatePreferences', () => { + it('should call service.updatePreferences with userId and dto', async () => { + const dto = { marketing: true }; + const mockResult: NotificationPreferencesDto = { + newSubscriber: true, + postFromSubscribedCreator: true, + securityAlerts: true, + marketing: true, + }; + mockService.updatePreferences.mockResolvedValue(mockResult); + + const result = await controller.updatePreferences(mockRequest, dto); + + expect(mockService.updatePreferences).toHaveBeenCalledWith(1, dto); + expect(result).toEqual(mockResult); + }); + }); +}); diff --git a/src/notifications/notifications.controller.ts b/src/notifications/notifications.controller.ts new file mode 100644 index 0000000..e6b9944 --- /dev/null +++ b/src/notifications/notifications.controller.ts @@ -0,0 +1,64 @@ +import { Body, Controller, Get, Patch, Req, UseGuards } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { NotificationsService } from './notifications.service'; +import { NotificationPreferencesDto } from './dtos/notification-preferences.dto'; +import { UpdateNotificationPreferencesDto } from './dtos/update-notification-preferences.dto'; + +export interface AuthenticatedRequest extends Request { + user: { + userId: number; + email: string; + username: string; + }; +} + +@ApiTags('Notifications') +@ApiBearerAuth('JWT-auth') +@Controller('users/me/notification-preferences') +@UseGuards(JwtAuthGuard) +export class NotificationsController { + constructor(private readonly notificationsService: NotificationsService) {} + + @Get() + @ApiOperation({ summary: 'Get current user notification preferences' }) + @ApiResponse({ + status: 200, + description: 'Notification preferences retrieved successfully', + type: NotificationPreferencesDto, + }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + async getPreferences( + @Req() req: AuthenticatedRequest, + ): Promise { + return this.notificationsService.getPreferences(req.user.userId); + } + + @Patch() + @ApiOperation({ + summary: 'Partially update current user notification preferences', + }) + @ApiBody({ type: UpdateNotificationPreferencesDto }) + @ApiResponse({ + status: 200, + description: 'Notification preferences updated successfully', + type: NotificationPreferencesDto, + }) + @ApiResponse({ status: 400, description: 'Invalid preference keys' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + async updatePreferences( + @Req() req: AuthenticatedRequest, + @Body() updateDto: UpdateNotificationPreferencesDto, + ): Promise { + return this.notificationsService.updatePreferences( + req.user.userId, + updateDto, + ); + } +} diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts new file mode 100644 index 0000000..0998139 --- /dev/null +++ b/src/notifications/notifications.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotificationPreference } from './notification-preference.entity'; +import { NotificationsService } from './notifications.service'; +import { NotificationsController } from './notifications.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([NotificationPreference])], + controllers: [NotificationsController], + providers: [NotificationsService], + exports: [NotificationsService], +}) +export class NotificationsModule {} diff --git a/src/notifications/notifications.service.spec.ts b/src/notifications/notifications.service.spec.ts new file mode 100644 index 0000000..010311e --- /dev/null +++ b/src/notifications/notifications.service.spec.ts @@ -0,0 +1,154 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { + NotificationsService, + NotificationEventType, +} from './notifications.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { NotificationPreference } from './notification-preference.entity'; +import { BadRequestException } from '@nestjs/common'; +import { UpdateNotificationPreferencesDto } from './dtos/update-notification-preferences.dto'; + +describe('NotificationsService', () => { + let service: NotificationsService; + + const mockRepository = { + findOneBy: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NotificationsService, + { + provide: getRepositoryToken(NotificationPreference), + useValue: mockRepository, + }, + ], + }).compile(); + + service = module.get(NotificationsService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('createDefaultPreferences', () => { + it('should return existing preferences if they exist', async () => { + const existingPref = { userId: 1, securityAlerts: true }; + mockRepository.findOneBy.mockResolvedValue(existingPref); + + const result = await service.createDefaultPreferences(1); + expect(result).toEqual(existingPref); + expect(mockRepository.create).not.toHaveBeenCalled(); + }); + + it('should create and save default preferences if none exist', async () => { + mockRepository.findOneBy.mockResolvedValue(null); + const newPref = { + userId: 1, + securityAlerts: true, + newSubscriber: true, + postFromSubscribedCreator: true, + marketing: false, + }; + mockRepository.create.mockReturnValue(newPref); + mockRepository.save.mockResolvedValue(newPref); + + const result = await service.createDefaultPreferences(1); + + expect(mockRepository.create).toHaveBeenCalledWith({ + userId: 1, + newSubscriber: true, + postFromSubscribedCreator: true, + securityAlerts: true, + marketing: false, + }); + expect(mockRepository.save).toHaveBeenCalledWith(newPref); + expect(result).toEqual(newPref); + }); + }); + + describe('getPreferences', () => { + it('should return preferences and transform to DTO', async () => { + mockRepository.findOneBy.mockResolvedValue({ + id: 1, + userId: 1, + securityAlerts: true, + marketing: false, + }); + + const result = await service.getPreferences(1); + expect(result.securityAlerts).toBe(true); + expect(result.marketing).toBe(false); + }); + + it('should lazy-create preferences if missing', async () => { + mockRepository.findOneBy + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + const newPref = { + id: 1, + userId: 1, + securityAlerts: true, + marketing: false, + }; + mockRepository.create.mockReturnValue(newPref); + mockRepository.save.mockResolvedValue(newPref); + + const result = await service.getPreferences(1); + expect(mockRepository.save).toHaveBeenCalled(); + expect(result.securityAlerts).toBe(true); + }); + }); + + describe('updatePreferences', () => { + it('should partially update preferences', async () => { + const existing = { userId: 1, securityAlerts: true, marketing: false }; + mockRepository.findOneBy.mockResolvedValue(existing); + mockRepository.save.mockResolvedValue({ ...existing, marketing: true }); + + const result = await service.updatePreferences(1, { marketing: true }); + expect(mockRepository.save).toHaveBeenCalledWith({ + ...existing, + marketing: true, + }); + expect(result.marketing).toBe(true); + }); + + it('should throw BadRequestException for invalid keys', async () => { + const existing = { userId: 1, securityAlerts: true, marketing: false }; + mockRepository.findOneBy.mockResolvedValue(existing); + + await expect( + service.updatePreferences(1, { + invalidKey: true, + } as unknown as UpdateNotificationPreferencesDto), + ).rejects.toThrow(BadRequestException); + expect(mockRepository.save).not.toHaveBeenCalled(); + }); + }); + + describe('shouldNotify', () => { + it('should return boolean for valid event type', async () => { + mockRepository.findOneBy.mockResolvedValue({ + userId: 1, + securityAlerts: true, + }); + + const result = await service.shouldNotify(1, 'securityAlerts'); + expect(result).toBe(true); + }); + + it('should throw BadRequestException for invalid event type', async () => { + await expect( + service.shouldNotify( + 1, + 'invalidEvent' as unknown as NotificationEventType, + ), + ).rejects.toThrow(BadRequestException); + }); + }); +}); diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts new file mode 100644 index 0000000..b1292e0 --- /dev/null +++ b/src/notifications/notifications.service.ts @@ -0,0 +1,110 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NotificationPreference } from './notification-preference.entity'; +import { NotificationPreferencesDto } from './dtos/notification-preferences.dto'; +import { UpdateNotificationPreferencesDto } from './dtos/update-notification-preferences.dto'; +import { plainToInstance } from 'class-transformer'; + +export type NotificationEventType = + | 'newSubscriber' + | 'postFromSubscribedCreator' + | 'securityAlerts' + | 'marketing'; + +@Injectable() +export class NotificationsService { + constructor( + @InjectRepository(NotificationPreference) + private readonly preferencesRepository: Repository, + ) {} + + async createDefaultPreferences( + userId: number, + ): Promise { + const existing = await this.preferencesRepository.findOneBy({ userId }); + if (existing) { + return existing; + } + + const preferences = this.preferencesRepository.create({ + userId, + newSubscriber: true, + postFromSubscribedCreator: true, + securityAlerts: true, + marketing: false, + }); + + return this.preferencesRepository.save(preferences); + } + + async getPreferences(userId: number): Promise { + let preferences = await this.preferencesRepository.findOneBy({ userId }); + + // Lazy-create on first GET if missing + if (!preferences) { + preferences = await this.createDefaultPreferences(userId); + } + + return plainToInstance(NotificationPreferencesDto, preferences, { + excludeExtraneousValues: true, + }); + } + + async updatePreferences( + userId: number, + updateDto: UpdateNotificationPreferencesDto, + ): Promise { + let preferences = await this.preferencesRepository.findOneBy({ userId }); + + if (!preferences) { + preferences = await this.createDefaultPreferences(userId); + } + + // Identify invalid keys not present in the entity + const allowedKeys = [ + 'newSubscriber', + 'postFromSubscribedCreator', + 'securityAlerts', + 'marketing', + ]; + const invalidKeys = Object.keys(updateDto).filter( + (key) => !allowedKeys.includes(key), + ); + if (invalidKeys.length > 0) { + throw new BadRequestException( + `Invalid preference keys: ${invalidKeys.join(', ')}`, + ); + } + + Object.assign(preferences, updateDto); + const updated = await this.preferencesRepository.save(preferences); + + return plainToInstance(NotificationPreferencesDto, updated, { + excludeExtraneousValues: true, + }); + } + + async shouldNotify( + userId: number, + eventType: NotificationEventType, + ): Promise { + const allowedEvents: NotificationEventType[] = [ + 'newSubscriber', + 'postFromSubscribedCreator', + 'securityAlerts', + 'marketing', + ]; + + if (!allowedEvents.includes(eventType)) { + throw new BadRequestException('Invalid event type'); + } + + let preferences = await this.preferencesRepository.findOneBy({ userId }); + if (!preferences) { + preferences = await this.createDefaultPreferences(userId); + } + + return preferences[eventType]; + } +} diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 1e8700b..1cfac54 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -8,11 +8,13 @@ import { RefreshToken } from '../auth/entities/refresh-token.entity'; import { UsersQueryService } from './services/users-query.service'; import { SearchService } from './services/search.service'; import { PermissionService } from './services/permission.service'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ imports: [ TypeOrmModule.forFeature([User, RefreshToken]), CacheModule.register(), + NotificationsModule, ], providers: [ UsersService, diff --git a/src/users/users.service.ts b/src/users/users.service.ts index d39fb7e..812d333 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -24,6 +24,7 @@ import { PermissionService, JwtPayload } from './services/permission.service'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import * as bcrypt from 'bcrypt'; +import { NotificationsService } from '../notifications/notifications.service'; @Injectable() export class UsersService { @@ -35,6 +36,7 @@ export class UsersService { private readonly queryService: UsersQueryService, private readonly searchService: SearchService, private readonly permissionService: PermissionService, + private readonly notificationsService: NotificationsService, @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {} @@ -106,6 +108,7 @@ export class UsersService { // Update search text and invalidate caches await this.searchService.updateSearchTextForUser(savedUser.id); await this.invalidateUserRelatedCaches(savedUser.id); + await this.notificationsService.createDefaultPreferences(savedUser.id); return plainToInstance( UserResponseDto,