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 item_outfit_relationships TO impress2020;
GRANT SELECT ON neopets_connections TO impress2020; GRANT SELECT ON neopets_connections TO impress2020;
GRANT SELECT ON outfits 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 -- mysqldump
GRANT LOCK TABLES ON * TO impress2020; GRANT LOCK TABLES ON * TO impress2020;

View file

@ -10,6 +10,11 @@ import {
InputGroup, InputGroup,
InputLeftElement, InputLeftElement,
InputRightElement, InputRightElement,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Wrap, Wrap,
VStack, VStack,
useToast, useToast,
@ -17,13 +22,14 @@ import {
import { import {
ArrowForwardIcon, ArrowForwardIcon,
CheckIcon, CheckIcon,
EditIcon,
EmailIcon, EmailIcon,
SearchIcon, SearchIcon,
StarIcon, StarIcon,
} from "@chakra-ui/icons"; } from "@chakra-ui/icons";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { useHistory, useParams } from "react-router-dom"; 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 SimpleMarkdown from "simple-markdown";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
@ -37,9 +43,15 @@ import ItemCard, {
YouWantThisBadge, YouWantThisBadge,
getZoneBadges, getZoneBadges,
} from "./components/ItemCard"; } from "./components/ItemCard";
import SupportOnly from "./WardrobePage/support/SupportOnly";
import useSupport from "./WardrobePage/support/useSupport";
import useCurrentUser from "./components/useCurrentUser"; import useCurrentUser from "./components/useCurrentUser";
import WIPCallout from "./components/WIPCallout"; import WIPCallout from "./components/WIPCallout";
const BadgeButton = React.forwardRef((props, ref) => (
<Badge as="button" ref={ref} {...props} />
));
function UserItemsPage() { function UserItemsPage() {
const { userId } = useParams(); const { userId } = useParams();
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
@ -164,6 +176,14 @@ function UserItemsPage() {
Neomail Neomail
</Badge> </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 {/* Usually I put "Own" before "Want", but this matches the natural
* order on the page: the _matches_ for things you want are things * 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 * _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) { function NeopetsStarIcon(props) {
// Converted from the Neopets favicon with https://www.vectorizer.io/. // Converted from the Neopets favicon with https://www.vectorizer.io/.
return ( return (

View file

@ -32,6 +32,9 @@ const typePolicies = {
color: (_, { args, toReference }) => { color: (_, { args, toReference }) => {
return toReference({ __typename: "Color", id: args.id }, true); 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 { 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 { const {
capitalize, capitalize,
getPoseFromPetState, getPoseFromPetState,
@ -51,6 +60,8 @@ const typeDefs = gql`
isGlitched: Boolean! isGlitched: Boolean!
supportSecret: String! supportSecret: String!
): PetAppearance! ): PetAppearance!
setUsername(userId: ID!, newUsername: String!, supportSecret: String!): User
} }
`; `;
@ -61,9 +72,7 @@ const resolvers = {
{ itemId, colorId, supportSecret }, { itemId, colorId, supportSecret },
{ itemLoader, itemTranslationLoader, colorTranslationLoader, db } { itemLoader, itemTranslationLoader, colorTranslationLoader, db }
) => { ) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) { assertSupportSecretOrThrow(supportSecret);
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
const oldItem = await itemLoader.load(itemId); const oldItem = await itemLoader.load(itemId);
@ -139,9 +148,7 @@ const resolvers = {
{ itemId, explicitlyBodySpecific, supportSecret }, { itemId, explicitlyBodySpecific, supportSecret },
{ itemLoader, itemTranslationLoader, db } { itemLoader, itemTranslationLoader, db }
) => { ) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) { assertSupportSecretOrThrow(supportSecret);
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
const oldItem = await itemLoader.load(itemId); const oldItem = await itemLoader.load(itemId);
@ -210,9 +217,7 @@ const resolvers = {
db, db,
} }
) => { ) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) { assertSupportSecretOrThrow(supportSecret);
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
const oldSwfAsset = await swfAssetLoader.load(layerId); const oldSwfAsset = await swfAssetLoader.load(layerId);
@ -298,9 +303,7 @@ const resolvers = {
db, db,
} }
) => { ) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) { assertSupportSecretOrThrow(supportSecret);
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
const oldSwfAsset = await swfAssetLoader.load(layerId); const oldSwfAsset = await swfAssetLoader.load(layerId);
@ -374,9 +377,7 @@ const resolvers = {
db, db,
} }
) => { ) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) { assertSupportSecretOrThrow(supportSecret);
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
const oldPetState = await petStateLoader.load(appearanceId); const oldPetState = await petStateLoader.load(appearanceId);
@ -457,9 +458,7 @@ const resolvers = {
db, db,
} }
) => { ) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) { assertSupportSecretOrThrow(supportSecret);
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
const oldPetState = await petStateLoader.load(appearanceId); const oldPetState = await petStateLoader.load(appearanceId);
@ -526,7 +525,85 @@ const resolvers = {
return { id: appearanceId }; 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 }; module.exports = { typeDefs, resolvers };