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 { 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) {
+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 { InferGetServerSidePropsType } from "next";
import Head from "next/head";
@@ -7,6 +7,7 @@ import { useSession } from "next-auth/react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
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";
@@ -99,11 +100,7 @@ const ManageUser = ({ user }: InferGetServerSidePropsType<typeof getServerSidePr
{({ field }) => (
<FormControl>
<FormLabel>Role</FormLabel>
<Select {...field}>
<option value="banned">Banned</option>
<option value="general">General</option>
<option value="admin">Admin</option>
</Select>
<RoleSelect {...field}></RoleSelect>
</FormControl>
)}
</Field>
+4 -2
View File
@@ -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({
+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 { 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<SignInErrorTypes, string> = {
default: "Unable to sign in.",
};
interface SigninProps {
providers: Awaited<ReturnType<typeof getProviders>>;
}
// 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 (
<div className={bgColorClass}>
<Head>
@@ -80,17 +77,7 @@ function Signin({ csrfToken, providers }) {
</Head>
<AuthLayout>
<Stack spacing="2">
{credentials && (
<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>
)}
{credentials && <DebugSigninForm credentials={credentials} bgColorClass={bgColorClass} />}
{email && (
<form onSubmit={signinWithEmail}>
<Stack>
@@ -102,16 +89,9 @@ function Signin({ csrfToken, providers }) {
placeholder="Email Address"
ref={emailEl}
/>
<Button
data-cy="signin-email-button"
size={"lg"}
leftIcon={<FaEnvelope />}
type="submit"
colorScheme={buttonColorScheme}
color="white"
>
<SigninButton data-cy="signin-email-button" leftIcon={<FaEnvelope />}>
Continue with Email
</Button>
</SigninButton>
</Stack>
</form>
)}
@@ -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 (
<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();
return {
props: {
csrfToken,
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;