Skip to content

Commit

Permalink
feat(members): mail change trigger
Browse files Browse the repository at this point in the history
  • Loading branch information
serge1peshcoff committed Apr 10, 2020
1 parent bebc261 commit 29fea0a
Show file tree
Hide file tree
Showing 12 changed files with 423 additions and 6 deletions.
1 change: 1 addition & 0 deletions config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const config = {
ttl: {
access_token: 10 * 60, // 10 minutes
mail_confirmation: 48 * 60 * 60, // 48 hours,
mail_change: 48 * 60 * 60, // 48 hours,
password_reset: 24 * 60 * 60 // 24 hours
},
filter_fields: [
Expand Down
4 changes: 3 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ module.exports = {
ACCESS_TOKEN: 32,
REFRESH_TOKEN: 128,
PASSWORD: 10,
PASSWORD_RESET: 128
PASSWORD_RESET: 128,
MAIL_CHANGE: 128
},
MAIL_SUBJECTS: {
MAIL_CONFIRMATION: 'MyAEGEE: Please confirm your account',
MAIL_CHANGE: 'MyAEGEE: Email change',
PASSWORD_RESET: 'MyAEGEE: password reset request',
NEW_JOIN_REQUEST: 'MyAEGEE: new join request for your body'
}
Expand Down
1 change: 1 addition & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ MemberRouter.use(middlewares.maybeAuthorize, middlewares.ensureAuthorized, fetch
MemberRouter.get('/my_permissions', myPermissions.getMyPermissions);
MemberRouter.put('/active', members.setUserActive);
MemberRouter.put('/primary-body', members.setPrimaryBody);
MemberRouter.put('/email', members.triggerEmailChange);
MemberRouter.put('/password', members.setUserPassword);
MemberRouter.get('/', members.getUser);
MemberRouter.put('/', members.updateUser);
Expand Down
29 changes: 28 additions & 1 deletion middlewares/members.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const { User, Body } = require('../models');
const { User, Body, MailChange } = require('../models');
const constants = require('../lib/constants');
const helpers = require('../lib/helpers');
const errors = require('../lib/errors');
const mailer = require('../lib/mailer');
const { sequelize } = require('../lib/sequelize');


exports.listAllUsers = async (req, res) => {
if (!req.permissions.hasPermission('global:view:member')) {
Expand Down Expand Up @@ -112,3 +115,27 @@ exports.setPrimaryBody = async (req, res) => {
data: req.currentUser
});
};

exports.triggerEmailChange = async (req, res) => {
if (!req.permissions.hasPermission('update:member') && req.user.id !== req.currentUser.id) {
return errors.makeForbiddenError(res, 'Permission update:member is required, but not present.');
}

await sequelize.transaction(async (t) => {
const mailChange = await MailChange.createForUser(req.currentUser, req.body.new_email, t);

await mailer.sendMail({
to: req.body.new_email,
subject: constants.MAIL_SUBJECTS.PASSWORD_RESET,
template: 'mail-change.html',
parameters: {
token: mailChange.value
}
});
});

return res.json({
success: true,
message: 'The mail change was triggered. Check your email.'
});
};
41 changes: 41 additions & 0 deletions migrations/20200410195944-create-mail-changes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('mail_changes', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
},
onDelete: 'CASCADE'
},
value: {
type: Sequelize.TEXT,
allowNull: false,
unique: true
},
new_email: {
type: Sequelize.STRING,
allowNull: false
},
created_at: {
allowNull: false,
type: Sequelize.DATE
},
updated_at: {
allowNull: false,
type: Sequelize.DATE
},
expires_at: {
allowNull: false,
type: Sequelize.DATE
},
}),
down: (queryInterface) => queryInterface.dropTable('mail_changes')
};
60 changes: 60 additions & 0 deletions models/MailChange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const moment = require('moment');

const { Sequelize, sequelize } = require('../lib/sequelize');
const helpers = require('../lib/helpers');
const constants = require('../lib/constants');
const config = require('../config');

const MailChange = sequelize.define('mail_change', {
user_id: {
type: Sequelize.INTEGER,
allowNull: false
},
value: {
type: Sequelize.TEXT,
allowNull: false,
validate: {
notNull: { msg: 'Value should be set.' },
notEmpty: { msg: 'Value should be set.' },
},
unique: true
},
new_email: {
type: Sequelize.STRING,
allowNull: false,
validate: {
notEmpty: { msg: 'New email should be set.' },
notNull: { msg: 'New email should be set.' },
isEmail: { msg: 'New email should be valid.' }
},
unique: true
},
expires_at: {
type: Sequelize.DATE,
allowNull: false
}
}, {
underscored: true,
tableName: 'mail_changes',
createdAt: 'created_at',
updatedAt: 'updated_at'
});

MailChange.beforeValidate(async (mailChange) => {
// skipping these fields if they are unset, will catch it later.
if (typeof mailChange.new_email === 'string') mailChange.new_email = mailChange.new_email.toLowerCase().trim();
});

MailChange.createForUser = async function createForUser(user, newEmail, transaction) {
const value = await helpers.getRandomBytes(constants.TOKEN_LENGTH.ACCESS_TOKEN);
const expiresAt = moment().add(config.ttl.mail_change, 'seconds');

return MailChange.create({
user_id: user.id,
value,
expires_at: expiresAt,
new_email: newEmail
}, { transaction });
};

module.exports = MailChange;
5 changes: 5 additions & 0 deletions models/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const User = require('./User');
const Campaign = require('./Campaign');
const MailConfirmation = require('./MailConfirmation');
const MailChange = require('./MailChange');
const AccessToken = require('./AccessToken');
const RefreshToken = require('./RefreshToken');
const PasswordReset = require('./PasswordReset');
Expand Down Expand Up @@ -34,6 +35,9 @@ RefreshToken.belongsTo(User, { foreignKey: 'user_id' });
User.hasMany(PasswordReset, { foreignKey: 'user_id' });
PasswordReset.belongsTo(User, { foreignKey: 'user_id' });

User.hasMany(MailChange, { foreignKey: 'user_id' });
MailChange.belongsTo(User, { foreignKey: 'user_id' });

Body.hasMany(Circle, { foreignKey: 'body_id' });
Circle.belongsTo(Body, { foreignKey: 'body_id' });

Expand Down Expand Up @@ -89,6 +93,7 @@ module.exports = {
AccessToken,
RefreshToken,
PasswordReset,
MailChange,
Body,
Circle,
Permission,
Expand Down
8 changes: 4 additions & 4 deletions test/api/users-editing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('User editing', () => {
const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken({}, user);

await generator.createPermission({ scope: 'global', action: 'view', object: 'member' });
await generator.createPermission({ scope: 'global', action: 'update', object: 'member' });

const res = await request({
uri: '/members/1337',
Expand All @@ -38,7 +38,7 @@ describe('User editing', () => {
const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken({}, user);

await generator.createPermission({ scope: 'global', action: 'view', object: 'member' });
await generator.createPermission({ scope: 'global', action: 'update', object: 'member' });

const res = await request({
uri: '/members/' + user.id,
Expand All @@ -58,7 +58,7 @@ describe('User editing', () => {
const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken({}, user);

await generator.createPermission({ scope: 'global', action: 'view', object: 'member' });
await generator.createPermission({ scope: 'global', action: 'update', object: 'member' });

const res = await request({
uri: '/members/' + user.id,
Expand All @@ -78,7 +78,7 @@ describe('User editing', () => {
const user = await generator.createUser({ superadmin: true });
const token = await generator.createAccessToken({}, user);

await generator.createPermission({ scope: 'global', action: 'view', object: 'member' });
await generator.createPermission({ scope: 'global', action: 'update', object: 'member' });

const res = await request({
uri: '/members/' + user.id,
Expand Down
Loading

0 comments on commit 29fea0a

Please sign in to comment.