support tool to edit usernames

This commit is contained in:
Emi Matchu 2020-11-18 07:42:40 -08:00
parent 6a43f92438
commit 7c9313f4a6
4 changed files with 191 additions and 20 deletions

View file

@ -30,7 +30,8 @@ GRANT SELECT ON closet_lists TO impress2020;
GRANT SELECT ON item_outfit_relationships TO impress2020;
GRANT SELECT ON neopets_connections TO impress2020;
GRANT SELECT ON outfits TO impress2020;
GRANT SELECT ON users TO impress2020;
GRANT SELECT, UPDATE ON users TO impress2020;
GRANT SELECT, UPDATE ON openneo_id.users TO impress2020;
-- mysqldump
GRANT LOCK TABLES ON * TO impress2020;

View file

@ -10,6 +10,11 @@ import {
InputGroup,
InputLeftElement,
InputRightElement,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Wrap,
VStack,
useToast,
@ -17,13 +22,14 @@ import {
import {
ArrowForwardIcon,
CheckIcon,
EditIcon,
EmailIcon,
SearchIcon,
StarIcon,
} from "@chakra-ui/icons";
import gql from "graphql-tag";
import { useHistory, useParams } from "react-router-dom";
import { useQuery, useLazyQuery } from "@apollo/client";
import { useQuery, useLazyQuery, useMutation } from "@apollo/client";
import SimpleMarkdown from "simple-markdown";
import DOMPurify from "dompurify";
@ -37,9 +43,15 @@ import ItemCard, {
YouWantThisBadge,
getZoneBadges,
} from "./components/ItemCard";
import SupportOnly from "./WardrobePage/support/SupportOnly";
import useSupport from "./WardrobePage/support/useSupport";
import useCurrentUser from "./components/useCurrentUser";
import WIPCallout from "./components/WIPCallout";
const BadgeButton = React.forwardRef((props, ref) => (
<Badge as="button" ref={ref} {...props} />
));
function UserItemsPage() {
const { userId } = useParams();
const currentUser = useCurrentUser();
@ -164,6 +176,14 @@ function UserItemsPage() {
Neomail
</Badge>
)}
<SupportOnly>
<UserSupportMenu user={data.user}>
<MenuButton as={BadgeButton} display="flex" alignItems="center">
<EditIcon marginRight="1" />
Support
</MenuButton>
</UserSupportMenu>
</SupportOnly>
{/* Usually I put "Own" before "Want", but this matches the natural
* order on the page: the _matches_ for things you want are things
* _this user_ owns, so they come first. I think it's also probably a
@ -454,6 +474,76 @@ function MarkdownAndSafeHTML({ children }) {
);
}
function UserSupportMenu({ children, user }) {
const { supportSecret } = useSupport();
const toast = useToast();
const [sendEditUsernameMutation] = useMutation(
gql`
mutation UserSupportMenuRename(
$userId: ID!
$newUsername: String!
$supportSecret: String!
) {
setUsername(
userId: $userId
newUsername: $newUsername
supportSecret: $supportSecret
) {
id
username
}
}
`,
{
onCompleted: (data) => {
const updatedUser = data.setUsername;
toast({
status: "success",
title: `Successfully renamed user ${updatedUser.id} to ${updatedUser.username}!`,
});
},
}
);
const editUsername = React.useCallback(() => {
const newUsername = prompt(
"What should this user's username be?",
user.username
);
if (!newUsername || newUsername === user.username) {
toast({
status: "info",
title: "Got it, no change!",
description: `User ${user.id}'s username will continue to be ${user.username}.`,
});
return;
}
sendEditUsernameMutation({
variables: { userId: user.id, newUsername, supportSecret },
}).catch((e) => {
console.error(e);
toast({
status: "error",
title: "Error renaming user.",
description: "See error details in the console!",
});
});
}, [sendEditUsernameMutation, user.id, user.username, supportSecret, toast]);
return (
<Menu>
{children}
<Portal>
<MenuList>
<MenuItem onClick={editUsername}>Edit username</MenuItem>
</MenuList>
</Portal>
</Menu>
);
}
function NeopetsStarIcon(props) {
// Converted from the Neopets favicon with https://www.vectorizer.io/.
return (

View file

@ -32,6 +32,9 @@ const typePolicies = {
color: (_, { args, toReference }) => {
return toReference({ __typename: "Color", id: args.id }, true);
},
user: (_, { args, toReference }) => {
return toReference({ __typename: "User", id: args.id }, true);
},
},
},

View file

@ -1,4 +1,13 @@
const { gql } = require("apollo-server");
const { ManagementClient } = require("auth0");
const auth0 = new ManagementClient({
domain: "openneo.us.auth0.com",
clientId: process.env.AUTH0_SUPPORT_CLIENT_ID,
clientSecret: process.env.AUTH0_SUPPORT_CLIENT_SECRET,
scope: "read:users update:users",
});
const {
capitalize,
getPoseFromPetState,
@ -51,6 +60,8 @@ const typeDefs = gql`
isGlitched: Boolean!
supportSecret: String!
): PetAppearance!
setUsername(userId: ID!, newUsername: String!, supportSecret: String!): User
}
`;
@ -61,9 +72,7 @@ const resolvers = {
{ itemId, colorId, supportSecret },
{ itemLoader, itemTranslationLoader, colorTranslationLoader, db }
) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
assertSupportSecretOrThrow(supportSecret);
const oldItem = await itemLoader.load(itemId);
@ -139,9 +148,7 @@ const resolvers = {
{ itemId, explicitlyBodySpecific, supportSecret },
{ itemLoader, itemTranslationLoader, db }
) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
assertSupportSecretOrThrow(supportSecret);
const oldItem = await itemLoader.load(itemId);
@ -210,9 +217,7 @@ const resolvers = {
db,
}
) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
assertSupportSecretOrThrow(supportSecret);
const oldSwfAsset = await swfAssetLoader.load(layerId);
@ -298,9 +303,7 @@ const resolvers = {
db,
}
) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
assertSupportSecretOrThrow(supportSecret);
const oldSwfAsset = await swfAssetLoader.load(layerId);
@ -374,9 +377,7 @@ const resolvers = {
db,
}
) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
assertSupportSecretOrThrow(supportSecret);
const oldPetState = await petStateLoader.load(appearanceId);
@ -457,9 +458,7 @@ const resolvers = {
db,
}
) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
assertSupportSecretOrThrow(supportSecret);
const oldPetState = await petStateLoader.load(appearanceId);
@ -526,7 +525,85 @@ const resolvers = {
return { id: appearanceId };
},
setUsername: async (
_,
{ userId, newUsername, supportSecret },
{ userLoader, db }
) => {
assertSupportSecretOrThrow(supportSecret);
const oldUser = await userLoader.load(userId);
if (!oldUser) {
return null;
}
const [[result1, result2]] = await db.query(
`
UPDATE users SET name = ? WHERE id = ? LIMIT 1;
UPDATE openneo_id.users SET name = ? WHERE id = ? LIMIT 1;
`,
[newUsername, userId, newUsername, oldUser.remoteId]
);
if (result1.affectedRows !== 1) {
throw new Error(
`[UPDATE 1] Expected to affect 1 user, but affected ${result1.affectedRows}`
);
}
if (result2.affectedRows !== 1) {
throw new Error(
`[UPDATE 2] Expected to affect 1 user, but affected ${result2.affectedRows}`
);
}
// we changed it, so clear it from cache
userLoader.clear(userId);
// We also want to update the username in Auth0, which is separate! It's
// possible that this will fail even though the db update succeeded, but
// I'm not going to bother to write recovery code; in that case, the
// error will reach the support user console, and we can work to manually
// fix it.
await auth0.users.update(
{ id: `auth0|impress-${userId}` },
{ username: newUsername }
);
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
try {
await logToDiscord({
embeds: [
{
title: `🛠 User ${oldUser.id}: ${newUsername}`,
fields: [
{
name: `Username`,
value: `${oldUser.name} → **${newUsername}**`,
},
],
timestamp: new Date().toISOString(),
url: `https://impress-2020.openneo.net/user/${oldUser.id}/items`,
},
],
});
} catch (e) {
console.error("Error sending Discord support log", e);
}
} else {
console.warn("No Discord support webhook provided, skipping");
}
return { id: userId };
},
},
};
function assertSupportSecretOrThrow(supportSecret) {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
}
module.exports = { typeDefs, resolvers };