diff --git a/example.env b/example.env index 614678be8a..1f6b1f5ee9 100644 --- a/example.env +++ b/example.env @@ -40,6 +40,7 @@ EXTENSIONS_PATH="./extensions" #################################################################################################### # Email +EMAIL_FROM="no-reply@directus.io" EMAIL_TRANSPORT="sendmail" ## Email (Sendmail Transport) diff --git a/src/mail/index.ts b/src/mail/index.ts index 067381aa16..bdf194b9f5 100644 --- a/src/mail/index.ts +++ b/src/mail/index.ts @@ -7,7 +7,10 @@ import { promisify } from 'util'; const readFile = promisify(fs.readFile); -const liquidEngine = new Liquid(); +const liquidEngine = new Liquid({ + root: path.resolve(__dirname, 'templates'), + extname: '.liquid', +}); logger.trace('[Email] Initializing email transport...'); @@ -52,6 +55,7 @@ if (emailTransport === 'sendmail') { export type EmailOptions = { to: string; // email address of the recipient + from: string; subject: string; text: string; html: string; @@ -61,6 +65,8 @@ export default async function sendMail(options: EmailOptions) { const templateString = await readFile(path.join(__dirname, 'templates/base.liquid'), 'utf8'); const html = await liquidEngine.parseAndRender(templateString, { html: options.html }); + options.from = options.from || process.env.EMAIL_FROM; + try { await transporter.sendMail({ ...options, html: html }); } catch (error) { @@ -68,3 +74,13 @@ export default async function sendMail(options: EmailOptions) { logger.warn(error); } } + +export async function sendInviteMail(email: string, url: string) { + /** + * @TODO pull this from directus_settings + */ + const projectName = 'directus'; + + const html = await liquidEngine.renderFile('user-invitation', { email, url, projectName }); + await transporter.sendMail({ from: process.env.EMAIL_FROM, to: email, html: html }); +} diff --git a/src/mail/templates/user-invitation.liquid b/src/mail/templates/user-invitation.liquid new file mode 100644 index 0000000000..eaba3a24a0 --- /dev/null +++ b/src/mail/templates/user-invitation.liquid @@ -0,0 +1,15 @@ +{% layout "base" %} +{% block content %} + +

You have been invited to {{ projectName }}. Please click the link below to join:

+ +

{{ url }}

+ +{% comment %} +@TODO +Make this white-labeled +{% endcomment %} + +

Love,
Directus

+ +{% endblock %} diff --git a/src/routes/users.ts b/src/routes/users.ts index bb9de1fadc..5b147c136c 100644 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -3,6 +3,7 @@ import asyncHandler from 'express-async-handler'; import sanitizeQuery from '../middleware/sanitize-query'; import validateQuery from '../middleware/validate-query'; import * as UsersService from '../services/users'; +import Joi from '@hapi/joi'; const router = express.Router(); @@ -14,6 +15,20 @@ router.post( }) ); +const inviteSchema = Joi.object({ + email: Joi.string().email().required(), + role: Joi.string().uuid({ version: 'uuidv4' }).required(), +}); + +router.post( + '/invite', + asyncHandler(async (req, res) => { + await inviteSchema.validateAsync(req.body); + await UsersService.inviteUser(req.body.email, req.body.role); + res.end(); + }) +); + router.get( '/', sanitizeQuery, diff --git a/src/services/users.ts b/src/services/users.ts index ad2a4df4ba..1060eb9119 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -1,22 +1,34 @@ import { Query } from '../types/query'; import * as ItemsService from './items'; +import jwt from 'jsonwebtoken'; +import { sendInviteMail } from '../mail'; -export const createUser = async (data: Record, query: Query) => { +export const createUser = async (data: Record, query?: Query) => { return await ItemsService.createItem('directus_users', data, query); }; -export const readUsers = async (query: Query) => { +export const readUsers = async (query?: Query) => { return await ItemsService.readItems('directus_users', query); }; -export const readUser = async (pk: string | number, query: Query) => { +export const readUser = async (pk: string | number, query?: Query) => { return await ItemsService.readItem('directus_users', pk, query); }; -export const updateUser = async (pk: string | number, data: Record, query: Query) => { +export const updateUser = async (pk: string | number, data: Record, query?: Query) => { return await ItemsService.updateItem('directus_users', pk, data, query); }; export const deleteUser = async (pk: string | number) => { await ItemsService.deleteItem('directus_users', pk); }; + +export const inviteUser = async (email: string, role: string) => { + await createUser({ email, role, status: 'invited' }); + + const payload = { email }; + const token = jwt.sign(payload, process.env.SECRET, { expiresIn: '7d' }); + const acceptURL = process.env.PUBLIC_URL + '/admin/accept-invite?token=' + token; + + await sendInviteMail(email, acceptURL); +};