mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Fix base email template footer link & logo aspect ratio (#16233)
* fix email base template footer link * fix logo box to be square * clean up unused classes & attributes * only add url if user has app access * add test for notifications service * re-add database mock * attempt to fix mock in test * mock PUBLIC_URL Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -125,14 +125,14 @@ blockquote > p {
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" valign="top" style="-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; mso-table-lspace:0pt; mso-table-rspace:0pt; padding:0 0 30px 0">
|
||||
<table><tbody><tr><td align="center" valign="middle" style="background-color:{{ projectColor }};width:48px;height:48px;border-radius:4px;padding:6px;">
|
||||
<img id="logo" src="{{ projectLogo }}" alt="{{ projectName }} Logo" width="40" height="auto" border="0" style="-ms-interpolation-mode:bicubic; border:0; height:auto; line-height:100%; outline:none; text-decoration:none; display:block; width:40px;object-fit:contain;">
|
||||
<table><tbody><tr><td align="center" valign="middle" style="background-color:{{ projectColor }};max-width:48px;max-height:48px;border-radius:4px;padding:6px;">
|
||||
<img id="logo" src="{{ projectLogo }}" alt="{{ projectName }} Logo" width="40" height="auto" border="0" style="-ms-interpolation-mode:bicubic; border:0; height:40px; line-height:100%; outline:none; text-decoration:none; display:block; width:40px; object-fit:contain;">
|
||||
</td></tr></tbody></table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="content" align="left" valign="top" style="-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; mso-table-lspace:0pt; mso-table-rspace:0pt; padding:40px 50px 50px 50px; font-family:Open Sans, Helvetica, Arial, sans-serif; border-radius:4px; box-shadow:0 4px 0 #15253A; background-color:#FFFFFE; color:#172940; font-size:15px; line-height:26px; margin:0" bgcolor="#FFFFFE">
|
||||
<div id="hs_cos_wrapper_email_template_main_email_body" class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_module" style="color: inherit; font-size: inherit; line-height: inherit;" data-hs-cos-general-type="widget" data-hs-cos-type="module">
|
||||
<div style="color: inherit; font-size: inherit; line-height: inherit;">
|
||||
|
||||
{% block content %}{{ html }}{% endblock %}
|
||||
|
||||
@@ -142,7 +142,7 @@ blockquote > p {
|
||||
<tr>
|
||||
<td align="center" valign="middle" style="-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; mso-table-lspace:0pt; mso-table-rspace:0pt; padding:25px 0; font-family:Open Sans, Helvetica, Arial, sans-serif; color:#FFFFFE">
|
||||
<p style="margin-bottom: 1em; color: #A2B5CD;font-size: 12px; line-height: 16px;">
|
||||
Sent by the team at {{ projectName }} — <a style="-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; text-decoration:none; color:#A2B5CD" class="unsubscribe" data-unsubscribe="true" href="{{ url }}" data-hs-link-id="0" target="_blank">Manage Emails</a><br>
|
||||
Sent by the team at {{ projectName }}{% if url %} — <a style="-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; text-decoration:none; color:#A2B5CD" href="{{ url }}" target="_blank">Manage Your Account</a>{% endif %}<br>
|
||||
{% block footer %}{% endblock %}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
215
api/src/services/notifications.test.ts
Normal file
215
api/src/services/notifications.test.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { NotificationsService, ItemsService } from '.';
|
||||
|
||||
jest.mock('../../src/env', () => ({
|
||||
...jest.requireActual('../../src/env').default,
|
||||
PUBLIC_URL: '/',
|
||||
}));
|
||||
|
||||
jest.mock('../../src/database/index', () => {
|
||||
return { __esModule: true, default: jest.fn(), getDatabaseClient: jest.fn().mockReturnValue('postgres') };
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
describe('Services / Notifications', () => {
|
||||
let service: NotificationsService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new NotificationsService({
|
||||
schema: { collections: {}, relations: [] },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('createOne', () => {
|
||||
let superCreateOneSpy: jest.SpyInstance;
|
||||
let thisSendEmailSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
superCreateOneSpy = jest.spyOn(ItemsService.prototype, 'createOne').mockImplementation(jest.fn());
|
||||
thisSendEmailSpy = jest.spyOn(NotificationsService.prototype, 'sendEmail').mockImplementation(jest.fn());
|
||||
});
|
||||
|
||||
it('create a notification and send email', async () => {
|
||||
const data = {
|
||||
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
|
||||
sender: null,
|
||||
subject: 'Notification Subject',
|
||||
message: 'Notification Message',
|
||||
};
|
||||
await service.createOne(data);
|
||||
|
||||
expect(superCreateOneSpy).toHaveBeenCalled();
|
||||
expect(superCreateOneSpy).toBeCalledWith(data, undefined);
|
||||
expect(thisSendEmailSpy).toHaveBeenCalled();
|
||||
expect(thisSendEmailSpy).toBeCalledWith(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMany', () => {
|
||||
let superCreateManySpy: jest.SpyInstance;
|
||||
let thisSendEmailSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
superCreateManySpy = jest.spyOn(ItemsService.prototype, 'createMany').mockImplementation(jest.fn());
|
||||
thisSendEmailSpy = jest.spyOn(NotificationsService.prototype, 'sendEmail').mockImplementation(jest.fn());
|
||||
});
|
||||
|
||||
it('create many notifications and send email for notification', async () => {
|
||||
const data = [
|
||||
{
|
||||
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
|
||||
sender: null,
|
||||
subject: 'Notification Subject',
|
||||
message: 'Notification Message',
|
||||
},
|
||||
{
|
||||
recipient: '51260c36-e944-4b0a-a370-dc83070d9d2b',
|
||||
sender: null,
|
||||
subject: 'Notification Subject',
|
||||
message: 'Notification Message',
|
||||
},
|
||||
];
|
||||
await service.createMany(data);
|
||||
|
||||
expect(superCreateManySpy).toBeCalledTimes(1);
|
||||
expect(superCreateManySpy).toBeCalledWith(data, undefined);
|
||||
expect(thisSendEmailSpy).toBeCalledTimes(data.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendEmail', () => {
|
||||
let usersServiceReadOneSpy: jest.SpyInstance;
|
||||
let mailServiceSendSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
usersServiceReadOneSpy = jest.spyOn(service.usersService, 'readOne').mockImplementation(jest.fn());
|
||||
mailServiceSendSpy = jest.spyOn(service.mailService, 'send').mockImplementation(jest.fn());
|
||||
});
|
||||
|
||||
it('do nothing when there is no recipient', async () => {
|
||||
await service.sendEmail({
|
||||
sender: null,
|
||||
subject: 'Notification Subject',
|
||||
message: 'Notification Message',
|
||||
});
|
||||
|
||||
expect(usersServiceReadOneSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('read recipient detail from userService when there is recipient', async () => {
|
||||
usersServiceReadOneSpy.mockReturnValue(Promise.resolve({ id: '5aa7ffb5-bd54-46ab-8654-6dfead39694d' }));
|
||||
|
||||
await service.sendEmail({
|
||||
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
|
||||
sender: null,
|
||||
subject: 'Notification Subject',
|
||||
message: 'Notification Message',
|
||||
});
|
||||
|
||||
expect(usersServiceReadOneSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('do not send email when user does not have email', async () => {
|
||||
usersServiceReadOneSpy.mockReturnValue(Promise.resolve({ id: '5aa7ffb5-bd54-46ab-8654-6dfead39694d' }));
|
||||
|
||||
await service.sendEmail({
|
||||
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
|
||||
sender: null,
|
||||
subject: 'Notification Subject',
|
||||
message: 'Notification Message',
|
||||
});
|
||||
|
||||
expect(usersServiceReadOneSpy).toHaveBeenCalled();
|
||||
expect(mailServiceSendSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('do not send email when user have email but disabled email notification', async () => {
|
||||
usersServiceReadOneSpy.mockReturnValue(
|
||||
Promise.resolve({
|
||||
id: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
|
||||
email: 'user@example.com',
|
||||
email_notifications: false,
|
||||
})
|
||||
);
|
||||
|
||||
await service.sendEmail({
|
||||
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
|
||||
sender: null,
|
||||
subject: 'Notification Subject',
|
||||
message: 'Notification Message',
|
||||
});
|
||||
|
||||
expect(usersServiceReadOneSpy).toHaveBeenCalled();
|
||||
expect(mailServiceSendSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('send email without url when user role does not have app access', async () => {
|
||||
const userDetail = {
|
||||
id: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
|
||||
email: 'user@example.com',
|
||||
email_notifications: true,
|
||||
role: {
|
||||
app_access: false,
|
||||
},
|
||||
};
|
||||
usersServiceReadOneSpy.mockReturnValue(Promise.resolve(userDetail));
|
||||
|
||||
const notificationDetail = {
|
||||
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
|
||||
sender: null,
|
||||
subject: 'Notification Subject',
|
||||
message: 'Notification Message',
|
||||
};
|
||||
await service.sendEmail(notificationDetail);
|
||||
|
||||
expect(usersServiceReadOneSpy).toHaveBeenCalled();
|
||||
expect(mailServiceSendSpy).toHaveBeenCalled();
|
||||
expect(mailServiceSendSpy).toHaveBeenCalledWith({
|
||||
template: {
|
||||
name: 'base',
|
||||
data: { html: `<p>${notificationDetail.message}</p>\n` },
|
||||
},
|
||||
to: userDetail.email,
|
||||
subject: notificationDetail.subject,
|
||||
});
|
||||
});
|
||||
|
||||
it('send email with url when user role have app access', async () => {
|
||||
const userDetail = {
|
||||
id: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
|
||||
email: 'user@example.com',
|
||||
email_notifications: true,
|
||||
role: {
|
||||
app_access: true,
|
||||
},
|
||||
};
|
||||
usersServiceReadOneSpy.mockReturnValue(Promise.resolve(userDetail));
|
||||
|
||||
const notificationDetail = {
|
||||
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
|
||||
sender: null,
|
||||
subject: 'Notification Subject',
|
||||
message: 'Notification Message',
|
||||
};
|
||||
await service.sendEmail(notificationDetail);
|
||||
|
||||
expect(usersServiceReadOneSpy).toHaveBeenCalled();
|
||||
expect(mailServiceSendSpy).toHaveBeenCalled();
|
||||
expect(mailServiceSendSpy).toHaveBeenCalledWith({
|
||||
template: {
|
||||
name: 'base',
|
||||
data: {
|
||||
url: `/admin/users/${userDetail.id}`,
|
||||
html: `<p>${notificationDetail.message}</p>\n`,
|
||||
},
|
||||
},
|
||||
to: userDetail.email,
|
||||
subject: notificationDetail.subject,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,9 +2,11 @@ import { AbstractServiceOptions, PrimaryKey, MutationOptions } from '../types';
|
||||
import { ItemsService } from './items';
|
||||
import { Notification } from '@directus/shared/types';
|
||||
import { md } from '../utils/md';
|
||||
import { Url } from '../utils/url';
|
||||
import { UsersService } from './users';
|
||||
import { MailService } from './mail';
|
||||
import logger from '../logger';
|
||||
import env from '../env';
|
||||
|
||||
export class NotificationsService extends ItemsService {
|
||||
usersService: UsersService;
|
||||
@@ -36,16 +38,19 @@ export class NotificationsService extends ItemsService {
|
||||
|
||||
async sendEmail(data: Partial<Notification>) {
|
||||
if (data.recipient) {
|
||||
const user = await this.usersService.readOne(data.recipient, { fields: ['email', 'email_notifications'] });
|
||||
const user = await this.usersService.readOne(data.recipient, {
|
||||
fields: ['id', 'email', 'email_notifications', 'role.app_access'],
|
||||
});
|
||||
const manageUserAccountUrl = new Url(env.PUBLIC_URL).addPath('admin', 'users', user.id).toString();
|
||||
|
||||
const html = data.message ? md(data.message) : '';
|
||||
|
||||
if (user.email && user.email_notifications === true) {
|
||||
try {
|
||||
await this.mailService.send({
|
||||
template: {
|
||||
name: 'base',
|
||||
data: {
|
||||
html: data.message ? md(data.message) : '',
|
||||
},
|
||||
data: user.role?.app_access ? { url: manageUserAccountUrl, html } : { html },
|
||||
},
|
||||
to: user.email,
|
||||
subject: data.subject,
|
||||
|
||||
Reference in New Issue
Block a user