diff --git a/src/main/java/org/ecocean/User.java b/src/main/java/org/ecocean/User.java index 54dd0205b8..3bf0c107ed 100644 --- a/src/main/java/org/ecocean/User.java +++ b/src/main/java/org/ecocean/User.java @@ -115,6 +115,13 @@ public User(String email, String uuid) { this.lastLogin = -1; } + /** + * @deprecated GH-1545: Callers have historically misused this constructor by passing + * an email address where a UUID is expected, producing accounts whose primary key + * is the email. Prefer {@code new User(email, Util.generateUUID())} or another + * constructor when you do not already have a real UUID. + */ + @Deprecated public User(String uuid) { this.uuid = uuid; setReceiveEmails(false); diff --git a/src/main/java/org/ecocean/servlet/ProjectCreate.java b/src/main/java/org/ecocean/servlet/ProjectCreate.java index 9393f649fb..722e92084a 100644 --- a/src/main/java/org/ecocean/servlet/ProjectCreate.java +++ b/src/main/java/org/ecocean/servlet/ProjectCreate.java @@ -112,17 +112,24 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) } } if (projectUserIds != null && projectUserIds.length() > 0) { + JSONArray unresolved = res.optJSONArray("unresolvedUserIds"); + if (unresolved == null) { + unresolved = new JSONArray(); + res.put("unresolvedUserIds", unresolved); + } for (int i = 0; i < projectUserIds.length(); i++) { String userIdentifier = projectUserIds.getString(i); if (!Util.stringIsEmptyOrNull(userIdentifier)) { - User user = null; - if (Util.isUUID(userIdentifier)) { - user = myShepherd.getUserByUUID(userIdentifier); - } else { - user = myShepherd.getUser(userIdentifier); - } + // GH-1545: use shared resolver with email-address fallback for + // legacy accounts whose UUID primary key stores an email. + User user = ProjectUpdate.resolveUser(myShepherd, userIdentifier); if (user != null) { newProject.addUser(user); + } else { + unresolved.put(userIdentifier); + System.out.println( + "ProjectCreate: could not resolve userIdentifier=" + + userIdentifier); } } } diff --git a/src/main/java/org/ecocean/servlet/ProjectUpdate.java b/src/main/java/org/ecocean/servlet/ProjectUpdate.java index bdc95b5356..bc4623705a 100644 --- a/src/main/java/org/ecocean/servlet/ProjectUpdate.java +++ b/src/main/java/org/ecocean/servlet/ProjectUpdate.java @@ -187,20 +187,25 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) private void addOrRemoveUsersFromProject(Project project, Shepherd myShepherd, JSONArray usersToAddJSONArr, String action, JSONObject res) { + JSONArray unresolved = res.optJSONArray("unresolvedUserIds"); + if (unresolved == null) { + unresolved = new JSONArray(); + res.put("unresolvedUserIds", unresolved); + } for (int i = 0; i < usersToAddJSONArr.length(); i++) { String userId = usersToAddJSONArr.getString(i); - User user = null; - if (Util.isUUID(userId)) { - user = myShepherd.getUserByUUID(userId); - } else { - user = myShepherd.getUser(userId); - } + User user = resolveUser(myShepherd, userId); if (user != null && !StringUtils.isNullOrEmpty(action)) { if ("add".equals(action)) { project.addUser(user); } else if ("remove".equals(action)) { project.removeUser(user); } + } else { + unresolved.put(userId); + System.out.println( + "ProjectUpdate.addOrRemoveUsersFromProject: could not resolve userId=" + + userId + " for action=" + action + " project=" + project.getId()); } } myShepherd.updateDBTransaction(); @@ -208,6 +213,37 @@ private void addOrRemoveUsersFromProject(Project project, Shepherd myShepherd, res.put("success", true); } + // GH-1545: Resolve a user from an autocomplete-supplied identifier. + // + // Ordering matters. Some legacy User accounts have an email stored in their UUID + // primary key (see UserCreate.java history and the deprecated User(String uuid) + // constructor). For those rows, the frontend posts `u.getId()` which is the email. + // + // We therefore try the PK lookup FIRST, not last — and regardless of whether the + // string looks UUID-shaped — because: + // (1) for real UUIDs it's the direct, correct path; and + // (2) for legacy email-as-UUID rows it still resolves via the literal PK + // ("alice@example.org" IS that user's primary key), which avoids a wrong + // match if some other account happens to have that email as their username. + // + // Only after the PK miss do we try username and then email-address lookups. + // Every email-fallback hit is logged so operators can gauge the migration backlog. + static User resolveUser(Shepherd myShepherd, String userId) { + if (StringUtils.isNullOrEmpty(userId)) return null; + User user = myShepherd.getUserByUUID(userId); + if (user != null) return user; + user = myShepherd.getUser(userId); + if (user != null) return user; + user = myShepherd.getUserByEmailAddress(userId); + if (user != null) { + System.out.println( + "ProjectUpdate.resolveUser: resolved userId=" + userId + + " via email-address fallback (GH-1545 legacy uuid). user.uuid=" + + user.getUUID()); + } + return user; + } + private JSONArray removeUnauthorizedEncounters(JSONArray encountersToAddJSONArr, Shepherd myShepherd, HttpServletRequest request) { JSONArray filteredResults = new JSONArray(); diff --git a/src/main/java/org/ecocean/servlet/UserCreate.java b/src/main/java/org/ecocean/servlet/UserCreate.java index 934f41d704..726e21aa15 100644 --- a/src/main/java/org/ecocean/servlet/UserCreate.java +++ b/src/main/java/org/ecocean/servlet/UserCreate.java @@ -100,7 +100,10 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) salt); newUser = new User(username, hashedPassword, salt); } else { - newUser = new User(email); + // GH-1545: use the (email, uuid) ctor so the email is not stored as the UUID. + // The single-arg User(String uuid) ctor was misused here, producing + // accounts whose primary key is the email address. + newUser = new User(email, Util.generateUUID()); } myShepherd.getPM().makePersistent(newUser); createThisUser = true;