Skip to content

Commit

Permalink
Add recaptcha support, redesign login page
Browse files Browse the repository at this point in the history
  • Loading branch information
nekiro committed Dec 10, 2024
1 parent 2f99e65 commit eace998
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 86 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ NEXT_PUBLIC_DISCORD_URL="https://discord.gg/invite/your_discord"
NEXT_PUBLIC_YOUTUBE_URL="https://youtube.com/your_channel"
NEXT_PUBLIC_GITHUB_URL="https://github.com/nekiro/shibaac"

# captcha
# empty means disabled
NEXT_PUBLIC_CAPTCHA_SITE_KEY=""
CATPCHA_VERIFY_URL="https://www.google.com/recaptcha/api/siteverify"
CAPTCHA_SECRET_KEY=""

DATABASE_URL="mysql://root:secret@localhost:3306/shibaac"

# protocol status
Expand Down
55 changes: 45 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"qrcode": "^1.5.4",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-google-recaptcha": "^3.1.0",
"react-hook-form": "^7.53.2",
"react-icons": "^5.3.0",
"sanitize-html": "^2.13.1",
Expand All @@ -68,9 +69,10 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@types/jest": "^29.5.14",
"@types/mercadopago": "^1.5.11",
"@types/node-cron": "^3.0.11",
"@types/react": "^18.3.12",
"@types/react-google-recaptcha": "^2.1.9",
"@types/recaptcha2": "^1.3.4",
"@types/sanitize-html": "^2.13.0",
"@types/speakeasy": "^2.0.10",
"@types/yup": "^0.29.14",
Expand Down
4 changes: 3 additions & 1 deletion src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import Link from "next/link";
import { Button as ChakraButton, ButtonProps as ChakraButtonProps } from "@chakra-ui/react";
import { useColors } from "@hook/useColors";

const btnTypeToColor = { danger: "red", primary: "violet" };

Expand All @@ -22,7 +23,7 @@ const Button = ({
value,
type = "button",
btnColorType,
size = "md",
size = "lg",
href,
isLoading = false,
isActive = false,
Expand All @@ -39,6 +40,7 @@ const Button = ({
isLoading={isLoading}
isActive={isActive}
loadingText={loadingText}
color="text.light"
{...props}
>
{value ?? children}
Expand Down
15 changes: 15 additions & 0 deletions src/components/Captcha.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { forwardRef } from "react";
import ReCAPTCHA from "react-google-recaptcha";

export interface CaptchaProps {
onChange: (token: string | null) => void;
}

export const Captcha = forwardRef<ReCAPTCHA, CaptchaProps>(({ onChange }, ref) => {
if (!process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY) {
return null;
}
return <ReCAPTCHA sitekey={process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY} onChange={onChange} ref={ref} />;
});

Captcha.displayName = "Captcha";
19 changes: 10 additions & 9 deletions src/components/FormField.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { FormControl, FormLabel, FormErrorMessage } from "@chakra-ui/react";
import { PropsWithChildren } from "react";
import { FormControl, FormLabel, FormErrorMessage, FormControlProps } from "@chakra-ui/react";

export interface FormFieldProps extends PropsWithChildren {
export interface FormFieldProps extends FormControlProps {
name: string;
label: string;
label?: string;
error?: string;
}

export const FormField = ({ name, label, error, children }: FormFieldProps) => {
export const FormField = ({ name, label, error, children, ...props }: FormFieldProps) => {
return (
<FormControl key={name} isInvalid={!!error}>
<FormLabel fontSize="sm" htmlFor={name}>
{label}
</FormLabel>
<FormControl key={name} isInvalid={!!error} {...props}>
{label && (
<FormLabel fontSize="sm" htmlFor={name}>
{label}
</FormLabel>
)}
{children}
{error && <FormErrorMessage fontSize="sm">{error}</FormErrorMessage>}
</FormControl>
Expand Down
1 change: 1 addition & 0 deletions src/components/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const TextInput = forwardRef<HTMLInputElement, InputProps>(({ ...props }, ref) =
borderColor="violet.200"
bg={inputBgColor}
color="black"
height="45px"
{...props}
/>
);
Expand Down
13 changes: 13 additions & 0 deletions src/lib/captcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const verifyCaptcha = async (token: string) => {
if (!process.env.CAPTCHA_SECRET_KEY || !process.env.CATPCHA_VERIFY_URL) {
return;
}

const verificationUrl = `${process.env.CATPCHA_VERIFY_URL}?secret=${process.env.CAPTCHA_SECRET_KEY}&response=${token}`;
const captchaResponse = await fetch(verificationUrl, { method: "POST" });
const captchaResult = await captchaResponse.json();

if (!captchaResult.success) {
throw new Error("Captcha verification failed.");
}
};
22 changes: 1 addition & 21 deletions src/pages/account/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
import React, { useEffect, useState } from "react";
import Panel from "@component/Panel";
import Head from "@layout/Head";
import { fetchApi } from "@lib/request";
import { withSessionSsr } from "@lib/session";
import Button from "@component/Button";
import StripedTable from "@component/StrippedTable";
import {
Alert,
AlertIcon,
Box,
Button as ChakraButton,
Center,
Image,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Spinner,
Text,
Wrap,
} from "@chakra-ui/react";
import { Text, Wrap } from "@chakra-ui/react";
import { timestampToDate, vocationIdToName } from "../../lib";
import { Toggle } from "../../components/Toggle";
import { appRouter } from "src/server/routers/_app";
import { createCallerFactory } from "src/server/trpc";
import type { AccountWithPlayers } from "@shared/types/PrismaAccount";
Expand Down
81 changes: 54 additions & 27 deletions src/pages/account/login.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from "react";
import React, { useRef } from "react";
import Panel from "@component/Panel";
import Head from "@layout/Head";
import Link from "@component/Link";
import { useRouter } from "next/router";
import { withSessionSsr } from "@lib/session";
import { Text, Container, VStack, Wrap } from "@chakra-ui/react";
import { Text, Container, VStack, Wrap, HStack } from "@chakra-ui/react";
import { trpc } from "@util/trpc";
import { useFormFeedback } from "@hook/useFormFeedback";
import TextInput from "@component/TextInput";
Expand All @@ -13,6 +13,8 @@ import { FormField } from "@component/FormField";
import { useForm, SubmitHandler } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Captcha } from "@component/Captcha";
import ReCAPTCHA from "react-google-recaptcha";

const fields = [
{ type: "input", name: "name", label: "Account Name" },
Expand All @@ -30,24 +32,28 @@ const fields = [
const schema = z.object({
name: z.string().min(5, { message: "Account name must be at least 5 characters long" }),
password: z.string().min(6, { message: "Password must be at least 6 characters long" }),
captcha: z.string({ message: "Captcha is required" }),
});

export default function Login() {
const {
register,
handleSubmit,
reset,
setValue,
trigger,
formState: { errors, isValid, isSubmitting },
} = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
});
const captchaRef = useRef<ReCAPTCHA>(null);
const router = useRouter();
const login = trpc.account.login.useMutation();
const { handleResponse, showResponse } = useFormFeedback();

const onSubmit: SubmitHandler<z.infer<typeof schema>> = async ({ name, password }) => {
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async ({ name, password, captcha }) => {
handleResponse(async () => {
const account = await login.mutateAsync({ name, password });
const account = await login.mutateAsync({ name, password, captchaToken: captcha });
if (account) {
const redirectUrl = (router.query.redirect as string) || "/account";
router.push(redirectUrl);
Expand All @@ -57,35 +63,56 @@ export default function Login() {
});

reset();

if (captchaRef.current) {
captchaRef.current.reset();
}
};

return (
<>
<Head title="Login" />
<Panel header="Login">
<Text align="center" margin="10px">
Please enter your account name and your password.
</Text>
<Text align="center" margin="10px">
<Link href="/account/register" text="Create an account " />
if you do not have one yet.
</Text>
<Head title="Log In" />
<Panel header="Log In">
<VStack>
<form onSubmit={handleSubmit(onSubmit)}>
<Container alignContent={"center"} padding={2}>
<VStack spacing={5}>
{fields.map((field) => (
<FormField key={field.name} error={(errors as any)[field.name]?.message} name={field.name} label={field.label}>
<TextInput type={field.type} {...register(field.name as any)} />
</FormField>
))}

<form onSubmit={handleSubmit(onSubmit)}>
<Container alignContent={"center"} padding={2}>
<VStack spacing={5}>
{fields.map((field) => (
<FormField key={field.name} error={(errors as any)[field.name]?.message} name={field.name} label={field.label}>
<TextInput type={field.type} {...register(field.name as any)} />
<FormField error={errors.captcha?.message} name="Captcha" justifyItems="center">
<Captcha
{...register("captcha")}
onChange={(token) => {
setValue("captcha", token ?? "");
trigger("captcha");
}}
ref={captchaRef}
/>
</FormField>
))}
<Wrap spacing={2} padding="10px">
<Button isLoading={isSubmitting} isActive={!isValid} loadingText="Submitting" type="submit" value="Submit" btnColorType="primary" />
<Button value="Lost Account?" btnColorType="danger" href="/account/lost" />
</Wrap>
</VStack>
</Container>
</form>

<Button
isLoading={isSubmitting}
isActive={!isValid}
width="100%"
loadingText="Submitting"
type="submit"
value="Log In"
btnColorType="primary"
/>

<Text align="center">
Don&apos;t have an account? <Link href="/account/register">Register</Link>
</Text>

<Link href="/account/lost">Forgot password?</Link>
</VStack>
</Container>
</form>
</VStack>
</Panel>
</>
);
Expand Down
Loading

0 comments on commit eace998

Please sign in to comment.