diff --git a/website/src/components/RoleSelect.tsx b/website/src/components/RoleSelect.tsx new file mode 100644 index 00000000..d39d3868 --- /dev/null +++ b/website/src/components/RoleSelect.tsx @@ -0,0 +1,25 @@ +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 type Role = ElementOf; + +type RoleSelectProps = Omit & { + defaultValue?: Role; + value?: Role; +}; + +export const RoleSelect = forwardRef((props, ref) => { + return ( + + ); +}); + +RoleSelect.displayName = "RoleSelect"; diff --git a/website/src/lib/auth.ts b/website/src/lib/auth.ts index 803550a5..42c6cf79 100644 --- a/website/src/lib/auth.ts +++ b/website/src/lib/auth.ts @@ -1,11 +1,12 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getToken, JWT } from "next-auth/jwt"; +import { Role } from "src/components/RoleSelect"; /** * Wraps any API Route handler and verifies that the user does not have the * specified role. Returns a 403 if they do, otherwise runs the handler. */ -const withoutRole = (role: string, handler: (arg0: NextApiRequest, arg1: NextApiResponse, arg2: JWT) => void) => { +const withoutRole = (role: Role, handler: (arg0: NextApiRequest, arg1: NextApiResponse, arg2: JWT) => void) => { return async (req: NextApiRequest, res: NextApiResponse) => { const token = await getToken({ req }); if (!token || token.role === role) { @@ -20,7 +21,7 @@ const withoutRole = (role: string, handler: (arg0: NextApiRequest, arg1: NextApi * Wraps any API Route handler and verifies that the user has the appropriate * role before running the handler. Returns a 403 otherwise. */ -const withRole = (role: string, handler: (arg0: NextApiRequest, arg1: NextApiResponse) => void) => { +const withRole = (role: Role, handler: (arg0: NextApiRequest, arg1: NextApiResponse) => void) => { return async (req: NextApiRequest, res: NextApiResponse) => { const token = await getToken({ req }); if (!token || token.role !== role) { diff --git a/website/src/pages/admin/manage_user/[id].tsx b/website/src/pages/admin/manage_user/[id].tsx index 90698f4a..ca7e087c 100644 --- a/website/src/pages/admin/manage_user/[id].tsx +++ b/website/src/pages/admin/manage_user/[id].tsx @@ -1,10 +1,11 @@ -import { Button, Container, FormControl, FormLabel, Input, Select, Stack, useToast } from "@chakra-ui/react"; +import { Button, Container, FormControl, FormLabel, Input, Stack, useToast } from "@chakra-ui/react"; import { Field, Form, Formik } from "formik"; import Head from "next/head"; import { useRouter } from "next/router"; import { useSession } from "next-auth/react"; import { useEffect } from "react"; import { getAdminLayout } from "src/components/Layout"; +import { RoleSelect } from "src/components/RoleSelect"; import { UserMessagesCell } from "src/components/UserMessagesCell"; import { post } from "src/lib/api"; import { oasstApiClient } from "src/lib/oasst_api_client"; @@ -84,11 +85,7 @@ const ManageUser = ({ user }) => { {({ field }) => ( Role - + )} diff --git a/website/src/pages/api/auth/[...nextauth].ts b/website/src/pages/api/auth/[...nextauth].ts index 412551fe..691cbcba 100644 --- a/website/src/pages/api/auth/[...nextauth].ts +++ b/website/src/pages/api/auth/[...nextauth].ts @@ -2,12 +2,13 @@ import { PrismaAdapter } from "@next-auth/prisma-adapter"; import { boolean } from "boolean"; import type { AuthOptions } from "next-auth"; import NextAuth from "next-auth"; +import { Provider } from "next-auth/providers"; import CredentialsProvider from "next-auth/providers/credentials"; import DiscordProvider from "next-auth/providers/discord"; import EmailProvider from "next-auth/providers/email"; import prisma from "src/lib/prismadb"; -const providers = []; +const providers: Provider[] = []; // Register an email magic link auth method. providers.push( @@ -39,12 +40,13 @@ if (boolean(process.env.DEBUG_LOGIN) || process.env.NODE_ENV === "development") name: "Debug Credentials", credentials: { username: { label: "Username", type: "text" }, + role: { label: "Role", type: "text" }, }, async authorize(credentials) { const user = { id: credentials.username, name: credentials.username, - role: "admin", + role: credentials.role, }; // save the user to the database await prisma.user.upsert({ diff --git a/website/src/pages/auth/signin.tsx b/website/src/pages/auth/signin.tsx index c2f05e69..ab1c74ca 100644 --- a/website/src/pages/auth/signin.tsx +++ b/website/src/pages/auth/signin.tsx @@ -1,14 +1,16 @@ -import { Button, Input, Stack } from "@chakra-ui/react"; +import { Button, ButtonProps, Input, Stack, useColorModeValue } from "@chakra-ui/react"; import { useColorMode } from "@chakra-ui/react"; +import { GetServerSideProps } from "next"; import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; -import { getCsrfToken, getProviders, signIn } from "next-auth/react"; +import { ClientSafeProvider, getProviders, signIn } from "next-auth/react"; import React, { useEffect, useRef, useState } from "react"; import { FaBug, FaDiscord, FaEnvelope, FaGithub } from "react-icons/fa"; import { AuthLayout } from "src/components/AuthLayout"; import { Footer } from "src/components/Footer"; import { Header } from "src/components/Header"; +import { RoleSelect } from "src/components/RoleSelect"; export type SignInErrorTypes = | "Signin" @@ -37,8 +39,11 @@ const errorMessages: Record = { default: "Unable to sign in.", }; +interface SigninProps { + providers: Awaited>; +} // eslint-disable-next-line @typescript-eslint/no-unused-vars -function Signin({ csrfToken, providers }) { +function Signin({ providers }: SigninProps) { const router = useRouter(); const { discord, email, github, credentials } = providers; const emailEl = useRef(null); @@ -60,18 +65,10 @@ function Signin({ csrfToken, providers }) { signIn(email.id, { callbackUrl: "/dashboard", email: emailEl.current.value }); }; - const debugUsernameEl = useRef(null); - function signinWithDebugCredentials(ev: React.FormEvent) { - ev.preventDefault(); - signIn(credentials.id, { callbackUrl: "/dashboard", username: debugUsernameEl.current.value }); - } - const { colorMode } = useColorMode(); const bgColorClass = colorMode === "light" ? "bg-gray-50" : "bg-chakra-gray-900"; const buttonBgColor = colorMode === "light" ? "#2563eb" : "#2563eb"; - const buttonColorScheme = colorMode === "light" ? "blue" : "dark-blue-btn"; - return (
@@ -80,17 +77,7 @@ function Signin({ csrfToken, providers }) { - {credentials && ( -
- For Debugging Only - - - - -
- )} + {credentials && } {email && (
@@ -102,16 +89,9 @@ function Signin({ csrfToken, providers }) { placeholder="Email Address" ref={emailEl} /> - +
)} @@ -179,13 +159,49 @@ Signin.getLayout = (page) => ( export default Signin; -export async function getServerSideProps() { - const csrfToken = await getCsrfToken(); +const SigninButton = (props: ButtonProps) => { + const buttonColorScheme = useColorModeValue("blue", "dark-blue-btn"); + + return ( + + ); +}; + +const DebugSigninForm = ({ credentials, bgColorClass }: { credentials: ClientSafeProvider; bgColorClass: string }) => { + const debugUsernameEl = useRef(null); + const roleRef = useRef(null); + function signinWithDebugCredentials(ev: React.FormEvent) { + ev.preventDefault(); + signIn(credentials.id, { + callbackUrl: "/dashboard", + username: debugUsernameEl.current.value, + role: roleRef.current.value, + }); + } + return ( +
+ For Debugging Only + + + + }>Continue with Debug User + +
+ ); +}; + +export const getServerSideProps: GetServerSideProps = async () => { const providers = await getProviders(); return { props: { - csrfToken, providers, }, }; -} +}; diff --git a/website/src/types/utils.ts b/website/src/types/utils.ts new file mode 100644 index 00000000..82c35036 --- /dev/null +++ b/website/src/types/utils.ts @@ -0,0 +1,3 @@ +// 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 (infer ET)[] ? ET : never;