From 934f76ae2892bad984f85b1b0c82cccb474ce3c4 Mon Sep 17 00:00:00 2001 From: Parth097 Date: Mon, 28 Jul 2025 15:32:09 +0100 Subject: [PATCH 01/14] Created privacy field for API to update the database --- .../cam/cl/dtg/isaac/api/IsaacController.java | 22 ++++++++++ .../dtg/isaac/dos/users/RegisteredUser.java | 23 +++++++++++ .../isaac/dto/users/RegisteredUserDTO.java | 28 ++++++++++++- .../users/UserSummaryForAdminUsersDTO.java | 20 +++++++++ .../cam/cl/dtg/isaac/mappers/UserMapper.java | 1 + .../api/managers/UserAccountManager.java | 11 +++++ .../dtg/segue/dao/users/IUserDataManager.java | 8 ++++ .../cam/cl/dtg/segue/dao/users/PgUsers.java | 41 ++++++++++++++++--- .../segue/api/managers/UserManagerTest.java | 4 +- 9 files changed, 149 insertions(+), 9 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java index 1072bd3808..ab698654cf 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java @@ -43,8 +43,10 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; @@ -489,6 +491,26 @@ public final Response getCurrentUserSnapshot(@Context final HttpServletRequest r return Response.ok(userSnapshot).cacheControl(getCacheControl(NEVER_CACHE_WITHOUT_ETAG_CHECK, false)).build(); } + @POST + @Path("/users/accept-privacy-policy") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @GZIP + @Operation(summary = "Accept the privacy policy for the current user.") + public Response acceptPrivacyPolicy(@Context final HttpServletRequest request) { + try { + RegisteredUserDTO user = userManager.getCurrentRegisteredUser(request); + userManager.updatePrivacyPolicyAcceptedTime(user); + log.info("User " + user.getEmail() + " accepted privacy policy"); + return Response.ok().build(); + } catch (NoUserLoggedInException e) { + return SegueErrorResponse.getNotLoggedInResponse(); + } catch (SegueDatabaseException e) { + log.error("Database error during privacy policy acceptance", e); + return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Database error").toResponse(); + } + } + /** * Get some statistics out of how many questions the user has completed. *
diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java index 163a6a4252..6f4561e303 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java @@ -46,6 +46,7 @@ public class RegisteredUser extends AbstractSegueUser { private Instant lastUpdated; private Instant lastSeen; + private Instant privacyPolicyAcceptedTime; /** * Full constructor for the User object. @@ -59,6 +60,7 @@ public class RegisteredUser extends AbstractSegueUser { * @param gender gender of the user * @param registrationDate date of registration * @param lastUpdated the date this user was last updated. + * @param privacyPolicyAcceptedTime the date this user accepted the privacy policy. * @param emailToVerify the most recent email for which a token has been generated * @param emailVerificationToken the most recent token generated to verify email addresses * @param emailVerificationStatus whether the user has verified their email or not @@ -72,6 +74,7 @@ public RegisteredUser( @JsonProperty("dateOfBirth") final Instant dateOfBirth, @JsonProperty("gender") final Gender gender, @JsonProperty("registrationDate") final Instant registrationDate, @JsonProperty("lastUpdated") final Instant lastUpdated, + @JsonProperty("privacyPolicyAcceptedTime") final Instant privacyPolicyAcceptedTime, @JsonProperty("emailToVerify") final String emailToVerify, @JsonProperty("emailVerificationToken") final String emailVerificationToken, @JsonProperty("emailVerificationStatus") final EmailVerificationStatus emailVerificationStatus, @@ -86,6 +89,7 @@ public RegisteredUser( this.gender = gender; this.registrationDate = registrationDate; this.lastUpdated = lastUpdated; + this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; this.emailToVerify = emailToVerify; this.emailVerificationToken = emailVerificationToken; this.emailVerificationStatus = emailVerificationStatus; @@ -375,6 +379,24 @@ public void setLastUpdated(final Instant lastUpdated) { this.lastUpdated = lastUpdated; } + /** + * Gets the privacyPolicyAcceptedTime. + * + * @return the privacyPolicyAcceptedTime + */ + public Instant getPrivacyPolicyAcceptedTime() { + return privacyPolicyAcceptedTime; + } + + /** + * Sets the privacyPolicyAcceptedTime. + * + * @param privacyPolicyAcceptedTime the privacyPolicyAcceptedTime to set + */ + public void setPrivacyPolicyAcceptedTime(final Instant privacyPolicyAcceptedTime) { + this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; + } + /** * Gets the lastSeen. * @@ -474,6 +496,7 @@ public String toString() { + ", emailVerificationStatus=" + emailVerificationStatus + ", teacherPending=" + teacherPending + ", lastUpdated=" + lastUpdated + + ", privacyPolicyAcceptedTime=" + privacyPolicyAcceptedTime + ", lastSeen=" + lastSeen + '}'; } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java index ce455ba335..732d750866 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java @@ -46,6 +46,7 @@ public class RegisteredUserDTO extends AbstractSegueUserDTO { private boolean firstLogin = false; private Instant lastUpdated; + private Instant privacyPolicyAcceptedTime; private Instant lastSeen; private EmailVerificationStatus emailVerificationStatus; private Boolean teacherPending; @@ -357,6 +358,24 @@ public void setLastUpdated(final Instant lastUpdated) { this.lastUpdated = lastUpdated; } + /** + * Gets the privacyPolicyAcceptedTime. + * + * @return the privacyPolicyAcceptedTime + */ + public Instant getPrivacyPolicyAcceptedTime() { + return privacyPolicyAcceptedTime; + } + + /** + * Sets the privacyPolicyAcceptedTime. + * + * @param privacyPolicyAcceptedTime the privacyPolicyAcceptedTime to set + */ + public void setPrivacyPolicyAcceptedTime(final Instant privacyPolicyAcceptedTime) { + this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; + } + /** * Gets the lastSeen. * @@ -509,7 +528,13 @@ public boolean strictEquals(final Object obj) { } else if (!lastUpdated.equals(other.lastUpdated)) { return false; } - + if (privacyPolicyAcceptedTime == null) { + if (other.privacyPolicyAcceptedTime != null) { + return false; + } + } else if (!privacyPolicyAcceptedTime.equals(other.privacyPolicyAcceptedTime)) { + return false; + } if (registrationDate == null) { if (other.registrationDate != null) { return false; @@ -561,6 +586,7 @@ public String toString() { + ", teacherPending=" + teacherPending + ", firstLogin=" + firstLogin + ", lastUpdated=" + lastUpdated + + ", privacyPolicyAcceptedTime=" + privacyPolicyAcceptedTime + "]"; } } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java index 0179598abb..ecc7031ade 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java @@ -23,6 +23,7 @@ */ public class UserSummaryForAdminUsersDTO extends UserSummaryWithEmailAddressDTO { private Instant lastUpdated; + private Instant privacyPolicyAcceptedTime; private Instant lastSeen; private Instant registrationDate; private String schoolId; @@ -53,6 +54,24 @@ public void setLastUpdated(final Instant lastUpdated) { this.lastUpdated = lastUpdated; } + /** + * Gets the privacyPolicyAcceptedTime. + * + * @return the privacyPolicyAcceptedTime + */ + public Instant getPrivacyPolicyAcceptedTime() { + return privacyPolicyAcceptedTime; + } + + /** + * Sets the privacyPolicyAcceptedTime. + * + * @param privacyPolicyAcceptedTime the privacyPolicyAcceptedTime to set + */ + public void setPrivacyPolicyAcceptedTime(final Instant privacyPolicyAcceptedTime) { + this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; + } + /** * Gets the lastSeen. * @@ -129,6 +148,7 @@ public void setSchoolOther(final String schoolOther) { public String toString() { return "lastUpdated=" + "UserSummaryForAdminUsersDTO{" + lastUpdated + ", lastSeen=" + lastSeen + + ", privacyPolicyAcceptedTime=" + privacyPolicyAcceptedTime + ", registrationDate=" + registrationDate + ", schoolId=" + schoolId + ", schoolOther=" + schoolOther diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/mappers/UserMapper.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/mappers/UserMapper.java index a86418cc56..8b026b692e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/mappers/UserMapper.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/mappers/UserMapper.java @@ -180,6 +180,7 @@ default T map(UserSummaryDTO source, Class targetClass) { @Mapping(target = "registeredContextsLastConfirmed", ignore = true) @Mapping(target = "registeredContexts", ignore = true) @Mapping(target = "lastUpdated", ignore = true) + @Mapping(target = "privacyPolicyAcceptedTime", ignore = true) @Mapping(target = "lastSeen", ignore = true) @Mapping(target = "id", ignore = true) @Mapping(target = "emailVerificationToken", ignore = true) diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java index b16441853b..b84e0942bd 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java @@ -1985,6 +1985,17 @@ private void updateLastSeen(final RegisteredUser user) throws SegueDatabaseExcep } } + /** + * Update the users' last policy accepted field. + * + * @param userDTO of interest + * @throws SegueDatabaseException if an error occurs with the update. + */ + public void updatePrivacyPolicyAcceptedTime(final RegisteredUserDTO userDTO) throws SegueDatabaseException { + RegisteredUser user = findUserById(userDTO.getId()); + this.database.updatePrivacyPolicyAcceptedTime(user); + } + /** * Generate a verification link to be sent to a new user's email address. * diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/IUserDataManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/IUserDataManager.java index 1e3c57b76d..5b2897fe96 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/IUserDataManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/IUserDataManager.java @@ -251,6 +251,14 @@ boolean linkAuthProviderToAccount(RegisteredUser user, AuthenticationProvider pr */ void createSessionToken(RegisteredUser user, Integer newTokenValue) throws SegueDatabaseException; + /** + * Update the privacy policy acceptance time for a user. + * + * @param user - the user to update + * @throws SegueDatabaseException - if there is a database error + */ + void updatePrivacyPolicyAcceptedTime(RegisteredUser user) throws SegueDatabaseException; + /** * Update the session token of a user object in the data store to a randomly generated value. * diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java index 5de1ca14ad..115bc6ab6d 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java @@ -441,8 +441,9 @@ public List findUsers(final List usersToLocate) throws Seg } String inParams = String.join(",", Collections.nCopies(usersToLocate.size(), "?")); - String query = String.format( - "SELECT * FROM users WHERE id IN (%s) AND NOT deleted ORDER BY family_name, given_name", inParams); + String query = + String.format("SELECT * FROM users WHERE id IN (%s) AND NOT deleted ORDER BY family_name, given_name", + inParams); try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query)) { @@ -694,6 +695,28 @@ public void updateUserLastSeen(final RegisteredUser user, final Instant date) th } } + @Override + public void updatePrivacyPolicyAcceptedTime(final RegisteredUser user) throws SegueDatabaseException { + requireNonNull(user); + + String query = "UPDATE users SET updated_privacy_policy_accepted = ?, last_updated = ? WHERE id = ?"; + try (Connection conn = database.getDatabaseConnection(); + PreparedStatement pst = conn.prepareStatement(query) + ) { + Instant now = Instant.now(); + pst.setTimestamp(1, Timestamp.from(now)); + pst.setTimestamp(2, Timestamp.from(now)); + pst.setLong(3, user.getId()); + pst.execute(); + + // Update the user object as well + user.setPrivacyPolicyAcceptedTime(now); + user.setLastUpdated(now); + } catch (SQLException e) { + throw new SegueDatabaseException("Unable to update privacy policy acceptance time", e); + } + } + @Override public Integer regenerateSessionToken(final RegisteredUser user) throws SegueDatabaseException { Integer newSessionTokenValue = generateRandomTokenInteger(); @@ -827,8 +850,8 @@ private RegisteredUser createUser(final RegisteredUser userToCreate) throws Segu String query = "INSERT INTO users(family_name, given_name, email, role, date_of_birth, gender," + " registration_date, school_id, school_other, last_updated, email_verification_status, last_seen," + " email_verification_token, email_to_verify, teacher_pending, registered_contexts," - + " registered_contexts_last_confirmed)" - + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; + + " registered_contexts_last_confirmed, updated_privacy_policy_accepted)" + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS) ) { @@ -922,7 +945,9 @@ private RegisteredUser updateUser(final Connection conn, final RegisteredUser us String query = "UPDATE users SET family_name = ?, given_name = ?, email = ?, role = ?, date_of_birth = ?," + " gender = ?, registration_date = ?, school_id = ?, school_other = ?, last_updated = ?," + " email_verification_status = ?, last_seen = ?, email_verification_token = ?, email_to_verify = ?," - + " teacher_pending = ?, registered_contexts = ?, registered_contexts_last_confirmed = ? WHERE id = ?;"; + + " teacher_pending = ?, registered_contexts = ?, registered_contexts_last_confirmed = ?," + + " updated_privacy_policy_accepted = ?" + + " WHERE id = ?;"; try (PreparedStatement pst = conn.prepareStatement(query)) { setValueHelper(pst, FIELD_CREATE_UPDATE_USER_FAMILY_NAME, userToCreate.getFamilyName()); setValueHelper(pst, FIELD_CREATE_UPDATE_USER_GIVEN_NAME, userToCreate.getGivenName()); @@ -940,6 +965,8 @@ private RegisteredUser updateUser(final Connection conn, final RegisteredUser us setValueHelper(pst, FIELD_CREATE_UPDATE_USER_EMAIL_VERIFICATION_TOKEN, userToCreate.getEmailVerificationToken()); setValueHelper(pst, FIELD_CREATE_UPDATE_USER_EMAIL_TO_VERIFY, userToCreate.getEmailToVerify()); setValueHelper(pst, FIELD_CREATE_UPDATE_USER_TEACHER_PENDING, userToCreate.getTeacherPending()); + setValueHelper(pst, FIELD_CREATE_UPDATE_USER_UPDATED_PRIVACY_POLICY_ACCEPTED, + userToCreate.getPrivacyPolicyAcceptedTime()); List userContextsJsonb = Lists.newArrayList(); if (userToCreate.getRegisteredContexts() != null) { for (UserContext registeredContext : userToCreate.getRegisteredContexts()) { @@ -1008,6 +1035,7 @@ private RegisteredUser buildRegisteredUser(final ResultSet results) throws SQLEx u.setEmailVerificationStatus(results.getString("email_verification_status") != null ? EmailVerificationStatus .valueOf(results.getString("email_verification_status")) : null); u.setTeacherPending(results.getBoolean("teacher_pending")); + u.setPrivacyPolicyAcceptedTime(getInstantFromTimestamp(results, "updated_privacy_policy_accepted")); return u; } @@ -1165,5 +1193,6 @@ private static RegisteredUser removePIIFromUserDO(final RegisteredUser user) { private static final int FIELD_CREATE_UPDATE_USER_TEACHER_PENDING = 15; private static final int FIELD_CREATE_UPDATE_USER_REGISTERED_CONTEXTS = 16; private static final int FIELD_CREATE_UPDATE_USER_REGISTERED_CONTEXTS_LAST_CONFIRMED = 17; - private static final int FIELD_UPDATE_USER_USER_ID = 18; + private static final int FIELD_CREATE_UPDATE_USER_UPDATED_PRIVACY_POLICY_ACCEPTED = 18; + private static final int FIELD_UPDATE_USER_USER_ID = 19; } diff --git a/src/test/java/uk/ac/cam/cl/dtg/segue/api/managers/UserManagerTest.java b/src/test/java/uk/ac/cam/cl/dtg/segue/api/managers/UserManagerTest.java index 39f64c9ee2..961aa4aa6c 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/segue/api/managers/UserManagerTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/segue/api/managers/UserManagerTest.java @@ -194,7 +194,7 @@ void getCurrentUser_IsAuthenticatedWithValidHMAC_userIsReturned() int sessionToken = 7; RegisteredUser returnUser = new RegisteredUser(validUserId, "TestFirstName", "TestLastName", "", Role.STUDENT, - Instant.now(), Gender.MALE, Instant.now(), null, null, null, null, false); + Instant.now(), Gender.MALE, Instant.now(), null, null, null, null, null, false); returnUser.setId(validUserId); Map sessionInformation = @@ -382,7 +382,7 @@ void authenticateCallback_checkNewUserIsAuthenticated_createInternalUserAccount( .atLeastOnce(); RegisteredUser mappedUser = new RegisteredUser(null, "TestFirstName", "testLastName", "test@test.com", Role.STUDENT, - Instant.now(), Gender.MALE, Instant.now(), null, null, null, null, false); + Instant.now(), Gender.MALE, Instant.now(), null, null, null, null, null, false); expect(dummyDatabase.getAuthenticationProvidersByUsers(Collections.singletonList(mappedUser))) .andReturn(new HashMap>() { From 692135283ff3b45fc3e5bf4cc60199e6fdf5b18c Mon Sep 17 00:00:00 2001 From: Parth097 Date: Thu, 31 Jul 2025 14:15:57 +0100 Subject: [PATCH 02/14] Updated create user for privacy policy --- src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java index 115bc6ab6d..2439a09a34 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java @@ -882,6 +882,8 @@ private RegisteredUser createUser(final RegisteredUser userToCreate) throws Segu pst.setArray(FIELD_CREATE_UPDATE_USER_REGISTERED_CONTEXTS, userContexts); setValueHelper(pst, FIELD_CREATE_UPDATE_USER_REGISTERED_CONTEXTS_LAST_CONFIRMED, userToCreate.getRegisteredContextsLastConfirmed()); + setValueHelper(pst, FIELD_CREATE_UPDATE_USER_UPDATED_PRIVACY_POLICY_ACCEPTED, + userToCreate.getPrivacyPolicyAcceptedTime()); if (pst.executeUpdate() == 0) { throw new SegueDatabaseException("Unable to save user."); From 838f41895b3d35f19b2754a8fcde4c10e2d4a0ce Mon Sep 17 00:00:00 2001 From: Madhura Date: Thu, 14 Aug 2025 13:48:38 +0100 Subject: [PATCH 03/14] Add the new column in the DB script --- .../resources/db_scripts/postgres-rutherford-create-script.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/db_scripts/postgres-rutherford-create-script.sql b/src/main/resources/db_scripts/postgres-rutherford-create-script.sql index f68a7189e2..5049655700 100644 --- a/src/main/resources/db_scripts/postgres-rutherford-create-script.sql +++ b/src/main/resources/db_scripts/postgres-rutherford-create-script.sql @@ -733,6 +733,7 @@ CREATE TABLE public.users ( email_verification_token text, deleted boolean DEFAULT false NOT NULL, teacher_pending boolean DEFAULT false NOT NULL + updated_privacy_policy_accepted timestamp without time zone, ); From 56aa77525fc12d36da28c37724eaac49ee439f8b Mon Sep 17 00:00:00 2001 From: Madhura Date: Thu, 14 Aug 2025 14:07:58 +0100 Subject: [PATCH 04/14] Fix linting error --- .../db_scripts/postgres-rutherford-create-script.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/db_scripts/postgres-rutherford-create-script.sql b/src/main/resources/db_scripts/postgres-rutherford-create-script.sql index 5049655700..11d9eaf5ef 100644 --- a/src/main/resources/db_scripts/postgres-rutherford-create-script.sql +++ b/src/main/resources/db_scripts/postgres-rutherford-create-script.sql @@ -732,8 +732,8 @@ CREATE TABLE public.users ( email_to_verify text, email_verification_token text, deleted boolean DEFAULT false NOT NULL, - teacher_pending boolean DEFAULT false NOT NULL - updated_privacy_policy_accepted timestamp without time zone, + teacher_pending boolean DEFAULT false NOT NULL, + updated_privacy_policy_accepted timestamp without time zone ); From a6f0fe8235f1fb08b7a584dd27e28b0fc91df455 Mon Sep 17 00:00:00 2001 From: Madhura Date: Thu, 14 Aug 2025 14:50:17 +0100 Subject: [PATCH 05/14] Add privacy policy accepted time field in Users object --- .../dtg/isaac/dto/users/UserSummaryDTO.java | 21 ++++++++++ .../users/UserSummaryForAdminUsersDTO.java | 38 +++++++++---------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryDTO.java index d41d2a2606..a61cf80c16 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryDTO.java @@ -17,6 +17,7 @@ package uk.ac.cam.cl.dtg.isaac.dto.users; import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; import java.util.List; import uk.ac.cam.cl.dtg.isaac.dos.users.EmailVerificationStatus; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; @@ -34,6 +35,7 @@ public class UserSummaryDTO extends AbstractSegueUserDTO { private EmailVerificationStatus emailVerificationStatus; private Boolean teacherPending; private List registeredContexts; + private Instant privacyPolicyAcceptedTime; /** * UserSummaryDTO. @@ -179,6 +181,24 @@ public void setRegisteredContexts(final List registeredContexts) { this.registeredContexts = registeredContexts; } + /** + * Gets the privacyPolicyAcceptedTime. + * + * @return the privacyPolicyAcceptedTime + */ + public Instant getPrivacyPolicyAcceptedTime() { + return privacyPolicyAcceptedTime; + } + + /** + * Sets the privacyPolicyAcceptedTime. + * + * @param privacyPolicyAcceptedTime the privacyPolicyAcceptedTime to set + */ + public void setPrivacyPolicyAcceptedTime(final Instant privacyPolicyAcceptedTime) { + this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; + } + @Override public String toString() { return "UserSummaryDTO [" @@ -190,6 +210,7 @@ public String toString() { + ", emailVerificationStatus=" + emailVerificationStatus + ", teacherPending=" + teacherPending + ", registeredContexts=" + registeredContexts + + ", privacyPolicyAcceptedTime=" + privacyPolicyAcceptedTime + ']'; } } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java index ecc7031ade..7d04a1ab67 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java @@ -23,7 +23,7 @@ */ public class UserSummaryForAdminUsersDTO extends UserSummaryWithEmailAddressDTO { private Instant lastUpdated; - private Instant privacyPolicyAcceptedTime; +// private Instant privacyPolicyAcceptedTime; private Instant lastSeen; private Instant registrationDate; private String schoolId; @@ -54,23 +54,23 @@ public void setLastUpdated(final Instant lastUpdated) { this.lastUpdated = lastUpdated; } - /** - * Gets the privacyPolicyAcceptedTime. - * - * @return the privacyPolicyAcceptedTime - */ - public Instant getPrivacyPolicyAcceptedTime() { - return privacyPolicyAcceptedTime; - } - - /** - * Sets the privacyPolicyAcceptedTime. - * - * @param privacyPolicyAcceptedTime the privacyPolicyAcceptedTime to set - */ - public void setPrivacyPolicyAcceptedTime(final Instant privacyPolicyAcceptedTime) { - this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; - } +// /** +// * Gets the privacyPolicyAcceptedTime. +// * +// * @return the privacyPolicyAcceptedTime +// */ +// public Instant getPrivacyPolicyAcceptedTime() { +// return privacyPolicyAcceptedTime; +// } +// +// /** +// * Sets the privacyPolicyAcceptedTime. +// * +// * @param privacyPolicyAcceptedTime the privacyPolicyAcceptedTime to set +// */ +// public void setPrivacyPolicyAcceptedTime(final Instant privacyPolicyAcceptedTime) { +// this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; +// } /** * Gets the lastSeen. @@ -148,7 +148,7 @@ public void setSchoolOther(final String schoolOther) { public String toString() { return "lastUpdated=" + "UserSummaryForAdminUsersDTO{" + lastUpdated + ", lastSeen=" + lastSeen - + ", privacyPolicyAcceptedTime=" + privacyPolicyAcceptedTime +// + ", privacyPolicyAcceptedTime=" + privacyPolicyAcceptedTime + ", registrationDate=" + registrationDate + ", schoolId=" + schoolId + ", schoolOther=" + schoolOther From 29bb68911ff42dea51e02c57ccc45ebbc4a52709 Mon Sep 17 00:00:00 2001 From: Madhura Date: Thu, 14 Aug 2025 15:21:38 +0100 Subject: [PATCH 06/14] Remove privacy column from UserSummayForAdminUsers --- .../users/UserSummaryForAdminUsersDTO.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java index 7d04a1ab67..99c3ad8188 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/UserSummaryForAdminUsersDTO.java @@ -23,7 +23,6 @@ */ public class UserSummaryForAdminUsersDTO extends UserSummaryWithEmailAddressDTO { private Instant lastUpdated; -// private Instant privacyPolicyAcceptedTime; private Instant lastSeen; private Instant registrationDate; private String schoolId; @@ -54,23 +53,6 @@ public void setLastUpdated(final Instant lastUpdated) { this.lastUpdated = lastUpdated; } -// /** -// * Gets the privacyPolicyAcceptedTime. -// * -// * @return the privacyPolicyAcceptedTime -// */ -// public Instant getPrivacyPolicyAcceptedTime() { -// return privacyPolicyAcceptedTime; -// } -// -// /** -// * Sets the privacyPolicyAcceptedTime. -// * -// * @param privacyPolicyAcceptedTime the privacyPolicyAcceptedTime to set -// */ -// public void setPrivacyPolicyAcceptedTime(final Instant privacyPolicyAcceptedTime) { -// this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; -// } /** * Gets the lastSeen. @@ -148,7 +130,6 @@ public void setSchoolOther(final String schoolOther) { public String toString() { return "lastUpdated=" + "UserSummaryForAdminUsersDTO{" + lastUpdated + ", lastSeen=" + lastSeen -// + ", privacyPolicyAcceptedTime=" + privacyPolicyAcceptedTime + ", registrationDate=" + registrationDate + ", schoolId=" + schoolId + ", schoolOther=" + schoolOther From 54f5db85c2e6a616467f36fbbefd4c08896afcd4 Mon Sep 17 00:00:00 2001 From: Madhura Date: Tue, 26 Aug 2025 11:32:38 +0100 Subject: [PATCH 07/14] Remove unnecessary annotation --- src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java index ab698654cf..7317d5dc21 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java @@ -495,7 +495,6 @@ public final Response getCurrentUserSnapshot(@Context final HttpServletRequest r @Path("/users/accept-privacy-policy") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @GZIP @Operation(summary = "Accept the privacy policy for the current user.") public Response acceptPrivacyPolicy(@Context final HttpServletRequest request) { try { From 2349b49f9e18815cbbd133bf6b43c746e8c5b675 Mon Sep 17 00:00:00 2001 From: Madhura Date: Tue, 26 Aug 2025 15:57:23 +0100 Subject: [PATCH 08/14] Fix indentation --- src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java index 2439a09a34..0ea5f02405 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java @@ -681,6 +681,7 @@ public void updateUserLastSeen(final RegisteredUser user) throws SegueDatabaseEx @Override public void updateUserLastSeen(final RegisteredUser user, final Instant date) throws SegueDatabaseException { + requireNonNull(user); String query = "UPDATE users SET last_seen = ? WHERE id = ?"; @@ -697,6 +698,7 @@ public void updateUserLastSeen(final RegisteredUser user, final Instant date) th @Override public void updatePrivacyPolicyAcceptedTime(final RegisteredUser user) throws SegueDatabaseException { + requireNonNull(user); String query = "UPDATE users SET updated_privacy_policy_accepted = ?, last_updated = ? WHERE id = ?"; From 02b736a81a3c32e27119ef5ce1b6dd55f6b15c78 Mon Sep 17 00:00:00 2001 From: Madhura Date: Fri, 29 Aug 2025 15:15:24 +0100 Subject: [PATCH 09/14] Get the policy accepted time to set in DB --- .../uk/ac/cam/cl/dtg/isaac/api/IsaacController.java | 10 ++++++++-- .../cl/dtg/segue/api/managers/UserAccountManager.java | 4 ++-- .../cam/cl/dtg/segue/dao/users/IUserDataManager.java | 2 +- .../java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java | 6 ++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java index 7317d5dc21..05a0074a30 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java @@ -16,6 +16,8 @@ package uk.ac.cam.cl.dtg.isaac.api; +import java.time.Instant; + import static uk.ac.cam.cl.dtg.isaac.api.Constants.DOCUMENT_PATH_LOG_FIELDNAME; import static uk.ac.cam.cl.dtg.isaac.api.Constants.IsaacServerLogType; import static uk.ac.cam.cl.dtg.isaac.api.Constants.SITE_WIDE_SEARCH_VALID_DOC_TYPES; @@ -87,6 +89,7 @@ import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager; import uk.ac.cam.cl.dtg.util.PropertiesLoader; + /** * Isaac Controller *
@@ -496,10 +499,13 @@ public final Response getCurrentUserSnapshot(@Context final HttpServletRequest r @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Accept the privacy policy for the current user.") - public Response acceptPrivacyPolicy(@Context final HttpServletRequest request) { + public Response acceptPrivacyPolicy(@Context final HttpServletRequest request, @QueryParam("privacyPolicyAcceptedTime") String privacyPolicyAcceptedTime) { + try { RegisteredUserDTO user = userManager.getCurrentRegisteredUser(request); - userManager.updatePrivacyPolicyAcceptedTime(user); + Instant policyAcceptedTime = Instant.ofEpochMilli(Long.parseLong(privacyPolicyAcceptedTime)); + + userManager.updatePrivacyPolicyAcceptedTime(user, policyAcceptedTime); log.info("User " + user.getEmail() + " accepted privacy policy"); return Response.ok().build(); } catch (NoUserLoggedInException e) { diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java index b84e0942bd..c260887a1f 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java @@ -1991,9 +1991,9 @@ private void updateLastSeen(final RegisteredUser user) throws SegueDatabaseExcep * @param userDTO of interest * @throws SegueDatabaseException if an error occurs with the update. */ - public void updatePrivacyPolicyAcceptedTime(final RegisteredUserDTO userDTO) throws SegueDatabaseException { + public void updatePrivacyPolicyAcceptedTime(final RegisteredUserDTO userDTO, Instant privacyPolicyAcceptedTime) throws SegueDatabaseException { RegisteredUser user = findUserById(userDTO.getId()); - this.database.updatePrivacyPolicyAcceptedTime(user); + this.database.updatePrivacyPolicyAcceptedTime(user, privacyPolicyAcceptedTime); } /** diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/IUserDataManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/IUserDataManager.java index 5b2897fe96..e98cb6bb73 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/IUserDataManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/IUserDataManager.java @@ -257,7 +257,7 @@ boolean linkAuthProviderToAccount(RegisteredUser user, AuthenticationProvider pr * @param user - the user to update * @throws SegueDatabaseException - if there is a database error */ - void updatePrivacyPolicyAcceptedTime(RegisteredUser user) throws SegueDatabaseException; + void updatePrivacyPolicyAcceptedTime(RegisteredUser user, Instant policyAcceptedTime) throws SegueDatabaseException; /** * Update the session token of a user object in the data store to a randomly generated value. diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java index 0ea5f02405..4e5d67b3f2 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java @@ -697,16 +697,18 @@ public void updateUserLastSeen(final RegisteredUser user, final Instant date) th } @Override - public void updatePrivacyPolicyAcceptedTime(final RegisteredUser user) throws SegueDatabaseException { + public void updatePrivacyPolicyAcceptedTime(final RegisteredUser user, final Instant acceptedTime) throws SegueDatabaseException { requireNonNull(user); + requireNonNull(acceptedTime); String query = "UPDATE users SET updated_privacy_policy_accepted = ?, last_updated = ? WHERE id = ?"; try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query) ) { Instant now = Instant.now(); - pst.setTimestamp(1, Timestamp.from(now)); + Instant privacyPolicyAcceptedTime = acceptedTime.isAfter(now) ? now : acceptedTime; + pst.setTimestamp(1, Timestamp.from(privacyPolicyAcceptedTime)); pst.setTimestamp(2, Timestamp.from(now)); pst.setLong(3, user.getId()); pst.execute(); From 9b759efb9f3b6b3fa0c2d46c87aae9c5fccaff74 Mon Sep 17 00:00:00 2001 From: "marius.marin" Date: Mon, 1 Sep 2025 11:37:34 +0300 Subject: [PATCH 10/14] 684 - Added service to handle the EpochTimeStamp from FE for privacy policy update --- .../cl/dtg/isaac/api/AssignmentFacade.java | 2 +- .../ac/cam/cl/dtg/isaac/api/EventsFacade.java | 14 +- .../cl/dtg/isaac/api/GameboardsFacade.java | 6 +- .../cam/cl/dtg/isaac/api/IsaacController.java | 24 ++- .../ac/cam/cl/dtg/isaac/api/QuizFacade.java | 8 +- .../AssignmentCancelledException.java | 2 +- .../AttemptCompletedException.java | 2 +- .../DueBeforeNowException.java | 2 +- .../DuplicateAssignmentException.java | 2 +- .../DuplicateBookingException.java | 2 +- .../DuplicateGameboardException.java | 2 +- .../EventBookingUpdateException.java | 2 +- .../EventDeadlineException.java | 2 +- .../EventGroupReservationLimitException.java | 2 +- .../EventIsCancelledException.java | 2 +- .../EventIsFullException.java | 2 +- .../EventIsNotFullException.java | 2 +- .../InvalidGameboardException.java | 2 +- .../exceptions/InvalidTimestampException.java | 7 + .../NoWildcardException.java | 2 +- .../isaac/api/managers/AssignmentManager.java | 1 + .../api/managers/EventBookingManager.java | 7 + .../dtg/isaac/api/managers/GameManager.java | 3 + .../api/managers/QuizAssignmentManager.java | 3 + .../api/managers/QuizAttemptManager.java | 1 + .../api/requests/PrivacyPolicyRequest.java | 20 +++ .../api/services/PrivacyPolicyService.java | 71 +++++++++ .../IQuizAssignmentPersistenceManager.java | 2 +- .../PgQuizAssignmentPersistenceManager.java | 2 +- .../cl/dtg/isaac/api/EventsFacadeTest.java | 4 +- .../api/managers/EventBookingManagerTest.java | 8 +- .../managers/QuizAssignmentManagerTest.java | 2 + .../api/managers/QuizAttemptManagerTest.java | 1 + .../dtg/isaac/app/GameboardsFacadeTest.java | 2 +- .../service/PrivacyPolicyServiceTest.java | 147 ++++++++++++++++++ 35 files changed, 319 insertions(+), 44 deletions(-) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/AssignmentCancelledException.java (74%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/AttemptCompletedException.java (94%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/DueBeforeNowException.java (96%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/DuplicateAssignmentException.java (95%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/DuplicateBookingException.java (90%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/DuplicateGameboardException.java (91%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/EventBookingUpdateException.java (80%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/EventDeadlineException.java (88%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/EventGroupReservationLimitException.java (89%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/EventIsCancelledException.java (92%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/EventIsFullException.java (88%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/EventIsNotFullException.java (88%) rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/InvalidGameboardException.java (91%) create mode 100644 src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/InvalidTimestampException.java rename src/main/java/uk/ac/cam/cl/dtg/isaac/api/{managers => exceptions}/NoWildcardException.java (94%) create mode 100644 src/main/java/uk/ac/cam/cl/dtg/isaac/api/requests/PrivacyPolicyRequest.java create mode 100644 src/main/java/uk/ac/cam/cl/dtg/isaac/api/services/PrivacyPolicyService.java create mode 100644 src/test/java/uk/ac/cam/cl/dtg/isaac/service/PrivacyPolicyServiceTest.java diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java index 4c342b9339..8b8f4b77df 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java @@ -74,7 +74,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.ac.cam.cl.dtg.isaac.api.managers.AssignmentManager; -import uk.ac.cam.cl.dtg.isaac.api.managers.DuplicateAssignmentException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateAssignmentException; import uk.ac.cam.cl.dtg.isaac.api.managers.GameManager; import uk.ac.cam.cl.dtg.isaac.api.services.AssignmentService; import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java index fc420a4c6d..470f706f14 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java @@ -92,14 +92,14 @@ import org.jboss.resteasy.annotations.GZIP; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import uk.ac.cam.cl.dtg.isaac.api.managers.DuplicateBookingException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateBookingException; import uk.ac.cam.cl.dtg.isaac.api.managers.EventBookingManager; -import uk.ac.cam.cl.dtg.isaac.api.managers.EventBookingUpdateException; -import uk.ac.cam.cl.dtg.isaac.api.managers.EventDeadlineException; -import uk.ac.cam.cl.dtg.isaac.api.managers.EventGroupReservationLimitException; -import uk.ac.cam.cl.dtg.isaac.api.managers.EventIsCancelledException; -import uk.ac.cam.cl.dtg.isaac.api.managers.EventIsFullException; -import uk.ac.cam.cl.dtg.isaac.api.managers.EventIsNotFullException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventBookingUpdateException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventDeadlineException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventGroupReservationLimitException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsCancelledException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsFullException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsNotFullException; import uk.ac.cam.cl.dtg.isaac.dos.EventStatus; import uk.ac.cam.cl.dtg.isaac.dos.eventbookings.BookingStatus; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java index e459c5f57a..420ca29f04 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java @@ -56,10 +56,10 @@ import org.jboss.resteasy.annotations.GZIP; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import uk.ac.cam.cl.dtg.isaac.api.managers.DuplicateGameboardException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateGameboardException; import uk.ac.cam.cl.dtg.isaac.api.managers.GameManager; -import uk.ac.cam.cl.dtg.isaac.api.managers.InvalidGameboardException; -import uk.ac.cam.cl.dtg.isaac.api.managers.NoWildcardException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.InvalidGameboardException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.NoWildcardException; import uk.ac.cam.cl.dtg.isaac.dos.GameboardCreationMethod; import uk.ac.cam.cl.dtg.isaac.dos.IsaacWildcard; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java index 05a0074a30..cc7776c73c 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java @@ -69,7 +69,10 @@ import org.jboss.resteasy.annotations.GZIP; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.InvalidTimestampException; +import uk.ac.cam.cl.dtg.isaac.api.requests.PrivacyPolicyRequest; import uk.ac.cam.cl.dtg.isaac.api.services.ContentSummarizerService; +import uk.ac.cam.cl.dtg.isaac.api.services.PrivacyPolicyService; import uk.ac.cam.cl.dtg.isaac.dos.IUserStreaksManager; import uk.ac.cam.cl.dtg.isaac.dto.ResultsWrapper; import uk.ac.cam.cl.dtg.isaac.dto.SegueErrorResponse; @@ -112,6 +115,7 @@ public class IsaacController extends AbstractIsaacFacade { private final UserBadgeManager userBadgeManager; private final IUserStreaksManager userStreaksManager; private final ContentSummarizerService contentSummarizerService; + private final PrivacyPolicyService privacyPolicyService; private static long lastQuestionCount = 0L; @@ -157,7 +161,8 @@ public IsaacController(final PropertiesLoader propertiesLoader, final ILogManage final GitContentManager contentManager, final UserAssociationManager associationManager, @Named(CONTENT_INDEX) final String contentIndex, final IUserStreaksManager userStreaksManager, final UserBadgeManager userBadgeManager, - final ContentSummarizerService contentSummarizerService) { + final ContentSummarizerService contentSummarizerService, + final PrivacyPolicyService privacyPolicyService) { super(propertiesLoader, logManager); this.statsManager = statsManager; this.userManager = userManager; @@ -167,6 +172,7 @@ public IsaacController(final PropertiesLoader propertiesLoader, final ILogManage this.userBadgeManager = userBadgeManager; this.userStreaksManager = userStreaksManager; this.contentSummarizerService = contentSummarizerService; + this.privacyPolicyService = privacyPolicyService; } /** @@ -499,17 +505,19 @@ public final Response getCurrentUserSnapshot(@Context final HttpServletRequest r @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Accept the privacy policy for the current user.") - public Response acceptPrivacyPolicy(@Context final HttpServletRequest request, @QueryParam("privacyPolicyAcceptedTime") String privacyPolicyAcceptedTime) { - + public Response acceptPrivacyPolicy(@Context final HttpServletRequest request, + PrivacyPolicyRequest privacyPolicyRequest) { try { - RegisteredUserDTO user = userManager.getCurrentRegisteredUser(request); - Instant policyAcceptedTime = Instant.ofEpochMilli(Long.parseLong(privacyPolicyAcceptedTime)); - - userManager.updatePrivacyPolicyAcceptedTime(user, policyAcceptedTime); - log.info("User " + user.getEmail() + " accepted privacy policy"); + privacyPolicyService.acceptPrivacyPolicy(request, privacyPolicyRequest); return Response.ok().build(); + } catch (NoUserLoggedInException e) { return SegueErrorResponse.getNotLoggedInResponse(); + + } catch (InvalidTimestampException e) { + log.warn("Invalid timestamp provided: {}", e.getMessage()); + return new SegueErrorResponse(Status.BAD_REQUEST, "Invalid timestamp: " + e.getMessage()).toResponse(); + } catch (SegueDatabaseException e) { log.error("Database error during privacy policy acceptance", e); return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Database error").toResponse(); diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacade.java index 5a10c63e48..34427c1bca 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacade.java @@ -71,10 +71,10 @@ import org.jboss.resteasy.annotations.GZIP; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import uk.ac.cam.cl.dtg.isaac.api.managers.AssignmentCancelledException; -import uk.ac.cam.cl.dtg.isaac.api.managers.AttemptCompletedException; -import uk.ac.cam.cl.dtg.isaac.api.managers.DueBeforeNowException; -import uk.ac.cam.cl.dtg.isaac.api.managers.DuplicateAssignmentException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.AssignmentCancelledException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.AttemptCompletedException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DueBeforeNowException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateAssignmentException; import uk.ac.cam.cl.dtg.isaac.api.managers.QuizAssignmentManager; import uk.ac.cam.cl.dtg.isaac.api.managers.QuizAttemptManager; import uk.ac.cam.cl.dtg.isaac.api.managers.QuizManager; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/AssignmentCancelledException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/AssignmentCancelledException.java similarity index 74% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/AssignmentCancelledException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/AssignmentCancelledException.java index 63ceb62021..94e0e313d2 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/AssignmentCancelledException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/AssignmentCancelledException.java @@ -1,4 +1,4 @@ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; public class AssignmentCancelledException extends Exception { private static final long serialVersionUID = 4862878947772927260L; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/AttemptCompletedException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/AttemptCompletedException.java similarity index 94% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/AttemptCompletedException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/AttemptCompletedException.java index 402a544433..7a710588fc 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/AttemptCompletedException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/AttemptCompletedException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; public class AttemptCompletedException extends Exception { private static final long serialVersionUID = -1062901117738824285L; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DueBeforeNowException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DueBeforeNowException.java similarity index 96% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DueBeforeNowException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DueBeforeNowException.java index 0bab79948f..0291dc9fe9 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DueBeforeNowException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DueBeforeNowException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DuplicateAssignmentException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DuplicateAssignmentException.java similarity index 95% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DuplicateAssignmentException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DuplicateAssignmentException.java index 1d4ffb7cf0..1299b50628 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DuplicateAssignmentException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DuplicateAssignmentException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DuplicateBookingException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DuplicateBookingException.java similarity index 90% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DuplicateBookingException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DuplicateBookingException.java index 9cd41be20e..e590bb9b29 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DuplicateBookingException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DuplicateBookingException.java @@ -1,4 +1,4 @@ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; /** * DuplicateBookingException diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DuplicateGameboardException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DuplicateGameboardException.java similarity index 91% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DuplicateGameboardException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DuplicateGameboardException.java index aaafa275f2..a34a40a6c4 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/DuplicateGameboardException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/DuplicateGameboardException.java @@ -1,4 +1,4 @@ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; /** * An exception that indicates a gameboard already exists with the id provided. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingUpdateException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventBookingUpdateException.java similarity index 80% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingUpdateException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventBookingUpdateException.java index 8d7b43df81..6c14fe4852 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingUpdateException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventBookingUpdateException.java @@ -1,4 +1,4 @@ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; /** * Created by sac92 on 07/06/2016. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventDeadlineException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventDeadlineException.java similarity index 88% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventDeadlineException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventDeadlineException.java index 97fd6e9e4d..4b28aeda10 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventDeadlineException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventDeadlineException.java @@ -1,4 +1,4 @@ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; /** * Indicates the event booking deadline has passed. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventGroupReservationLimitException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventGroupReservationLimitException.java similarity index 89% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventGroupReservationLimitException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventGroupReservationLimitException.java index 5255cb36a0..a0e3336241 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventGroupReservationLimitException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventGroupReservationLimitException.java @@ -1,4 +1,4 @@ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; /** * Indicates that the user is trying to request reservations exceeding the limit specified. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventIsCancelledException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventIsCancelledException.java similarity index 92% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventIsCancelledException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventIsCancelledException.java index 3fba380b6a..b998cf527e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventIsCancelledException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventIsCancelledException.java @@ -1,4 +1,4 @@ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; /** * EventCancelledException. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventIsFullException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventIsFullException.java similarity index 88% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventIsFullException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventIsFullException.java index 52a17fd1ef..6cc61b767a 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventIsFullException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventIsFullException.java @@ -1,4 +1,4 @@ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; /** * Indicates the event is full. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventIsNotFullException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventIsNotFullException.java similarity index 88% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventIsNotFullException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventIsNotFullException.java index 71668a80e3..8e537e5ac8 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventIsNotFullException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/EventIsNotFullException.java @@ -1,4 +1,4 @@ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; /** * Indicates the event is NOT full. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/InvalidGameboardException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/InvalidGameboardException.java similarity index 91% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/InvalidGameboardException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/InvalidGameboardException.java index 9f93794610..ac047d0706 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/InvalidGameboardException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/InvalidGameboardException.java @@ -1,4 +1,4 @@ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; /** * An exception that indicates an invalid gameboard has been provided. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/InvalidTimestampException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/InvalidTimestampException.java new file mode 100644 index 0000000000..64e333b2c1 --- /dev/null +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/InvalidTimestampException.java @@ -0,0 +1,7 @@ +package uk.ac.cam.cl.dtg.isaac.api.exceptions; + +public class InvalidTimestampException extends Exception { + public InvalidTimestampException(String message) { + super(message); + } +} diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/NoWildcardException.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/NoWildcardException.java similarity index 94% rename from src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/NoWildcardException.java rename to src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/NoWildcardException.java index e51accbc28..9fd0353f96 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/NoWildcardException.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/exceptions/NoWildcardException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package uk.ac.cam.cl.dtg.isaac.api.managers; +package uk.ac.cam.cl.dtg.isaac.api.exceptions; /** * Exception for when an IsaacWildcard cannot be found/provided. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/AssignmentManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/AssignmentManager.java index 0dc714da15..1c9f95319f 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/AssignmentManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/AssignmentManager.java @@ -32,6 +32,7 @@ import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateAssignmentException; import uk.ac.cam.cl.dtg.isaac.api.services.EmailService; import uk.ac.cam.cl.dtg.isaac.dao.IAssignmentPersistenceManager; import uk.ac.cam.cl.dtg.isaac.dto.AssignmentDTO; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingManager.java index 298fcc53b6..31d9a80d83 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingManager.java @@ -61,6 +61,13 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateBookingException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventBookingUpdateException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventDeadlineException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventGroupReservationLimitException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsCancelledException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsFullException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsNotFullException; import uk.ac.cam.cl.dtg.isaac.dao.EventBookingPersistenceManager; import uk.ac.cam.cl.dtg.isaac.dos.AssociationToken; import uk.ac.cam.cl.dtg.isaac.dos.EventStatus; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java index 2727ea000b..c6a12b8c29 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java @@ -67,6 +67,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.ac.cam.cl.dtg.isaac.api.Constants; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateGameboardException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.InvalidGameboardException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.NoWildcardException; import uk.ac.cam.cl.dtg.isaac.dao.GameboardPersistenceManager; import uk.ac.cam.cl.dtg.isaac.dos.AudienceContext; import uk.ac.cam.cl.dtg.isaac.dos.GameboardContentDescriptor; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAssignmentManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAssignmentManager.java index 698a8a96f8..741754c392 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAssignmentManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAssignmentManager.java @@ -29,6 +29,9 @@ import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.AssignmentCancelledException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DueBeforeNowException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateAssignmentException; import uk.ac.cam.cl.dtg.isaac.api.services.EmailService; import uk.ac.cam.cl.dtg.isaac.dao.IQuizAssignmentPersistenceManager; import uk.ac.cam.cl.dtg.isaac.dto.IAssignmentLike; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAttemptManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAttemptManager.java index bd050688fa..2aa11342fa 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAttemptManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAttemptManager.java @@ -24,6 +24,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.AttemptCompletedException; import uk.ac.cam.cl.dtg.isaac.dao.IQuizAttemptPersistenceManager; import uk.ac.cam.cl.dtg.isaac.dto.IsaacQuizDTO; import uk.ac.cam.cl.dtg.isaac.dto.QuizAssignmentDTO; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/requests/PrivacyPolicyRequest.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/requests/PrivacyPolicyRequest.java new file mode 100644 index 0000000000..e6b5616a24 --- /dev/null +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/requests/PrivacyPolicyRequest.java @@ -0,0 +1,20 @@ +package uk.ac.cam.cl.dtg.isaac.api.requests; + +import java.time.Instant; + + +public class PrivacyPolicyRequest { + private long privacyPolicyAcceptedTime; + + public Instant getPrivacyPolicyAcceptedTimeInstant() { + return Instant.ofEpochMilli(privacyPolicyAcceptedTime); + } + + public long getPrivacyPolicyAcceptedTime() { + return privacyPolicyAcceptedTime; + } + + public void setPrivacyPolicyAcceptedTime(long privacyPolicyAcceptedTime) { + this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; + } +} diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/services/PrivacyPolicyService.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/services/PrivacyPolicyService.java new file mode 100644 index 0000000000..7a5188bd6f --- /dev/null +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/services/PrivacyPolicyService.java @@ -0,0 +1,71 @@ +package uk.ac.cam.cl.dtg.isaac.api.services; + +import com.google.inject.Inject; +import jakarta.servlet.http.HttpServletRequest; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.InvalidTimestampException; +import uk.ac.cam.cl.dtg.isaac.api.requests.PrivacyPolicyRequest; +import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO; +import uk.ac.cam.cl.dtg.segue.api.managers.UserAccountManager; +import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserLoggedInException; +import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; + +public class PrivacyPolicyService { + + private static final Logger log = LoggerFactory.getLogger(PrivacyPolicyService.class); + private static final long TIME_TOLERANCE_SECONDS = 30; + + private final UserAccountManager userManager; + private final Clock clock; + + @Inject + public PrivacyPolicyService(UserAccountManager userManager, Clock clock) { + this.userManager = userManager; + this.clock = clock; + } + + /** + * Accept privacy policy with timestamp validation + * @param request HTTP request to get current user + * @param privacyPolicyRequest Request containing the timestamp + * @throws NoUserLoggedInException if no user is logged in + * @throws SegueDatabaseException if database error occurs + * @throws InvalidTimestampException if timestamp is too far from current time + */ + public void acceptPrivacyPolicy(HttpServletRequest request, PrivacyPolicyRequest privacyPolicyRequest) + throws NoUserLoggedInException, SegueDatabaseException, InvalidTimestampException { + + // Get current user + RegisteredUserDTO user = userManager.getCurrentRegisteredUser(request); + + // Validate timestamp + Instant providedTime = privacyPolicyRequest.getPrivacyPolicyAcceptedTimeInstant(); + validateTimestamp(providedTime); + + // Update privacy policy acceptance + userManager.updatePrivacyPolicyAcceptedTime(user, providedTime); + + log.info("User {} accepted privacy policy at {}", user.getEmail(), providedTime); + } + + /** + * Validates that the provided timestamp is close to the current time + * @param providedTime The timestamp from the client + * @throws InvalidTimestampException if timestamp is outside allowed tolerance + */ + private void validateTimestamp(Instant providedTime) throws InvalidTimestampException { + Instant currentTime = clock.instant(); + long secondsDifference = Math.abs(Duration.between(providedTime, currentTime).getSeconds()); + + if (secondsDifference > TIME_TOLERANCE_SECONDS) { + throw new InvalidTimestampException( + String.format("Timestamp too far from current time. Difference: %d seconds, Max allowed: %d seconds", + secondsDifference, TIME_TOLERANCE_SECONDS) + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/IQuizAssignmentPersistenceManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/IQuizAssignmentPersistenceManager.java index 76f096be8d..49ca2baf1f 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/IQuizAssignmentPersistenceManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/IQuizAssignmentPersistenceManager.java @@ -17,7 +17,7 @@ package uk.ac.cam.cl.dtg.isaac.dao; import java.util.List; -import uk.ac.cam.cl.dtg.isaac.api.managers.AssignmentCancelledException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.AssignmentCancelledException; import uk.ac.cam.cl.dtg.isaac.dto.QuizAssignmentDTO; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizAssignmentPersistenceManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizAssignmentPersistenceManager.java index 392465857c..424f4d5364 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizAssignmentPersistenceManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizAssignmentPersistenceManager.java @@ -31,7 +31,7 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import uk.ac.cam.cl.dtg.isaac.api.managers.AssignmentCancelledException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.AssignmentCancelledException; import uk.ac.cam.cl.dtg.isaac.dos.QuizAssignmentDO; import uk.ac.cam.cl.dtg.isaac.dos.QuizFeedbackMode; import uk.ac.cam.cl.dtg.isaac.dto.QuizAssignmentDTO; diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacadeTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacadeTest.java index 2e8e43e9b0..0549574f06 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacadeTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacadeTest.java @@ -27,8 +27,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import uk.ac.cam.cl.dtg.isaac.api.managers.EventBookingManager; -import uk.ac.cam.cl.dtg.isaac.api.managers.EventBookingUpdateException; -import uk.ac.cam.cl.dtg.isaac.api.managers.EventIsCancelledException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventBookingUpdateException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsCancelledException; import uk.ac.cam.cl.dtg.isaac.dos.EventStatus; import uk.ac.cam.cl.dtg.isaac.dos.eventbookings.BookingStatus; import uk.ac.cam.cl.dtg.isaac.dto.IsaacEventPageDTO; diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingManagerTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingManagerTest.java index c5277a23e1..a1cea9ea9e 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingManagerTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/EventBookingManagerTest.java @@ -1,6 +1,5 @@ package uk.ac.cam.cl.dtg.isaac.api.managers; -import static java.time.ZoneId.getAvailableZoneIds; import static java.time.ZoneOffset.UTC; import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.createMock; @@ -32,7 +31,6 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -46,6 +44,12 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateBookingException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventDeadlineException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventGroupReservationLimitException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsCancelledException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsFullException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsNotFullException; import uk.ac.cam.cl.dtg.isaac.dao.EventBookingPersistenceManager; import uk.ac.cam.cl.dtg.isaac.dos.AssociationToken; import uk.ac.cam.cl.dtg.isaac.dos.EventStatus; diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAssignmentManagerTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAssignmentManagerTest.java index 93127dccd5..6a7782699e 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAssignmentManagerTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAssignmentManagerTest.java @@ -33,6 +33,8 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DueBeforeNowException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateAssignmentException; import uk.ac.cam.cl.dtg.isaac.api.services.EmailService; import uk.ac.cam.cl.dtg.isaac.dao.IQuizAssignmentPersistenceManager; import uk.ac.cam.cl.dtg.isaac.dos.QuizFeedbackMode; diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAttemptManagerTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAttemptManagerTest.java index d30df8e4b7..9ec2bd71a1 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAttemptManagerTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizAttemptManagerTest.java @@ -31,6 +31,7 @@ import org.easymock.IArgumentMatcher; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.AttemptCompletedException; import uk.ac.cam.cl.dtg.isaac.dao.IQuizAttemptPersistenceManager; import uk.ac.cam.cl.dtg.isaac.dto.QuizAttemptDTO; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/app/GameboardsFacadeTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/app/GameboardsFacadeTest.java index 6a6c3baef8..d3bea5f87e 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/app/GameboardsFacadeTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/app/GameboardsFacadeTest.java @@ -30,7 +30,7 @@ import org.junit.jupiter.api.Test; import uk.ac.cam.cl.dtg.isaac.api.GameboardsFacade; import uk.ac.cam.cl.dtg.isaac.api.managers.GameManager; -import uk.ac.cam.cl.dtg.isaac.api.managers.NoWildcardException; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.NoWildcardException; import uk.ac.cam.cl.dtg.isaac.dto.GameFilter; import uk.ac.cam.cl.dtg.isaac.dto.users.AbstractSegueUserDTO; import uk.ac.cam.cl.dtg.isaac.dto.users.AnonymousUserDTO; diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/service/PrivacyPolicyServiceTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/service/PrivacyPolicyServiceTest.java new file mode 100644 index 0000000000..91fce17783 --- /dev/null +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/service/PrivacyPolicyServiceTest.java @@ -0,0 +1,147 @@ +package uk.ac.cam.cl.dtg.isaac.service; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.servlet.http.HttpServletRequest; +import java.time.Clock; +import java.time.Instant; +import org.easymock.EasyMockExtension; +import org.easymock.Mock; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import uk.ac.cam.cl.dtg.isaac.api.exceptions.InvalidTimestampException; +import uk.ac.cam.cl.dtg.isaac.api.requests.PrivacyPolicyRequest; +import uk.ac.cam.cl.dtg.isaac.api.services.PrivacyPolicyService; +import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO; +import uk.ac.cam.cl.dtg.segue.api.managers.UserAccountManager; +import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserLoggedInException; +import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; + +@ExtendWith(EasyMockExtension.class) +public class PrivacyPolicyServiceTest { + + @Mock + private UserAccountManager userManager; + + @Mock + private Clock mockClock; + + @Mock + private HttpServletRequest request; + + private PrivacyPolicyService service; + private PrivacyPolicyRequest privacyPolicyRequest; + private RegisteredUserDTO testUser; + private final Instant baseTime = Instant.parse("2023-08-29T12:00:00Z"); + + @BeforeEach + public void setUp() { + service = new PrivacyPolicyService(userManager, mockClock); + + privacyPolicyRequest = new PrivacyPolicyRequest(); + privacyPolicyRequest.setPrivacyPolicyAcceptedTime(baseTime.toEpochMilli()); + + testUser = new RegisteredUserDTO(); + testUser.setEmail("test@example.com"); + + expect(mockClock.instant()).andReturn(baseTime).anyTimes(); + replay(mockClock); + } + + @Test + public void testAcceptPrivacyPolicy_Success() throws Exception { + expect(userManager.getCurrentRegisteredUser(request)).andReturn(testUser); + userManager.updatePrivacyPolicyAcceptedTime(testUser, baseTime); + expectLastCall().once(); + + replay(userManager); + + service.acceptPrivacyPolicy(request, privacyPolicyRequest); + + verify(userManager); + } + + @Test + public void testAcceptPrivacyPolicy_TimestampWithinTolerance() throws Exception { + // Timestamp 20 seconds in the future (within 30s tolerance) + Instant futureTime = baseTime.plusSeconds(20); + privacyPolicyRequest.setPrivacyPolicyAcceptedTime(futureTime.toEpochMilli()); + + expect(userManager.getCurrentRegisteredUser(request)).andReturn(testUser); + userManager.updatePrivacyPolicyAcceptedTime(testUser, futureTime); + expectLastCall().once(); + + replay(userManager); + + service.acceptPrivacyPolicy(request, privacyPolicyRequest); + verify(userManager); + } + + @Test + public void testAcceptPrivacyPolicy_TimestampTooFarInFuture() throws Exception { + // Timestamp 60 seconds in the future (outside 30s tolerance) + Instant farFutureTime = baseTime.plusSeconds(60); + privacyPolicyRequest.setPrivacyPolicyAcceptedTime(farFutureTime.toEpochMilli()); + + expect(userManager.getCurrentRegisteredUser(request)).andReturn(testUser); + replay(userManager); + + InvalidTimestampException exception = assertThrows(InvalidTimestampException.class, () -> { + service.acceptPrivacyPolicy(request, privacyPolicyRequest); + }); + + assertTrue(exception.getMessage().contains("Timestamp too far from current time")); + verify(userManager); + } + + @Test + public void testAcceptPrivacyPolicy_TimestampTooFarInPast() throws Exception { + Instant pastTime = baseTime.minusSeconds(45); + privacyPolicyRequest.setPrivacyPolicyAcceptedTime(pastTime.toEpochMilli()); + + expect(userManager.getCurrentRegisteredUser(request)).andReturn(testUser); + replay(userManager); + + InvalidTimestampException exception = assertThrows(InvalidTimestampException.class, () -> { + service.acceptPrivacyPolicy(request, privacyPolicyRequest); + }); + + assertTrue(exception.getMessage().contains("Timestamp too far from current time")); + verify(userManager); + } + + @Test + public void testAcceptPrivacyPolicy_NoUserLoggedIn() throws Exception { + expect(userManager.getCurrentRegisteredUser(request)) + .andThrow(new NoUserLoggedInException()); + + replay(userManager); + + assertThrows(NoUserLoggedInException.class, () -> { + service.acceptPrivacyPolicy(request, privacyPolicyRequest); + }); + + verify(userManager); + } + + @Test + public void testAcceptPrivacyPolicy_DatabaseError() throws Exception { + expect(userManager.getCurrentRegisteredUser(request)).andReturn(testUser); + userManager.updatePrivacyPolicyAcceptedTime(testUser, baseTime); + expectLastCall().andThrow(new SegueDatabaseException("Database connection failed")); + + replay(userManager); + + assertThrows(SegueDatabaseException.class, () -> { + service.acceptPrivacyPolicy(request, privacyPolicyRequest); + }); + + verify(userManager); + } +} From 74d1c8db0ba571806143e3550f95c285c1fef052 Mon Sep 17 00:00:00 2001 From: Madhura Date: Tue, 2 Sep 2025 11:24:57 +0100 Subject: [PATCH 11/14] Fix check style error in EventsFacade file --- src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java index 470f706f14..9db85ce15b 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java @@ -93,13 +93,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateBookingException; -import uk.ac.cam.cl.dtg.isaac.api.managers.EventBookingManager; import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventBookingUpdateException; import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventDeadlineException; import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventGroupReservationLimitException; import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsCancelledException; import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsFullException; import uk.ac.cam.cl.dtg.isaac.api.exceptions.EventIsNotFullException; +import uk.ac.cam.cl.dtg.isaac.api.managers.EventBookingManager; import uk.ac.cam.cl.dtg.isaac.dos.EventStatus; import uk.ac.cam.cl.dtg.isaac.dos.eventbookings.BookingStatus; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; From f25e426d4acb180e9891dd4bb572ec441e7b53dc Mon Sep 17 00:00:00 2001 From: "marius.marin" Date: Wed, 3 Sep 2025 15:54:35 +0300 Subject: [PATCH 12/14] 684 - Added class to handle the EpochTimeStamp from FE --- .../cl/dtg/isaac/api/AssignmentFacade.java | 2 +- .../cl/dtg/isaac/api/GameboardsFacade.java | 2 +- .../cam/cl/dtg/isaac/api/IsaacController.java | 2 - .../api/services/PrivacyPolicyService.java | 37 +-- .../service/PrivacyPolicyServiceTest.java | 211 +++++++++++------- 5 files changed, 142 insertions(+), 112 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java index 8b8f4b77df..17baa431b8 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/AssignmentFacade.java @@ -73,8 +73,8 @@ import org.jboss.resteasy.annotations.GZIP; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import uk.ac.cam.cl.dtg.isaac.api.managers.AssignmentManager; import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateAssignmentException; +import uk.ac.cam.cl.dtg.isaac.api.managers.AssignmentManager; import uk.ac.cam.cl.dtg.isaac.api.managers.GameManager; import uk.ac.cam.cl.dtg.isaac.api.services.AssignmentService; import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java index 420ca29f04..12a6754c0c 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java @@ -57,9 +57,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.ac.cam.cl.dtg.isaac.api.exceptions.DuplicateGameboardException; -import uk.ac.cam.cl.dtg.isaac.api.managers.GameManager; import uk.ac.cam.cl.dtg.isaac.api.exceptions.InvalidGameboardException; import uk.ac.cam.cl.dtg.isaac.api.exceptions.NoWildcardException; +import uk.ac.cam.cl.dtg.isaac.api.managers.GameManager; import uk.ac.cam.cl.dtg.isaac.dos.GameboardCreationMethod; import uk.ac.cam.cl.dtg.isaac.dos.IsaacWildcard; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java index cc7776c73c..12e43e2d26 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/IsaacController.java @@ -16,8 +16,6 @@ package uk.ac.cam.cl.dtg.isaac.api; -import java.time.Instant; - import static uk.ac.cam.cl.dtg.isaac.api.Constants.DOCUMENT_PATH_LOG_FIELDNAME; import static uk.ac.cam.cl.dtg.isaac.api.Constants.IsaacServerLogType; import static uk.ac.cam.cl.dtg.isaac.api.Constants.SITE_WIDE_SEARCH_VALID_DOC_TYPES; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/services/PrivacyPolicyService.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/services/PrivacyPolicyService.java index 7a5188bd6f..ab44b9ff66 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/services/PrivacyPolicyService.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/services/PrivacyPolicyService.java @@ -2,8 +2,6 @@ import com.google.inject.Inject; import jakarta.servlet.http.HttpServletRequest; -import java.time.Clock; -import java.time.Duration; import java.time.Instant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,19 +15,17 @@ public class PrivacyPolicyService { private static final Logger log = LoggerFactory.getLogger(PrivacyPolicyService.class); - private static final long TIME_TOLERANCE_SECONDS = 30; private final UserAccountManager userManager; - private final Clock clock; @Inject - public PrivacyPolicyService(UserAccountManager userManager, Clock clock) { + public PrivacyPolicyService(UserAccountManager userManager) { this.userManager = userManager; - this.clock = clock; } /** - * Accept privacy policy with timestamp validation + * Accept privacy policy with timestamp validation. + * * @param request HTTP request to get current user * @param privacyPolicyRequest Request containing the timestamp * @throws NoUserLoggedInException if no user is logged in @@ -39,33 +35,16 @@ public PrivacyPolicyService(UserAccountManager userManager, Clock clock) { public void acceptPrivacyPolicy(HttpServletRequest request, PrivacyPolicyRequest privacyPolicyRequest) throws NoUserLoggedInException, SegueDatabaseException, InvalidTimestampException { - // Get current user RegisteredUserDTO user = userManager.getCurrentRegisteredUser(request); - // Validate timestamp Instant providedTime = privacyPolicyRequest.getPrivacyPolicyAcceptedTimeInstant(); - validateTimestamp(providedTime); - // Update privacy policy acceptance - userManager.updatePrivacyPolicyAcceptedTime(user, providedTime); + Instant now = Instant.now(); + Instant privacyPolicyTime = providedTime.isAfter(now) ? now : providedTime; - log.info("User {} accepted privacy policy at {}", user.getEmail(), providedTime); - } + userManager.updatePrivacyPolicyAcceptedTime(user, privacyPolicyTime); - /** - * Validates that the provided timestamp is close to the current time - * @param providedTime The timestamp from the client - * @throws InvalidTimestampException if timestamp is outside allowed tolerance - */ - private void validateTimestamp(Instant providedTime) throws InvalidTimestampException { - Instant currentTime = clock.instant(); - long secondsDifference = Math.abs(Duration.between(providedTime, currentTime).getSeconds()); - - if (secondsDifference > TIME_TOLERANCE_SECONDS) { - throw new InvalidTimestampException( - String.format("Timestamp too far from current time. Difference: %d seconds, Max allowed: %d seconds", - secondsDifference, TIME_TOLERANCE_SECONDS) - ); - } + log.info("User {} accepted privacy policy at {}", user.getEmail(), privacyPolicyTime); } + } \ No newline at end of file diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/service/PrivacyPolicyServiceTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/service/PrivacyPolicyServiceTest.java index 91fce17783..0ca4862228 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/service/PrivacyPolicyServiceTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/service/PrivacyPolicyServiceTest.java @@ -1,17 +1,17 @@ package uk.ac.cam.cl.dtg.isaac.service; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.verify; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import jakarta.servlet.http.HttpServletRequest; -import java.time.Clock; import java.time.Instant; +import java.time.temporal.ChronoUnit; +import org.easymock.EasyMock; import org.easymock.EasyMockExtension; -import org.easymock.Mock; +import org.easymock.IMocksControl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,124 +24,177 @@ import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; @ExtendWith(EasyMockExtension.class) -public class PrivacyPolicyServiceTest { +class PrivacyPolicyServiceTest { - @Mock - private UserAccountManager userManager; + private IMocksControl control; + private HttpServletRequest mockRequest; + private PrivacyPolicyRequest mockPrivacyPolicyRequest; + private UserAccountManager mockUserManager; + private RegisteredUserDTO mockUser; - @Mock - private Clock mockClock; + private PrivacyPolicyService privacyPolicyService; - @Mock - private HttpServletRequest request; + @BeforeEach + void setUp() { + control = EasyMock.createControl(); + mockRequest = control.createMock(HttpServletRequest.class); + mockPrivacyPolicyRequest = control.createMock(PrivacyPolicyRequest.class); + mockUserManager = control.createMock(UserAccountManager.class); + mockUser = control.createMock(RegisteredUserDTO.class); + + privacyPolicyService = new PrivacyPolicyService(mockUserManager); + } - private PrivacyPolicyService service; - private PrivacyPolicyRequest privacyPolicyRequest; - private RegisteredUserDTO testUser; - private final Instant baseTime = Instant.parse("2023-08-29T12:00:00Z"); + @Test + void acceptPrivacyPolicy_WithValidPastTime_ShouldUseProvidedTime() + throws NoUserLoggedInException, SegueDatabaseException, InvalidTimestampException { - @BeforeEach - public void setUp() { - service = new PrivacyPolicyService(userManager, mockClock); + Instant pastTime = Instant.now().minus(1, ChronoUnit.HOURS); + String userEmail = "test@example.com"; + + expect(mockUserManager.getCurrentRegisteredUser(mockRequest)).andReturn(mockUser); + expect(mockPrivacyPolicyRequest.getPrivacyPolicyAcceptedTimeInstant()).andReturn(pastTime); + expect(mockUser.getEmail()).andReturn(userEmail); - privacyPolicyRequest = new PrivacyPolicyRequest(); - privacyPolicyRequest.setPrivacyPolicyAcceptedTime(baseTime.toEpochMilli()); + mockUserManager.updatePrivacyPolicyAcceptedTime(mockUser, pastTime); + expectLastCall(); - testUser = new RegisteredUserDTO(); - testUser.setEmail("test@example.com"); + expectLastCall(); - expect(mockClock.instant()).andReturn(baseTime).anyTimes(); - replay(mockClock); + control.replay(); + + privacyPolicyService.acceptPrivacyPolicy(mockRequest, mockPrivacyPolicyRequest); + + control.verify(); } @Test - public void testAcceptPrivacyPolicy_Success() throws Exception { - expect(userManager.getCurrentRegisteredUser(request)).andReturn(testUser); - userManager.updatePrivacyPolicyAcceptedTime(testUser, baseTime); - expectLastCall().once(); + void acceptPrivacyPolicy_WithFutureTime_ShouldUseCurrentTime() + throws NoUserLoggedInException, SegueDatabaseException, InvalidTimestampException { + + Instant futureTime = Instant.now().plus(1, ChronoUnit.HOURS); + String userEmail = "test@example.com"; + + expect(mockUserManager.getCurrentRegisteredUser(mockRequest)).andReturn(mockUser); + expect(mockPrivacyPolicyRequest.getPrivacyPolicyAcceptedTimeInstant()).andReturn(futureTime); + expect(mockUser.getEmail()).andReturn(userEmail); - replay(userManager); + mockUserManager.updatePrivacyPolicyAcceptedTime(eq(mockUser), anyObject(Instant.class)); + expectLastCall(); - service.acceptPrivacyPolicy(request, privacyPolicyRequest); + expectLastCall(); - verify(userManager); + control.replay(); + + privacyPolicyService.acceptPrivacyPolicy(mockRequest, mockPrivacyPolicyRequest); + + control.verify(); } @Test - public void testAcceptPrivacyPolicy_TimestampWithinTolerance() throws Exception { - // Timestamp 20 seconds in the future (within 30s tolerance) - Instant futureTime = baseTime.plusSeconds(20); - privacyPolicyRequest.setPrivacyPolicyAcceptedTime(futureTime.toEpochMilli()); + void acceptPrivacyPolicy_WithCurrentTime_ShouldUseProvidedTime() + throws NoUserLoggedInException, SegueDatabaseException, InvalidTimestampException { + + Instant currentTime = Instant.now(); + String userEmail = "test@example.com"; + + expect(mockUserManager.getCurrentRegisteredUser(mockRequest)).andReturn(mockUser); + expect(mockPrivacyPolicyRequest.getPrivacyPolicyAcceptedTimeInstant()).andReturn(currentTime); + expect(mockUser.getEmail()).andReturn(userEmail); + + mockUserManager.updatePrivacyPolicyAcceptedTime(mockUser, currentTime); + expectLastCall(); - expect(userManager.getCurrentRegisteredUser(request)).andReturn(testUser); - userManager.updatePrivacyPolicyAcceptedTime(testUser, futureTime); - expectLastCall().once(); + expectLastCall(); - replay(userManager); + control.replay(); - service.acceptPrivacyPolicy(request, privacyPolicyRequest); - verify(userManager); + privacyPolicyService.acceptPrivacyPolicy(mockRequest, mockPrivacyPolicyRequest); + + control.verify(); } @Test - public void testAcceptPrivacyPolicy_TimestampTooFarInFuture() throws Exception { - // Timestamp 60 seconds in the future (outside 30s tolerance) - Instant farFutureTime = baseTime.plusSeconds(60); - privacyPolicyRequest.setPrivacyPolicyAcceptedTime(farFutureTime.toEpochMilli()); + void acceptPrivacyPolicy_WhenNoUserLoggedIn_ShouldThrowException() + throws NoUserLoggedInException { + + expect(mockUserManager.getCurrentRegisteredUser(mockRequest)) + .andThrow(new NoUserLoggedInException()); - expect(userManager.getCurrentRegisteredUser(request)).andReturn(testUser); - replay(userManager); + control.replay(); - InvalidTimestampException exception = assertThrows(InvalidTimestampException.class, () -> { - service.acceptPrivacyPolicy(request, privacyPolicyRequest); + assertThrows(NoUserLoggedInException.class, () -> { + privacyPolicyService.acceptPrivacyPolicy(mockRequest, mockPrivacyPolicyRequest); }); - assertTrue(exception.getMessage().contains("Timestamp too far from current time")); - verify(userManager); + control.verify(); } @Test - public void testAcceptPrivacyPolicy_TimestampTooFarInPast() throws Exception { - Instant pastTime = baseTime.minusSeconds(45); - privacyPolicyRequest.setPrivacyPolicyAcceptedTime(pastTime.toEpochMilli()); + void acceptPrivacyPolicy_WhenDatabaseException_ShouldThrowException() + throws NoUserLoggedInException, SegueDatabaseException, InvalidTimestampException { - expect(userManager.getCurrentRegisteredUser(request)).andReturn(testUser); - replay(userManager); + // Arrange + Instant pastTime = Instant.now().minus(1, ChronoUnit.HOURS); - InvalidTimestampException exception = assertThrows(InvalidTimestampException.class, () -> { - service.acceptPrivacyPolicy(request, privacyPolicyRequest); + expect(mockUserManager.getCurrentRegisteredUser(mockRequest)).andReturn(mockUser); + expect(mockPrivacyPolicyRequest.getPrivacyPolicyAcceptedTimeInstant()).andReturn(pastTime); + + mockUserManager.updatePrivacyPolicyAcceptedTime(mockUser, pastTime); + expectLastCall().andThrow(new SegueDatabaseException("Database error")); + + control.replay(); + + assertThrows(SegueDatabaseException.class, () -> { + privacyPolicyService.acceptPrivacyPolicy(mockRequest, mockPrivacyPolicyRequest); }); - assertTrue(exception.getMessage().contains("Timestamp too far from current time")); - verify(userManager); + control.verify(); } @Test - public void testAcceptPrivacyPolicy_NoUserLoggedIn() throws Exception { - expect(userManager.getCurrentRegisteredUser(request)) - .andThrow(new NoUserLoggedInException()); + void acceptPrivacyPolicy_LogsCorrectInformation() + throws NoUserLoggedInException, SegueDatabaseException, InvalidTimestampException { - replay(userManager); + Instant pastTime = Instant.now().minus(30, ChronoUnit.MINUTES); + String userEmail = "user@test.com"; - assertThrows(NoUserLoggedInException.class, () -> { - service.acceptPrivacyPolicy(request, privacyPolicyRequest); - }); + expect(mockUserManager.getCurrentRegisteredUser(mockRequest)).andReturn(mockUser); + expect(mockPrivacyPolicyRequest.getPrivacyPolicyAcceptedTimeInstant()).andReturn(pastTime); + expect(mockUser.getEmail()).andReturn(userEmail); + + mockUserManager.updatePrivacyPolicyAcceptedTime(mockUser, pastTime); + expectLastCall(); + + expectLastCall(); - verify(userManager); + control.replay(); + + privacyPolicyService.acceptPrivacyPolicy(mockRequest, mockPrivacyPolicyRequest); + + control.verify(); } @Test - public void testAcceptPrivacyPolicy_DatabaseError() throws Exception { - expect(userManager.getCurrentRegisteredUser(request)).andReturn(testUser); - userManager.updatePrivacyPolicyAcceptedTime(testUser, baseTime); - expectLastCall().andThrow(new SegueDatabaseException("Database connection failed")); + void acceptPrivacyPolicy_WithBoundaryTime_JustBeforeNow_ShouldUseProvidedTime() + throws NoUserLoggedInException, SegueDatabaseException, InvalidTimestampException { - replay(userManager); + Instant almostNow = Instant.now().minus(1, ChronoUnit.MILLIS); + String userEmail = "boundary@example.com"; - assertThrows(SegueDatabaseException.class, () -> { - service.acceptPrivacyPolicy(request, privacyPolicyRequest); - }); + expect(mockUserManager.getCurrentRegisteredUser(mockRequest)).andReturn(mockUser); + expect(mockPrivacyPolicyRequest.getPrivacyPolicyAcceptedTimeInstant()).andReturn(almostNow); + expect(mockUser.getEmail()).andReturn(userEmail); + + mockUserManager.updatePrivacyPolicyAcceptedTime(mockUser, almostNow); + expectLastCall(); + + expectLastCall(); + + control.replay(); + + privacyPolicyService.acceptPrivacyPolicy(mockRequest, mockPrivacyPolicyRequest); - verify(userManager); + control.verify(); } -} +} \ No newline at end of file From 116851c09dee164247ec95d4933965fb2d23a13f Mon Sep 17 00:00:00 2001 From: "marius.marin" Date: Wed, 3 Sep 2025 16:13:54 +0300 Subject: [PATCH 13/14] 684 - Added class to handle the EpochTimeStamp from FE --- .../dtg/isaac/dos/users/RegisteredUser.java | 347 +++------------- .../isaac/dto/users/RegisteredUserDTO.java | 381 ++---------------- .../api/managers/UserAccountManager.java | 3 +- .../cam/cl/dtg/segue/dao/users/PgUsers.java | 3 +- 4 files changed, 112 insertions(+), 622 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java index 6f4561e303..76873cb1a4 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java @@ -15,18 +15,17 @@ */ package uk.ac.cam.cl.dtg.isaac.dos.users; - import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import java.time.Instant; import java.util.List; +import java.util.Objects; /** * Data Object to represent a user of the system. This object will be persisted in the database. */ public class RegisteredUser extends AbstractSegueUser { private Long id; - private String givenName; private String familyName; private String email; @@ -38,40 +37,26 @@ public class RegisteredUser extends AbstractSegueUser { private String schoolOther; private List registeredContexts; private Instant registeredContextsLastConfirmed; - private String emailVerificationToken; private String emailToVerify; private EmailVerificationStatus emailVerificationStatus; private Boolean teacherPending; - private Instant lastUpdated; private Instant lastSeen; private Instant privacyPolicyAcceptedTime; /** * Full constructor for the User object. - * - * @param id Our database Unique ID - * @param givenName Equivalent to firstname - * @param familyName Equivalent to second name - * @param email primary e-mail address - * @param role role description - * @param dateOfBirth date of birth to help with monitoring - * @param gender gender of the user - * @param registrationDate date of registration - * @param lastUpdated the date this user was last updated. - * @param privacyPolicyAcceptedTime the date this user accepted the privacy policy. - * @param emailToVerify the most recent email for which a token has been generated - * @param emailVerificationToken the most recent token generated to verify email addresses - * @param emailVerificationStatus whether the user has verified their email or not - * @param teacherPending the teacherPending flag value */ @JsonCreator public RegisteredUser( @JsonProperty("id") final Long id, - @JsonProperty("givenName") final String givenName, @JsonProperty("familyName") final String familyName, - @JsonProperty("email") final String email, @JsonProperty("role") final Role role, - @JsonProperty("dateOfBirth") final Instant dateOfBirth, @JsonProperty("gender") final Gender gender, + @JsonProperty("givenName") final String givenName, + @JsonProperty("familyName") final String familyName, + @JsonProperty("email") final String email, + @JsonProperty("role") final Role role, + @JsonProperty("dateOfBirth") final Instant dateOfBirth, + @JsonProperty("gender") final Gender gender, @JsonProperty("registrationDate") final Instant registrationDate, @JsonProperty("lastUpdated") final Instant lastUpdated, @JsonProperty("privacyPolicyAcceptedTime") final Instant privacyPolicyAcceptedTime, @@ -83,352 +68,167 @@ public RegisteredUser( this.id = id; this.familyName = familyName; this.givenName = givenName; - this.email = email; this.role = role; this.dateOfBirth = dateOfBirth; this.gender = gender; this.registrationDate = registrationDate; this.lastUpdated = lastUpdated; this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; - this.emailToVerify = emailToVerify; this.emailVerificationToken = emailVerificationToken; this.emailVerificationStatus = emailVerificationStatus; this.teacherPending = teacherPending; + + setEmail(email); + setEmailToVerify(emailToVerify); } /** * Default constructor required for Jackson. */ public RegisteredUser() { - } - /** - * Gets the id (integer form). - * - * @return the id - * @deprecated use getId - */ + // ID Management @JsonProperty("_id") @Deprecated - // TODO: Deprecate all usage of old mongo ids e.g. _id public Long getLegacyId() { - return this.getId(); + return this.id; } - - /** - * Gets the id (integer form). - * - * @return the id - */ @JsonProperty("id") public Long getId() { return id; } - /** - * Sets the id. - * - * @param id the id to set - */ @JsonProperty("_id") public void setId(final Long id) { this.id = id; } - /** - * Gets the givenName. - * - * @return the givenName - */ - public final String getGivenName() { + public String getGivenName() { return givenName; } - /** - * Sets the givenName. - * - * @param givenName the givenName to set - */ - public final void setGivenName(final String givenName) { + public void setGivenName(final String givenName) { this.givenName = givenName; } - /** - * Gets the familyName. - * - * @return the familyName - */ - public final String getFamilyName() { + public String getFamilyName() { return familyName; } - /** - * Sets the familyName. - * - * @param familyName the familyName to set - */ - public final void setFamilyName(final String familyName) { + public void setFamilyName(final String familyName) { this.familyName = familyName; } - /** - * Gets the email. - * - * @return the email - */ - public final String getEmail() { + public String getEmail() { return email; } - /** - * Sets the email. - * - * @param email the email to set - */ - public final void setEmail(final String email) { - if (email != null) { - this.email = email.trim(); - } else { - this.email = email; - } + public void setEmail(final String email) { + this.email = trimIfNotNull(email); } - /** - * Gets the role. - * - * @return the role - */ - public final Role getRole() { + public Role getRole() { return role; } - /** - * Sets the role. - * - * @param role the role to set - */ - public final void setRole(final Role role) { + public void setRole(final Role role) { this.role = role; } - /** - * Gets the dateOfBirth. - * - * @return the dateOfBirth - */ - public final Instant getDateOfBirth() { + public Instant getDateOfBirth() { return dateOfBirth; } - /** - * Sets the dateOfBirth. - * - * @param dateOfBirth the dateOfBirth to set - */ - public final void setDateOfBirth(final Instant dateOfBirth) { + public void setDateOfBirth(final Instant dateOfBirth) { this.dateOfBirth = dateOfBirth; } - /** - * Gets the gender. - * - * @return the gender - */ - public final Gender getGender() { + public Gender getGender() { return gender; } - /** - * Sets the gender. - * - * @param gender the gender to set - */ - public final void setGender(final Gender gender) { + public void setGender(final Gender gender) { this.gender = gender; } - /** - * Gets the registrationDate. - * - * @return the registrationDate - */ - public final Instant getRegistrationDate() { + public Instant getRegistrationDate() { return registrationDate; } - /** - * Sets the registrationDate. - * - * @param registrationDate the registrationDate to set - */ - public final void setRegistrationDate(final Instant registrationDate) { + public void setRegistrationDate(final Instant registrationDate) { this.registrationDate = registrationDate; } - /** - * Gets the schoolId. - * - * @return the schoolId - */ - public final String getSchoolId() { + public String getSchoolId() { return schoolId; } - /** - * Sets the schoolId. - * - * @param schoolId the schoolId to set - */ - public final void setSchoolId(final String schoolId) { + public void setSchoolId(final String schoolId) { this.schoolId = schoolId; } - /** - * Gets the schoolOther. - * - * @return the schoolOther - */ public String getSchoolOther() { return schoolOther; } - /** - * Sets the schoolOther. - * - * @param schoolOther the schoolOther to set - */ public void setSchoolOther(final String schoolOther) { this.schoolOther = schoolOther; } - /** - * Gets the email. - * - * @return the email to verify - */ - public final String getEmailToVerify() { + public String getEmailToVerify() { return emailToVerify; } - /** - * Sets the email. - * - * @param emailToVerify the email to verify - */ - public final void setEmailToVerify(final String emailToVerify) { - if (emailToVerify != null) { - this.emailToVerify = emailToVerify.trim(); - } else { - this.emailToVerify = emailToVerify; - } + public void setEmailToVerify(final String emailToVerify) { + this.emailToVerify = trimIfNotNull(emailToVerify); } - /** - * Sets the email verification token. - * - * @param verificationToken token created by authenticator - */ - public final void setEmailVerificationToken(final String verificationToken) { - this.emailVerificationToken = verificationToken; + public String getEmailVerificationToken() { + return emailVerificationToken; } - /** - * Gets the email verification token. - * - * @return the email verification token - */ - public final String getEmailVerificationToken() { - return this.emailVerificationToken; + public void setEmailVerificationToken(final String emailVerificationToken) { + this.emailVerificationToken = emailVerificationToken; } - /** - * Get the verification status of the provided email address. - * - * @return the EmailVerificationStatus - */ public EmailVerificationStatus getEmailVerificationStatus() { return emailVerificationStatus; } - /** - * Set the verification status of the provided email address. - * - * @param status sets the EmailVerificationStatus - */ - public void setEmailVerificationStatus(final EmailVerificationStatus status) { - this.emailVerificationStatus = status; + public void setEmailVerificationStatus(final EmailVerificationStatus emailVerificationStatus) { + this.emailVerificationStatus = emailVerificationStatus; } - /** - * Gets the lastUpdated. - * - * @return the lastUpdated - */ public Instant getLastUpdated() { return lastUpdated; } - /** - * Sets the lastUpdated. - * - * @param lastUpdated the lastUpdated to set - */ public void setLastUpdated(final Instant lastUpdated) { this.lastUpdated = lastUpdated; } - /** - * Gets the privacyPolicyAcceptedTime. - * - * @return the privacyPolicyAcceptedTime - */ public Instant getPrivacyPolicyAcceptedTime() { return privacyPolicyAcceptedTime; } - /** - * Sets the privacyPolicyAcceptedTime. - * - * @param privacyPolicyAcceptedTime the privacyPolicyAcceptedTime to set - */ public void setPrivacyPolicyAcceptedTime(final Instant privacyPolicyAcceptedTime) { this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; } - /** - * Gets the lastSeen. - * - * @return the lastSeen - */ public Instant getLastSeen() { return lastSeen; } - /** - * Sets the lastSeen. - * - * @param lastSeen the lastSeen to set - */ public void setLastSeen(final Instant lastSeen) { this.lastSeen = lastSeen; } - /** - * Gets the teacherPending flag. - * - * @return the teacherPending flag - */ public Boolean getTeacherPending() { return teacherPending; } - /** - * Sets the teacherPending flag. - * - * @param teacherPending the teacherPending flag value to set - */ public void setTeacherPending(final Boolean teacherPending) { this.teacherPending = teacherPending; } @@ -449,55 +249,38 @@ public void setRegisteredContextsLastConfirmed(final Instant registeredContextsL this.registeredContextsLastConfirmed = registeredContextsLastConfirmed; } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((id == null) ? 0 : id.hashCode()); - return result; + /** + * Utility method to trim strings consistently, handling null values. + * Eliminates duplicated trimming logic in email setters. + */ + private String trimIfNotNull(final String value) { + return value != null ? value.trim() : null; } @Override public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof RegisteredUser other)) { - return false; - } - if (id == null) { - if (other.id != null) { - return false; - } - } else if (!id.equals(other.id)) { - return false; - } - return true; + if (this == obj) return true; + if (!(obj instanceof RegisteredUser other)) return false; + return Objects.equals(id, other.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); } @Override public String toString() { - return "RegisteredUser{" - + "id=" + id - + ", givenName='" + givenName + '\'' - + ", familyName='" + familyName + '\'' - + ", email='" + email + '\'' - + ", role=" + role - + ", dateOfBirth=" + dateOfBirth - + ", gender=" + gender - + ", registrationDate=" + registrationDate - + ", schoolId='" + schoolId + '\'' - + ", schoolOther='" + schoolOther + '\'' - + ", emailVerificationToken='" + emailVerificationToken + '\'' - + ", emailToVerify='" + emailToVerify + '\'' - + ", emailVerificationStatus=" + emailVerificationStatus - + ", teacherPending=" + teacherPending - + ", lastUpdated=" + lastUpdated - + ", privacyPolicyAcceptedTime=" + privacyPolicyAcceptedTime - + ", lastSeen=" + lastSeen - + '}'; - } -} + return String.format( + "RegisteredUser{id=%d, givenName='%s', familyName='%s', email='%s', role=%s, " + + "dateOfBirth=%s, gender=%s, registrationDate=%s, schoolId='%s', schoolOther='%s', " + + "emailVerificationToken='%s', emailToVerify='%s', emailVerificationStatus=%s, " + + "teacherPending=%s, lastUpdated=%s, privacyPolicyAcceptedTime=%s, lastSeen=%s, " + + "registeredContexts=%s, registeredContextsLastConfirmed=%s}", + id, givenName, familyName, email, role, dateOfBirth, gender, registrationDate, + schoolId, schoolOther, emailVerificationToken, emailToVerify, emailVerificationStatus, + teacherPending, lastUpdated, privacyPolicyAcceptedTime, lastSeen, + registeredContexts, registeredContextsLastConfirmed + ); + } +} \ No newline at end of file diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java index 732d750866..b5055d7683 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.time.Instant; import java.util.List; +import java.util.Objects; import uk.ac.cam.cl.dtg.isaac.dos.users.EmailVerificationStatus; import uk.ac.cam.cl.dtg.isaac.dos.users.Gender; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; @@ -30,7 +31,6 @@ */ public class RegisteredUserDTO extends AbstractSegueUserDTO { private Long id; - private String givenName; private String familyName; private String email; @@ -38,12 +38,10 @@ public class RegisteredUserDTO extends AbstractSegueUserDTO { private Instant dateOfBirth; private Gender gender; private Instant registrationDate; - private String schoolId; private String schoolOther; private List registeredContexts; private Instant registeredContextsLastConfirmed; - private boolean firstLogin = false; private Instant lastUpdated; private Instant privacyPolicyAcceptedTime; @@ -53,16 +51,6 @@ public class RegisteredUserDTO extends AbstractSegueUserDTO { /** * Full constructor for the User object. - * - * @param givenName Equivalent to firstname - * @param familyName Equivalent to second name - * @param email primary e-mail address - * @param emailVerificationStatus verification status of email address - * @param dateOfBirth date of birth to help with monitoring - * @param gender gender of the user - * @param registrationDate date of registration - * @param schoolId the list of linked authentication provider accounts. - * @param teacherPending the teacherPending flag value to set */ @JsonCreator public RegisteredUserDTO( @@ -87,327 +75,150 @@ public RegisteredUserDTO( this.teacherPending = teacherPending; } - /** * Default constructor required for Jackson. */ public RegisteredUserDTO() { - } - /** - * Gets the id. - * - * @return the id - */ @JsonProperty("id") public Long getId() { return id; } - - /** - * Sets the id. - * - * @param id the id to set - */ @JsonProperty("id") public void setId(final Long id) { this.id = id; } - /** - * Gets the id. - * - * @return the id - * @deprecated - TODO need to remove _id from frontend - */ @JsonProperty("_id") @Deprecated public Long getLegacyId() { return this.getId(); } - - /** - * Sets the id. - * - * @param id the id to set - * @deprecated - TODO need to remove _id from frontend - */ @JsonProperty("_id") @Deprecated public void setLegacyId(final Long id) { this.setId(id); } - /** - * Gets the givenName. - * - * @return the givenName - */ public String getGivenName() { return givenName; } - /** - * Sets the givenName. - * - * @param givenName the givenName to set - */ public void setGivenName(final String givenName) { this.givenName = givenName; } - /** - * Gets the familyName. - * - * @return the familyName - */ public String getFamilyName() { return familyName; } - /** - * Sets the familyName. - * - * @param familyName the familyName to set - */ public void setFamilyName(final String familyName) { this.familyName = familyName; } - /** - * Gets the email. - * - * @return the email - */ public String getEmail() { return email; } - /** - * Sets the email. - * - * @param email the email to set - */ public void setEmail(final String email) { this.email = email; } - /** - * Gets the role. - * - * @return the role - */ public Role getRole() { return role; } - /** - * Sets the role. - * - * @param role the role to set - */ public void setRole(final Role role) { this.role = role; } - /** - * Gets the dateOfBirth. - * - * @return the dateOfBirth - */ public Instant getDateOfBirth() { return dateOfBirth; } - /** - * Sets the dateOfBirth. - * - * @param dateOfBirth the dateOfBirth to set - */ public void setDateOfBirth(final Instant dateOfBirth) { this.dateOfBirth = dateOfBirth; } - - /** - * Get the verification status of the provided email address. - * - * @return the EmailVerificationStatus - */ public EmailVerificationStatus getEmailVerificationStatus() { return this.emailVerificationStatus; } - /** - * Set the verification status of the provided email address. - * - * @param emailVerificationStatus sets the EmailVerificationStatus - */ public void setEmailVerificationStatus(final EmailVerificationStatus emailVerificationStatus) { this.emailVerificationStatus = emailVerificationStatus; } - /** - * Gets the gender. - * - * @return the gender - */ public Gender getGender() { return gender; } - /** - * Sets the gender. - * - * @param gender the gender to set - */ public void setGender(final Gender gender) { this.gender = gender; } - /** - * Gets the registrationDate. - * - * @return the registrationDate - */ public Instant getRegistrationDate() { return registrationDate; } - /** - * Sets the registrationDate. - * - * @param registrationDate the registrationDate to set - */ public void setRegistrationDate(final Instant registrationDate) { this.registrationDate = registrationDate; } - /** - * Gets the schoolId. - * - * @return the schoolId - */ public String getSchoolId() { return schoolId; } - /** - * Sets the schoolId. - * - * @param schoolId the schoolId to set - */ public void setSchoolId(final String schoolId) { this.schoolId = schoolId; } - /** - * Gets the schoolOther. - * - * @return the schoolOther - */ public String getSchoolOther() { return schoolOther; } - /** - * Sets the schoolOther. - * - * @param schoolOther the schoolOther to set - */ public void setSchoolOther(final String schoolOther) { this.schoolOther = schoolOther; } - /** - * Gets the firstLogin. - * - * @return the firstLogin - */ public boolean isFirstLogin() { return firstLogin; } - /** - * Sets the firstLogin. - * - * @param firstLogin the firstLogin to set - */ public void setFirstLogin(final boolean firstLogin) { this.firstLogin = firstLogin; } - /** - * Gets the lastUpdated. - * - * @return the lastUpdated - */ public Instant getLastUpdated() { return lastUpdated; } - /** - * Sets the lastUpdated. - * - * @param lastUpdated the lastUpdated to set - */ public void setLastUpdated(final Instant lastUpdated) { this.lastUpdated = lastUpdated; } - /** - * Gets the privacyPolicyAcceptedTime. - * - * @return the privacyPolicyAcceptedTime - */ public Instant getPrivacyPolicyAcceptedTime() { return privacyPolicyAcceptedTime; } - /** - * Sets the privacyPolicyAcceptedTime. - * - * @param privacyPolicyAcceptedTime the privacyPolicyAcceptedTime to set - */ public void setPrivacyPolicyAcceptedTime(final Instant privacyPolicyAcceptedTime) { this.privacyPolicyAcceptedTime = privacyPolicyAcceptedTime; } - /** - * Gets the lastSeen. - * - * @return the lastSeen - */ public Instant getLastSeen() { return lastSeen; } - /** - * Sets the lastSeen. - * - * @param lastSeen the lastSeen to set - */ public void setLastSeen(final Instant lastSeen) { this.lastSeen = lastSeen; } - /** - * Gets the teacherPending flag. - * - * @return the teacherPending flag - */ public Boolean getTeacherPending() { return teacherPending; } - /** - * Sets the teacherPending flag. - * - * @param teacherPending the teacherPending flag value to set - */ public void setTeacherPending(final Boolean teacherPending) { this.teacherPending = teacherPending; } @@ -428,165 +239,59 @@ public void setRegisteredContextsLastConfirmed(final Instant registeredContextsL this.registeredContextsLastConfirmed = registeredContextsLastConfirmed; } + // Object Methods - Using modern Java and Objects utility @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((id == null) ? 0 : id.hashCode()); - return result; + public boolean equals(final Object obj) { + if (this == obj) return true; + if (!(obj instanceof RegisteredUserDTO other)) return false; + return Objects.equals(id, other.id); } @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof RegisteredUserDTO other)) { - return false; - } - if (id == null) { - if (other.id != null) { - return false; - } - } else if (!id.equals(other.id)) { - return false; - } - return true; + public int hashCode() { + return Objects.hash(id); } /** * A method that tests if each field in the object is equal to each in the other. - * - * @param obj to check - * @return true if the same false if not. + * Optimized using Objects.equals for cleaner null handling. */ public boolean strictEquals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof RegisteredUserDTO other)) { - return false; - } - if (id == null) { - if (other.id != null) { - return false; - } - } else if (!id.equals(other.id)) { - return false; - } - if (dateOfBirth == null) { - if (other.dateOfBirth != null) { - return false; - } - } else if (!dateOfBirth.equals(other.dateOfBirth)) { - return false; - } - if (email == null) { - if (other.email != null) { - return false; - } - } else if (!email.equals(other.email)) { - return false; - } - if (emailVerificationStatus == null) { - if (other.emailVerificationStatus != null) { - return false; - } - } else if (emailVerificationStatus.compareTo(other.emailVerificationStatus) != 0) { - return false; - } - if (familyName == null) { - if (other.familyName != null) { - return false; - } - } else if (!familyName.equals(other.familyName)) { - return false; - } - if (firstLogin != other.firstLogin) { - return false; - } - if (gender != other.gender) { - return false; - } - if (givenName == null) { - if (other.givenName != null) { - return false; - } - } else if (!givenName.equals(other.givenName)) { - return false; - } - if (lastUpdated == null) { - if (other.lastUpdated != null) { - return false; - } - } else if (!lastUpdated.equals(other.lastUpdated)) { - return false; - } - if (privacyPolicyAcceptedTime == null) { - if (other.privacyPolicyAcceptedTime != null) { - return false; - } - } else if (!privacyPolicyAcceptedTime.equals(other.privacyPolicyAcceptedTime)) { - return false; - } - if (registrationDate == null) { - if (other.registrationDate != null) { - return false; - } - } else if (!registrationDate.equals(other.registrationDate)) { - return false; - } - if (role != other.role) { - return false; - } - if (schoolId == null) { - if (other.schoolId != null) { - return false; - } - } else if (!schoolId.equals(other.schoolId)) { - return false; - } - if (schoolOther == null) { - if (other.schoolOther != null) { - return false; - } - } else if (!schoolOther.equals(other.schoolOther)) { - return false; - } - if (teacherPending == null) { - if (other.teacherPending != null) { - return false; - } - } else if (!teacherPending.equals(other.teacherPending)) { - return false; - } - return true; + if (this == obj) return true; + if (!(obj instanceof RegisteredUserDTO other)) return false; + + return Objects.equals(id, other.id) && + Objects.equals(dateOfBirth, other.dateOfBirth) && + Objects.equals(email, other.email) && + Objects.equals(emailVerificationStatus, other.emailVerificationStatus) && + Objects.equals(familyName, other.familyName) && + firstLogin == other.firstLogin && + gender == other.gender && + Objects.equals(givenName, other.givenName) && + Objects.equals(lastUpdated, other.lastUpdated) && + Objects.equals(privacyPolicyAcceptedTime, other.privacyPolicyAcceptedTime) && + Objects.equals(registrationDate, other.registrationDate) && + role == other.role && + Objects.equals(schoolId, other.schoolId) && + Objects.equals(schoolOther, other.schoolOther) && + Objects.equals(teacherPending, other.teacherPending) && + Objects.equals(lastSeen, other.lastSeen) && + Objects.equals(registeredContexts, other.registeredContexts) && + Objects.equals(registeredContextsLastConfirmed, other.registeredContextsLastConfirmed); } @Override public String toString() { - return "RegisteredUserDTO [" - + "id=" + id - + ", givenName=" + givenName - + ", familyName=" + familyName - + ", email=" + email - + ", role=" + role - + ", dateOfBirth=" + dateOfBirth - + ", gender=" + gender - + ", registrationDate=" + registrationDate - + ", schoolId=" + schoolId - + ", schoolOther=" + schoolOther - + ", emailVerificationStatus=" + emailVerificationStatus - + ", teacherPending=" + teacherPending - + ", firstLogin=" + firstLogin - + ", lastUpdated=" + lastUpdated - + ", privacyPolicyAcceptedTime=" + privacyPolicyAcceptedTime - + "]"; - } -} + return String.format( + "RegisteredUserDTO{id=%d, givenName='%s', familyName='%s', email='%s', role=%s, " + + "dateOfBirth=%s, gender=%s, registrationDate=%s, schoolId='%s', schoolOther='%s', " + + "emailVerificationStatus=%s, teacherPending=%s, firstLogin=%s, lastUpdated=%s, " + + "privacyPolicyAcceptedTime=%s, lastSeen=%s, registeredContexts=%s, " + + "registeredContextsLastConfirmed=%s}", + id, givenName, familyName, email, role, dateOfBirth, gender, registrationDate, + schoolId, schoolOther, emailVerificationStatus, teacherPending, firstLogin, + lastUpdated, privacyPolicyAcceptedTime, lastSeen, registeredContexts, + registeredContextsLastConfirmed + ); + } +} \ No newline at end of file diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java index c260887a1f..dbca46de44 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/UserAccountManager.java @@ -1991,7 +1991,8 @@ private void updateLastSeen(final RegisteredUser user) throws SegueDatabaseExcep * @param userDTO of interest * @throws SegueDatabaseException if an error occurs with the update. */ - public void updatePrivacyPolicyAcceptedTime(final RegisteredUserDTO userDTO, Instant privacyPolicyAcceptedTime) throws SegueDatabaseException { + public void updatePrivacyPolicyAcceptedTime(final RegisteredUserDTO userDTO, Instant privacyPolicyAcceptedTime) + throws SegueDatabaseException { RegisteredUser user = findUserById(userDTO.getId()); this.database.updatePrivacyPolicyAcceptedTime(user, privacyPolicyAcceptedTime); } diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java index 4e5d67b3f2..3c80aa4657 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/users/PgUsers.java @@ -697,7 +697,8 @@ public void updateUserLastSeen(final RegisteredUser user, final Instant date) th } @Override - public void updatePrivacyPolicyAcceptedTime(final RegisteredUser user, final Instant acceptedTime) throws SegueDatabaseException { + public void updatePrivacyPolicyAcceptedTime(final RegisteredUser user, final Instant acceptedTime) + throws SegueDatabaseException { requireNonNull(user); requireNonNull(acceptedTime); From c703fafc39c8a6cf957c08245b80c04b730d2e25 Mon Sep 17 00:00:00 2001 From: "marius.marin" Date: Wed, 3 Sep 2025 16:22:36 +0300 Subject: [PATCH 14/14] 684 - Added class to handle the EpochTimeStamp from FE --- .../cl/dtg/isaac/dos/users/RegisteredUser.java | 14 ++++++++------ .../dtg/isaac/dto/users/RegisteredUserDTO.java | 18 +++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java index 76873cb1a4..012a6583ae 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/users/RegisteredUser.java @@ -259,7 +259,9 @@ private String trimIfNotNull(final String value) { @Override public boolean equals(final Object obj) { - if (this == obj) return true; + if (this == obj) { + return true; + } if (!(obj instanceof RegisteredUser other)) return false; return Objects.equals(id, other.id); } @@ -272,11 +274,11 @@ public int hashCode() { @Override public String toString() { return String.format( - "RegisteredUser{id=%d, givenName='%s', familyName='%s', email='%s', role=%s, " + - "dateOfBirth=%s, gender=%s, registrationDate=%s, schoolId='%s', schoolOther='%s', " + - "emailVerificationToken='%s', emailToVerify='%s', emailVerificationStatus=%s, " + - "teacherPending=%s, lastUpdated=%s, privacyPolicyAcceptedTime=%s, lastSeen=%s, " + - "registeredContexts=%s, registeredContextsLastConfirmed=%s}", + "RegisteredUser{id=%d, givenName='%s', familyName='%s', email='%s', role=%s, " + + "dateOfBirth=%s, gender=%s, registrationDate=%s, schoolId='%s', schoolOther='%s', " + + "emailVerificationToken='%s', emailToVerify='%s', emailVerificationStatus=%s, " + + "teacherPending=%s, lastUpdated=%s, privacyPolicyAcceptedTime=%s, lastSeen=%s, " + + "registeredContexts=%s, registeredContextsLastConfirmed=%s}", id, givenName, familyName, email, role, dateOfBirth, gender, registrationDate, schoolId, schoolOther, emailVerificationToken, emailToVerify, emailVerificationStatus, teacherPending, lastUpdated, privacyPolicyAcceptedTime, lastSeen, diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java index b5055d7683..b1b1665083 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/users/RegisteredUserDTO.java @@ -242,7 +242,9 @@ public void setRegisteredContextsLastConfirmed(final Instant registeredContextsL // Object Methods - Using modern Java and Objects utility @Override public boolean equals(final Object obj) { - if (this == obj) return true; + if (this == obj) { + return true; + } if (!(obj instanceof RegisteredUserDTO other)) return false; return Objects.equals(id, other.id); } @@ -257,7 +259,9 @@ public int hashCode() { * Optimized using Objects.equals for cleaner null handling. */ public boolean strictEquals(final Object obj) { - if (this == obj) return true; + if (this == obj) { + return true; + } if (!(obj instanceof RegisteredUserDTO other)) return false; return Objects.equals(id, other.id) && @@ -283,11 +287,11 @@ public boolean strictEquals(final Object obj) { @Override public String toString() { return String.format( - "RegisteredUserDTO{id=%d, givenName='%s', familyName='%s', email='%s', role=%s, " + - "dateOfBirth=%s, gender=%s, registrationDate=%s, schoolId='%s', schoolOther='%s', " + - "emailVerificationStatus=%s, teacherPending=%s, firstLogin=%s, lastUpdated=%s, " + - "privacyPolicyAcceptedTime=%s, lastSeen=%s, registeredContexts=%s, " + - "registeredContextsLastConfirmed=%s}", + "RegisteredUserDTO{id=%d, givenName='%s', familyName='%s', email='%s', role=%s, " + + "dateOfBirth=%s, gender=%s, registrationDate=%s, schoolId='%s', schoolOther='%s', " + + "emailVerificationStatus=%s, teacherPending=%s, firstLogin=%s, lastUpdated=%s, " + + "privacyPolicyAcceptedTime=%s, lastSeen=%s, registeredContexts=%s, " + + "registeredContextsLastConfirmed=%s}", id, givenName, familyName, email, role, dateOfBirth, gender, registrationDate, schoolId, schoolOther, emailVerificationStatus, teacherPending, firstLogin, lastUpdated, privacyPolicyAcceptedTime, lastSeen, registeredContexts,