diff --git a/website/src/components/UsersCell.tsx b/website/src/components/UsersCell.tsx
new file mode 100644
index 00000000..5c6f4ce8
--- /dev/null
+++ b/website/src/components/UsersCell.tsx
@@ -0,0 +1,46 @@
+import { Table, TableCaption, TableContainer, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react";
+import { useState } from "react";
+import fetcher from "src/lib/fetcher";
+import useSWR from "swr";
+
+/**
+ * Fetches users from the users api route and then presents them in a simple Chakra table.
+ */
+const UsersCell = () => {
+ // Fetch and save the users.
+ const [users, setUsers] = useState([]);
+ const { isLoading } = useSWR("/api/admin/users", fetcher, {
+ onSuccess: (data) => {
+ setUsers(data);
+ },
+ });
+
+ // Present users in a naive table.
+ return (
+
+
+ Users
+
+
+ | Id |
+ Email |
+ Name |
+ Role |
+
+
+
+ {users.map((user, index) => (
+
+ | {user.id} |
+ {user.email} |
+ {user.name} |
+ {user.role} |
+
+ ))}
+
+
+
+ );
+};
+
+export default UsersCell;
diff --git a/website/src/pages/admin/index.tsx b/website/src/pages/admin/index.tsx
new file mode 100644
index 00000000..e048915e
--- /dev/null
+++ b/website/src/pages/admin/index.tsx
@@ -0,0 +1,67 @@
+import Head from "next/head";
+import { useRouter } from "next/router";
+import { useSession } from "next-auth/react";
+import { useEffect } from "react";
+import { getTransparentHeaderLayout } from "src/components/Layout";
+import UsersCell from "src/components/UsersCell";
+
+/**
+ * Provides the admin index page that will display a list of users and give
+ * admins the ability to manage their access rights.
+ */
+const AdminIndex = () => {
+ const router = useRouter();
+ const { data: session, status } = useSession();
+
+ // Check when the user session is loaded and re-route if the user is not an
+ // admin. This follows the suggestion by NextJS for handling private pages:
+ // https://nextjs.org/docs/api-reference/next/router#usage
+ //
+ // All admin pages should use the same check and routing steps.
+ useEffect(() => {
+ if (status === "loading") {
+ return;
+ }
+ if (session?.user?.role === "admin") {
+ return;
+ }
+ router.push("/");
+ }, [session, status]);
+
+ // While loading, just show something.
+ if (status === "loading") {
+ return (
+ <>
+
+ Open Assistant
+
+
+ loading...
+ >
+ );
+ }
+
+ // Show the final page.
+ // TODO(#237): Display a component that fetches actual user data.
+ return (
+ <>
+
+ Open Assistant
+
+
+
+
+
+ >
+ );
+};
+
+AdminIndex.getLayout = getTransparentHeaderLayout;
+
+export default AdminIndex;
diff --git a/website/src/pages/api/admin/users.ts b/website/src/pages/api/admin/users.ts
new file mode 100644
index 00000000..186bb253
--- /dev/null
+++ b/website/src/pages/api/admin/users.ts
@@ -0,0 +1,31 @@
+import { getToken } from "next-auth/jwt";
+import client from "src/lib/prismadb";
+
+/**
+ * Returns a list of user results from the database when the requesting user is
+ * a logged in admin.
+ */
+const handler = async (req, res) => {
+ const token = await getToken({ req });
+
+ // Return nothing if the user isn't registered or if the user isn't an admin.
+ if (!token || token.role !== "admin") {
+ res.status(403).end();
+ return;
+ }
+
+ // Fetch 20 users.
+ const users = await client.user.findMany({
+ select: {
+ id: true,
+ role: true,
+ name: true,
+ email: true,
+ },
+ take: 20,
+ });
+
+ res.status(200).json(users);
+};
+
+export default handler;