mirror of
https://github.com/immich-app/immich.git
synced 2024-11-24 08:52:28 +02:00
feat(server): album's email notification (#9439)
* feat(server): album's email notification * same size button * skeleton for album invite and album update event * album invite content * album update * fix(server): smtp certificate validation (#9506) * album update content * send mail * album invite with thumbnail * pr feedback * styling * Send email to update album event * better naming * add tests * Update album-invite.email.tsx Co-authored-by: bo0tzz <git@bo0tzz.me> * Update album-update.email.tsx Co-authored-by: bo0tzz <git@bo0tzz.me> * fix: unit tests * typo * Update server/src/services/notification.service.ts Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * PR feedback * Update server/src/emails/album-update.email.tsx Co-authored-by: Zack Pollard <zackpollard@ymail.com> --------- Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Co-authored-by: bo0tzz <git@bo0tzz.me> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Zack Pollard <zackpollard@ymail.com>
This commit is contained in:
parent
832084687d
commit
1f9158c545
BIN
docs/static/img/ios-app-store-badge.png
vendored
BIN
docs/static/img/ios-app-store-badge.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 15 KiB |
174
server/src/emails/album-invite.email.tsx
Normal file
174
server/src/emails/album-invite.email.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Row,
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components';
|
||||
import * as CSS from 'csstype';
|
||||
import * as React from 'react';
|
||||
import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface';
|
||||
|
||||
export const AlbumInviteEmail = ({
|
||||
baseUrl,
|
||||
albumName,
|
||||
recipientName,
|
||||
senderName,
|
||||
albumId,
|
||||
cid,
|
||||
}: AlbumInviteEmailProps) => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>You have been added to a shared album.</Preview>
|
||||
<Body
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
backgroundColor: '#ffffff',
|
||||
color: 'rgb(28,28,28)',
|
||||
fontFamily: 'Overpass, sans-serif',
|
||||
fontSize: '18px',
|
||||
lineHeight: '24px',
|
||||
}}
|
||||
>
|
||||
<Container
|
||||
style={{
|
||||
width: '540px',
|
||||
maxWidth: '100%',
|
||||
padding: '10px',
|
||||
margin: '0 auto',
|
||||
}}
|
||||
>
|
||||
<Section
|
||||
style={{
|
||||
padding: '36px',
|
||||
tableLayout: 'fixed',
|
||||
backgroundColor: 'rgb(226, 232, 240)',
|
||||
border: 'solid 0px rgb(248 113 113)',
|
||||
borderRadius: '50px',
|
||||
textAlign: 'center' as const,
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src="https://immich.app/img/immich-logo-inline-light.png"
|
||||
alt="Immich"
|
||||
style={{
|
||||
height: 'auto',
|
||||
margin: '0 auto 48px auto',
|
||||
width: '50%',
|
||||
alignSelf: 'center',
|
||||
color: 'white',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text style={text}>Hey {recipientName}!</Text>
|
||||
|
||||
<Text style={text}>
|
||||
{senderName} has added you to the album <strong>{albumName}</strong>.
|
||||
</Text>
|
||||
|
||||
{cid && (
|
||||
<Row>
|
||||
<Column align="center">
|
||||
<Img
|
||||
src={`cid:${cid}`}
|
||||
width="300"
|
||||
style={{
|
||||
borderRadius: '20px',
|
||||
boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row style={{ marginBottom: '36px', marginTop: '36px' }}>
|
||||
<Text style={{ ...text }}>To view the album, open the link in a browser, or click the button below.</Text>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column align="center">
|
||||
<Link style={{ marginTop: '50px' }} href={`${baseUrl}/albums/${albumId}`}>
|
||||
{baseUrl}/albums/{albumId}
|
||||
</Link>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column align="center">
|
||||
<Button style={button} href={`${baseUrl}/albums/${albumId}`}>
|
||||
View album
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Hr style={{ color: 'rgb(66, 80, 175)', marginTop: '24px' }} />
|
||||
|
||||
<Section style={{ textAlign: 'center' }}>
|
||||
<Row>
|
||||
<Column align="center">
|
||||
<Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
|
||||
<Img src={`https://immich.app/img/google-play-badge.png`} height="96px" alt="Immich" />
|
||||
</Link>
|
||||
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
|
||||
<Img
|
||||
src={`https://immich.app/img/ios-app-store-badge.png`}
|
||||
alt="Immich"
|
||||
style={{ height: '72px', padding: '14px' }}
|
||||
/>
|
||||
</Link>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: '#6a737d',
|
||||
fontSize: '0.8rem',
|
||||
textAlign: 'center' as const,
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
<Link href="https://immich.app">Immich</Link> project is available under GNU AGPL v3 license.
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
AlbumInviteEmail.PreviewProps = {
|
||||
baseUrl: 'https://demo.immich.app',
|
||||
albumName: 'Trip to Europe',
|
||||
albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539',
|
||||
senderName: 'Owner User',
|
||||
recipientName: 'Guest User',
|
||||
cid: '',
|
||||
} as AlbumInviteEmailProps;
|
||||
|
||||
export default AlbumInviteEmail;
|
||||
|
||||
const text = {
|
||||
margin: '0 0 24px 0',
|
||||
textAlign: 'left' as const,
|
||||
fontSize: '18px',
|
||||
lineHeight: '24px',
|
||||
};
|
||||
|
||||
const button: CSS.Properties = {
|
||||
backgroundColor: 'rgb(66, 80, 175)',
|
||||
margin: '1em 0',
|
||||
padding: '0.75em 3em',
|
||||
color: '#fff',
|
||||
fontSize: '1em',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.5,
|
||||
textTransform: 'uppercase',
|
||||
borderRadius: '9999px',
|
||||
};
|
165
server/src/emails/album-update.email.tsx
Normal file
165
server/src/emails/album-update.email.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Row,
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components';
|
||||
import * as CSS from 'csstype';
|
||||
import * as React from 'react';
|
||||
import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface';
|
||||
|
||||
export const AlbumUpdateEmail = ({ baseUrl, albumName, recipientName, albumId, cid }: AlbumUpdateEmailProps) => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>New media has been added to a shared album.</Preview>
|
||||
<Body
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
backgroundColor: '#ffffff',
|
||||
color: 'rgb(28,28,28)',
|
||||
fontFamily: 'Overpass, sans-serif',
|
||||
fontSize: '18px',
|
||||
lineHeight: '24px',
|
||||
}}
|
||||
>
|
||||
<Container
|
||||
style={{
|
||||
width: '540px',
|
||||
maxWidth: '100%',
|
||||
padding: '10px',
|
||||
margin: '0 auto',
|
||||
}}
|
||||
>
|
||||
<Section
|
||||
style={{
|
||||
padding: '36px',
|
||||
tableLayout: 'fixed',
|
||||
backgroundColor: 'rgb(226, 232, 240)',
|
||||
border: 'solid 0px rgb(248 113 113)',
|
||||
borderRadius: '50px',
|
||||
textAlign: 'center' as const,
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src="https://immich.app/img/immich-logo-inline-light.png"
|
||||
alt="Immich"
|
||||
style={{
|
||||
height: 'auto',
|
||||
margin: '0 auto 48px auto',
|
||||
width: '50%',
|
||||
alignSelf: 'center',
|
||||
color: 'white',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text style={text}>Hey {recipientName}!</Text>
|
||||
|
||||
<Text style={text}>
|
||||
New media has been added to <strong>{albumName}</strong>, check it out!
|
||||
</Text>
|
||||
|
||||
{cid && (
|
||||
<Row>
|
||||
<Column align="center">
|
||||
<Img
|
||||
src={`cid:${cid}`}
|
||||
width="300"
|
||||
style={{
|
||||
borderRadius: '20px',
|
||||
boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row style={{ marginBottom: '36px', marginTop: '36px' }}>
|
||||
<Text style={{ ...text }}>To view the album, open the link in a browser, or click the button below.</Text>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column align="center">
|
||||
<Link style={{ marginTop: '50px' }} href={`${baseUrl}/albums/${albumId}`}>
|
||||
{baseUrl}/albums/{albumId}
|
||||
</Link>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column align="center">
|
||||
<Button style={button} href={`${baseUrl}/albums/${albumId}`}>
|
||||
View album
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Hr style={{ color: 'rgb(66, 80, 175)', marginTop: '24px' }} />
|
||||
|
||||
<Section style={{ textAlign: 'center' }}>
|
||||
<Row>
|
||||
<Column align="center">
|
||||
<Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
|
||||
<Img src={`https://immich.app/img/google-play-badge.png`} height="96px" alt="Immich" />
|
||||
</Link>
|
||||
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
|
||||
<Img
|
||||
src={`https://immich.app/img/ios-app-store-badge.png`}
|
||||
alt="Immich"
|
||||
style={{ height: '72px', padding: '14px' }}
|
||||
/>
|
||||
</Link>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: '#6a737d',
|
||||
fontSize: '0.8rem',
|
||||
textAlign: 'center' as const,
|
||||
marginTop: '12px',
|
||||
}}
|
||||
>
|
||||
<Link href="https://immich.app">Immich</Link> project is available under GNU AGPL v3 license.
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
AlbumUpdateEmail.PreviewProps = {
|
||||
baseUrl: 'https://demo.immich.app',
|
||||
albumName: 'Trip to Europe',
|
||||
albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539',
|
||||
recipientName: 'Alex Tran',
|
||||
} as AlbumUpdateEmailProps;
|
||||
|
||||
export default AlbumUpdateEmail;
|
||||
|
||||
const text = {
|
||||
margin: '0 0 24px 0',
|
||||
textAlign: 'left' as const,
|
||||
fontSize: '18px',
|
||||
lineHeight: '24px',
|
||||
};
|
||||
|
||||
const button: CSS.Properties = {
|
||||
backgroundColor: 'rgb(66, 80, 175)',
|
||||
margin: '1em 0',
|
||||
padding: '0.75em 3em',
|
||||
color: '#fff',
|
||||
fontSize: '1em',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.5,
|
||||
textTransform: 'uppercase',
|
||||
borderRadius: '9999px',
|
||||
};
|
@ -107,7 +107,7 @@ export const WelcomeEmail = ({ baseUrl, displayName, username, password }: Welco
|
||||
<Img
|
||||
src={`https://immich.app/img/ios-app-store-badge.png`}
|
||||
alt="Immich"
|
||||
style={{ height: '68px', padding: '14px' }}
|
||||
style={{ height: '72px', padding: '14px' }}
|
||||
/>
|
||||
</Link>
|
||||
</Column>
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { EmailImageAttachment } from 'src/interfaces/notification.interface';
|
||||
|
||||
export enum QueueName {
|
||||
THUMBNAIL_GENERATION = 'thumbnailGeneration',
|
||||
METADATA_EXTRACTION = 'metadataExtraction',
|
||||
@ -99,6 +101,8 @@ export enum JobName {
|
||||
|
||||
// Notification
|
||||
NOTIFY_SIGNUP = 'notify-signup',
|
||||
NOTIFY_ALBUM_INVITE = 'notify-album-invite',
|
||||
NOTIFY_ALBUM_UPDATE = 'notify-album-update',
|
||||
SEND_EMAIL = 'notification-send-email',
|
||||
|
||||
// Version check
|
||||
@ -150,12 +154,21 @@ export interface IEmailJob {
|
||||
subject: string;
|
||||
html: string;
|
||||
text: string;
|
||||
imageAttachments?: EmailImageAttachment[];
|
||||
}
|
||||
|
||||
export interface INotifySignupJob extends IEntityJob {
|
||||
tempPassword?: string;
|
||||
}
|
||||
|
||||
export interface INotifyAlbumInviteJob extends IEntityJob {
|
||||
recipientId: string;
|
||||
}
|
||||
|
||||
export interface INotifyAlbumUpdateJob extends IEntityJob {
|
||||
senderId: string;
|
||||
}
|
||||
|
||||
export interface JobCounts {
|
||||
active: number;
|
||||
completed: number;
|
||||
@ -246,6 +259,8 @@ export type JobItem =
|
||||
|
||||
// Notification
|
||||
| { name: JobName.SEND_EMAIL; data: IEmailJob }
|
||||
| { name: JobName.NOTIFY_ALBUM_INVITE; data: INotifyAlbumInviteJob }
|
||||
| { name: JobName.NOTIFY_ALBUM_UPDATE; data: INotifyAlbumUpdateJob }
|
||||
| { name: JobName.NOTIFY_SIGNUP; data: INotifySignupJob }
|
||||
|
||||
// Version check
|
||||
|
@ -1,5 +1,11 @@
|
||||
export const INotificationRepository = 'INotificationRepository';
|
||||
|
||||
export type EmailImageAttachment = {
|
||||
filename: string;
|
||||
path: string;
|
||||
cid: string;
|
||||
};
|
||||
|
||||
export type SendEmailOptions = {
|
||||
from: string;
|
||||
to: string;
|
||||
@ -7,6 +13,7 @@ export type SendEmailOptions = {
|
||||
subject: string;
|
||||
html: string;
|
||||
text: string;
|
||||
imageAttachments?: EmailImageAttachment[];
|
||||
smtp: SmtpOptions;
|
||||
};
|
||||
|
||||
@ -19,18 +26,53 @@ export type SmtpOptions = {
|
||||
};
|
||||
|
||||
export enum EmailTemplate {
|
||||
// AUTH
|
||||
WELCOME = 'welcome',
|
||||
RESET_PASSWORD = 'reset-password',
|
||||
|
||||
// ALBUM
|
||||
ALBUM_INVITE = 'album-invite',
|
||||
ALBUM_UPDATE = 'album-update',
|
||||
}
|
||||
|
||||
export interface WelcomeEmailProps {
|
||||
interface BaseEmailProps {
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export interface WelcomeEmailProps extends BaseEmailProps {
|
||||
displayName: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export type EmailRenderRequest = { template: EmailTemplate.WELCOME; data: WelcomeEmailProps };
|
||||
export interface AlbumInviteEmailProps extends BaseEmailProps {
|
||||
albumName: string;
|
||||
albumId: string;
|
||||
senderName: string;
|
||||
recipientName: string;
|
||||
cid?: string;
|
||||
}
|
||||
|
||||
export interface AlbumUpdateEmailProps extends BaseEmailProps {
|
||||
albumName: string;
|
||||
albumId: string;
|
||||
recipientName: string;
|
||||
cid?: string;
|
||||
}
|
||||
|
||||
export type EmailRenderRequest =
|
||||
| {
|
||||
template: EmailTemplate.WELCOME;
|
||||
data: WelcomeEmailProps;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.ALBUM_INVITE;
|
||||
data: AlbumInviteEmailProps;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.ALBUM_UPDATE;
|
||||
data: AlbumUpdateEmailProps;
|
||||
};
|
||||
|
||||
export type SendEmailResponse = {
|
||||
messageId: string;
|
||||
|
@ -85,6 +85,8 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
|
||||
|
||||
// Notification
|
||||
[JobName.SEND_EMAIL]: QueueName.NOTIFICATION,
|
||||
[JobName.NOTIFY_ALBUM_INVITE]: QueueName.NOTIFICATION,
|
||||
[JobName.NOTIFY_ALBUM_UPDATE]: QueueName.NOTIFICATION,
|
||||
[JobName.NOTIFY_SIGNUP]: QueueName.NOTIFICATION,
|
||||
|
||||
// Version check
|
||||
|
@ -2,6 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { render } from '@react-email/render';
|
||||
import { createTransport } from 'nodemailer';
|
||||
import React from 'react';
|
||||
import { AlbumInviteEmail } from 'src/emails/album-invite.email';
|
||||
import { AlbumUpdateEmail } from 'src/emails/album-update.email';
|
||||
import { WelcomeEmail } from 'src/emails/welcome.email';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import {
|
||||
@ -37,11 +39,18 @@ export class NotificationRepository implements INotificationRepository {
|
||||
return { html, text };
|
||||
}
|
||||
|
||||
sendEmail({ to, from, subject, html, text, smtp }: SendEmailOptions): Promise<SendEmailResponse> {
|
||||
sendEmail({ to, from, subject, html, text, smtp, imageAttachments }: SendEmailOptions): Promise<SendEmailResponse> {
|
||||
this.logger.debug(`Sending email to ${to} with subject: ${subject}`);
|
||||
const transport = this.createTransport(smtp);
|
||||
|
||||
const attachments = imageAttachments?.map((attachment) => ({
|
||||
filename: attachment.filename,
|
||||
path: attachment.path,
|
||||
cid: attachment.cid,
|
||||
}));
|
||||
|
||||
try {
|
||||
return transport.sendMail({ to, from, subject, html, text });
|
||||
return transport.sendMail({ to, from, subject, html, text, attachments });
|
||||
} finally {
|
||||
transport.close();
|
||||
}
|
||||
@ -52,6 +61,14 @@ export class NotificationRepository implements INotificationRepository {
|
||||
case EmailTemplate.WELCOME: {
|
||||
return React.createElement(WelcomeEmail, data);
|
||||
}
|
||||
|
||||
case EmailTemplate.ALBUM_INVITE: {
|
||||
return React.createElement(AlbumInviteEmail, data);
|
||||
}
|
||||
|
||||
case EmailTemplate.ALBUM_UPDATE: {
|
||||
return React.createElement(AlbumUpdateEmail, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { AlbumUserRole } from 'src/entities/album-user.entity';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AlbumService } from 'src/services/album.service';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
@ -14,6 +15,7 @@ import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositorie
|
||||
import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
@ -24,6 +26,7 @@ describe(AlbumService.name, () => {
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let albumUserMock: Mocked<IAlbumUserRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
accessMock = newAccessRepositoryMock();
|
||||
@ -31,8 +34,9 @@ describe(AlbumService.name, () => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
albumUserMock = newAlbumUserRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
|
||||
sut = new AlbumService(accessMock, albumMock, assetMock, userMock, albumUserMock);
|
||||
sut = new AlbumService(accessMock, albumMock, assetMock, userMock, albumUserMock, jobMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
@ -377,6 +381,14 @@ describe(AlbumService.name, () => {
|
||||
userId: authStub.user2.user.id,
|
||||
albumId: albumStub.sharedWithAdmin.id,
|
||||
});
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.NOTIFY_ALBUM_INVITE,
|
||||
data: { id: albumStub.sharedWithAdmin.id, recipientId: authStub.user2.user.id },
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -561,6 +573,14 @@ describe(AlbumService.name, () => {
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.NOTIFY_ALBUM_UPDATE,
|
||||
data: { id: 'album-123', senderId: authStub.admin.user.id },
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not set the thumbnail if the album has one already', async () => {
|
||||
@ -601,6 +621,14 @@ describe(AlbumService.name, () => {
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.NOTIFY_ALBUM_UPDATE,
|
||||
data: { id: 'album-123', senderId: authStub.user1.user.id },
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not allow a shared user with viewer access to add assets', async () => {
|
||||
|
@ -21,6 +21,7 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
|
||||
@ -33,6 +34,7 @@ export class AlbumService {
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
@ -188,6 +190,11 @@ export class AlbumService {
|
||||
});
|
||||
}
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.NOTIFY_ALBUM_UPDATE,
|
||||
data: { id, senderId: auth.user.id },
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@ -234,6 +241,11 @@ export class AlbumService {
|
||||
}
|
||||
|
||||
await this.albumUserRepository.create({ userId: userId, albumId: id, role });
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.NOTIFY_ALBUM_INVITE,
|
||||
data: { id: album.id, recipientId: user.id },
|
||||
});
|
||||
}
|
||||
|
||||
return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets);
|
||||
|
@ -90,6 +90,8 @@ export class MicroservicesService {
|
||||
[JobName.LIBRARY_QUEUE_SCAN_ALL]: (data) => this.libraryService.handleQueueAllScan(data),
|
||||
[JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(),
|
||||
[JobName.SEND_EMAIL]: (data) => this.notificationService.handleSendEmail(data),
|
||||
[JobName.NOTIFY_ALBUM_INVITE]: (data) => this.notificationService.handleAlbumInvite(data),
|
||||
[JobName.NOTIFY_ALBUM_UPDATE]: (data) => this.notificationService.handleAlbumUpdate(data),
|
||||
[JobName.NOTIFY_SIGNUP]: (data) => this.notificationService.handleUserSignup(data),
|
||||
[JobName.VERSION_CHECK]: () => this.versionService.handleVersionCheck(),
|
||||
});
|
||||
|
@ -1,10 +1,21 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { OnServerEvent } from 'src/decorators';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ServerAsyncEvent, ServerAsyncEventMap } from 'src/interfaces/event.interface';
|
||||
import { IEmailJob, IJobRepository, INotifySignupJob, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import {
|
||||
IEmailJob,
|
||||
IJobRepository,
|
||||
INotifyAlbumInviteJob,
|
||||
INotifyAlbumUpdateJob,
|
||||
INotifySignupJob,
|
||||
JobName,
|
||||
JobStatus,
|
||||
} from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
|
||||
import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
|
||||
@ -18,6 +29,8 @@ export class NotificationService {
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
) {
|
||||
this.logger.setContext(NotificationService.name);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
||||
@ -70,6 +83,90 @@ export class NotificationService {
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
async handleAlbumInvite({ id, recipientId }: INotifyAlbumInviteJob) {
|
||||
const album = await this.albumRepository.getById(id, { withAssets: false });
|
||||
if (!album) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const recipient = await this.userRepository.get(recipientId, { withDeleted: false });
|
||||
if (!recipient) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const attachment = await this.getAlbumThumbnailAttachment(album);
|
||||
|
||||
const { server } = await this.configCore.getConfig();
|
||||
const { html, text } = this.notificationRepository.renderEmail({
|
||||
template: EmailTemplate.ALBUM_INVITE,
|
||||
data: {
|
||||
baseUrl: server.externalDomain || 'http://localhost:2283',
|
||||
albumId: album.id,
|
||||
albumName: album.albumName,
|
||||
senderName: album.owner.name,
|
||||
recipientName: recipient.name,
|
||||
cid: attachment ? attachment.cid : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.SEND_EMAIL,
|
||||
data: {
|
||||
to: recipient.email,
|
||||
subject: `You have been added to a shared album - ${album.albumName}`,
|
||||
html,
|
||||
text,
|
||||
imageAttachments: attachment ? [attachment] : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
async handleAlbumUpdate({ id, senderId }: INotifyAlbumUpdateJob) {
|
||||
const album = await this.albumRepository.getById(id, { withAssets: false });
|
||||
|
||||
if (!album) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const owner = await this.userRepository.get(album.ownerId, { withDeleted: false });
|
||||
if (!owner) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const recipients = [...album.albumUsers.map((user) => user.user), owner].filter((user) => user.id !== senderId);
|
||||
const attachment = await this.getAlbumThumbnailAttachment(album);
|
||||
|
||||
const { server } = await this.configCore.getConfig();
|
||||
|
||||
for (const recipient of recipients) {
|
||||
const { html, text } = this.notificationRepository.renderEmail({
|
||||
template: EmailTemplate.ALBUM_UPDATE,
|
||||
data: {
|
||||
baseUrl: server.externalDomain || 'http://localhost:2283',
|
||||
albumId: album.id,
|
||||
albumName: album.albumName,
|
||||
recipientName: recipient.name,
|
||||
cid: attachment ? attachment.cid : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.SEND_EMAIL,
|
||||
data: {
|
||||
to: recipient.email,
|
||||
subject: `New media has been added to an album - ${album.albumName}`,
|
||||
html,
|
||||
text,
|
||||
imageAttachments: attachment ? [attachment] : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
async handleSendEmail(data: IEmailJob): Promise<JobStatus> {
|
||||
const { notifications } = await this.configCore.getConfig();
|
||||
if (!notifications.smtp.enabled) {
|
||||
@ -85,6 +182,7 @@ export class NotificationService {
|
||||
from: notifications.smtp.from,
|
||||
replyTo: notifications.smtp.replyTo || notifications.smtp.from,
|
||||
smtp: notifications.smtp.transport,
|
||||
imageAttachments: data.imageAttachments,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
@ -95,4 +193,21 @@ export class NotificationService {
|
||||
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
private async getAlbumThumbnailAttachment(album: AlbumEntity): Promise<EmailImageAttachment | undefined> {
|
||||
if (!album.albumThumbnailAssetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId);
|
||||
if (!albumThumbnail?.thumbnailPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
filename: 'album-thumbnail.jpg',
|
||||
path: albumThumbnail.thumbnailPath,
|
||||
cid: 'album-thumbnail',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user