Merge pull request #25 from directus/user-invite

User invite
This commit is contained in:
Rijk van Zanten
2020-06-25 15:38:44 -04:00
committed by GitHub
5 changed files with 104 additions and 5 deletions

View File

@@ -40,6 +40,7 @@ EXTENSIONS_PATH="./extensions"
####################################################################################################
# Email
EMAIL_FROM="no-reply@directus.io"
EMAIL_TRANSPORT="sendmail"
## Email (Sendmail Transport)

View File

@@ -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 });
}

View File

@@ -0,0 +1,15 @@
{% layout "base" %}
{% block content %}
<p>You have been invited to {{ projectName }}. Please click the link below to join:</p>
<p><a href="url">{{ url }}</a></p>
{% comment %}
@TODO
Make this white-labeled
{% endcomment %}
<p>Love,<br>Directus</p>
{% endblock %}

View File

@@ -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();
@@ -50,4 +51,32 @@ router.delete(
})
);
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();
})
);
const acceptInviteSchema = Joi.object({
token: Joi.string().required(),
password: Joi.string().required(),
});
router.post(
'/invite/accept',
asyncHandler(async (req, res) => {
await acceptInviteSchema.validateAsync(req.body);
await UsersService.acceptInvite(req.body.token, req.body.password);
res.end();
})
);
export default router;

View File

@@ -1,22 +1,60 @@
import { Query } from '../types/query';
import * as ItemsService from './items';
import jwt from 'jsonwebtoken';
import { sendInviteMail } from '../mail';
import database from '../database';
import APIError, { ErrorCode } from '../error';
import bcrypt from 'bcrypt';
export const createUser = async (data: Record<string, any>, query: Query) => {
export const createUser = async (data: Record<string, any>, 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<string, any>, query: Query) => {
export const updateUser = async (pk: string | number, data: Record<string, any>, 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);
};
export const acceptInvite = async (token: string, password: string) => {
const { email } = jwt.verify(token, process.env.SECRET) as Record<string, any>;
const user = await database
.select('id', 'status')
.from('directus_users')
.where({ email })
.first();
if (!user) {
throw new APIError(ErrorCode.USER_NOT_FOUND, `Email address ${email} hasn't been invited.`);
}
if (user.status !== 'invited') {
throw new APIError(ErrorCode.USER_NOT_FOUND, `Email address ${email} hasn't been invited.`);
}
const passwordHashed = await bcrypt.hash(password, Number(process.env.SALT_ROUNDS));
await database('directus_users')
.update({ password: passwordHashed, status: 'active' })
.where({ id: user.id });
};