diff --git a/src/mail_templates/new_login_location-html.ejs b/src/mail_templates/new_login_location-html.ejs new file mode 100644 index 000000000..8c45b79b5 --- /dev/null +++ b/src/mail_templates/new_login_location-html.ejs @@ -0,0 +1,19 @@ +
+ + + +

Dear <%= user %>,

+ +

+ Someone logged into your Cloudron <%= cloudronName %> with this account.
+ If it was not you, please reset your password and logout from all sessions in the profile view. +

+ +
+
+ +
+ Powered by Cloudron +
+ +
diff --git a/src/mail_templates/new_login_location-text.ejs b/src/mail_templates/new_login_location-text.ejs new file mode 100644 index 000000000..c4ce3ed69 --- /dev/null +++ b/src/mail_templates/new_login_location-text.ejs @@ -0,0 +1,9 @@ +Dear <%= user %>, + +someone logged into your Cloudron <%= cloudronName %> with this account. +If it was not you, please reset your password and logout from all sessions in the profile view. + + +Powered by https://cloudron.io + +Sent at: <%= new Date().toUTCString() %> diff --git a/src/mailer.js b/src/mailer.js index bf60871cf..ee020d49a 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -6,6 +6,7 @@ exports = module.exports = { appUpdatesAvailable, sendInvite, + sendNewLoginLocation, backupFailed, @@ -141,6 +142,37 @@ function sendInvite(user, invitor, inviteLink) { }); } +function sendNewLoginLocation(user, newLocation) { + assert.strictEqual(typeof user, 'object'); + assert.strictEqual(typeof newLocation, 'string'); + + debug('Sending new login location mail'); + + getMailConfig(function (error, mailConfig) { + if (error) return debug('Error getting mail details:', error); + + translation.getTranslations(function (error, translationAssets) { + if (error) return debug('Error getting translations:', error); + + var templateData = { + user: user.displayName || user.username || user.email, + cloudronName: mailConfig.cloudronName, + cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar' + }; + + var mailOptions = { + from: mailConfig.notificationFrom, + to: user.fallbackEmail, + subject: `[${mailConfig.cloudronName}] Login from new location detected`, + text: render('new_login_location-text.ejs', templateData, translationAssets), + html: render('new_login_location-html.ejs', templateData, translationAssets) + }; + + sendMail(mailOptions); + }); + }); +} + function passwordReset(user) { assert.strictEqual(typeof user, 'object'); diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index 470cb16b9..d7aa07580 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -34,6 +34,7 @@ let assert = require('assert'), externalLdap = require('../externalldap.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + mailer = require('../mailer.js'), sysinfo = require('../sysinfo.js'), system = require('../system.js'), tokendb = require('../tokendb.js'), @@ -55,12 +56,18 @@ function login(req, res, next) { const error = tokens.validateTokenType(type); if (error) return next(new HttpError(400, error.message)); - tokens.add(type, req.user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { + tokens.add(type, req.user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, token) { if (error) return next(new HttpError(500, error)); - eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) }); + eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], ip, 1, 100, function (error, result) { + if (error) console.error(error); - next(new HttpSuccess(200, result)); + if (!error && result.length === 0) mailer.sendNewLoginLocation(req.user, ip); + + eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) }); + + next(new HttpSuccess(200, token)); + }); }); }