support tool to edit usernames
This commit is contained in:
parent
6a43f92438
commit
7c9313f4a6
4 changed files with 191 additions and 20 deletions
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in a new issue