Skip to content

Commit

Permalink
Display name validation (#1633)
Browse files Browse the repository at this point in the history
Adding some extra validation for input fields in the login and edit
display name pages so that empty or whitespace names are no longer
allowed. This should fix the #1565 issue.
It also changes all instances of the display name to the user's auth ID
in the website only in case it doesn't match the validation.
I added a small file with the function that's used to validate these
display names in `website/src/lib/display_name_validation.ts`.

---------

Co-authored-by: notmd <tinhmeo10@gmail.com>
  • Loading branch information
markuschue and notmd committed Feb 22, 2023
1 parent e38d639 commit ed0e3cb
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 16 deletions.
17 changes: 17 additions & 0 deletions website/src/lib/display_name_validation.ts
@@ -0,0 +1,17 @@
export const validDisplayNameRegex = /^\S+/g;
/**
* Given a user's display name and its ID, returns a valid display name,
* checking if the original display name is invalid (e.g. empty or starts
* with whitespace).
*
* @param {string} displayName The user's display name.
* @param {string} id The user's ID.
* @returns {string} A valid display name.
*/
export const getValidDisplayName = (displayName: string, id: string): string => {
return !isValidDisplayName(displayName) ? id : displayName;
};

export const isValidDisplayName = (displayName: string) => {
return displayName && displayName.match(validDisplayNameRegex);
};
22 changes: 19 additions & 3 deletions website/src/pages/account/edit.tsx
@@ -1,9 +1,10 @@
import { Button, Input, InputGroup } from "@chakra-ui/react";
import { Button, FormControl, FormErrorMessage, Input, InputGroup } from "@chakra-ui/react";
import Head from "next/head";
import Router from "next/router";
import { useSession } from "next-auth/react";
import React from "react";
import { Control, useForm, useWatch } from "react-hook-form";
import { validDisplayNameRegex } from "src/lib/display_name_validation";
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";

export default function Account() {
Expand Down Expand Up @@ -49,7 +50,12 @@ const EditForm = () => {
}
};

const { register, handleSubmit, control } = useForm<{ username: string }>({
const {
register,
formState: { errors },
handleSubmit,
control,
} = useForm<{ username: string }>({
defaultValues: {
username: session?.user.name,
},
Expand All @@ -58,7 +64,17 @@ const EditForm = () => {
return (
<form onSubmit={handleSubmit(updateUser)}>
<InputGroup>
<Input placeholder="Edit Username" type="text" {...register("username")}></Input>
<FormControl isInvalid={errors.username ? true : false}>
<Input
placeholder="Edit Username"
type="text"
{...register("username", { required: true, pattern: validDisplayNameRegex })}
></Input>
<FormErrorMessage>
{errors.username?.type === "required" && "Username is required"}
{errors.username?.type === "pattern" && "Username is invalid"}
</FormErrorMessage>
</FormControl>
<SubmitButton control={control}></SubmitButton>
</InputGroup>
</form>
Expand Down
2 changes: 2 additions & 0 deletions website/src/pages/admin/manage_user/[id].tsx
Expand Up @@ -33,6 +33,7 @@ import prisma from "src/lib/prismadb";
import { getFrontendUserIdForDiscordUser } from "src/lib/users";
import { FetchUserMessagesCursorResponse } from "src/types/Conversation";
import { User } from "src/types/Users";
import { getValidDisplayName } from "src/lib/display_name_validation";
import useSWRImmutable from "swr/immutable";
import useSWRMutation from "swr/mutation";

Expand Down Expand Up @@ -204,6 +205,7 @@ export const getServerSideProps: GetServerSideProps<{ user: User<Role> }, { id:
...backend_user,
role: (local_user?.role || "general") as Role,
};
user.display_name = getValidDisplayName(user.display_name, user.id);
return {
props: {
user,
Expand Down
5 changes: 5 additions & 0 deletions website/src/pages/api/admin/trollboard.ts
@@ -1,6 +1,7 @@
import { withAnyRole } from "src/lib/auth";
import { createApiClient } from "src/lib/oasst_client_factory";
import { TrollboardTimeFrame } from "src/types/Trollboard";
import { getValidDisplayName } from "src/lib/display_name_validation";

export default withAnyRole(["admin", "moderator"], async (req, res, token) => {
const client = await createApiClient(token);
Expand All @@ -9,5 +10,9 @@ export default withAnyRole(["admin", "moderator"], async (req, res, token) => {
limit: req.query.limit as unknown as number,
});

trollboard.trollboard.forEach((troll) => {
troll.display_name = getValidDisplayName(troll.display_name, troll.username);
});

return res.status(200).json(trollboard);
});
3 changes: 3 additions & 0 deletions website/src/pages/api/admin/users.ts
Expand Up @@ -2,6 +2,7 @@ import { withAnyRole } from "src/lib/auth";
import { createApiClient } from "src/lib/oasst_client_factory";
import prisma from "src/lib/prismadb";
import { FetchUsersParams } from "src/types/Users";
import { getValidDisplayName } from "src/lib/display_name_validation";

/**
* The number of users to fetch in a single request. Could later be a query parameter.
Expand Down Expand Up @@ -46,13 +47,15 @@ const handler = withAnyRole(["admin", "moderator"], async (req, res, token) => {

// Combine the information by updating the set of full users with their role.
// Default any users without a role set locally as "general".
// If the user's display name is invalid, change it to its ID.
const local_user_map = local_users.reduce((result, user) => {
result.set(user.id, user.role);
return result;
}, new Map());

const users = all_users.map((user) => {
const role = local_user_map.get(user.id) || "general";
user.display_name = getValidDisplayName(user.display_name, user.id);
return {
...user,
role,
Expand Down
15 changes: 13 additions & 2 deletions website/src/pages/api/leaderboard.ts
Expand Up @@ -2,6 +2,7 @@ import { withoutRole } from "src/lib/auth";
import { createApiClient } from "src/lib/oasst_client_factory";
import { getBackendUserCore } from "src/lib/users";
import { LeaderboardTimeFrame } from "src/types/Leaderboard";
import { getValidDisplayName } from "src/lib/display_name_validation";

/**
* Returns the set of valid labels that can be applied to messages.
Expand All @@ -13,9 +14,10 @@ const handler = withoutRole("banned", async (req, res, token) => {
const includeUserStats = req.query.includeUserStats;

if (includeUserStats !== "true") {
const leaderboard = await oasstApiClient.fetch_leaderboard(time_frame, {
let leaderboard = await oasstApiClient.fetch_leaderboard(time_frame, {
limit: req.query.limit as unknown as number,
});
leaderboard = getValidLeaderboard(leaderboard);
return res.status(200).json(leaderboard);
}
const user = await oasstApiClient.fetch_frontend_user(backendUser);
Expand All @@ -27,10 +29,19 @@ const handler = withoutRole("banned", async (req, res, token) => {
oasstApiClient.fetch_user_stats_window(user.user_id, time_frame, 3),
]);

const validLeaderboard = getValidLeaderboard(leaderboard);

res.status(200).json({
...leaderboard,
...validLeaderboard,
user_stats_window: user_stats?.leaderboard.map((stats) => ({ ...stats, is_window: true })),
});
});

const getValidLeaderboard = (leaderboard) => {
leaderboard.leaderboard.forEach((user) => {
user.display_name = getValidDisplayName(user.display_name, user.username);
});
return leaderboard;
};

export default handler;
5 changes: 5 additions & 0 deletions website/src/pages/api/username.ts
@@ -1,11 +1,16 @@
import { withoutRole } from "src/lib/auth";
import { isValidDisplayName } from "src/lib/display_name_validation";
import prisma from "src/lib/prismadb";

/**
* Updates the user's `name` field in the `User` table.
*/
const handler = withoutRole("banned", async (req, res, token) => {
const { username } = req.body;
if (!isValidDisplayName(username)) {
return res.status(400).json({ message: "Invalid username" });
}

const { name } = await prisma.user.update({
where: {
id: token.sub,
Expand Down
39 changes: 28 additions & 11 deletions website/src/pages/auth/signin.tsx
@@ -1,4 +1,4 @@
import { Button, ButtonProps, Input, Stack, useColorModeValue } from "@chakra-ui/react";
import { Button, ButtonProps, FormControl, FormErrorMessage, Input, Stack, useColorModeValue } from "@chakra-ui/react";
import { useColorMode } from "@chakra-ui/react";
import { TurnstileInstance } from "@marsidev/react-turnstile";
import { boolean } from "boolean";
Expand Down Expand Up @@ -164,7 +164,11 @@ const EmailSignInForm = ({
enableEmailSigninCaptcha: boolean;
cloudflareCaptchaSiteKey: string;
}) => {
const { register, handleSubmit } = useForm<{ email: string }>();
const {
register,
formState: { errors },
handleSubmit,
} = useForm<{ email: string }>();
const captcha = useRef<TurnstileInstance>(null);
const [captchaSuccess, setCaptchaSuccess] = useState(false);
const signinWithEmail = (data: { email: string }) => {
Expand All @@ -177,14 +181,21 @@ const EmailSignInForm = ({
return (
<form onSubmit={handleSubmit(signinWithEmail)}>
<Stack>
<Input
type="email"
data-cy="email-address"
variant="outline"
size="lg"
placeholder="Email Address"
{...register("email")}
/>
<FormControl isInvalid={errors.email ? true : false}>
<Input
type="email"
data-cy="email-address"
variant="outline"
size="lg"
placeholder="Email Address"
{...register("email", { required: true, pattern: /[^\s@]+@[^\s@]+\.[^\s@]+/g })}
errorBorderColor="orange.600"
/>
<FormErrorMessage>
{errors.email?.type === "required" && "Email is required"}
{errors.email?.type === "pattern" && "Email is invalid"}
</FormErrorMessage>
</FormControl>
{enableEmailSigninCaptcha && (
<CloudFlareCaptcha
siteKey={cloudflareCaptchaSiteKey}
Expand Down Expand Up @@ -248,7 +259,13 @@ const DebugSigninForm = ({ providerId, bgColorClass }: { providerId: string; bgC
>
<span className={`text-orange-600 absolute -top-3 left-5 ${bgColorClass} px-1`}>For Debugging Only</span>
<Stack>
<Input variant="outline" size="lg" placeholder="Username" {...register("username")} />
<Input
variant="outline"
size="lg"
placeholder="Username"
{...register("username")}
errorBorderColor="orange.600"
/>
<RoleSelect {...register("role")}></RoleSelect>
<SigninButton leftIcon={<Bug />}>Continue with Debug User</SigninButton>
</Stack>
Expand Down

0 comments on commit ed0e3cb

Please sign in to comment.