Merge remote-tracking branch 'origin/main' into react-hook-form

This commit is contained in:
notmd
2023-01-16 13:05:01 +07:00
6 changed files with 89 additions and 45 deletions
+25
View File
@@ -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<typeof roles>;
type RoleSelectProps = Omit<SelectProps, "defaultValue"> & {
defaultValue?: Role;
value?: Role;
};
export const RoleSelect = forwardRef<HTMLSelectElement, RoleSelectProps>((props, ref) => {
return (
<Select {...props} ref={ref}>
{roles.map((role) => (
<option value={role} key={role}>
{role}
</option>
))}
</Select>
);
});
RoleSelect.displayName = "RoleSelect";
+3 -2
View File
@@ -1,11 +1,12 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { getToken, JWT } from "next-auth/jwt"; 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 * 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. * 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) => { return async (req: NextApiRequest, res: NextApiResponse) => {
const token = await getToken({ req }); const token = await getToken({ req });
if (!token || token.role === role) { 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 * Wraps any API Route handler and verifies that the user has the appropriate
* role before running the handler. Returns a 403 otherwise. * 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) => { return async (req: NextApiRequest, res: NextApiResponse) => {
const token = await getToken({ req }); const token = await getToken({ req });
if (!token || token.role !== role) { if (!token || token.role !== role) {
+3 -6
View File
@@ -1,4 +1,4 @@
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 { Field, Form, Formik } from "formik";
import { InferGetServerSidePropsType } from "next"; import { InferGetServerSidePropsType } from "next";
import Head from "next/head"; import Head from "next/head";
@@ -7,6 +7,7 @@ import { useSession } from "next-auth/react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { getAdminLayout } from "src/components/Layout"; import { getAdminLayout } from "src/components/Layout";
import { RoleSelect } from "src/components/RoleSelect";
import { UserMessagesCell } from "src/components/UserMessagesCell"; import { UserMessagesCell } from "src/components/UserMessagesCell";
import { post } from "src/lib/api"; import { post } from "src/lib/api";
import { oasstApiClient } from "src/lib/oasst_api_client"; import { oasstApiClient } from "src/lib/oasst_api_client";
@@ -99,11 +100,7 @@ const ManageUser = ({ user }: InferGetServerSidePropsType<typeof getServerSidePr
{({ field }) => ( {({ field }) => (
<FormControl> <FormControl>
<FormLabel>Role</FormLabel> <FormLabel>Role</FormLabel>
<Select {...field}> <RoleSelect {...field}></RoleSelect>
<option value="banned">Banned</option>
<option value="general">General</option>
<option value="admin">Admin</option>
</Select>
</FormControl> </FormControl>
)} )}
</Field> </Field>
+4 -2
View File
@@ -2,12 +2,13 @@ import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { boolean } from "boolean"; import { boolean } from "boolean";
import type { AuthOptions } from "next-auth"; import type { AuthOptions } from "next-auth";
import NextAuth from "next-auth"; import NextAuth from "next-auth";
import { Provider } from "next-auth/providers";
import CredentialsProvider from "next-auth/providers/credentials"; import CredentialsProvider from "next-auth/providers/credentials";
import DiscordProvider from "next-auth/providers/discord"; import DiscordProvider from "next-auth/providers/discord";
import EmailProvider from "next-auth/providers/email"; import EmailProvider from "next-auth/providers/email";
import prisma from "src/lib/prismadb"; import prisma from "src/lib/prismadb";
const providers = []; const providers: Provider[] = [];
// Register an email magic link auth method. // Register an email magic link auth method.
providers.push( providers.push(
@@ -39,12 +40,13 @@ if (boolean(process.env.DEBUG_LOGIN) || process.env.NODE_ENV === "development")
name: "Debug Credentials", name: "Debug Credentials",
credentials: { credentials: {
username: { label: "Username", type: "text" }, username: { label: "Username", type: "text" },
role: { label: "Role", type: "text" },
}, },
async authorize(credentials) { async authorize(credentials) {
const user = { const user = {
id: credentials.username, id: credentials.username,
name: credentials.username, name: credentials.username,
role: "admin", role: credentials.role,
}; };
// save the user to the database // save the user to the database
await prisma.user.upsert({ await prisma.user.upsert({
+51 -35
View File
@@ -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 { useColorMode } from "@chakra-ui/react";
import { GetServerSideProps } from "next";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; 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 React, { useEffect, useRef, useState } from "react";
import { FaBug, FaDiscord, FaEnvelope, FaGithub } from "react-icons/fa"; import { FaBug, FaDiscord, FaEnvelope, FaGithub } from "react-icons/fa";
import { AuthLayout } from "src/components/AuthLayout"; import { AuthLayout } from "src/components/AuthLayout";
import { Footer } from "src/components/Footer"; import { Footer } from "src/components/Footer";
import { Header } from "src/components/Header"; import { Header } from "src/components/Header";
import { RoleSelect } from "src/components/RoleSelect";
export type SignInErrorTypes = export type SignInErrorTypes =
| "Signin" | "Signin"
@@ -37,8 +39,11 @@ const errorMessages: Record<SignInErrorTypes, string> = {
default: "Unable to sign in.", default: "Unable to sign in.",
}; };
interface SigninProps {
providers: Awaited<ReturnType<typeof getProviders>>;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
function Signin({ csrfToken, providers }) { function Signin({ providers }: SigninProps) {
const router = useRouter(); const router = useRouter();
const { discord, email, github, credentials } = providers; const { discord, email, github, credentials } = providers;
const emailEl = useRef(null); const emailEl = useRef(null);
@@ -60,18 +65,10 @@ function Signin({ csrfToken, providers }) {
signIn(email.id, { callbackUrl: "/dashboard", email: emailEl.current.value }); 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 { colorMode } = useColorMode();
const bgColorClass = colorMode === "light" ? "bg-gray-50" : "bg-chakra-gray-900"; const bgColorClass = colorMode === "light" ? "bg-gray-50" : "bg-chakra-gray-900";
const buttonBgColor = colorMode === "light" ? "#2563eb" : "#2563eb"; const buttonBgColor = colorMode === "light" ? "#2563eb" : "#2563eb";
const buttonColorScheme = colorMode === "light" ? "blue" : "dark-blue-btn";
return ( return (
<div className={bgColorClass}> <div className={bgColorClass}>
<Head> <Head>
@@ -80,17 +77,7 @@ function Signin({ csrfToken, providers }) {
</Head> </Head>
<AuthLayout> <AuthLayout>
<Stack spacing="2"> <Stack spacing="2">
{credentials && ( {credentials && <DebugSigninForm credentials={credentials} bgColorClass={bgColorClass} />}
<form onSubmit={signinWithDebugCredentials} className="border-2 border-orange-600 rounded-md p-4 relative">
<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" ref={debugUsernameEl} />
<Button size={"lg"} leftIcon={<FaBug />} colorScheme={buttonColorScheme} color="white" type="submit">
Continue with Debug User
</Button>
</Stack>
</form>
)}
{email && ( {email && (
<form onSubmit={signinWithEmail}> <form onSubmit={signinWithEmail}>
<Stack> <Stack>
@@ -102,16 +89,9 @@ function Signin({ csrfToken, providers }) {
placeholder="Email Address" placeholder="Email Address"
ref={emailEl} ref={emailEl}
/> />
<Button <SigninButton data-cy="signin-email-button" leftIcon={<FaEnvelope />}>
data-cy="signin-email-button"
size={"lg"}
leftIcon={<FaEnvelope />}
type="submit"
colorScheme={buttonColorScheme}
color="white"
>
Continue with Email Continue with Email
</Button> </SigninButton>
</Stack> </Stack>
</form> </form>
)} )}
@@ -179,13 +159,49 @@ Signin.getLayout = (page) => (
export default Signin; export default Signin;
export async function getServerSideProps() { const SigninButton = (props: ButtonProps) => {
const csrfToken = await getCsrfToken(); const buttonColorScheme = useColorModeValue("blue", "dark-blue-btn");
return (
<Button
size={"lg"}
leftIcon={<FaEnvelope />}
type="submit"
colorScheme={buttonColorScheme}
color="white"
{...props}
></Button>
);
};
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 (
<form onSubmit={signinWithDebugCredentials} className="border-2 border-orange-600 rounded-md p-4 relative">
<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" ref={debugUsernameEl} />
<RoleSelect defaultValue={"general"} ref={roleRef}></RoleSelect>
<SigninButton leftIcon={<FaBug />}>Continue with Debug User</SigninButton>
</Stack>
</form>
);
};
export const getServerSideProps: GetServerSideProps<SigninProps> = async () => {
const providers = await getProviders(); const providers = await getProviders();
return { return {
props: { props: {
csrfToken,
providers, providers,
}, },
}; };
} };
+3
View File
@@ -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 any[]> = T extends readonly (infer ET)[] ? ET : never;