From 33b834e1d1a6fc0a6e1fe73fcda471b2691cc150 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Wed, 6 Apr 2022 10:08:01 -0500 Subject: [PATCH] +++CLDR-14802 users download wip - add a /users endpoint for listing users - add LocaleSet.toStringArray() and unit tests - add helper function in UserRegistry - add an initial UserList ( #users ) page. For now it only has the download button. - add a link to the UserList page. --- tools/cldr-apps/js/src/esm/cldrAccount.js | 13 +-- .../cldr-apps/js/src/specialToComponentMap.js | 2 + tools/cldr-apps/js/src/views/UserList.vue | 31 ++++++ .../org/unicode/cldr/web/UserRegistry.java | 57 ++++++++++- .../org/unicode/cldr/web/api/UserItem.java | 32 +++++++ .../org/unicode/cldr/web/api/UserLevels.java | 2 +- .../org/unicode/cldr/web/api/UsersAPI.java | 95 +++++++++++++++++++ .../org/unicode/cldr/util/LocaleSetTest.java | 24 +++++ .../java/org/unicode/cldr/util/LocaleSet.java | 22 +++++ 9 files changed, 262 insertions(+), 16 deletions(-) create mode 100644 tools/cldr-apps/js/src/views/UserList.vue create mode 100644 tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UserItem.java create mode 100644 tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UsersAPI.java create mode 100644 tools/cldr-apps/src/test/java/org/unicode/cldr/util/LocaleSetTest.java diff --git a/tools/cldr-apps/js/src/esm/cldrAccount.js b/tools/cldr-apps/js/src/esm/cldrAccount.js index d14505b3a2d..98979745444 100644 --- a/tools/cldr-apps/js/src/esm/cldrAccount.js +++ b/tools/cldr-apps/js/src/esm/cldrAccount.js @@ -270,9 +270,9 @@ function getHtml(json) { html += getBulkActionMenu(json); html += getListHiderControls(json); } + html += getDownloadCsvForm(json); html += getTable(json); html += getInterestLocalesHtml(json); - html += getDownloadCsvForm(json); if (json.exception) { html += "

Failure: " + json.exception + "

\n"; } @@ -1255,17 +1255,6 @@ function getDownloadCsvForm(json) { if (isJustMe || !json.userPerms || !json.userPerms.canModifyUsers) { return ""; } - // TODO: not jsp; see https://unicode-org.atlassian.net/browse/CLDR-14475 - return ( - "
\n" + - "
\n" + - " \n" + - " \n" + - " \n" + - "
\n" - ); } function setOnClicks() { diff --git a/tools/cldr-apps/js/src/specialToComponentMap.js b/tools/cldr-apps/js/src/specialToComponentMap.js index b4a3377d55a..d1c17d058ab 100644 --- a/tools/cldr-apps/js/src/specialToComponentMap.js +++ b/tools/cldr-apps/js/src/specialToComponentMap.js @@ -8,6 +8,7 @@ import MainMenu from "./views/MainMenu.vue"; import TestPanel from "./views/TestPanel.vue"; import TransferVotes from "./views/TransferVotes.vue"; import UnknownPanel from "./views/UnknownPanel.vue"; +import UserList from "./views/UserList.vue"; import VettingParticipation2 from "./views/VettingParticipation2.vue"; import VettingSummary from "./views/VettingSummary.vue"; import WaitingPanel from "./views/WaitingPanel.vue"; @@ -27,6 +28,7 @@ const specialToComponentMap = { retry_inplace: WaitingPanel, // Like retry, but do NOT redirect after resume. test_panel: TestPanel, // for testing transfervotes: TransferVotes, + users: UserList, vetting_participation2: VettingParticipation2, vsummary: VettingSummary, // If no match, end up here diff --git a/tools/cldr-apps/js/src/views/UserList.vue b/tools/cldr-apps/js/src/views/UserList.vue new file mode 100644 index 00000000000..7869ef56007 --- /dev/null +++ b/tools/cldr-apps/js/src/views/UserList.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/tools/cldr-apps/src/main/java/org/unicode/cldr/web/UserRegistry.java b/tools/cldr-apps/src/main/java/org/unicode/cldr/web/UserRegistry.java index df1b9667cb0..ac47e24ee79 100644 --- a/tools/cldr-apps/src/main/java/org/unicode/cldr/web/UserRegistry.java +++ b/tools/cldr-apps/src/main/java/org/unicode/cldr/web/UserRegistry.java @@ -23,6 +23,9 @@ import javax.json.bind.annotation.JsonbProperty; +import com.ibm.icu.dev.util.ElapsedTimer; +import com.ibm.icu.lang.UCharacter; + import org.apache.commons.codec.digest.DigestUtils; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.json.JSONException; @@ -40,9 +43,7 @@ import org.unicode.cldr.util.VoteResolver; import org.unicode.cldr.util.VoteResolver.Level; import org.unicode.cldr.util.VoteResolver.VoterInfo; - -import com.ibm.icu.dev.util.ElapsedTimer; -import com.ibm.icu.lang.UCharacter; +import org.unicode.cldr.web.api.UserItem; /** * This class represents the list of all registered users. It contains an inner @@ -597,6 +598,22 @@ public CLDRLocale exampleLocale() { } return null; } + + public UserItem toUserItem() { + UserItem u = new UserItem(); + u.id = this.id; + u.level = getLevel().name(); + u.name = this.name; + u.org = this.org; + u.email = this.email; + u.assignedLocales = this.getAuthorizedLocaleSet().toStringArray(); + if (this.last_connect != null) { + u.lastLogin = this.last_connect.toInstant(); + } else { + u.lastLogin = null; + } + return u; + } } public static void printPasswordLink(WebContext ctx, String email, String password) { @@ -993,6 +1010,40 @@ public UserRegistry.User getEmptyUser() { public UserRegistry() { } + /** + * Get a list of user ids for a particular org, or all. + * Anonymous users are always skipped. + * @param org org to filter on, or null for all + * @param includeLocked if true, include LOCKED users, otherwise they are omitted + * @return + * @throws SQLException + */ + public List getUserIdList(String org, boolean includeLocked) throws SQLException { + // lifted from UserList.java + Connection conn = null; + PreparedStatement ps = null; + java.sql.ResultSet rs = null; + // List users = new LinkedList<>(); + // collect list of ids + List ids = new LinkedList<>(); + conn = DBUtils.getInstance().getAConnection(); + ps = CookieSession.sm.reg.list(org, conn); // org = null to list all + rs = ps.executeQuery(); + while (rs.next()) { + int level = rs.getInt(2); + if (level == UserRegistry.ANONYMOUS) { + continue; + } + if (!includeLocked + && level >= UserRegistry.LOCKED) { + continue; + } + int id = rs.getInt(1); + ids.add(id); + } + return ids; + } + // ------- special things for "list" mode: public java.sql.PreparedStatement list(String organization, Connection conn) throws SQLException { diff --git a/tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UserItem.java b/tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UserItem.java new file mode 100644 index 00000000000..96da1b6ed4b --- /dev/null +++ b/tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UserItem.java @@ -0,0 +1,32 @@ +package org.unicode.cldr.web.api; + +import java.time.Instant; + +/** + * This class exists because UserRegistry.User is unserializable + * at present. + */ +public class UserItem implements Comparable { + public String level; + public String name; + public String org; + public int id; + public String email; + public String assignedLocales[]; + public Instant lastLogin; + + /** + * Order by org, name, id + */ + @Override + public int compareTo(UserItem o) { + int rc = org.compareTo(o.org); + if (rc == 0) { + rc = name.compareTo(o.name); + } + if (rc == 0) { + rc = Integer.compare(id, o.id); + } + return rc; + } +} diff --git a/tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UserLevels.java b/tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UserLevels.java index ef259fb450d..ea201aeeaab 100644 --- a/tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UserLevels.java +++ b/tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UserLevels.java @@ -18,7 +18,7 @@ import org.unicode.cldr.web.UserRegistry; @Path("/userlevels") -@Tag(name = "userlevels", description = "Get the list of Survey Tool user levels") +@Tag(name = "user", description = "APIs for user management") public class UserLevels { @GET diff --git a/tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UsersAPI.java b/tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UsersAPI.java new file mode 100644 index 00000000000..8775f06fefd --- /dev/null +++ b/tools/cldr-apps/src/main/java/org/unicode/cldr/web/api/UsersAPI.java @@ -0,0 +1,95 @@ +package org.unicode.cldr.web.api; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.unicode.cldr.web.CookieSession; +import org.unicode.cldr.web.DBUtils; +import org.unicode.cldr.web.SurveyLog; +import org.unicode.cldr.web.UserRegistry; + +@Path("/users") +@Tag(name = "user", description = "APIs for user management") +public class UsersAPI { + private static final Logger logger = SurveyLog.forClass(UsersAPI.class); + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "List Users", + description = "List users, optionally filtered by organization") + @APIResponses( + value = { + @APIResponse( + responseCode = "200", + description = "User list", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = UserList.class)) + ) + } + ) + public Response listUsers( + @HeaderParam(Auth.SESSION_HEADER) String session, + @QueryParam("org") @Schema(required = false, description = "Organization Filter (ignored unless admin)", example = "Guest") String org, + @QueryParam("includeLocked") @Schema(required = false, description = "True to include locked users", defaultValue = "false", example = "false") boolean includeLocked + ) { + // boilerplate: login + final CookieSession mySession = Auth.getSession(session); + if (mySession == null) { + return Auth.noSessionResponse(); + } + if (mySession.user == null || !UserRegistry.userCanCreateUsers(mySession.user)) { + return Response.status(Status.UNAUTHORIZED).build(); + } + + if (!UserRegistry.userIsAdmin(mySession.user)) { + // non-admins can only list their own org + org = mySession.user.org; + } + // collect list of ids + List ids = null; + try { + ids = CookieSession.sm.reg.getUserIdList(org, includeLocked); + } catch(SQLException se) { + logger.log(java.util.logging.Level.WARNING, + "Query for org " + org + " failed: " + DBUtils.unchainSqlException(se), se); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity(new STError(se)).build(); + } + // now collect the actual users + Set users = new TreeSet<>(); + for (final int id : ids) { + UserItem u = CookieSession.sm.reg.getInfo(id).toUserItem(); + users.add(u); + } + + return Response.ok(new UserList(users)).build(); + } + + public static final class UserList { + UserList(Collection c) { + users = c.toArray(new UserItem[c.size()]); + } + public UserItem users[]; + } +} diff --git a/tools/cldr-apps/src/test/java/org/unicode/cldr/util/LocaleSetTest.java b/tools/cldr-apps/src/test/java/org/unicode/cldr/util/LocaleSetTest.java new file mode 100644 index 00000000000..2113e821f7b --- /dev/null +++ b/tools/cldr-apps/src/test/java/org/unicode/cldr/util/LocaleSetTest.java @@ -0,0 +1,24 @@ +package org.unicode.cldr.util; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.Collections; + +import com.google.common.collect.ImmutableSet; + +import org.junit.jupiter.api.Test; + +public class LocaleSetTest { + @Test + public void testToString() { + final String[] ALL_LOCALES = { "*" }; + assertArrayEquals(ALL_LOCALES, new LocaleSet(true).toStringArray(), "for all-locales set"); + + final String[] NO_LOCALES = {}; + assertArrayEquals(NO_LOCALES, new LocaleSet().toStringArray(), "for no-locales set"); + assertArrayEquals(NO_LOCALES, new LocaleSet(Collections.emptySet()).toStringArray(), "for empty set"); + + final String[] ONE_LOCALE = { "tlh" }; + assertArrayEquals(ONE_LOCALE, new LocaleSet(ImmutableSet.of("tlh")).toStringArray(), "for one-locale set"); + } +} diff --git a/tools/cldr-code/src/main/java/org/unicode/cldr/util/LocaleSet.java b/tools/cldr-code/src/main/java/org/unicode/cldr/util/LocaleSet.java index ee9f32902d6..1a23d040083 100644 --- a/tools/cldr-code/src/main/java/org/unicode/cldr/util/LocaleSet.java +++ b/tools/cldr-code/src/main/java/org/unicode/cldr/util/LocaleSet.java @@ -75,4 +75,26 @@ public Set getSet() { return set; } + private String[] getAllStringArray() { + String s[] = new String[1]; + s[0] = "*"; + return s; + } + + /** + * Return the LocaleSet as an array of Strings + * (for API use). Returns {"*"} for allLocales. + * @return + */ + public String[] toStringArray() { + if (isAllLocales) { + return getAllStringArray(); + } else { + Set str = new TreeSet<>(); + for (final CLDRLocale l : set) { + str.add(l.getBaseName()); + } + return str.toArray(new String[str.size()]); + } + } }