Skip to content

Commit fa109d5

Browse files
authored
Mfancher/remove inactive users (#1022)
* Fixes and change model and migration for user to add column * set last login whenever logins or password resets occur * Remove Inactive accounts This sets up a job that runs every 12 hours to delete user accounts that are over 180 days since last login * clarify messaging * Fix Messaging and Remove Admin email since workflow is no longer required * rename function to clarify
1 parent 89bca0b commit fa109d5

File tree

11 files changed

+428
-3
lines changed

11 files changed

+428
-3
lines changed

Tombolo/server/config/monitorings.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ const cluster_reachability_monitoring = {
1212
passwordExpiryAlertDaysForCluster: [10, 5, 4, 3, 2, 1],
1313
};
1414

15-
// password expiry alert days for user, we send an email on each day, limited to 3 for now, need to be in descending order to function
15+
// password expiry alert days for user, we send an email on each day, limited to 3 for now, need to be in descending order to function properly
1616
const passwordExpiryAlertDaysForUser = [10, 3, 1];
1717

18+
// account lock alert days for user, we send an email on eahc day, limited to 3 for now, need to be in descending order to function properly
19+
const accountDeleteAlertDaysForUser = [10, 3, 1];
20+
1821
// Export
1922
module.exports = {
2023
...jobMonitoringConfig,
2124
...cluster_reachability_monitoring,
2225
passwordExpiryAlertDaysForUser,
26+
accountDeleteAlertDaysForUser,
2327
};

Tombolo/server/controllers/authController.js

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
setTokenCookie,
1313
trimURL,
1414
setPasswordExpiry,
15+
setLastLoginAndReturn,
1516
sendPasswordExpiredEmail,
1617
checkPasswordSecurityViolations,
1718
generateAndSetCSRFToken,
@@ -85,6 +86,7 @@ const createApplicationOwner = async (req, res) => {
8586
payload.hash = bcrypt.hashSync(req.body.password, salt);
8687
setPasswordExpiry(payload);
8788
setPreviousPasswords(payload);
89+
setLastLoginAndReturn(payload);
8890

8991
// Save user to DB
9092
const user = await User.create(payload);
@@ -171,6 +173,7 @@ const createBasicUser = async (req, res) => {
171173
payload.hash = bcrypt.hashSync(req.body.password, salt);
172174
setPasswordExpiry(payload);
173175
setPreviousPasswords(payload);
176+
setLastLoginAndReturn(payload);
174177

175178
// Save user to DB
176179
const user = await User.create(payload);
@@ -381,6 +384,7 @@ const resetPasswordWithToken = async (req, res) => {
381384
user.forcePasswordReset = false;
382385
setPasswordExpiry(user);
383386
setPreviousPasswords(user);
387+
setLastLoginAndReturn(user);
384388

385389
// Save user with updated details
386390
await User.update(
@@ -389,6 +393,7 @@ const resetPasswordWithToken = async (req, res) => {
389393
metaData: user.metaData,
390394
passwordExpiresAt: user.passwordExpiresAt,
391395
forcePasswordReset: user.forcePasswordReset,
396+
lastLoginAt: user.lastLoginAt,
392397
},
393398
{
394399
where: { id: user.id },
@@ -511,6 +516,7 @@ const resetTempPassword = async (req, res) => {
511516
user.forcePasswordReset = false;
512517
setPasswordExpiry(user);
513518
setPreviousPasswords(user);
519+
setLastLoginAndReturn(user);
514520

515521
// Save user with updated details
516522
await User.update(
@@ -519,6 +525,7 @@ const resetTempPassword = async (req, res) => {
519525
metaData: user.metaData,
520526
passwordExpiresAt: user.passwordExpiresAt,
521527
forcePasswordReset: user.forcePasswordReset,
528+
lastLoginAt: user.lastLoginAt,
522529
},
523530
{
524531
where: { id: user.id },

Tombolo/server/controllers/userController.js

+6
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,12 @@ const createUser = async (req, res) => {
437437
third: false,
438438
final: false,
439439
},
440+
accountDeleteEmailSent: {
441+
first: false,
442+
second: false,
443+
third: false,
444+
final: false,
445+
},
440446
},
441447
});
442448

Tombolo/server/controllers/wizardController.js

+6
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ const createUser = async (
204204
third: false,
205205
final: false,
206206
},
207+
accountDeleteEmailSent: {
208+
first: false,
209+
second: false,
210+
third: false,
211+
final: false,
212+
},
207213
previousPasswords: [hash],
208214
},
209215
},

Tombolo/server/jobSchedular/job-scheduler.js

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const {
6060
const {
6161
removeUnverifiedUser,
6262
sendPasswordExpiryEmails,
63+
sendAccountDeleteEmails,
6364
} = require("../jobSchedularMethods/userManagementJobs.js");
6465

6566
class JobScheduler {
@@ -142,6 +143,7 @@ class JobScheduler {
142143
await this.checkClusterReachability();
143144
await removeUnverifiedUser.call(this);
144145
await sendPasswordExpiryEmails.call(this);
146+
await sendAccountDeleteEmails.call(this);
145147
logger.info("-----------------------------");
146148
logger.info("Server is finished intializing, and is now running");
147149
logger.info("-----------------------------");

Tombolo/server/jobSchedularMethods/userManagementJobs.js

+29
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,36 @@ async function sendPasswordExpiryEmails() {
5959
}
6060
}
6161

62+
async function sendAccountDeleteEmails() {
63+
try {
64+
let jobName = "account-delete-" + new Date().getTime();
65+
this.bree.add({
66+
name: jobName,
67+
interval: "12h",
68+
path: path.join(
69+
__dirname,
70+
"..",
71+
"jobs",
72+
"userManagement",
73+
"accountDelete.js"
74+
),
75+
worker: {
76+
workerData: {
77+
jobName: jobName,
78+
WORKER_CREATED_AT: Date.now(),
79+
},
80+
},
81+
});
82+
83+
this.bree.start(jobName);
84+
logger.info("User management (account inactivity emails) initialized ...");
85+
} catch (err) {
86+
logger.error(err);
87+
}
88+
}
89+
6290
module.exports = {
6391
removeUnverifiedUser,
6492
sendPasswordExpiryEmails,
93+
sendAccountDeleteEmails,
6594
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// imports from node modules
2+
const { parentPort } = require("worker_threads");
3+
const { Op } = require("sequelize");
4+
const { v4: uuidv4 } = require("uuid");
5+
6+
//Local Imports
7+
const models = require("../../models");
8+
const { trimURL, getSupportContactEmails } = require("../../utils/authUtil");
9+
const { trim } = require("lodash");
10+
11+
// Constants
12+
const User = models.user;
13+
const UserRoles = models.UserRoles;
14+
const RoleTypes = models.RoleTypes;
15+
const NotificationQueue = models.notification_queue;
16+
const accountUnlockLink = `${trimURL(process.env.WEB_URL)}`;
17+
18+
const accountDeleteAlertDaysForUser =
19+
require("../../config/monitorings.js").accountDeleteAlertDaysForUser;
20+
21+
const updateUserAndSendNotification = async (user, daysToExpiry, version) => {
22+
const expiryDate = new Date(
23+
Date.now() + 1000 * 60 * 60 * 24 * daysToExpiry
24+
).toDateString();
25+
26+
// Queue notification
27+
await NotificationQueue.create({
28+
type: "email",
29+
templateName: "accountDeleteWarning",
30+
notificationOrigin: "Account Delete Warning",
31+
deliveryType: "immediate",
32+
createdBy: "System",
33+
updatedBy: "System",
34+
metaData: {
35+
notificationId: uuidv4(),
36+
recipientName: `${user.dataValues.firstName}`,
37+
notificationOrigin: "Account Delete Warning",
38+
subject: "Account Deletion Warning",
39+
mainRecipients: [user.dataValues.email],
40+
notificationDescription: "Account Delete Warning",
41+
daysToExpiry: daysToExpiry,
42+
expiryDate: expiryDate,
43+
accountUnlockLink: accountUnlockLink,
44+
},
45+
});
46+
47+
await user.update({
48+
metaData: {
49+
...user.metaData,
50+
accountDeleteEmailSent: {
51+
...user.metaData.accountDeleteEmailSent,
52+
[version]: true,
53+
},
54+
},
55+
});
56+
};
57+
58+
(async () => {
59+
try {
60+
parentPort &&
61+
parentPort.postMessage({
62+
level: "info",
63+
text: "Account Deletion Job started ...",
64+
});
65+
66+
//get all relevant dates in easy to understand variables
67+
const now = Date.now();
68+
const deletionDate = now - 1000 * 60 * 60 * 24 * 180; // 180 days
69+
const firstDeleteWarningDate =
70+
deletionDate + 1000 * 60 * 60 * 24 * accountDeleteAlertDaysForUser[0]; // first accountDeleteAlertDaysForUser[0] days
71+
72+
//get all users where lastLoginAt is older than firstDeleteWarningDate
73+
const users = await User.findAll({
74+
where: {
75+
lastLoginAt: {
76+
[Op.lt]: firstDeleteWarningDate,
77+
},
78+
},
79+
include: [
80+
{
81+
model: UserRoles,
82+
attributes: ["id"],
83+
as: "roles",
84+
include: [
85+
{
86+
model: RoleTypes,
87+
as: "role_details",
88+
attributes: ["id", "roleName"],
89+
},
90+
],
91+
},
92+
],
93+
});
94+
95+
//filter out admins and owners
96+
const filteredUsers = users.filter((user) => {
97+
return !user.roles.some((role) => {
98+
return (
99+
role?.role_details?.roleName === "administrator" ||
100+
role?.role_details?.roleName === "owner"
101+
);
102+
});
103+
});
104+
105+
for (const user of filteredUsers) {
106+
//pull out the internal user object for easier use below
107+
let userInternal = user.dataValues;
108+
const lastLoginAt = userInternal.lastLoginAt;
109+
const daysToExpiry =
110+
Math.floor((lastLoginAt - deletionDate) / (1000 * 60 * 60 * 24)) + 1;
111+
112+
if (
113+
daysToExpiry <= accountDeleteAlertDaysForUser[0] &&
114+
daysToExpiry > accountDeleteAlertDaysForUser[1] &&
115+
!userInternal.metaData?.accountDeleteEmailSent?.first
116+
) {
117+
parentPort &&
118+
parentPort.postMessage({
119+
level: "verbose",
120+
text:
121+
"User with email " +
122+
userInternal.email +
123+
" is within " +
124+
daysToExpiry +
125+
" days of account deletion due to inactivity.",
126+
});
127+
128+
let version = "first";
129+
await updateUserAndSendNotification(user, daysToExpiry, version);
130+
}
131+
132+
if (
133+
daysToExpiry <= accountDeleteAlertDaysForUser[1] &&
134+
daysToExpiry > accountDeleteAlertDaysForUser[2] &&
135+
!userInternal.metaData?.accountDeleteEmailSent?.second
136+
) {
137+
parentPort &&
138+
parentPort.postMessage({
139+
level: "verbose",
140+
text:
141+
"User with email " +
142+
userInternal.email +
143+
" is within " +
144+
daysToExpiry +
145+
" days of account deletion due to inactivity.",
146+
});
147+
let version = "second";
148+
await updateUserAndSendNotification(user, daysToExpiry, version);
149+
}
150+
if (
151+
daysToExpiry <= accountDeleteAlertDaysForUser[2] &&
152+
daysToExpiry > 0 &&
153+
!userInternal.metaData?.accountDeleteEmailSent?.third
154+
) {
155+
parentPort &&
156+
parentPort.postMessage({
157+
level: "verbose",
158+
text:
159+
"User with email " +
160+
userInternal.email +
161+
" is within " +
162+
daysToExpiry +
163+
" days of account deletion due to inactivity",
164+
});
165+
166+
let version = "third";
167+
await updateUserAndSendNotification(user, daysToExpiry, version);
168+
}
169+
170+
if (
171+
daysToExpiry <= 0 &&
172+
!userInternal.metaData?.accountDeleteEmailSent?.final
173+
) {
174+
parentPort &&
175+
parentPort.postMessage({
176+
level: "verbose",
177+
text:
178+
"User with email " +
179+
userInternal.email +
180+
" has been removed due to inactivity.",
181+
});
182+
183+
const accountUnlockLink = `${trimURL(process.env.WEB_URL)}/register`;
184+
// Queue notification
185+
await NotificationQueue.create({
186+
type: "email",
187+
templateName: "accountDeleted",
188+
notificationOrigin: "Account Deleted",
189+
deliveryType: "immediate",
190+
createdBy: "System",
191+
updatedBy: "System",
192+
metaData: {
193+
notificationId: uuidv4(),
194+
recipientName: `${userInternal.firstName}`,
195+
notificationOrigin: "Account Deleted",
196+
subject: "Account Deleted",
197+
mainRecipients: [userInternal.email],
198+
notificationDescription: "Account Deleted",
199+
accountUnlockLink: accountUnlockLink,
200+
},
201+
});
202+
203+
// delete user account
204+
// TODO -- Move user to archive table
205+
await user.destroy();
206+
}
207+
}
208+
209+
parentPort &&
210+
parentPort.postMessage({
211+
level: "info",
212+
text: "Account lock check job completed ...",
213+
});
214+
} catch (error) {
215+
parentPort &&
216+
parentPort.postMessage({ level: "error", text: error.message });
217+
}
218+
})();

Tombolo/server/jobs/userManagement/passwordExpiry.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ const { v4: uuidv4 } = require("uuid");
55

66
//Local Imports
77
const models = require("../../models");
8-
const logger = require("../../config/logger");
98
const { trimURL, getSupportContactEmails } = require("../../utils/authUtil");
109

1110
// Constants
1211
const user = models.user;
1312
const NotificationQueue = models.notification_queue;
1413
const passwordResetLink = `${trimURL(process.env.WEB_URL)}/myaccount`;
15-
const passwordExpiryAlertDaysForUser = require("../../config/monitorings.js").passwordExpiryAlertDaysForUser;
14+
const passwordExpiryAlertDaysForUser =
15+
require("../../config/monitorings.js").passwordExpiryAlertDaysForUser;
1616

1717
const updateUserAndSendNotification = async (user, daysToExpiry, version) => {
1818
// Queue notification

0 commit comments

Comments
 (0)