From bee69786631e0eef3cee17e12edf967b43b2a214 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 21 Dec 2024 12:18:20 +0100 Subject: [PATCH 1/6] added translation strings --- src/main/resources/i18n/messages.properties | 9 +++++++++ src/main/resources/i18n/messages_de.properties | 8 ++++++++ src/main/resources/i18n/messages_en.properties | 8 ++++++++ src/main/webapp/i18n/de/notification.json | 8 ++++++-- src/main/webapp/i18n/en/notification.json | 8 ++++++-- 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 6580e702bd3c..81e506aa51c7 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -137,6 +137,12 @@ email.notification.sshKeyExpiry.renew = You can renew your SSH key here: email.notification.sshKeyExpiry.sshKeyHash = SSH key hash: email.notification.sshKeyExpiry.sshKeyLabel = SSH key label: +# VCS Token Settings +email.notification.vcsAccessTokenAdded.title = A new VCS access token was added to your account. +email.notification.vcsAccessTokenAdded.ifMistake = If you believe this token was added in error, you can remove it and disable access at the following location: +email.notification.vcsAccessTokenExpiry.title = Your personal VCS access token has expired. +email.notification.vcsAccessTokenExpiry.renew = You can renew it here: + # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there artemisApp.groupNotification.title.attachmentChange = Attachment updated @@ -166,3 +172,6 @@ artemisApp.singleUserNotification.title.dataExportFailed = Your Artemis data exp artemisApp.singleUserNotification.title.sshKeyAdded = New SSH key added to account artemisApp.singleUserNotification.title.sshKeyExpiresSoon = SSH key expires soon artemisApp.singleUserNotification.title.sshKeyHasExpired = SSH key has expired +artemisApp.singleUserNotification.title.vcsAccessTokenAdded = New VCS access token added to account +artemisApp.singleUserNotification.title.vcsAccessTokenExpired = VCS access token has expired + diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties index 0960e6223fec..ea364a60fec5 100644 --- a/src/main/resources/i18n/messages_de.properties +++ b/src/main/resources/i18n/messages_de.properties @@ -137,6 +137,12 @@ email.notification.sshKeyExpiry.renew = Hier kannst du deinen SSH-Schlüssel akt email.notification.sshKeyExpiry.sshKeyHash = SSH-Schlüssel-Hash: email.notification.sshKeyExpiry.sshKeyLabel = SSH-Schlüssel-Label: +# VCS access token User Settings +email.notification.vcsAccessTokenAdded.title = Ein neues VCS Zugriffstoken wurde zu deinem Benutzerkonto hinzugefügt. +email.notification.vcsAccessTokenAdded.ifMistake = Wenn du glaubst, dass dieses Zugriffstoken irrtümlich hinzugefügt wurde, kannst du es entfernen und den Zugriff an folgender Stelle deaktivieren: +email.notification.vcsAccessTokenExpiry.title = Your personal VCS access token has expired. +email.notification.vcsAccessTokenExpiry.renew = You can renew it here: + # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there artemisApp.groupNotification.title.attachmentChange = Anhang aktualisiert @@ -166,3 +172,5 @@ artemisApp.singleUserNotification.title.dataExportFailed = Dein Artemis Datenexp artemisApp.singleUserNotification.title.sshKeyAdded = Neuer SSH-Schlüssel hinzugefügt artemisApp.singleUserNotification.title.sshKeyExpiresSoon = SSH-Schlüssel läuft bald ab artemisApp.singleUserNotification.title.sshKeyHasExpired = SSH-Schlüssel ist abgelaufen +artemisApp.singleUserNotification.title.vcsAccessTokenAdded = Neues VCS Zugriffstoken hinzugefügt +artemisApp.singleUserNotification.title.vcsAccessTokenExpired = VCS Zugriffstoken ist abgelaufen diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties index 01e56d22ce6a..f3acae3d9d6c 100644 --- a/src/main/resources/i18n/messages_en.properties +++ b/src/main/resources/i18n/messages_en.properties @@ -137,6 +137,12 @@ email.notification.sshKeyExpiry.renew = You can renew your SSH key here: email.notification.sshKeyExpiry.sshKeyHash = SSH key hash: email.notification.sshKeyExpiry.sshKeyLabel = SSH key label: +# VCS Token Settings +email.notification.vcsAccessTokenAdded.title = A new VCS access token was added to your account. +email.notification.vcsAccessTokenAdded.ifMistake = If you believe this token was added in error, you can remove it and disable access at the following location: +email.notification.vcsAccessTokenExpiry.title = Your personal VCS access token has expired. +email.notification.vcsAccessTokenExpiry.renew = You can renew it here: + # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there artemisApp.groupNotification.title.attachmentChange = Attachment updated @@ -165,3 +171,5 @@ artemisApp.singleUserNotification.title.dataExportFailed = Your Artemis data exp artemisApp.singleUserNotification.title.sshKeyAdded = New SSH key added to account artemisApp.singleUserNotification.title.sshKeyExpiresSoon = SSH key expires soon artemisApp.singleUserNotification.title.sshKeyHasExpired = SSH key has expired +artemisApp.singleUserNotification.title.vcsAccessTokenAdded = New VCS access token added to account +artemisApp.singleUserNotification.title.vcsAccessTokenExpired = VCS access token has expired diff --git a/src/main/webapp/i18n/de/notification.json b/src/main/webapp/i18n/de/notification.json index 0f5dd8ff6261..d98ca682a349 100644 --- a/src/main/webapp/i18n/de/notification.json +++ b/src/main/webapp/i18n/de/notification.json @@ -71,7 +71,9 @@ "courseArchiveFailed": "Kursarchivierung fehlgeschlagen", "examArchiveStarted": "Klausurarchivierung gestartet", "examArchiveFinished": "Klausurarchivierung beendet", - "examArchiveFailed": "Klausurarchivierung fehlgeschlagen" + "examArchiveFailed": "Klausurarchivierung fehlgeschlagen", + "vcsAccessTokenAdded": "Neues VCS Zugriffstoken hinzugefügt", + "vcsAccessTokenExpired": "VCS Zugriffstoken abgelaufen" }, "text": { "attachmentChange": "Der Anhang \"{{ placeholderValues.1 }}\" von \"{{ placeholderValues.2 }}\" im Kurs \"{{ placeholderValues.0}}\" wurde aktualisiert.", @@ -97,7 +99,9 @@ "examArchiveStarted": "Die Klausur \"{{ placeholderValues.1 }}\" wird archiviert.", "examArchiveFinishedWithoutErrors": "Die Klausur \"{{ placeholderValues.1 }}\" wurde archiviert.", "examArchiveFinishedWithErrors": "Die Klausur \"{{ placeholderValues.1 }}\" wurde archiviert. Einige Aufgaben konnten nicht in das Archiv aufgenommen werden:

{{ placeholderValues.2 }}", - "examArchiveFailed": "Es gab ein Problem beim Archivieren der Klausur \"{{ placeholderValues.1 }}\":

{{ placeholderValues.2 }}" + "examArchiveFailed": "Es gab ein Problem beim Archivieren der Klausur \"{{ placeholderValues.1 }}\":

{{ placeholderValues.2 }}", + "vcsAccessTokenAdded": "Du hast erfolgreich ein persönliches Zugriffstoken hinzugefügt.", + "vcsAccessTokenExpired": "Dein persönliches Zugriffstoken ist abgelaufen." } }, "singleUserNotification": { diff --git a/src/main/webapp/i18n/en/notification.json b/src/main/webapp/i18n/en/notification.json index e4ba3381e92c..3a3b27b699e6 100644 --- a/src/main/webapp/i18n/en/notification.json +++ b/src/main/webapp/i18n/en/notification.json @@ -126,7 +126,9 @@ "dataExportFailed": "Data export failed", "sshKeyAdded": "New SSH key added", "sshKeyExpiresSoon": "SSH key will expire soon", - "sshKeyHasExpired": "SSH key has expired" + "sshKeyHasExpired": "SSH key has expired", + "vcsAccessTokenAdded": "New VCS access token added", + "vcsAccessTokenExpired": "VCS access token has expired" }, "text": { "newReplyForExercisePost": "Your post regarding exercise \"{{ placeholderValues.8 }}\" in the course \"{{ placeholderValues.0 }}\" got a new reply: \"{{ placeholderValues.5 }}\"", @@ -156,7 +158,9 @@ "dataExportFailed": "Your data export could not be created. Please try again later.", "sshKeyAdded": "You have successfully added a new SSH key", "sshKeyExpiresSoon": "Your SSH key with the label \"{{ placeholderValues.0}}\" will expire on {{ placeholderValues.1 }}.", - "sshKeyHasExpired": "Your SSH key with the label \"{{ placeholderValues.0}}\" has expired on {{ placeholderValues.1 }}." + "sshKeyHasExpired": "Your SSH key with the label \"{{ placeholderValues.0}}\" has expired on {{ placeholderValues.1 }}.", + "vcsAccessTokenAdded": "You have successfully added a new VCS access token", + "vcsAccessTokenExpired": "Your personal version control access token has expired." } }, "tutorialGroupNotification": { From 773f84c2be92f274dd5bcc0abaf764117fc55790 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 21 Dec 2024 12:18:35 +0100 Subject: [PATCH 2/6] added email templates --- .../vcsAccessTokenAddedEmail.html | 28 +++++++++++++++++++ .../vcsAccessTokenExpiredEmail.html | 28 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/main/resources/templates/mail/notification/vcsAccessTokenAddedEmail.html create mode 100644 src/main/resources/templates/mail/notification/vcsAccessTokenExpiredEmail.html diff --git a/src/main/resources/templates/mail/notification/vcsAccessTokenAddedEmail.html b/src/main/resources/templates/mail/notification/vcsAccessTokenAddedEmail.html new file mode 100644 index 000000000000..54a6a2c8dd7e --- /dev/null +++ b/src/main/resources/templates/mail/notification/vcsAccessTokenAddedEmail.html @@ -0,0 +1,28 @@ + + + + + + + + + +
+ + +

+ The following SSH key was added to your account: +

+ +

+ If adding it was by mistake, remove it here: + + Login + link +

+ + +
+ + + diff --git a/src/main/resources/templates/mail/notification/vcsAccessTokenExpiredEmail.html b/src/main/resources/templates/mail/notification/vcsAccessTokenExpiredEmail.html new file mode 100644 index 000000000000..71194367aa37 --- /dev/null +++ b/src/main/resources/templates/mail/notification/vcsAccessTokenExpiredEmail.html @@ -0,0 +1,28 @@ + + + + + + + + + +
+ + +

+ Your VCS access token has expired +

+ +

+ You can renew it here + + Login + link +

+ + +
+ + + From 62980dde4ca856e0b349dd865fe042d3199b7eb4 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 21 Dec 2024 12:18:45 +0100 Subject: [PATCH 3/6] added tests --- .../SingleUserNotificationServiceTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java index 0dbdfae61e53..956696bfd53b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java @@ -34,6 +34,8 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_TUTOR_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_UNASSIGNED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_UNASSIGNED_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_ADDED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRED_TEXT; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsService.NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsService.NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_FAILED; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsService.NOTIFICATION__EXERCISE_NOTIFICATION__EXERCISE_SUBMISSION_ASSESSED; @@ -111,6 +113,7 @@ import de.tum.cit.aet.artemis.programming.dto.UserSshPublicKeyDTO; import de.tum.cit.aet.artemis.programming.service.sshuserkeys.UserSshPublicKeyExpiryNotificationService; import de.tum.cit.aet.artemis.programming.service.sshuserkeys.UserSshPublicKeyService; +import de.tum.cit.aet.artemis.programming.service.tokens.UserTokenExpiryNotificationService; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; import de.tum.cit.aet.artemis.text.domain.TextExercise; import de.tum.cit.aet.artemis.text.util.TextExerciseFactory; @@ -147,6 +150,9 @@ class SingleUserNotificationServiceTest extends AbstractSpringIntegrationIndepen @Autowired private UserSshPublicKeyExpiryNotificationService userSshPublicKeyExpiryNotificationService; + @Autowired + private UserTokenExpiryNotificationService userTokenExpiryNotificationService; + @Autowired private UserSshPublicKeyService userSshPublicKeyService; @@ -503,6 +509,58 @@ void checkFirstNotification() { } } + // User VCS access token related (expiry warning and newly added token) + + @Nested + class UserTokenExpiryNotification { + + List sentNotifications; + + @AfterEach + void tearDown() throws Exception { + user.setVcsAccessTokenExpiryDate(null); + user.setVcsAccessToken(null); + userTestRepository.save(user); + } + + @Test + void shouldNotifyUserAboutNewlyAddedVcsAccessToken() { + singleUserNotificationService.notifyUserAboutNewlyAddedVcsAccessToken(user); + + sentNotifications = notificationRepository.findAll(); + assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.getFirst()).getText()).isEqualTo(VCS_ACCESS_TOKEN_ADDED_TEXT); + } + + @Test + void shouldNotifyUserAboutExpiredVcsAccessToken() { + user.setVcsAccessToken("token"); + user.setVcsAccessTokenExpiryDate(ZonedDateTime.now().minusHours(5)); + userTestRepository.save(user); + + userTokenExpiryNotificationService.sendTokenExpirationNotifications(); + + sentNotifications = notificationRepository.findAll(); + assertThat(sentNotifications).hasSize(1); + assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.getFirst()).getText()).isEqualTo(VCS_ACCESS_TOKEN_EXPIRED_TEXT); + } + + @Test + void shouldNotNotifyUserAboutVcsAccessTokenExpiryWhenTokenIsNotExpired() { + user.setVcsAccessToken("token"); + user.setVcsAccessTokenExpiryDate(ZonedDateTime.now().plusDays(5)); + userTestRepository.save(user); + + userTokenExpiryNotificationService.sendTokenExpirationNotifications(); + + sentNotifications = notificationRepository.findAll(); + assertThat(sentNotifications).hasSize(0); + } + } + // Plagiarism related /** From 17cff21522041e7f70bae04ee6474ffe1d0ae77a Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 21 Dec 2024 12:19:23 +0100 Subject: [PATCH 4/6] added Notifications for new tokens and token expirz --- .../domain/NotificationType.java | 2 +- .../notification/NotificationConstants.java | 13 +++- .../SingleUserNotificationFactory.java | 25 ++++++++ .../service/notifications/MailService.java | 2 + .../NotificationSettingsService.java | 16 ++++- .../SingleUserNotificationService.java | 23 ++++++- .../core/repository/UserRepository.java | 2 + .../aet/artemis/core/web/AccountResource.java | 9 ++- .../UserTokenExpiryNotificationService.java | 64 +++++++++++++++++++ 9 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java index 2a85412663bb..b05165088c28 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java @@ -10,5 +10,5 @@ public enum NotificationType { TUTORIAL_GROUP_UNASSIGNED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, CONVERSATION_USER_MENTIONED, CONVERSATION_CREATE_ONE_TO_ONE_CHAT, CONVERSATION_CREATE_GROUP_CHAT, CONVERSATION_ADD_USER_GROUP_CHAT, CONVERSATION_ADD_USER_CHANNEL, CONVERSATION_REMOVE_USER_GROUP_CHAT, CONVERSATION_REMOVE_USER_CHANNEL, CONVERSATION_DELETE_CHANNEL, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_BUILD_RUN_UPDATE, SSH_KEY_ADDED, SSH_KEY_EXPIRES_SOON, - SSH_KEY_HAS_EXPIRED + SSH_KEY_HAS_EXPIRED, VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_EXPIRED } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java index b9c8b651c3e3..eb1e60262243 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java @@ -55,6 +55,8 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_TUTOR; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UNASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UPDATED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRED; import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; @@ -156,6 +158,10 @@ public class NotificationConstants { public static final String SSH_KEY_HAS_EXPIRED_TITLE = "artemisApp.singleUserNotification.title.sshKeyHasExpired"; + public static final String VCS_ACCESS_TOKEN_ADDED_TITLE = "artemisApp.singleUserNotification.title.vcsAccessTokenAdded"; + + public static final String VCS_ACCESS_TOKEN_EXPIRED_TITLE = "artemisApp.singleUserNotification.title.vcsAccessTokenExpired"; + // Texts public static final String LIVE_EXAM_EXERCISE_UPDATE_NOTIFICATION_TEXT = "artemisApp.groupNotification.text.liveExamExerciseUpdate"; @@ -295,6 +301,10 @@ public class NotificationConstants { public static final String SSH_KEY_HAS_EXPIRED_TEXT = "artemisApp.singleUserNotification.text.sshKeyHasExpired"; + public static final String VCS_ACCESS_TOKEN_ADDED_TEXT = "artemisApp.singleUserNotification.text.vcsAccessTokenAdded"; + + public static final String VCS_ACCESS_TOKEN_EXPIRED_TEXT = "artemisApp.singleUserNotification.text.vcsAccessTokenExpired"; + // bidirectional map private static final BiMap NOTIFICATION_TYPE_AND_TITLE_MAP = new ImmutableBiMap.Builder() .put(EXERCISE_SUBMISSION_ASSESSED, EXERCISE_SUBMISSION_ASSESSED_TITLE).put(ATTACHMENT_CHANGE, ATTACHMENT_CHANGE_TITLE).put(EXERCISE_RELEASED, EXERCISE_RELEASED_TITLE) @@ -321,7 +331,8 @@ public class NotificationConstants { .put(CONVERSATION_REMOVE_USER_CHANNEL, CONVERSATION_REMOVE_USER_CHANNEL_TITLE).put(CONVERSATION_DELETE_CHANNEL, CONVERSATION_DELETE_CHANNEL_TITLE) .put(DATA_EXPORT_CREATED, DATA_EXPORT_CREATED_TITLE).put(DATA_EXPORT_FAILED, DATA_EXPORT_FAILED_TITLE) .put(PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_REPOSITORY_LOCKS_TITLE).put(PROGRAMMING_BUILD_RUN_UPDATE, PROGRAMMING_BUILD_RUN_UPDATE_TITLE) - .put(SSH_KEY_ADDED, SSH_KEY_ADDED_TITLE).put(SSH_KEY_EXPIRES_SOON, SSH_KEY_EXPIRES_SOON_TITLE).put(SSH_KEY_HAS_EXPIRED, SSH_KEY_HAS_EXPIRED_TITLE).build(); + .put(SSH_KEY_ADDED, SSH_KEY_ADDED_TITLE).put(SSH_KEY_EXPIRES_SOON, SSH_KEY_EXPIRES_SOON_TITLE).put(SSH_KEY_HAS_EXPIRED, SSH_KEY_HAS_EXPIRED_TITLE) + .put(VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_ADDED_TITLE).put(VCS_ACCESS_TOKEN_EXPIRED, VCS_ACCESS_TOKEN_EXPIRED_TITLE).build(); /** * Finds the corresponding NotificationType for the provided notification title diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java index 89df0016e4da..deb80dcdcca3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java @@ -64,6 +64,10 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_STUDENT_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_TUTOR_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_UNASSIGNED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_ADDED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_ADDED_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRED_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.findCorrespondingNotificationTitleOrThrow; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createConversationCreationTarget; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createConversationDeletionTarget; @@ -191,6 +195,27 @@ public static SingleUserNotification createNotification(UserSshPublicKey key, No } } + /** + * Creates a user notification based on the given SSH key and notification type. + * + * @param vcsAccessToken The access token of the user + * @param notificationType The type of notification to create (e.g., key added, expiring, or expired). + * @param recipient The user who will receive the notification. + * @return A configured {@link SingleUserNotification}. + * @throws UnsupportedOperationException if the notification type is unsupported. + */ + public static SingleUserNotification createNotification(String vcsAccessToken, NotificationType notificationType, User recipient) { + switch (notificationType) { + case VCS_ACCESS_TOKEN_ADDED -> { + return new SingleUserNotification(recipient, VCS_ACCESS_TOKEN_ADDED_TITLE, VCS_ACCESS_TOKEN_ADDED_TEXT, true, new String[] {}); + } + case VCS_ACCESS_TOKEN_EXPIRED -> { + return new SingleUserNotification(recipient, VCS_ACCESS_TOKEN_EXPIRED_TITLE, VCS_ACCESS_TOKEN_EXPIRED_TEXT, true, new String[] {}); + } + default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType); + } + } + /** * Creates an instance of SingleUserNotification based on plagiarisms. * diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java index 2d7b4214aa58..8ea46c7998d2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java @@ -408,6 +408,8 @@ private String createContentForNotificationEmailByType(NotificationType notifica case SSH_KEY_ADDED -> templateEngine.process("mail/notification/sshKeyAddedEmail", context); case SSH_KEY_EXPIRES_SOON -> templateEngine.process("mail/notification/sshKeyExpiresSoonEmail", context); case SSH_KEY_HAS_EXPIRED -> templateEngine.process("mail/notification/sshKeyHasExpiredEmail", context); + case VCS_ACCESS_TOKEN_ADDED -> templateEngine.process("mail/notification/vcsAccessTokenAddedEmail", context); + case VCS_ACCESS_TOKEN_EXPIRED -> templateEngine.process("mail/notification/vcsAccessTokenExpiredEmail", context); default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType); }; diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java index c953c3700dae..b5bf3adf9aac 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java @@ -44,6 +44,8 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_TUTOR; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UNASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UPDATED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRED; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.findCorrespondingNotificationType; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; @@ -144,6 +146,10 @@ public class NotificationSettingsService { public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED = "notification.user-notification.ssh-key-has-expired"; + public static final String NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_ADDED = "notification.user-notification.vcs-access-token-added"; + + public static final String NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRED = "notification.user-notification.vcs-access-token-expired"; + // if webapp or email is not explicitly set for a specific setting -> no support for this communication channel for this setting // this has to match the properties in the notification settings structure file on the client that hides the related UI elements public static final Set DEFAULT_NOTIFICATION_SETTINGS = new HashSet<>(Arrays.asList( @@ -188,7 +194,9 @@ public class NotificationSettingsService { new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED), new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED), new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON), - new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED))); + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED), + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_ADDED), + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRED))); /** * This is the place where the mapping between SettingId and NotificationTypes happens on the server side @@ -225,7 +233,9 @@ public class NotificationSettingsService { Map.entry(NOTIFICATION__USER_NOTIFICATION__USER_MENTION, new NotificationType[] { CONVERSATION_USER_MENTIONED }), Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED, new NotificationType[] { SSH_KEY_ADDED }), Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON, new NotificationType[] { SSH_KEY_EXPIRES_SOON }), - Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED, new NotificationType[] { SSH_KEY_HAS_EXPIRED })); + Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED, new NotificationType[] { SSH_KEY_HAS_EXPIRED }), + Map.entry(NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_ADDED, new NotificationType[] { VCS_ACCESS_TOKEN_ADDED }), + Map.entry(NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRED, new NotificationType[] { VCS_ACCESS_TOKEN_EXPIRED })); // This set has to equal the UI configuration in the client notification settings structure file! // More information on supported notification types can be found here: https://docs.artemis.cit.tum.de/user/notifications/ @@ -236,7 +246,7 @@ public class NotificationSettingsService { TUTORIAL_GROUP_DEREGISTRATION_STUDENT, TUTORIAL_GROUP_DEREGISTRATION_TUTOR, TUTORIAL_GROUP_DELETED, TUTORIAL_GROUP_UPDATED, TUTORIAL_GROUP_ASSIGNED, TUTORIAL_GROUP_UNASSIGNED, NEW_EXERCISE_POST, NEW_LECTURE_POST, NEW_REPLY_FOR_LECTURE_POST, NEW_COURSE_POST, NEW_REPLY_FOR_COURSE_POST, NEW_REPLY_FOR_EXERCISE_POST, QUIZ_EXERCISE_STARTED, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, SSH_KEY_ADDED, SSH_KEY_EXPIRES_SOON, - SSH_KEY_HAS_EXPIRED); + SSH_KEY_HAS_EXPIRED, VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_EXPIRED); // More information on supported notification types can be found here: https://docs.artemis.cit.tum.de/user/notifications/ // Please adapt the above docs if you change the supported notification types diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java index f052c6362dea..06382c8dd458 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java @@ -23,6 +23,8 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_STUDENT; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_TUTOR; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UNASSIGNED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRED; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.CONVERSATION_ADD_USER_CHANNEL_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.CONVERSATION_ADD_USER_GROUP_CHAT_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.CONVERSATION_CREATE_GROUP_CHAT_TITLE; @@ -150,6 +152,7 @@ private SingleUserNotification createSingleUserNotification(Object notificationS ((NewReplyNotificationSubject) notificationSubject).responsibleUser); case DATA_EXPORT_CREATED, DATA_EXPORT_FAILED -> createNotification((DataExport) notificationSubject, notificationType, typeSpecificInformation); case SSH_KEY_ADDED, SSH_KEY_EXPIRES_SOON, SSH_KEY_HAS_EXPIRED -> createNotification((UserSshPublicKey) notificationSubject, notificationType, typeSpecificInformation); + case VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_EXPIRED -> createNotification(typeSpecificInformation.getVcsAccessToken(), notificationType, typeSpecificInformation); default -> throw new UnsupportedOperationException("Can not create notification for type : " + notificationType); }; } @@ -282,7 +285,7 @@ public void notifyUserAboutSoonExpiringSshKey(User recipient, UserSshPublicKey k } /** - * Notify user about an upcoming expiry of an SSH key + * Notify user about the expiration of an SSH key * * @param recipient the user to whose account the SSH key was added * @param key the key which was added @@ -291,6 +294,24 @@ public void notifyUserAboutExpiredSshKey(User recipient, UserSshPublicKey key) { notifyRecipientWithNotificationType(key, SSH_KEY_HAS_EXPIRED, recipient, null); } + /** + * Notify user about the addition of a VCS access token + * + * @param recipient the user to whose account the SSH key was added + */ + public void notifyUserAboutNewlyAddedVcsAccessToken(User recipient) { + notifyRecipientWithNotificationType(null, VCS_ACCESS_TOKEN_ADDED, recipient, null); + } + + /** + * Notify user about the expiration of the VCS access token + * + * @param recipient the user to whose account the SSH key was added + */ + public void notifyUserAboutExpiredVcsAccessToken(User recipient) { + notifyRecipientWithNotificationType(null, VCS_ACCESS_TOKEN_EXPIRED, recipient, null); + } + /** * Notify student about possible plagiarism case. * diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java index e2a8206a1b78..5846c4c449a2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java @@ -85,6 +85,8 @@ public interface UserRepository extends ArtemisJpaRepository, JpaSpe Optional findOneByEmailIgnoreCase(String email); + List findByvcsAccessTokenExpiryDateBetween(ZonedDateTime from, ZonedDateTime to); + @EntityGraph(type = LOAD, attributePaths = { "groups" }) Optional findOneWithGroupsByEmailIgnoreCase(String email); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 16c94629047c..8b8989827483 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.dto.PasswordChangeDTO; import de.tum.cit.aet.artemis.core.dto.UserDTO; @@ -64,15 +65,18 @@ public class AccountResource { private final FileService fileService; + private final SingleUserNotificationService singleUserNotificationService; + private static final float MAX_PROFILE_PICTURE_FILESIZE_IN_MEGABYTES = 0.1f; - public AccountResource(UserRepository userRepository, UserService userService, UserCreationService userCreationService, AccountService accountService, - FileService fileService) { + public AccountResource(UserRepository userRepository, UserService userService, UserCreationService userCreationService, AccountService accountService, FileService fileService, + SingleUserNotificationService singleUserNotificationService) { this.userRepository = userRepository; this.userService = userService; this.userCreationService = userCreationService; this.accountService = accountService; this.fileService = fileService; + this.singleUserNotificationService = singleUserNotificationService; } /** @@ -140,6 +144,7 @@ public ResponseEntity createVcsAccessToken(@RequestParam("expiryDate") userRepository.updateUserVcsAccessToken(user.getId(), LocalVCPersonalAccessTokenManagementService.generateSecureVCSAccessToken(), expiryDate); log.debug("Successfully created a VCS access token for user {}", user.getLogin()); + singleUserNotificationService.notifyUserAboutNewlyAddedVcsAccessToken(user); user = userRepository.getUser(); UserDTO userDTO = new UserDTO(); userDTO.setLogin(user.getLogin()); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java new file mode 100644 index 000000000000..f5759b4c368a --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java @@ -0,0 +1,64 @@ +package de.tum.cit.aet.artemis.programming.service.tokens; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; +import static java.time.ZonedDateTime.now; + +import java.time.ZonedDateTime; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService; +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.repository.UserRepository; + +@Profile(PROFILE_SCHEDULING) +@Service +public class UserTokenExpiryNotificationService { + + private static final Logger log = LoggerFactory.getLogger(UserTokenExpiryNotificationService.class); + + private final SingleUserNotificationService singleUserNotificationService; + + private final UserRepository userRepository; + + public UserTokenExpiryNotificationService(SingleUserNotificationService singleUserNotificationService, UserRepository userRepository) { + this.singleUserNotificationService = singleUserNotificationService; + this.userRepository = userRepository; + } + + /** + * Schedules VCS access token expiry notifications to users every morning at 6:00:00 am + */ + @Scheduled(cron = "0 * * * * *") + public void sendTokenExpirationNotifications() { + log.info("Sending Token expiration notifications to single user"); + notifyOnExpiredToken(); + } + + /** + * Notifies the user at the day of key expiry, that the key has expired + */ + public void notifyOnExpiredToken() { + notifyUsersForKeyExpiryWindow(now().minusDays(1), now(), (user) -> { + singleUserNotificationService.notifyUserAboutExpiredVcsAccessToken(user); + return null; + }); + } + + /** + * Notifies users whose SSH keys are expiring within the specified date range, with the notification specified by the + * notifyFunction + * + * @param fromDate the start of the expiry date range + * @param toDate the end of the expiry date range + * @param notifyFunction a function to handle user notification + */ + private void notifyUsersForKeyExpiryWindow(ZonedDateTime fromDate, ZonedDateTime toDate, Function notifyFunction) { + userRepository.findByvcsAccessTokenExpiryDateBetween(fromDate, toDate).forEach(notifyFunction::apply); + } +} From ce143013975722dd2e34220d29809e8652f4d7d6 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 21 Dec 2024 12:32:51 +0100 Subject: [PATCH 5/6] changed cron job task back to 6am --- .../service/tokens/UserTokenExpiryNotificationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java index f5759b4c368a..20753cf486bf 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/tokens/UserTokenExpiryNotificationService.java @@ -34,7 +34,7 @@ public UserTokenExpiryNotificationService(SingleUserNotificationService singleUs /** * Schedules VCS access token expiry notifications to users every morning at 6:00:00 am */ - @Scheduled(cron = "0 * * * * *") + @Scheduled(cron = "0 0 6 * * *") public void sendTokenExpirationNotifications() { log.info("Sending Token expiration notifications to single user"); notifyOnExpiredToken(); From 5bf95eafeb8b582493033ee437239838eceb00a5 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 22 Dec 2024 09:10:47 +0100 Subject: [PATCH 6/6] fix notification translation --- src/main/webapp/i18n/de/notification.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/i18n/de/notification.json b/src/main/webapp/i18n/de/notification.json index d98ca682a349..51f46fc033a0 100644 --- a/src/main/webapp/i18n/de/notification.json +++ b/src/main/webapp/i18n/de/notification.json @@ -71,9 +71,7 @@ "courseArchiveFailed": "Kursarchivierung fehlgeschlagen", "examArchiveStarted": "Klausurarchivierung gestartet", "examArchiveFinished": "Klausurarchivierung beendet", - "examArchiveFailed": "Klausurarchivierung fehlgeschlagen", - "vcsAccessTokenAdded": "Neues VCS Zugriffstoken hinzugefügt", - "vcsAccessTokenExpired": "VCS Zugriffstoken abgelaufen" + "examArchiveFailed": "Klausurarchivierung fehlgeschlagen" }, "text": { "attachmentChange": "Der Anhang \"{{ placeholderValues.1 }}\" von \"{{ placeholderValues.2 }}\" im Kurs \"{{ placeholderValues.0}}\" wurde aktualisiert.", @@ -99,9 +97,7 @@ "examArchiveStarted": "Die Klausur \"{{ placeholderValues.1 }}\" wird archiviert.", "examArchiveFinishedWithoutErrors": "Die Klausur \"{{ placeholderValues.1 }}\" wurde archiviert.", "examArchiveFinishedWithErrors": "Die Klausur \"{{ placeholderValues.1 }}\" wurde archiviert. Einige Aufgaben konnten nicht in das Archiv aufgenommen werden:

{{ placeholderValues.2 }}", - "examArchiveFailed": "Es gab ein Problem beim Archivieren der Klausur \"{{ placeholderValues.1 }}\":

{{ placeholderValues.2 }}", - "vcsAccessTokenAdded": "Du hast erfolgreich ein persönliches Zugriffstoken hinzugefügt.", - "vcsAccessTokenExpired": "Dein persönliches Zugriffstoken ist abgelaufen." + "examArchiveFailed": "Es gab ein Problem beim Archivieren der Klausur \"{{ placeholderValues.1 }}\":

{{ placeholderValues.2 }}" } }, "singleUserNotification": { @@ -130,7 +126,9 @@ "dataExportFailed": "Datenexport fehlgeschlagen", "sshKeyAdded": "Neuer SSH-Schlüssel hinzugefügt", "sshKeyExpiresSoon": "SSH-Schlüssel läuft bald ab", - "sshKeyHasExpired": "SSH-Schlüssel ist abgelaufen" + "sshKeyHasExpired": "SSH-Schlüssel ist abgelaufen", + "vcsAccessTokenAdded": "Neues VCS Zugriffstoken hinzugefügt", + "vcsAccessTokenExpired": "VCS Zugriffstoken abgelaufen" }, "text": { "newReplyForExercisePost": "Auf deinen Beitrag zur Aufgabe \"{{ placeholderValues.8 }}\" im Kurs \"{{ placeholderValues.0 }}\" wurde geantwortet: \"{{ placeholderValues.5 }}\"", @@ -160,7 +158,9 @@ "dataExportFailed": "Dein Datenexport konnte nicht erstellt werden. Bitte versuche es später erneut.", "sshKeyAdded": "Du hast erfolgreich einen SSH-Schlüssel hinzugefügt", "sshKeyExpiresSoon": "Dein SSH-Schlüssel mit dem Label \"{{ placeholderValues.0}}\" läuft am {{ placeholderValues.1 }} ab.", - "sshKeyHasExpired": "Dein SSH-Schlüssel mit dem Label \"{{ placeholderValues.0}}\" ist am {{ placeholderValues.1 }} abgelaufen." + "sshKeyHasExpired": "Dein SSH-Schlüssel mit dem Label \"{{ placeholderValues.0}}\" ist am {{ placeholderValues.1 }} abgelaufen.", + "vcsAccessTokenAdded": "Du hast erfolgreich ein persönliches Zugriffstoken hinzugefügt.", + "vcsAccessTokenExpired": "Dein persönliches Zugriffstoken ist abgelaufen." } }, "tutorialGroupNotification": {