diff --git a/src/app/UserItemsPage.js b/src/app/UserItemsPage.js index 336da4c..61f0606 100644 --- a/src/app/UserItemsPage.js +++ b/src/app/UserItemsPage.js @@ -265,10 +265,12 @@ function UserItemsPage() { function UserSearchForm() { const [query, setQuery] = React.useState(""); + + const { isSupportUser, supportSecret } = useSupport(); const history = useHistory(); const toast = useToast(); - const [loadUserSearch, { loading }] = useLazyQuery( + const [loadUserSearch, { loading: loading1 }] = useLazyQuery( gql` query UserSearchForm($name: String!) { userByName(name: $name) { @@ -302,11 +304,58 @@ function UserSearchForm() { } ); + const [loadUserByEmail, { loading: loading2 }] = useLazyQuery( + gql` + query UserSearchFormByEmail($email: String!, $supportSecret: String!) { + userByEmail(email: $email, supportSecret: $supportSecret) { + id + # Consider preloading UserItemsPage fields here, too? + } + } + `, + { + onCompleted: (data) => { + const user = data.userByEmail; + if (!user) { + toast({ + status: "warning", + title: "We couldn't find that email address!", + description: "Check the spelling and try again?", + }); + return; + } + + history.push(`/user/${user.id}/items`); + }, + onError: (error) => { + console.error(error); + toast({ + status: "error", + title: "Error loading user by email!", + description: "Check your connection and try again?", + }); + }, + } + ); + return ( { - loadUserSearch({ variables: { name: query } }); + const isSupportOnlyEmailSearch = + isSupportUser && query.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/); + + if (isSupportOnlyEmailSearch) { + toast({ + status: "info", + title: "Searching by email! (💖 Support-only)", + description: "The email field is protected from most users.", + }); + loadUserByEmail({ variables: { email: query, supportSecret } }); + } else { + loadUserSearch({ variables: { name: query } }); + } + e.preventDefault(); }} > @@ -326,7 +375,7 @@ function UserSearchForm() { variant="ghost" icon={} aria-label="Search" - isLoading={loading} + isLoading={loading1 || loading2} minWidth="1.5rem" minHeight="1.5rem" width="1.5rem" diff --git a/src/server/loaders.js b/src/server/loaders.js index c6521cf..b5f6858 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -774,6 +774,29 @@ const buildUserByNameLoader = (db) => ); }); +const buildUserByEmailLoader = (db) => + new DataLoader(async (emails) => { + const qs = emails.map((_) => "?").join(","); + const [rows, _] = await db.execute( + { + sql: ` + SELECT users.*, id_users.email FROM users + INNER JOIN openneo_id.users id_users ON id_users.id = users.remote_id + WHERE id_users.email IN (${qs}) + `, + nestTables: true, + }, + emails + ); + + const entities = rows.map((row) => ({ + user: normalizeRow(row.users), + email: row.id_users.email, + })); + + return emails.map((email) => entities.find((e) => e.email === email).user); + }); + const buildUserClosetHangersLoader = (db) => new DataLoader(async (userIds) => { const qs = userIds.map((_) => "?").join(","); @@ -907,6 +930,7 @@ function buildLoaders(db) { loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db); loaders.userLoader = buildUserLoader(db); loaders.userByNameLoader = buildUserByNameLoader(db); + loaders.userByEmailLoader = buildUserByEmailLoader(db); loaders.userClosetHangersLoader = buildUserClosetHangersLoader(db); loaders.userClosetListsLoader = buildUserClosetListsLoader(db); loaders.zoneLoader = buildZoneLoader(db); diff --git a/src/server/types/User.js b/src/server/types/User.js index e6231a9..2f02387 100644 --- a/src/server/types/User.js +++ b/src/server/types/User.js @@ -41,6 +41,7 @@ const typeDefs = gql` extend type Query { user(id: ID!): User userByName(name: String!): User + userByEmail(email: String!, supportSecret: String!): User currentUser: User } `; @@ -224,6 +225,19 @@ const resolvers = { return { id: user.id }; }, + userByEmail: async (_, { email, supportSecret }, { userByEmailLoader }) => { + if (supportSecret !== process.env["SUPPORT_SECRET"]) { + throw new Error(`Support secret is incorrect. Try setting up again?`); + } + + const user = await userByEmailLoader.load(email); + if (!user) { + return null; + } + + return { id: user.id }; + }, + currentUser: async (_, __, { currentUserId, userLoader }) => { if (currentUserId == null) { return null;