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.
+
+
+
+
+
+
+
+
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));
+ });
});
}