feat: notifications (#17701)
* feat: notifications * UI works * chore: pr feedback * initial fetch and clear notification upon logging out * fix: merge --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -142,52 +142,55 @@ describe(BackupService.name, () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||
mocks.storage.createWriteStream.mockReturnValue(new PassThrough());
|
||||
});
|
||||
|
||||
it('should run a database backup successfully', async () => {
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
expect(mocks.storage.createWriteStream).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rename file on success', async () => {
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.SUCCESS);
|
||||
expect(mocks.storage.rename).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if pg_dumpall fails', async () => {
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||
});
|
||||
|
||||
it('should not rename file if pgdump fails and gzip succeeds', async () => {
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if gzip fails', async () => {
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(0, 'data', ''));
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('Gzip failed with code 1');
|
||||
});
|
||||
|
||||
it('should fail if write stream fails', async () => {
|
||||
mocks.storage.createWriteStream.mockImplementation(() => {
|
||||
throw new Error('error');
|
||||
});
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('error');
|
||||
});
|
||||
|
||||
it('should fail if rename fails', async () => {
|
||||
mocks.storage.rename.mockRejectedValue(new Error('error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('error');
|
||||
});
|
||||
|
||||
it('should ignore unlink failing and still return failed job status', async () => {
|
||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||
mocks.storage.unlink.mockRejectedValue(new Error('error'));
|
||||
const result = await sut.handleBackupDatabase();
|
||||
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||
expect(mocks.storage.unlink).toHaveBeenCalled();
|
||||
expect(result).toBe(JobStatus.FAILED);
|
||||
});
|
||||
|
||||
it.each`
|
||||
postgresVersion | expectedVersion
|
||||
${'14.10'} | ${14}
|
||||
|
||||
@@ -174,7 +174,7 @@ export class BackupService extends BaseService {
|
||||
await this.storageRepository
|
||||
.unlink(backupFilePath)
|
||||
.catch((error) => this.logger.error('Failed to delete failed backup file', error));
|
||||
return JobStatus.FAILED;
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.log(`Database Backup Success`);
|
||||
|
||||
@@ -29,6 +29,7 @@ import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
@@ -80,6 +81,7 @@ export class BaseService {
|
||||
protected memoryRepository: MemoryRepository,
|
||||
protected metadataRepository: MetadataRepository,
|
||||
protected moveRepository: MoveRepository,
|
||||
protected notificationRepository: NotificationRepository,
|
||||
protected oauthRepository: OAuthRepository,
|
||||
protected partnerRepository: PartnerRepository,
|
||||
protected personRepository: PersonRepository,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { MapService } from 'src/services/map.service';
|
||||
import { MediaService } from 'src/services/media.service';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { MetadataService } from 'src/services/metadata.service';
|
||||
import { NotificationAdminService } from 'src/services/notification-admin.service';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
@@ -60,6 +61,7 @@ export const services = [
|
||||
MemoryService,
|
||||
MetadataService,
|
||||
NotificationService,
|
||||
NotificationAdminService,
|
||||
PartnerService,
|
||||
PersonService,
|
||||
SearchService,
|
||||
|
||||
@@ -215,11 +215,7 @@ export class JobService extends BaseService {
|
||||
await this.onDone(job);
|
||||
}
|
||||
} catch (error: Error | any) {
|
||||
this.logger.error(
|
||||
`Unable to run job handler (${queueName}/${job.name}): ${error}`,
|
||||
error?.stack,
|
||||
JSON.stringify(job.data),
|
||||
);
|
||||
await this.eventRepository.emit('job.failed', { job, error });
|
||||
} finally {
|
||||
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
|
||||
}
|
||||
|
||||
111
server/src/services/notification-admin.service.spec.ts
Normal file
111
server/src/services/notification-admin.service.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const smtpTransport = Object.freeze<SystemConfig>({
|
||||
...defaults,
|
||||
notifications: {
|
||||
smtp: {
|
||||
...defaults.notifications.smtp,
|
||||
enabled: true,
|
||||
transport: {
|
||||
ignoreCert: false,
|
||||
host: 'localhost',
|
||||
port: 587,
|
||||
username: 'test',
|
||||
password: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe(NotificationService.name, () => {
|
||||
let sut: NotificationService;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, mocks } = newTestService(NotificationService));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('sendTestEmail', () => {
|
||||
it('should throw error if user could not be found', async () => {
|
||||
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).rejects.toThrow('User not found');
|
||||
});
|
||||
|
||||
it('should throw error if smtp validation fails', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockRejectedValue('');
|
||||
|
||||
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).rejects.toThrow(
|
||||
'Failed to verify SMTP configuration',
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email to default domain', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: smtpTransport.notifications.smtp.transport,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email to external domain', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: smtpTransport.notifications.smtp.transport,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email with replyTo', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(
|
||||
sut.sendTestEmail('', { ...smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }),
|
||||
).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: smtpTransport.notifications.smtp.transport,
|
||||
replyTo: 'demo@immich.app',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
120
server/src/services/notification-admin.service.ts
Normal file
120
server/src/services/notification-admin.service.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { mapNotification, NotificationCreateDto } from 'src/dtos/notification.dto';
|
||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationAdminService extends BaseService {
|
||||
async create(auth: AuthDto, dto: NotificationCreateDto) {
|
||||
const item = await this.notificationRepository.create({
|
||||
userId: dto.userId,
|
||||
type: dto.type ?? NotificationType.Custom,
|
||||
level: dto.level ?? NotificationLevel.Info,
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
data: dto.data,
|
||||
});
|
||||
|
||||
return mapNotification(item);
|
||||
}
|
||||
|
||||
async sendTestEmail(id: string, dto: SystemConfigSmtpDto, tempTemplate?: string) {
|
||||
const user = await this.userRepository.get(id, { withDeleted: false });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.emailRepository.verifySmtp(dto.transport);
|
||||
} catch (error) {
|
||||
throw new BadRequestException('Failed to verify SMTP configuration', { cause: error });
|
||||
}
|
||||
|
||||
const { server } = await this.getConfig({ withCache: false });
|
||||
const { html, text } = await this.emailRepository.renderEmail({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server),
|
||||
displayName: user.name,
|
||||
},
|
||||
customTemplate: tempTemplate!,
|
||||
});
|
||||
const { messageId } = await this.emailRepository.sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Test email from Immich',
|
||||
html,
|
||||
text,
|
||||
from: dto.from,
|
||||
replyTo: dto.replyTo || dto.from,
|
||||
smtp: dto.transport,
|
||||
});
|
||||
|
||||
return { messageId };
|
||||
}
|
||||
|
||||
async getTemplate(name: EmailTemplate, customTemplate: string) {
|
||||
const { server, templates } = await this.getConfig({ withCache: false });
|
||||
|
||||
let templateResponse = '';
|
||||
|
||||
switch (name) {
|
||||
case EmailTemplate.WELCOME: {
|
||||
const { html: _welcomeHtml } = await this.emailRepository.renderEmail({
|
||||
template: EmailTemplate.WELCOME,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server),
|
||||
displayName: 'John Doe',
|
||||
username: 'john@doe.com',
|
||||
password: 'thisIsAPassword123',
|
||||
},
|
||||
customTemplate: customTemplate || templates.email.welcomeTemplate,
|
||||
});
|
||||
|
||||
templateResponse = _welcomeHtml;
|
||||
break;
|
||||
}
|
||||
case EmailTemplate.ALBUM_UPDATE: {
|
||||
const { html: _updateAlbumHtml } = await this.emailRepository.renderEmail({
|
||||
template: EmailTemplate.ALBUM_UPDATE,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server),
|
||||
albumId: '1',
|
||||
albumName: 'Favorite Photos',
|
||||
recipientName: 'Jane Doe',
|
||||
cid: undefined,
|
||||
},
|
||||
customTemplate: customTemplate || templates.email.albumInviteTemplate,
|
||||
});
|
||||
templateResponse = _updateAlbumHtml;
|
||||
break;
|
||||
}
|
||||
|
||||
case EmailTemplate.ALBUM_INVITE: {
|
||||
const { html } = await this.emailRepository.renderEmail({
|
||||
template: EmailTemplate.ALBUM_INVITE,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server),
|
||||
albumId: '1',
|
||||
albumName: "John Doe's Favorites",
|
||||
senderName: 'John Doe',
|
||||
recipientName: 'Jane Doe',
|
||||
cid: undefined,
|
||||
},
|
||||
customTemplate: customTemplate || templates.email.albumInviteTemplate,
|
||||
});
|
||||
templateResponse = html;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
templateResponse = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { name, html: templateResponse };
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { defaults, SystemConfig } from 'src/config';
|
||||
import { AlbumUser } from 'src/database';
|
||||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||
import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum';
|
||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
import { INotifyAlbumUpdateJob } from 'src/types';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
@@ -241,82 +240,6 @@ describe(NotificationService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendTestEmail', () => {
|
||||
it('should throw error if user could not be found', async () => {
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow('User not found');
|
||||
});
|
||||
|
||||
it('should throw error if smtp validation fails', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockRejectedValue('');
|
||||
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow(
|
||||
'Failed to verify SMTP configuration',
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email to default domain', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email to external domain', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send email with replyTo', async () => {
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||
|
||||
await expect(
|
||||
sut.sendTestEmail('', { ...configs.smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }),
|
||||
).resolves.not.toThrow();
|
||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
||||
});
|
||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subject: 'Test email from Immich',
|
||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
||||
replyTo: 'demo@immich.app',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleUserSignup', () => {
|
||||
it('should skip if user could not be found', async () => {
|
||||
await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SKIPPED);
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
mapNotification,
|
||||
NotificationDeleteAllDto,
|
||||
NotificationDto,
|
||||
NotificationSearchDto,
|
||||
NotificationUpdateAllDto,
|
||||
NotificationUpdateDto,
|
||||
} from 'src/dtos/notification.dto';
|
||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||
import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import {
|
||||
AssetFileType,
|
||||
JobName,
|
||||
JobStatus,
|
||||
NotificationLevel,
|
||||
NotificationType,
|
||||
Permission,
|
||||
QueueName,
|
||||
} from 'src/enum';
|
||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
@@ -15,6 +32,80 @@ import { getPreferences } from 'src/utils/preferences';
|
||||
export class NotificationService extends BaseService {
|
||||
private static albumUpdateEmailDelayMs = 300_000;
|
||||
|
||||
async search(auth: AuthDto, dto: NotificationSearchDto): Promise<NotificationDto[]> {
|
||||
const items = await this.notificationRepository.search(auth.user.id, dto);
|
||||
return items.map((item) => mapNotification(item));
|
||||
}
|
||||
|
||||
async updateAll(auth: AuthDto, dto: NotificationUpdateAllDto) {
|
||||
await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NOTIFICATION_UPDATE });
|
||||
await this.notificationRepository.updateAll(dto.ids, {
|
||||
readAt: dto.readAt,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAll(auth: AuthDto, dto: NotificationDeleteAllDto) {
|
||||
await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NOTIFICATION_DELETE });
|
||||
await this.notificationRepository.deleteAll(dto.ids);
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string) {
|
||||
await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_READ });
|
||||
const item = await this.notificationRepository.get(id);
|
||||
if (!item) {
|
||||
throw new BadRequestException('Notification not found');
|
||||
}
|
||||
return mapNotification(item);
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: NotificationUpdateDto) {
|
||||
await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_UPDATE });
|
||||
const item = await this.notificationRepository.update(id, {
|
||||
readAt: dto.readAt,
|
||||
});
|
||||
return mapNotification(item);
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string) {
|
||||
await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_DELETE });
|
||||
await this.notificationRepository.delete(id);
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.NOTIFICATIONS_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
||||
async onNotificationsCleanup() {
|
||||
await this.notificationRepository.cleanup();
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'job.failed' })
|
||||
async onJobFailed({ job, error }: ArgOf<'job.failed'>) {
|
||||
const admin = await this.userRepository.getAdmin();
|
||||
if (!admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.error(`Unable to run job handler (${job.name}): ${error}`, error?.stack, JSON.stringify(job.data));
|
||||
|
||||
switch (job.name) {
|
||||
case JobName.BACKUP_DATABASE: {
|
||||
const errorMessage = error instanceof Error ? error.message : error;
|
||||
const item = await this.notificationRepository.create({
|
||||
userId: admin.id,
|
||||
type: NotificationType.JobFailed,
|
||||
level: NotificationLevel.Error,
|
||||
title: 'Job Failed',
|
||||
description: `Job ${[job.name]} failed with error: ${errorMessage}`,
|
||||
});
|
||||
|
||||
this.eventRepository.clientSend('on_notification', admin.id, mapNotification(item));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.update' })
|
||||
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
|
||||
this.eventRepository.clientBroadcast('on_config_update');
|
||||
|
||||
Reference in New Issue
Block a user