Skip to content

Commit

Permalink
add moderator role (#1419)
Browse files Browse the repository at this point in the history
  • Loading branch information
notmd authored Feb 10, 2023
1 parent 1439dd6 commit 7a0bfa2
Show file tree
Hide file tree
Showing 18 changed files with 95 additions and 32 deletions.
1 change: 1 addition & 0 deletions website/.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ADMIN_USERS = "credentials:admin,discord:root,email:[email protected]"
MODERATOR_USERS = "credentials:mod,discord:mod,email:[email protected]"

# The database created by running the jobs in /scripts/frontend-development/docker-compose.yaml
DATABASE_URL=postgres://postgres:postgres@localhost:5433/oasst_web
Expand Down
5 changes: 4 additions & 1 deletion website/src/components/AdminArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ export const AdminArea = ({ children }: { children: ReactNode }) => {
if (status === "loading") {
return;
}
if (session?.user.role === "admin") {
const role = session?.user.role;

if (role === "admin" || role === "moderator") {
return;
}

router.push("/");
}, [router, session, status]);
return <main>{status === "loading" ? "loading..." : children}</main>;
Expand Down
15 changes: 12 additions & 3 deletions website/src/components/Header/UserMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Avatar,
Badge,
Box,
Link,
Menu,
Expand All @@ -16,6 +17,7 @@ import NextLink from "next/link";
import { signOut, useSession } from "next-auth/react";
import { useTranslation } from "next-i18next";
import React, { ElementType, useCallback } from "react";
import { useHasAnyRole } from "src/hooks/auth/useHasAnyRole";

interface MenuOption {
name: string;
Expand All @@ -31,7 +33,7 @@ export function UserMenu() {
signOut({ callbackUrl: "/" });
}, []);
const { data: session, status } = useSession();

const isAdminOrMod = useHasAnyRole(["admin", "moderator"]);
if (!session || status !== "authenticated") {
return null;
}
Expand All @@ -56,7 +58,7 @@ export function UserMenu() {
},
];

if (session.user.role === "admin") {
if (isAdminOrMod) {
options.unshift({
name: t("admin_dashboard"),
href: "/admin",
Expand All @@ -77,7 +79,14 @@ export function UserMenu() {
</MenuButton>
<MenuList p="2" borderRadius="xl" shadow="none">
<Box display="flex" flexDirection="column" alignItems="center" borderRadius="md" p="4">
<Text>{session.user.name}</Text>
<Text>
{session.user.name}
{isAdminOrMod ? (
<Badge size="xs" ml="2" fontSize="xs" textTransform="capitalize">
{session.user.role}
</Badge>
) : null}
</Text>
{/* <Text color="blue.500" fontWeight="bold" fontSize="xl">
3,200
</Text> */}
Expand Down
10 changes: 5 additions & 5 deletions website/src/components/LeaderboardTable/LeaderboardTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MoreHorizontal } from "lucide-react";
import NextLink from "next/link";
import { useTranslation } from "next-i18next";
import React, { useMemo } from "react";
import { useHasRole } from "src/hooks/auth/useHasRole";
import { useHasAnyRole } from "src/hooks/auth/useHasAnyRole";
import { LeaderboardEntity, LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";

import { DataTable, DataTableColumnDef } from "../DataTable/DataTable";
Expand Down Expand Up @@ -41,7 +41,7 @@ export const LeaderboardTable = ({
`/api/leaderboard?time_frame=${timeFrame}&limit=${limit}&includeUserStats=${!hideCurrentUserRanking}`
);

const isAdmin = useHasRole("admin");
const isAdminOrMod = useHasAnyRole(["admin", "moderator"]);

const columns: DataTableColumnDef<WindowLeaderboardEntity>[] = useMemo(
() => [
Expand All @@ -51,7 +51,7 @@ export const LeaderboardTable = ({
cell: (ctx) =>
ctx.row.original.isSpaceRow ? (
<SpaceRow></SpaceRow>
) : isAdmin ? (
) : isAdminOrMod ? (
jsonExpandRowModel.renderCell(ctx)
) : (
ctx.getValue()
Expand All @@ -62,7 +62,7 @@ export const LeaderboardTable = ({
columnHelper.accessor("display_name", {
header: t("user"),
cell: ({ getValue, row }) =>
isAdmin ? (
isAdminOrMod ? (
<Link as={NextLink} href={`/admin/manage_user/${row.original.user_id}`}>
{getValue()}
</Link>
Expand All @@ -83,7 +83,7 @@ export const LeaderboardTable = ({
header: t("label"),
}),
],
[isAdmin, t]
[isAdminOrMod, t]
);

const {
Expand Down
6 changes: 3 additions & 3 deletions website/src/components/Messages/MessageEmojiButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import { useHasRole } from "src/hooks/auth/useHasRole";
import { useHasAnyRole } from "src/hooks/auth/useHasAnyRole";
import { MessageEmoji } from "src/types/Conversation";
import { emojiIcons } from "src/types/Emoji";

Expand All @@ -23,12 +23,12 @@ export const MessageEmojiButton = ({
sx,
}: MessageEmojiButtonProps) => {
const EmojiIcon = emojiIcons.get(emoji.name);
const isAdmin = useHasRole("admin");
const isAdminOrMod = useHasAnyRole(["admin", "moderator"]);

if (!EmojiIcon) return null;

const isDisabled = !!(userIsAuthor ? true : disabled);
const showCount = (emoji.count > 0 && userReacted) || userIsAuthor || isAdmin;
const showCount = (emoji.count > 0 && userReacted) || userIsAuthor || isAdminOrMod;

return (
<Button
Expand Down
6 changes: 3 additions & 3 deletions website/src/components/Messages/MessageTableEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { LabelMessagePopup } from "src/components/Messages/LabelPopup";
import { MessageEmojiButton } from "src/components/Messages/MessageEmojiButton";
import { ReportPopup } from "src/components/Messages/ReportPopup";
import { useHasRole } from "src/hooks/auth/useHasRole";
import { useHasAnyRole } from "src/hooks/auth/useHasAnyRole";
import { del, post, put } from "src/lib/api";
import { colors } from "src/styles/Theme/colors";
import { Message, MessageEmojis } from "src/types/Conversation";
Expand Down Expand Up @@ -210,7 +210,7 @@ const MessageActions = ({
});
};

const isAdmin = useHasRole("admin");
const isAdminOrMod = useHasAnyRole(["admin", "moderator"]);

return (
<Menu>
Expand Down Expand Up @@ -243,7 +243,7 @@ const MessageActions = ({
>
{t("copy_message_link")}
</MenuItem>
{!!isAdmin && (
{!!isAdminOrMod && (
<>
<MenuDivider />
<MenuItem onClick={() => handleCopy(id)} icon={<Copy />}>
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/RoleSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Select, SelectProps } from "@chakra-ui/react";
import { forwardRef } from "react";
import { ElementOf } from "src/types/utils";

export const roles = ["general", "admin", "banned"] as const;
export const roles = ["general", "admin", "banned", "moderator"] as const;
export type Role = ElementOf<typeof roles>;

type RoleSelectProps = Omit<SelectProps, "defaultValue"> & {
Expand Down
8 changes: 8 additions & 0 deletions website/src/hooks/auth/useHasAnyRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useSession } from "next-auth/react";
import { Role } from "src/components/RoleSelect";

export const useHasAnyRole = (roles: Role[]) => {
const { data: session } = useSession();

return roles.some((role) => role === session?.user?.role);
};
14 changes: 14 additions & 0 deletions website/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,18 @@ const withRole = (role: Role, handler: (arg0: NextApiRequest, arg1: NextApiRespo
};
};

export const withAnyRole = (
roles: Role[],
handler: (arg0: NextApiRequest, arg1: NextApiResponse, token: JWT) => void
) => {
return async (req: NextApiRequest, res: NextApiResponse) => {
const token = await getToken({ req });
if (!token || roles.every((role) => token.role !== role)) {
res.status(403).end();
return;
}
return handler(req, res, token);
};
};

export { withoutRole, withRole };
4 changes: 2 additions & 2 deletions website/src/pages/api/admin/delete_message/[id].ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { withRole } from "src/lib/auth";
import { withAnyRole } from "src/lib/auth";
import { createApiClient } from "src/lib/oasst_client_factory";

const handler = withRole("admin", async (req, res, token) => {
const handler = withAnyRole(["admin", "moderator"], async (req, res, token) => {
const { id } = req.query;
try {
const client = await createApiClient(token);
Expand Down
10 changes: 8 additions & 2 deletions website/src/pages/api/admin/parameters.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { withRole } from "src/lib/auth";
import { withAnyRole } from "src/lib/auth";
import { createApiClient } from "src/lib/oasst_client_factory";

export default withRole("admin", async (_, res, token) => {
export default withAnyRole(["admin", "moderator"], async (_, res, token) => {
const client = await createApiClient(token);

if (token.role === "moderator") {
const publicSettings = await client.fetch_public_settings();

return res.json(publicSettings);
}

try {
const fullSettings = await client.fetch_full_settings();

Expand Down
4 changes: 2 additions & 2 deletions website/src/pages/api/admin/status.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { withRole } from "src/lib/auth";
import { withAnyRole } from "src/lib/auth";
import { createApiClientFromUser } from "src/lib/oasst_client_factory";

/**
* Returns tasks availability, stats, and tree manager stats.
*/
const handler = withRole("admin", async (req, res) => {
const handler = withAnyRole(["admin", "moderator"], async (req, res) => {
// NOTE: why are we using a dummy user here?
const dummyUser = {
id: "__dummy_user__",
Expand Down
4 changes: 2 additions & 2 deletions website/src/pages/api/admin/stop_tree/[id].ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { withRole } from "src/lib/auth";
import { withAnyRole } from "src/lib/auth";
import { createApiClient } from "src/lib/oasst_client_factory";

const handler = withRole("admin", async (req, res, token) => {
const handler = withAnyRole(["admin", "moderator"], async (req, res, token) => {
const { id } = req.query;
try {
const client = await createApiClient(token);
Expand Down
4 changes: 2 additions & 2 deletions website/src/pages/api/admin/trollboard.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { withRole } from "src/lib/auth";
import { withAnyRole } from "src/lib/auth";
import { createApiClient } from "src/lib/oasst_client_factory";
import { TrollboardTimeFrame } from "src/types/Trollboard";

export default withRole("admin", async (req, res, token) => {
export default withAnyRole(["admin", "moderator"], async (req, res, token) => {
const client = await createApiClient(token);

const trollboard = await client.fetch_trollboard(req.query.time_frame as TrollboardTimeFrame, {
Expand Down
4 changes: 2 additions & 2 deletions website/src/pages/api/admin/user_messages.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { withRole } from "src/lib/auth";
import { withAnyRole } from "src/lib/auth";
import { createApiClient } from "src/lib/oasst_client_factory";

const LIMIT = 10;

/**
* Returns the messages recorded by the backend for a user.
*/
const handler = withRole("admin", async (req, res, token) => {
const handler = withAnyRole(["admin", "moderator"], async (req, res, token) => {
const { cursor, direction, user } = req.query;

const oasstApiClient = await createApiClient(token);
Expand Down
4 changes: 2 additions & 2 deletions website/src/pages/api/admin/users.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withRole } from "src/lib/auth";
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";
Expand All @@ -17,7 +17,7 @@ const PAGE_SIZE = 20;
* - `direction`: Either "forward" or "backward" representing the pagination
* direction.
*/
const handler = withRole("admin", async (req, res, token) => {
const handler = withAnyRole(["admin", "moderator"], async (req, res, token) => {
const { cursor, direction, searchDisplayName = "", sortKey = "username" } = req.query;

const oasstApiClient = await createApiClient(token);
Expand Down
24 changes: 22 additions & 2 deletions website/src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { boolean } from "boolean";
import { generateUsername } from "friendly-username-generator";
import { NextApiRequest, NextApiResponse } from "next";
import type { AuthOptions } from "next-auth";
import NextAuth from "next-auth";
Expand All @@ -11,7 +12,6 @@ import { checkCaptcha } from "src/lib/captcha";
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
import prisma from "src/lib/prismadb";
import { BackendUserCore } from "src/types/Users";
import { generateUsername } from "friendly-username-generator";

const providers: Provider[] = [];

Expand Down Expand Up @@ -78,6 +78,14 @@ const adminUserMap = process.env.ADMIN_USERS.split(",").reduce((result, entry) =
return result;
}, new Map());

const moderatorUserMap = process.env.MODERATOR_USERS.split(",").reduce((result, entry) => {
const [authType, id] = entry.split(":");
const s = result.get(authType) || new Set();
s.add(id);
result.set(authType, s);
return result;
}, new Map());

const authOptions: AuthOptions = {
// Ensure we can store user data in a database.
adapter: PrismaAdapter(prisma),
Expand Down Expand Up @@ -161,9 +169,10 @@ const authOptions: AuthOptions = {

// Get the admin list for the user's auth type.
const adminForAccountType = adminUserMap.get(account.provider);
const moderatorForAccountType = moderatorUserMap.get(account.provider);

// Return early if there's no admin list.
if (!adminForAccountType) {
if (!adminForAccountType && !moderatorForAccountType) {
return;
}

Expand All @@ -180,6 +189,17 @@ const authOptions: AuthOptions = {
},
});
}

if (moderatorForAccountType.has(account.providerAccountId)) {
await prisma.user.update({
data: {
role: "moderator",
},
where: {
id: user.id,
},
});
}
},
},
};
Expand Down
2 changes: 2 additions & 0 deletions website/types/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ declare global {
CLOUDFLARE_CAPTCHA_SECRET_KEY: string;
NEXT_PUBLIC_ENABLE_EMAIL_SIGNIN_CAPTCHA: boolean;
NEXT_PUBLIC_ENABLE_EMAIL_SIGNIN: boolean;
ADMIN_USERS: string;
MODERATOR_USERS: string;
}
}
}
Expand Down

0 comments on commit 7a0bfa2

Please sign in to comment.