From a5e537b3147f543a9c3411c91c3a1ebd04ed546f Mon Sep 17 00:00:00 2001 From: Okke Harsta Date: Mon, 16 Dec 2024 16:34:11 +0100 Subject: [PATCH 1/3] WIP for #606 --- .github/workflows/actions.yml | 2 +- .../java/myconext/cron/InactivityMail.java | 56 ++++++++++++++++++- .../src/main/java/myconext/mail/MailBox.java | 9 +++ .../src/main/java/myconext/model/User.java | 3 + .../java/myconext/model/UserInactivity.java | 22 ++++++++ .../myconext/repository/UserRepository.java | 2 + .../src/main/resources/application.yml | 4 ++ .../inactivity_warning_short_term_en.html | 26 +++++++++ .../inactivity_warning_short_term_en.txt | 7 +++ .../inactivity_warning_short_term_nl.html | 27 +++++++++ .../inactivity_warning_short_term_nl.txt | 7 +++ .../inactivity_warning_years_ahead_en.html | 26 +++++++++ .../inactivity_warning_years_ahead_en.txt | 7 +++ .../inactivity_warning_years_ahead_nl.html | 27 +++++++++ .../inactivity_warning_years_ahead_nl.txt | 7 +++ .../resources/mail_templates/subjects.json | 8 +++ public-gui/package.json | 14 ++--- 17 files changed, 243 insertions(+), 11 deletions(-) create mode 100644 myconext-server/src/main/java/myconext/model/UserInactivity.java create mode 100644 myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_en.html create mode 100644 myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_en.txt create mode 100644 myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_nl.html create mode 100644 myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_nl.txt create mode 100644 myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_en.html create mode 100644 myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_en.txt create mode 100644 myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_nl.html create mode 100644 myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_nl.txt diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index ce99d8b1..338e48c8 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -36,7 +36,7 @@ jobs: distribution: 'temurin' cache: 'maven' - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} diff --git a/myconext-server/src/main/java/myconext/cron/InactivityMail.java b/myconext-server/src/main/java/myconext/cron/InactivityMail.java index 7b6c0bdb..ce56f01c 100644 --- a/myconext-server/src/main/java/myconext/cron/InactivityMail.java +++ b/myconext-server/src/main/java/myconext/cron/InactivityMail.java @@ -1,8 +1,58 @@ package myconext.cron; +import myconext.mail.MailBox; +import myconext.manage.Manage; +import myconext.model.User; +import myconext.model.UserInactivity; +import myconext.repository.UserRepository; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component public class InactivityMail { - //TODO, keep track of the mails send bu using an enum indicating the level of inactivity - //In the variables for the mail separate the NL variable and EN variable and use - //different variables in the mail templates + private static final Log LOG = LogFactory.getLog(InactivityMail.class); + + private final MailBox mailBox; + private final UserRepository userRepository; + private final boolean mailInactivityMails; + private final boolean cronJobResponsible; + + @Autowired + public InactivityMail(Manage manage, + MailBox mailBox, + UserRepository userRepository, + @Value("${cron.node-cron-job-responsible}") boolean cronJobResponsible, + @Value("${feature.mail_inactivity_mails}") boolean mailInactivityMails) { + this.mailBox = mailBox; + this.userRepository = userRepository; + this.cronJobResponsible = cronJobResponsible; + this.mailInactivityMails = mailInactivityMails; + } + + @Scheduled(cron = "${cron.inactivity-users-expression}") + @SuppressWarnings("unchecked") + public void mailInactiveUsers() { + if (!mailInactivityMails || !cronJobResponsible) { + return; + } + long start = System.currentTimeMillis(); + try { + + List users = userRepository.findByLastLoginBeforeAndLastLoginAfterAndUserInactivity(0L, 0L, null); + + LOG.info(String.format("Mailed %s users who has been inactive for %s in for %s ms", + users.size(), UserInactivity.YEAR_1_INTERVAL, System.currentTimeMillis() - start)); + } catch (Exception e) { + LOG.error("Error in mailInactiveUsers", e); + } + } + + } diff --git a/myconext-server/src/main/java/myconext/mail/MailBox.java b/myconext-server/src/main/java/myconext/mail/MailBox.java index 554fbb9c..67a2c65e 100644 --- a/myconext-server/src/main/java/myconext/mail/MailBox.java +++ b/myconext-server/src/main/java/myconext/mail/MailBox.java @@ -113,6 +113,15 @@ public void sendInstitutionMailWarning(User user) { sendMail("institution_mail_warning", title, variables, preferredLanguage(user), user.getEmail(), false); } + public void sendUserInactivityMail(User user, Map localeVariables, boolean yearsAhead) { + String title = this.getTitle(yearsAhead ? "inactivity_warning_years_ahead" : "inactivity_warning_short_term", user); + Map variables = variables(user, title); + variables.put("mySurfConextURL", mySURFconextURL); + variables.putAll(localeVariables); + String templateName = yearsAhead ? "inactivity_warning_years_ahead" : "inactivity_warning_short_term"; + sendMail(templateName, title, variables, preferredLanguage(user), user.getEmail(), false); + } + public void sendNudgeAppMail(User user) { String title = this.getTitle("nudge_eduid_app", user); Map variables = variables(user, title); diff --git a/myconext-server/src/main/java/myconext/model/User.java b/myconext-server/src/main/java/myconext/model/User.java index 1dfd26da..3c41e2cf 100644 --- a/myconext-server/src/main/java/myconext/model/User.java +++ b/myconext-server/src/main/java/myconext/model/User.java @@ -97,6 +97,9 @@ public class User implements Serializable, UserDetails { @Setter private boolean mobileAuthentication; + @Setter + private UserInactivity userInactivity; + public User(CreateInstitutionEduID createInstitutionEduID, Map userInfo) { this.email = createInstitutionEduID.getEmail(); this.chosenName = (String) userInfo.get("given_name"); diff --git a/myconext-server/src/main/java/myconext/model/UserInactivity.java b/myconext-server/src/main/java/myconext/model/UserInactivity.java new file mode 100644 index 00000000..075205c0 --- /dev/null +++ b/myconext-server/src/main/java/myconext/model/UserInactivity.java @@ -0,0 +1,22 @@ +package myconext.model; + +public enum UserInactivity { + + //1 year (= 4 year before the 5-year mark) + YEAR_1_INTERVAL(4L * 365, 365L), + //3 year (= 3 year before the 5-year mark) + YEAR_3_INTERVAL(3L * 365, 2L * 365), + // + MONTH_1_BEFORE_5_YEARS(30L, (4L * 365) + (11L * 30)), + WEEK_1_BEFORE_5_YEARS(7L, (4L * 365) + (51L * 7)); + + private final long beforeDays; + private final long afterDays; + + + UserInactivity(long beforeDays, long afterDays) { + this.afterDays = afterDays; + this.beforeDays = beforeDays; + } + + } diff --git a/myconext-server/src/main/java/myconext/repository/UserRepository.java b/myconext-server/src/main/java/myconext/repository/UserRepository.java index 7d09446e..861727d8 100644 --- a/myconext-server/src/main/java/myconext/repository/UserRepository.java +++ b/myconext-server/src/main/java/myconext/repository/UserRepository.java @@ -2,6 +2,7 @@ import myconext.model.User; +import myconext.model.UserInactivity; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; import org.springframework.stereotype.Repository; @@ -53,6 +54,7 @@ public interface UserRepository extends MongoRepository { @Query("{'email' : {$regex : ?0, $options: 'i'}}") List findByEmailDomain(String regex); + List findByLastLoginBeforeAndLastLoginAfterAndUserInactivity(long lastLoginBefore, long lastLoginAfter, UserInactivity userInactivity); @Query(""" { $and: [ { $or: [ diff --git a/myconext-server/src/main/resources/application.yml b/myconext-server/src/main/resources/application.yml index 528ad313..71a71417 100644 --- a/myconext-server/src/main/resources/application.yml +++ b/myconext-server/src/main/resources/application.yml @@ -54,6 +54,8 @@ cron: nudge-app-mail-expression: "0 30 6 * * ?" # Number of days after creation of the eduID account which the nudge mail is send nudge-app-mail-days-after-creation: 14 + # Every day at 7:30AM + inactivity-users-expression: "0 30 7 * * ?" manage: username: myconext @@ -105,6 +107,8 @@ feature: remote_creation_api: True # Do we periodically mail users who have used their institution account mail_institution_mail_usage: True + # Do we periodically mail users who are inactive and might have their account deleted + mail_inactivity_mails: True # Do we mail users who have not installed the eduID app nudge_app_mail: True secure_cookie: false diff --git a/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_en.html b/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_en.html new file mode 100644 index 00000000..cbd13692 --- /dev/null +++ b/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_en.html @@ -0,0 +1,26 @@ + + + + {{title}} + + +
+
+

You + have not used your eidUD account for {{inactivity_period_en}}

+
+
+

Hi {{name}},

+

You have not used your eduID account for {{inactivity_period_en}}. If you remain inactive than your eduID + account will be deleted within {{deletion_period_en}}, on {{account_delete_date_en}}. + Go to your eduID account and login to keep your account active.

+
+

+ Kind regards, + eduID. +

+
+ {{> footer_nl.html}} +
+ + diff --git a/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_en.txt b/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_en.txt new file mode 100644 index 00000000..9947d29b --- /dev/null +++ b/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_en.txt @@ -0,0 +1,7 @@ +Hi {{name}}, + +You have not used your eduID account for {{inactivity_period_en}}. If you remain inactive than your eduID +account will be deleted within {{deletion_period_en}}, on {{account_delete_date_en}}. +Go to your {{mySurfConextURL}} and login to keep your account active. + +{{> footer_en.txt}} diff --git a/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_nl.html b/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_nl.html new file mode 100644 index 00000000..deb76b0c --- /dev/null +++ b/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_nl.html @@ -0,0 +1,27 @@ + + + + {{title}} + + +
+
+

You + have created an eduID with your institutional account

+
+
+

Hi {{name}},

+ +

Je hebt je eduID account niet gebruikt in {{inactivity_period_nl}}. Als je je eduID account niet gebruikt dan + zullen we deze verwijderen in {{deletion_period_nl}}, op {{account_delete_date_nl}}. + Ga naar je eduID account en login om je account actief te houden.

+
+

+ Vriendelijke groeten, + eduID. +

+
+ {{> footer_nl.html}} +
+ + diff --git a/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_nl.txt b/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_nl.txt new file mode 100644 index 00000000..bb8e7556 --- /dev/null +++ b/myconext-server/src/main/resources/mail_templates/inactivity_warning_short_term_nl.txt @@ -0,0 +1,7 @@ +Hi {{name}}, + +Je hebt je eduID account niet gebruikt in {{inactivity_period_nl}}. Als je je eduID account niet gebruikt dan zullen we +deze verwijderen in {{deletion_period_nl}}, op {{account_delete_date_nl}}. +Ga naar {{mySurfConextURL}} en login om je account actief te houden. + +{{> footer_en.txt}} diff --git a/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_en.html b/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_en.html new file mode 100644 index 00000000..cbd13692 --- /dev/null +++ b/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_en.html @@ -0,0 +1,26 @@ + + + + {{title}} + + +
+
+

You + have not used your eidUD account for {{inactivity_period_en}}

+
+
+

Hi {{name}},

+

You have not used your eduID account for {{inactivity_period_en}}. If you remain inactive than your eduID + account will be deleted within {{deletion_period_en}}, on {{account_delete_date_en}}. + Go to your eduID account and login to keep your account active.

+
+

+ Kind regards, + eduID. +

+
+ {{> footer_nl.html}} +
+ + diff --git a/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_en.txt b/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_en.txt new file mode 100644 index 00000000..9947d29b --- /dev/null +++ b/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_en.txt @@ -0,0 +1,7 @@ +Hi {{name}}, + +You have not used your eduID account for {{inactivity_period_en}}. If you remain inactive than your eduID +account will be deleted within {{deletion_period_en}}, on {{account_delete_date_en}}. +Go to your {{mySurfConextURL}} and login to keep your account active. + +{{> footer_en.txt}} diff --git a/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_nl.html b/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_nl.html new file mode 100644 index 00000000..deb76b0c --- /dev/null +++ b/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_nl.html @@ -0,0 +1,27 @@ + + + + {{title}} + + +
+
+

You + have created an eduID with your institutional account

+
+
+

Hi {{name}},

+ +

Je hebt je eduID account niet gebruikt in {{inactivity_period_nl}}. Als je je eduID account niet gebruikt dan + zullen we deze verwijderen in {{deletion_period_nl}}, op {{account_delete_date_nl}}. + Ga naar je eduID account en login om je account actief te houden.

+
+

+ Vriendelijke groeten, + eduID. +

+
+ {{> footer_nl.html}} +
+ + diff --git a/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_nl.txt b/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_nl.txt new file mode 100644 index 00000000..bb8e7556 --- /dev/null +++ b/myconext-server/src/main/resources/mail_templates/inactivity_warning_years_ahead_nl.txt @@ -0,0 +1,7 @@ +Hi {{name}}, + +Je hebt je eduID account niet gebruikt in {{inactivity_period_nl}}. Als je je eduID account niet gebruikt dan zullen we +deze verwijderen in {{deletion_period_nl}}, op {{account_delete_date_nl}}. +Ga naar {{mySurfConextURL}} en login om je account actief te houden. + +{{> footer_en.txt}} diff --git a/myconext-server/src/main/resources/mail_templates/subjects.json b/myconext-server/src/main/resources/mail_templates/subjects.json index 5596ee19..12491cb7 100644 --- a/myconext-server/src/main/resources/mail_templates/subjects.json +++ b/myconext-server/src/main/resources/mail_templates/subjects.json @@ -43,6 +43,14 @@ "nudge_eduid_app": { "en": "Installeer de eduID app", "nl": "Install the eduID app" + }, + "inactivity_warning_years_ahead": { + "en": "Inactivity warning", + "nl": "Inactiviteit waarschuwing" + }, + "inactivity_warning_short_term": { + "en": "Inactivity warning", + "nl": "Inactiviteit waarschuwing" } } diff --git a/public-gui/package.json b/public-gui/package.json index c5dca0ea..13052c88 100644 --- a/public-gui/package.json +++ b/public-gui/package.json @@ -10,21 +10,21 @@ "preview": "vite preview" }, "dependencies": { - "@surfnet/sds": "^0.0.118", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "@surfnet/sds": "^0.0.119", + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "devDependencies": { "@eslint/js": "^9.16.0", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.16.0", "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^15.13.0", "sass": "^1.82.0", - "vite": "^6.0.2" + "vite": "^6.0.3" } } From b38a02bb53bfca7f6b11044daa58295384d18943 Mon Sep 17 00:00:00 2001 From: Okke Harsta Date: Wed, 18 Dec 2024 16:54:39 +0100 Subject: [PATCH 2/3] WIP for #606 --- .../java/myconext/cron/InactivityMail.java | 27 ++++++++++++++--- .../src/main/java/myconext/mail/MailBox.java | 6 ++-- .../java/myconext/model/UserInactivity.java | 29 ++++++++++--------- .../myconext/repository/UserRepository.java | 2 +- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/myconext-server/src/main/java/myconext/cron/InactivityMail.java b/myconext-server/src/main/java/myconext/cron/InactivityMail.java index ce56f01c..f4c55b79 100644 --- a/myconext-server/src/main/java/myconext/cron/InactivityMail.java +++ b/myconext-server/src/main/java/myconext/cron/InactivityMail.java @@ -12,7 +12,8 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import java.util.List; +import java.text.DateFormat; +import java.util.*; @Component public class InactivityMail { @@ -42,13 +43,31 @@ public void mailInactiveUsers() { if (!mailInactivityMails || !cronJobResponsible) { return; } - long start = System.currentTimeMillis(); + long nowInMillis = System.currentTimeMillis(); + long oneDayInMillis = 24 * 60 * 60 * 1000; + DateFormat dateFormatNL = DateFormat.getDateInstance(DateFormat.LONG, Locale.of("nl")); + DateFormat dateFormatUS = DateFormat.getDateInstance(DateFormat.LONG, Locale.of("us")); try { + //For all UserInactivity we check the last activity date and ensure they only receive one mail about this + long lastLoginBefore = nowInMillis - (oneDayInMillis * UserInactivity.YEAR_1_INTERVAL.getInactivityDays()); + List users = userRepository.findByLastLoginBeforeAndUserInactivity(UserInactivity.YEAR_1_INTERVAL.getInactivityDays(), null); + //We need the following localized variables + //inactivity_period_en, deletion_period_en, account_delete_date_en, inactivity_period_nl, deletion_period_nl, account_delete_date_nl + Map localeVariables = new HashMap(); + Date date = new Date(lastLoginBefore); + localeVariables.put("inactivity_period_en", "1 year"); + localeVariables.put("deletion_period_en", "4 years"); + localeVariables.put("account_delete_date_en", dateFormatUS.format(date)); + localeVariables.put("inactivity_period_nl", "1 jaar"); + localeVariables.put("deletion_period_nl", "4 jaren"); + localeVariables.put("account_delete_date_nl", dateFormatNL.format(date)); + users.forEach(user -> { + mailBox.sendUserInactivityMail(user, true); + }); - List users = userRepository.findByLastLoginBeforeAndLastLoginAfterAndUserInactivity(0L, 0L, null); LOG.info(String.format("Mailed %s users who has been inactive for %s in for %s ms", - users.size(), UserInactivity.YEAR_1_INTERVAL, System.currentTimeMillis() - start)); + users.size(), UserInactivity.YEAR_1_INTERVAL, System.currentTimeMillis() - nowInMillis)); } catch (Exception e) { LOG.error("Error in mailInactiveUsers", e); } diff --git a/myconext-server/src/main/java/myconext/mail/MailBox.java b/myconext-server/src/main/java/myconext/mail/MailBox.java index 67a2c65e..93bd43b1 100644 --- a/myconext-server/src/main/java/myconext/mail/MailBox.java +++ b/myconext-server/src/main/java/myconext/mail/MailBox.java @@ -113,12 +113,12 @@ public void sendInstitutionMailWarning(User user) { sendMail("institution_mail_warning", title, variables, preferredLanguage(user), user.getEmail(), false); } - public void sendUserInactivityMail(User user, Map localeVariables, boolean yearsAhead) { - String title = this.getTitle(yearsAhead ? "inactivity_warning_years_ahead" : "inactivity_warning_short_term", user); + public void sendUserInactivityMail(User user, Map localeVariables, boolean firstTwoWarnings) { + String title = this.getTitle(firstTwoWarnings ? "inactivity_warning_years_ahead" : "inactivity_warning_short_term", user); Map variables = variables(user, title); variables.put("mySurfConextURL", mySURFconextURL); variables.putAll(localeVariables); - String templateName = yearsAhead ? "inactivity_warning_years_ahead" : "inactivity_warning_short_term"; + String templateName = firstTwoWarnings ? "inactivity_warning_years_ahead" : "inactivity_warning_short_term"; sendMail(templateName, title, variables, preferredLanguage(user), user.getEmail(), false); } diff --git a/myconext-server/src/main/java/myconext/model/UserInactivity.java b/myconext-server/src/main/java/myconext/model/UserInactivity.java index 075205c0..cc034409 100644 --- a/myconext-server/src/main/java/myconext/model/UserInactivity.java +++ b/myconext-server/src/main/java/myconext/model/UserInactivity.java @@ -1,22 +1,23 @@ package myconext.model; -public enum UserInactivity { +import lombok.Getter; - //1 year (= 4 year before the 5-year mark) - YEAR_1_INTERVAL(4L * 365, 365L), - //3 year (= 3 year before the 5-year mark) - YEAR_3_INTERVAL(3L * 365, 2L * 365), - // - MONTH_1_BEFORE_5_YEARS(30L, (4L * 365) + (11L * 30)), - WEEK_1_BEFORE_5_YEARS(7L, (4L * 365) + (51L * 7)); +@Getter +public enum UserInactivity { - private final long beforeDays; - private final long afterDays; + // 1 year (= 4 year before the 5-year mark) + YEAR_1_INTERVAL(365L), + // 3 year (= 3 year before the 5-year mark) + YEAR_3_INTERVAL(2L * 365), + // 1 month before the 5-year mark + MONTH_1_BEFORE_5_YEARS((4L * 365) + (11L * 30)), + // 1 week before the-5 year mark + WEEK_1_BEFORE_5_YEARS((4L * 365) + (51L * 7)); + private final long inactivityDays; - UserInactivity(long beforeDays, long afterDays) { - this.afterDays = afterDays; - this.beforeDays = beforeDays; + UserInactivity(long inactivityDays) { + this.inactivityDays = inactivityDays; } - } +} diff --git a/myconext-server/src/main/java/myconext/repository/UserRepository.java b/myconext-server/src/main/java/myconext/repository/UserRepository.java index 861727d8..1d2afb18 100644 --- a/myconext-server/src/main/java/myconext/repository/UserRepository.java +++ b/myconext-server/src/main/java/myconext/repository/UserRepository.java @@ -54,7 +54,7 @@ public interface UserRepository extends MongoRepository { @Query("{'email' : {$regex : ?0, $options: 'i'}}") List findByEmailDomain(String regex); - List findByLastLoginBeforeAndLastLoginAfterAndUserInactivity(long lastLoginBefore, long lastLoginAfter, UserInactivity userInactivity); + List findByLastLoginBeforeAndUserInactivity(long lastLoginBefore, UserInactivity userInactivity); @Query(""" { $and: [ { $or: [ From b9ae6e3ce8c4245f3a6ce458fb093826e3250ba4 Mon Sep 17 00:00:00 2001 From: Okke Harsta Date: Thu, 19 Dec 2024 11:57:35 +0100 Subject: [PATCH 3/3] Fixes #606 --- .../java/myconext/cron/InactivityMail.java | 79 ++++++++++++------- .../src/main/java/myconext/model/User.java | 1 + .../java/myconext/model/UserInactivity.java | 45 +++++++++-- .../myconext/cron/InactivityMailTest.java | 77 ++++++++++++++++++ 4 files changed, 167 insertions(+), 35 deletions(-) create mode 100644 myconext-server/src/test/java/myconext/cron/InactivityMailTest.java diff --git a/myconext-server/src/main/java/myconext/cron/InactivityMail.java b/myconext-server/src/main/java/myconext/cron/InactivityMail.java index f4c55b79..5757b30a 100644 --- a/myconext-server/src/main/java/myconext/cron/InactivityMail.java +++ b/myconext-server/src/main/java/myconext/cron/InactivityMail.java @@ -1,7 +1,6 @@ package myconext.cron; import myconext.mail.MailBox; -import myconext.manage.Manage; import myconext.model.User; import myconext.model.UserInactivity; import myconext.repository.UserRepository; @@ -14,6 +13,8 @@ import java.text.DateFormat; import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Component public class InactivityMail { @@ -24,17 +25,20 @@ public class InactivityMail { private final UserRepository userRepository; private final boolean mailInactivityMails; private final boolean cronJobResponsible; + private final DateFormat dateFormatUS; + private final DateFormat dateFormatNL; @Autowired - public InactivityMail(Manage manage, - MailBox mailBox, - UserRepository userRepository, - @Value("${cron.node-cron-job-responsible}") boolean cronJobResponsible, - @Value("${feature.mail_inactivity_mails}") boolean mailInactivityMails) { + public InactivityMail(MailBox mailBox, + UserRepository userRepository, + @Value("${cron.node-cron-job-responsible}") boolean cronJobResponsible, + @Value("${feature.mail_inactivity_mails}") boolean mailInactivityMails) { this.mailBox = mailBox; this.userRepository = userRepository; this.cronJobResponsible = cronJobResponsible; this.mailInactivityMails = mailInactivityMails; + this.dateFormatUS = DateFormat.getDateInstance(DateFormat.LONG, Locale.of("us")); + this.dateFormatNL = DateFormat.getDateInstance(DateFormat.LONG, Locale.of("nl")); } @Scheduled(cron = "${cron.inactivity-users-expression}") @@ -43,35 +47,50 @@ public void mailInactiveUsers() { if (!mailInactivityMails || !cronJobResponsible) { return; } - long nowInMillis = System.currentTimeMillis(); - long oneDayInMillis = 24 * 60 * 60 * 1000; - DateFormat dateFormatNL = DateFormat.getDateInstance(DateFormat.LONG, Locale.of("nl")); - DateFormat dateFormatUS = DateFormat.getDateInstance(DateFormat.LONG, Locale.of("us")); try { - //For all UserInactivity we check the last activity date and ensure they only receive one mail about this - long lastLoginBefore = nowInMillis - (oneDayInMillis * UserInactivity.YEAR_1_INTERVAL.getInactivityDays()); - List users = userRepository.findByLastLoginBeforeAndUserInactivity(UserInactivity.YEAR_1_INTERVAL.getInactivityDays(), null); - //We need the following localized variables - //inactivity_period_en, deletion_period_en, account_delete_date_en, inactivity_period_nl, deletion_period_nl, account_delete_date_nl - Map localeVariables = new HashMap(); - Date date = new Date(lastLoginBefore); - localeVariables.put("inactivity_period_en", "1 year"); - localeVariables.put("deletion_period_en", "4 years"); - localeVariables.put("account_delete_date_en", dateFormatUS.format(date)); - localeVariables.put("inactivity_period_nl", "1 jaar"); - localeVariables.put("deletion_period_nl", "4 jaren"); - localeVariables.put("account_delete_date_nl", dateFormatNL.format(date)); - users.forEach(user -> { - mailBox.sendUserInactivityMail(user, true); - }); - - - LOG.info(String.format("Mailed %s users who has been inactive for %s in for %s ms", - users.size(), UserInactivity.YEAR_1_INTERVAL, System.currentTimeMillis() - nowInMillis)); + Stream.of(UserInactivity.values()).forEach(this::doMailInactiveUsers); + this.doDeleteInactiveUsers(); } catch (Exception e) { + //swallow exception as the scheduling stops then LOG.error("Error in mailInactiveUsers", e); } } + private void doMailInactiveUsers(UserInactivity userInactivity) { + long nowInMillis = System.currentTimeMillis(); + long oneDayInMillis = 24 * 60 * 60 * 1000L; + long fiveYearsInMillis = 5 * 365 * oneDayInMillis; + + long lastLoginBefore = nowInMillis - (oneDayInMillis * userInactivity.getInactivityDays()); + List users = userRepository.findByLastLoginBeforeAndUserInactivity(lastLoginBefore, userInactivity.getPreviousUserInactivity()); + + Map localeVariables = new HashMap<>(); + //This is the future date when the user will be deleted based on the inactivityDays of the userInactivity + Date date = new Date(nowInMillis + (fiveYearsInMillis - (oneDayInMillis * userInactivity.getInactivityDays()))); + localeVariables.put("inactivity_period_en", userInactivity.getInactivityPeriodEn()); + localeVariables.put("inactivity_period_nl", userInactivity.getInactivityPeriodNl()); + localeVariables.put("deletion_period_en", userInactivity.getDeletionPeriodEn()); + localeVariables.put("deletion_period_nl", userInactivity.getDeletionPeriodNl()); + localeVariables.put("account_delete_date_en", dateFormatUS.format(date)); + localeVariables.put("account_delete_date_nl", dateFormatNL.format(date)); + users.forEach(user -> { + mailBox.sendUserInactivityMail(user, localeVariables, true); + user.setUserInactivity(userInactivity); + userRepository.save(user); + }); + LOG.info(String.format("Mailed %s users who has been inactive for %s period in for %s ms", + users.size(), userInactivity, System.currentTimeMillis() - nowInMillis)); + } + + private void doDeleteInactiveUsers() { + long nowInMillis = System.currentTimeMillis(); + long oneDayInMillis = 24 * 60 * 60 * 1000L; + long lastLoginBefore = nowInMillis - (oneDayInMillis * 5L * 365); + List users = userRepository.findByLastLoginBeforeAndUserInactivity(lastLoginBefore, UserInactivity.WEEK_1_BEFORE_5_YEARS); + userRepository.deleteAll(users); + LOG.info(String.format("Deleted %s users (%s) who has been inactive for 5 years in for %s ms", + users.size(), users.stream().map(User::getEmail).collect(Collectors.joining(", ")), + System.currentTimeMillis() - nowInMillis)); + } } diff --git a/myconext-server/src/main/java/myconext/model/User.java b/myconext-server/src/main/java/myconext/model/User.java index 3c41e2cf..3210ca01 100644 --- a/myconext-server/src/main/java/myconext/model/User.java +++ b/myconext-server/src/main/java/myconext/model/User.java @@ -84,6 +84,7 @@ public class User implements Serializable, UserDetails { private List eduIDS = new ArrayList<>(); private long created; + @Setter private long lastLogin; @Setter diff --git a/myconext-server/src/main/java/myconext/model/UserInactivity.java b/myconext-server/src/main/java/myconext/model/UserInactivity.java index cc034409..b1c48829 100644 --- a/myconext-server/src/main/java/myconext/model/UserInactivity.java +++ b/myconext-server/src/main/java/myconext/model/UserInactivity.java @@ -6,18 +6,53 @@ public enum UserInactivity { // 1 year (= 4 year before the 5-year mark) - YEAR_1_INTERVAL(365L), + YEAR_1_INTERVAL(365L, + null, + "1 year", + "1 jaar", + "4 years", + "4 jaren"), // 3 year (= 3 year before the 5-year mark) - YEAR_3_INTERVAL(2L * 365), + YEAR_3_INTERVAL(2L * 365, + YEAR_1_INTERVAL, + "2 years", + "2 jaren", + "3 years", + "3 jaren"), // 1 month before the 5-year mark - MONTH_1_BEFORE_5_YEARS((4L * 365) + (11L * 30)), + MONTH_1_BEFORE_5_YEARS((4L * 365) + (11L * 30), + YEAR_3_INTERVAL, + "almost 5 years", + "bijna 5 jaar", + "1 month", + "1 maand"), // 1 week before the-5 year mark - WEEK_1_BEFORE_5_YEARS((4L * 365) + (51L * 7)); + WEEK_1_BEFORE_5_YEARS((4L * 365) + (51L * 7), + MONTH_1_BEFORE_5_YEARS, + "almost 5 years", + "bijna 5 jaar", + "1 week", + "1 week"); private final long inactivityDays; + private final UserInactivity previousUserInactivity; + private final String inactivityPeriodEn; + private final String inactivityPeriodNl; + private final String deletionPeriodEn; + private final String deletionPeriodNl; - UserInactivity(long inactivityDays) { + UserInactivity(long inactivityDays, + UserInactivity previousUserInactivity, + String inactivityPeriodEn, + String inactivityPeriodNl, + String deletionPeriodEn, + String deletionPeriodNl) { this.inactivityDays = inactivityDays; + this.previousUserInactivity = previousUserInactivity; + this.inactivityPeriodEn = inactivityPeriodEn; + this.inactivityPeriodNl = inactivityPeriodNl; + this.deletionPeriodEn = deletionPeriodEn; + this.deletionPeriodNl = deletionPeriodNl; } } diff --git a/myconext-server/src/test/java/myconext/cron/InactivityMailTest.java b/myconext-server/src/test/java/myconext/cron/InactivityMailTest.java new file mode 100644 index 00000000..c3bf9639 --- /dev/null +++ b/myconext-server/src/test/java/myconext/cron/InactivityMailTest.java @@ -0,0 +1,77 @@ +package myconext.cron; + +import myconext.AbstractMailBoxTest; +import myconext.model.User; +import myconext.model.UserInactivity; +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.mail.internet.MimeMessage; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "mongodb_db=surf_id_test", + "cron.node-cron-job-responsible=true", + "email_guessing_sleep_millis=1", + "sp_entity_id=https://engine.test.surfconext.nl/authentication/sp/metadata", + "sp_entity_metadata_url=https://engine.test.surfconext.nl/authentication/sp/metadata", + "spring.main.lazy-initialization=true", + "eduid_api.oidcng_introspection_uri=http://localhost:8098/introspect", + "cron.service-name-resolver-initial-delay-milliseconds=60000", + "oidc.base-url=http://localhost:8098/", + "sso_mfa_duration_seconds=-1000", + "feature.requires_signed_authn_request=false", + "feature.deny_disposable_email_providers=false", + "verify.base_uri=http://localhost:8098" + }) +public class InactivityMailTest extends AbstractMailBoxTest { + + private final String DELETED_EMAIL = "DELETED"; + + @Autowired + protected InactivityMail inactivityMail; + + @Test + public void mailInactivityMail() { + inactivityUserSeed(); + + inactivityMail.mailInactiveUsers(); + + List mimeMessages = mailMessages(); + assertEquals(4, mimeMessages.size()); + Stream.of(UserInactivity.values()).forEach(userInactivity -> { + User user = userRepository.findOneUserByEmail(userInactivity.name()); + assertEquals(userInactivity, user.getUserInactivity()); + }); + Optional optionalUser = userRepository.findUserByEmail(DELETED_EMAIL); + assertFalse(optionalUser.isPresent()); + } + + private void inactivityUserSeed() { + long oneDayInMillis = 24 * 60 * 60 * 1000L; + long yesterday = System.currentTimeMillis() - oneDayInMillis; + Stream.of(UserInactivity.values()).forEach(userInactivity -> { + User user = new User(); + user.setLastLogin(yesterday - (userInactivity.getInactivityDays() * oneDayInMillis)); + user.setEmail(userInactivity.name()); + user.setUserInactivity(userInactivity.getPreviousUserInactivity()); + userRepository.save(user); + }); + //And one extra User who is to be deleted + User user = new User(); + user.setLastLogin(yesterday - (((5L * 365) + 5) * oneDayInMillis)); + user.setEmail(DELETED_EMAIL); + user.setUserInactivity(UserInactivity.WEEK_1_BEFORE_5_YEARS); + userRepository.save(user); + + } + +} \ No newline at end of file