allow moderators to ban user
diff --git a/website/package-lock.json b/website/package-lock.json
index 9b369be40f..18b1fc19ab 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -85,6 +85,7 @@
         "msw-storybook-addon": "^1.7.0",
         "prettier": "2.8.1",
         "prisma": "^4.7.1",
+        "ts-essentials": "^9.3.0",
         "ts-node": "^10.9.1",
         "typescript": "^4.9.4"
       }
@@ -36509,6 +36510,15 @@
         "node": ">=6.10"
       }
     },
+    "node_modules/ts-essentials": {
+      "version": "9.3.0",
+      "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.3.0.tgz",
+      "integrity": "sha512-XeiCboEyBG8UqXZtXl59bWEi4ZgOqRsogFDI6WDGIF1LmzbYiAkIwjkXN6zZWWl4re/lsOqMlYfe8KA0XiiEPw==",
+      "dev": true,
+      "peerDependencies": {
+        "typescript": ">=4.1.0"
+      }
+    },
     "node_modules/ts-node": {
       "version": "10.9.1",
       "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@@ -66035,6 +66045,13 @@
       "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
       "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="
     },
+    "ts-essentials": {
+      "version": "9.3.0",
+      "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.3.0.tgz",
+      "integrity": "sha512-XeiCboEyBG8UqXZtXl59bWEi4ZgOqRsogFDI6WDGIF1LmzbYiAkIwjkXN6zZWWl4re/lsOqMlYfe8KA0XiiEPw==",
+      "dev": true,
+      "requires": {}
+    },
     "ts-node": {
       "version": "10.9.1",
       "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
diff --git a/website/package.json b/website/package.json
index bb09c986b7..5e90432a48 100644
--- a/website/package.json
+++ b/website/package.json
@@ -103,6 +103,7 @@
     "msw-storybook-addon": "^1.7.0",
     "prettier": "2.8.1",
     "prisma": "^4.7.1",
+    "ts-essentials": "^9.3.0",
     "ts-node": "^10.9.1",
     "typescript": "^4.9.4"
   },
diff --git a/website/src/components/Icons/Discord.tsx b/website/src/components/Icons/Discord.tsx
index ea1118fb7c..68a0d32896 100644
--- a/website/src/components/Icons/Discord.tsx
+++ b/website/src/components/Icons/Discord.tsx
@@ -1,8 +1,11 @@
-import { LucideIcon } from "lucide-react";
+import { LucideIcon, LucideProps } from "lucide-react";
+import { forwardRef } from "react";
 
-export const Discord: LucideIcon = ({ size = 24, ...rest }) => {
+// eslint-disable-next-line react/display-name
+export const Discord: LucideIcon = forwardRef<SVGSVGElement, LucideProps>(function ({ size = 24, ...rest }, ref) {
   return (
     <svg
+      ref={ref}
       xmlns="http://www.w3.org/2000/svg"
       viewBox="0 0 127.14 96.36"
       fill="currentColor"
@@ -13,4 +16,4 @@ export const Discord: LucideIcon = ({ size = 24, ...rest }) => {
       <path d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z" />
     </svg>
   );
-};
+});
diff --git a/website/src/components/Messages/AdminMessageTable.tsx b/website/src/components/Messages/AdminMessageTable.tsx
index 90866459ce..7339d68602 100644
--- a/website/src/components/Messages/AdminMessageTable.tsx
+++ b/website/src/components/Messages/AdminMessageTable.tsx
@@ -8,7 +8,7 @@ import NextLink from "next/link";
 import { ROUTES } from "src/lib/routes";
 import { Message } from "src/types/Conversation";
 import { isKnownEmoji } from "src/types/Emoji";
-import { StrictOmit } from "src/types/utils";
+import { StrictOmit } from "ts-essentials";
 
 import { DataTable, DataTableProps } from "../DataTable/DataTable";
 import { DataTableAction } from "../DataTable/DataTableAction";
diff --git a/website/src/components/RoleSelect.tsx b/website/src/components/RoleSelect.tsx
index 970ffdce52..e5f8e59ae4 100644
--- a/website/src/components/RoleSelect.tsx
+++ b/website/src/components/RoleSelect.tsx
@@ -1,9 +1,15 @@
 import { Select, SelectProps } from "@chakra-ui/react";
 import { forwardRef } from "react";
-import { ElementOf } from "src/types/utils";
+import { ValueOf } from "ts-essentials";
 
-export const roles = ["general", "admin", "banned", "moderator"] as const;
-export type Role = ElementOf<typeof roles>;
+export const ROLES = {
+  GERNERAL: "general",
+  BANNED: "banned",
+  ADMIN: "admin",
+  MODERATOR: "moderator",
+} as const;
+
+export type Role = ValueOf<typeof ROLES>;
 
 type RoleSelectProps = Omit<SelectProps, "defaultValue"> & {
   defaultValue?: Role;
@@ -13,7 +19,7 @@ type RoleSelectProps = Omit<SelectProps, "defaultValue"> & {
 export const RoleSelect = forwardRef<HTMLSelectElement, RoleSelectProps>((props, ref) => {
   return (
     <Select {...props} ref={ref}>
-      {roles.map((role) => (
+      {Object.values(ROLES).map((role) => (
         <option value={role} key={role}>
           {role}
         </option>
diff --git a/website/src/pages/api/admin/update_user.ts b/website/src/pages/api/admin/update_user.ts
index 13ee97f51a..af914b431f 100644
--- a/website/src/pages/api/admin/update_user.ts
+++ b/website/src/pages/api/admin/update_user.ts
@@ -1,21 +1,23 @@
-import { withRole } from "src/lib/auth";
+import { ROLES } from "src/components/RoleSelect";
+import { withAnyRole } from "src/lib/auth";
 import { createApiClient } from "src/lib/oasst_client_factory";
 import prisma from "src/lib/prismadb";
 
 /**
  * Update's the user's data in the database.  Accessible only to admins.
  */
-const handler = withRole("admin", async (req, res, token) => {
-  const { id, auth_method, user_id, notes, role, show_on_leaderboard } = req.body;
-
-  const oasstApiClient = await createApiClient(token);
-  // If the user is authorized by the web, update their role.
-  if (auth_method === "local") {
-    await prisma.user.update({
-      where: { id },
-      data: { role },
-    });
+const handler = withAnyRole(["admin", "moderator"], async (req, res, token) => {
+  const { id, user_id, notes, role, show_on_leaderboard } = req.body;
+  // mod can't update user role to mod or admin
+  if (token.role === ROLES.MODERATOR && (role === ROLES.MODERATOR || role === ROLES.ADMIN)) {
+    return res.status(403).json({});
   }
+  const oasstApiClient = await createApiClient(token);
+  await prisma.user.update({
+    where: { id },
+    data: { role },
+  });
+
   // Tell the backend the user's enabled or not enabled status.
   await oasstApiClient.set_user_status(user_id, role !== "banned", notes, show_on_leaderboard);
 
diff --git a/website/src/types/utils.ts b/website/src/types/utils.ts
deleted file mode 100644
index f3ac0af3e9..0000000000
--- a/website/src/types/utils.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-// https://github.com/ts-essentials/ts-essentials/blob/25cae45c162f8784e3cdae8f43783d0c66370a57/lib/types.ts#L437
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type ElementOf<T extends readonly any[]> = T extends readonly (infer ET)[] ? ET : never;
-type AnyRecord<T = any> = Record<KeyofBase, T>;
-type KeyofBase = keyof any;
-
-export type AnyArray<T = any> = Array<T> | ReadonlyArray<T>;
-
-export type StrictOmit<T extends AnyRecord, K extends keyof T> = T extends AnyArray ? never : Omit<T, K>;

- Allow mod to ban users (mods can only ban and unban user, they can't promote user to mod or admin, they can't ban other mods and admins too).
- Fix a bug where the admin can not ban user login in discord.
- Forward ref on discord icon component to support tooltip