Skip to content

Commit eace998

Browse files
committed
Add recaptcha support, redesign login page
1 parent 2f99e65 commit eace998

File tree

11 files changed

+177
-86
lines changed

11 files changed

+177
-86
lines changed

.env.example

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ NEXT_PUBLIC_DISCORD_URL="https://discord.gg/invite/your_discord"
55
NEXT_PUBLIC_YOUTUBE_URL="https://youtube.com/your_channel"
66
NEXT_PUBLIC_GITHUB_URL="https://github.com/nekiro/shibaac"
77

8+
# captcha
9+
# empty means disabled
10+
NEXT_PUBLIC_CAPTCHA_SITE_KEY=""
11+
CATPCHA_VERIFY_URL="https://www.google.com/recaptcha/api/siteverify"
12+
CAPTCHA_SECRET_KEY=""
13+
814
DATABASE_URL="mysql://root:secret@localhost:3306/shibaac"
915

1016
# protocol status

package-lock.json

+45-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"qrcode": "^1.5.4",
5656
"react": "18.3.1",
5757
"react-dom": "18.3.1",
58+
"react-google-recaptcha": "^3.1.0",
5859
"react-hook-form": "^7.53.2",
5960
"react-icons": "^5.3.0",
6061
"sanitize-html": "^2.13.1",
@@ -68,9 +69,10 @@
6869
"@testing-library/jest-dom": "^6.6.3",
6970
"@testing-library/react": "^16.0.1",
7071
"@types/jest": "^29.5.14",
71-
"@types/mercadopago": "^1.5.11",
7272
"@types/node-cron": "^3.0.11",
7373
"@types/react": "^18.3.12",
74+
"@types/react-google-recaptcha": "^2.1.9",
75+
"@types/recaptcha2": "^1.3.4",
7476
"@types/sanitize-html": "^2.13.0",
7577
"@types/speakeasy": "^2.0.10",
7678
"@types/yup": "^0.29.14",

src/components/Button.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import Link from "next/link";
33
import { Button as ChakraButton, ButtonProps as ChakraButtonProps } from "@chakra-ui/react";
4+
import { useColors } from "@hook/useColors";
45

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

@@ -22,7 +23,7 @@ const Button = ({
2223
value,
2324
type = "button",
2425
btnColorType,
25-
size = "md",
26+
size = "lg",
2627
href,
2728
isLoading = false,
2829
isActive = false,
@@ -39,6 +40,7 @@ const Button = ({
3940
isLoading={isLoading}
4041
isActive={isActive}
4142
loadingText={loadingText}
43+
color="text.light"
4244
{...props}
4345
>
4446
{value ?? children}

src/components/Captcha.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { forwardRef } from "react";
2+
import ReCAPTCHA from "react-google-recaptcha";
3+
4+
export interface CaptchaProps {
5+
onChange: (token: string | null) => void;
6+
}
7+
8+
export const Captcha = forwardRef<ReCAPTCHA, CaptchaProps>(({ onChange }, ref) => {
9+
if (!process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY) {
10+
return null;
11+
}
12+
return <ReCAPTCHA sitekey={process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY} onChange={onChange} ref={ref} />;
13+
});
14+
15+
Captcha.displayName = "Captcha";

src/components/FormField.tsx

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import { FormControl, FormLabel, FormErrorMessage } from "@chakra-ui/react";
2-
import { PropsWithChildren } from "react";
1+
import { FormControl, FormLabel, FormErrorMessage, FormControlProps } from "@chakra-ui/react";
32

4-
export interface FormFieldProps extends PropsWithChildren {
3+
export interface FormFieldProps extends FormControlProps {
54
name: string;
6-
label: string;
5+
label?: string;
76
error?: string;
87
}
98

10-
export const FormField = ({ name, label, error, children }: FormFieldProps) => {
9+
export const FormField = ({ name, label, error, children, ...props }: FormFieldProps) => {
1110
return (
12-
<FormControl key={name} isInvalid={!!error}>
13-
<FormLabel fontSize="sm" htmlFor={name}>
14-
{label}
15-
</FormLabel>
11+
<FormControl key={name} isInvalid={!!error} {...props}>
12+
{label && (
13+
<FormLabel fontSize="sm" htmlFor={name}>
14+
{label}
15+
</FormLabel>
16+
)}
1617
{children}
1718
{error && <FormErrorMessage fontSize="sm">{error}</FormErrorMessage>}
1819
</FormControl>

src/components/TextInput.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const TextInput = forwardRef<HTMLInputElement, InputProps>(({ ...props }, ref) =
1717
borderColor="violet.200"
1818
bg={inputBgColor}
1919
color="black"
20+
height="45px"
2021
{...props}
2122
/>
2223
);

src/lib/captcha.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const verifyCaptcha = async (token: string) => {
2+
if (!process.env.CAPTCHA_SECRET_KEY || !process.env.CATPCHA_VERIFY_URL) {
3+
return;
4+
}
5+
6+
const verificationUrl = `${process.env.CATPCHA_VERIFY_URL}?secret=${process.env.CAPTCHA_SECRET_KEY}&response=${token}`;
7+
const captchaResponse = await fetch(verificationUrl, { method: "POST" });
8+
const captchaResult = await captchaResponse.json();
9+
10+
if (!captchaResult.success) {
11+
throw new Error("Captcha verification failed.");
12+
}
13+
};

src/pages/account/index.tsx

+1-21
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,10 @@
1-
import React, { useEffect, useState } from "react";
21
import Panel from "@component/Panel";
32
import Head from "@layout/Head";
4-
import { fetchApi } from "@lib/request";
53
import { withSessionSsr } from "@lib/session";
64
import Button from "@component/Button";
75
import StripedTable from "@component/StrippedTable";
8-
import {
9-
Alert,
10-
AlertIcon,
11-
Box,
12-
Button as ChakraButton,
13-
Center,
14-
Image,
15-
Modal,
16-
ModalBody,
17-
ModalCloseButton,
18-
ModalContent,
19-
ModalFooter,
20-
ModalHeader,
21-
ModalOverlay,
22-
Spinner,
23-
Text,
24-
Wrap,
25-
} from "@chakra-ui/react";
6+
import { Text, Wrap } from "@chakra-ui/react";
267
import { timestampToDate, vocationIdToName } from "../../lib";
27-
import { Toggle } from "../../components/Toggle";
288
import { appRouter } from "src/server/routers/_app";
299
import { createCallerFactory } from "src/server/trpc";
3010
import type { AccountWithPlayers } from "@shared/types/PrismaAccount";

src/pages/account/login.tsx

+54-27
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import React from "react";
1+
import React, { useRef } from "react";
22
import Panel from "@component/Panel";
33
import Head from "@layout/Head";
44
import Link from "@component/Link";
55
import { useRouter } from "next/router";
66
import { withSessionSsr } from "@lib/session";
7-
import { Text, Container, VStack, Wrap } from "@chakra-ui/react";
7+
import { Text, Container, VStack, Wrap, HStack } from "@chakra-ui/react";
88
import { trpc } from "@util/trpc";
99
import { useFormFeedback } from "@hook/useFormFeedback";
1010
import TextInput from "@component/TextInput";
@@ -13,6 +13,8 @@ import { FormField } from "@component/FormField";
1313
import { useForm, SubmitHandler } from "react-hook-form";
1414
import { z } from "zod";
1515
import { zodResolver } from "@hookform/resolvers/zod";
16+
import { Captcha } from "@component/Captcha";
17+
import ReCAPTCHA from "react-google-recaptcha";
1618

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

3538
export default function Login() {
3639
const {
3740
register,
3841
handleSubmit,
3942
reset,
43+
setValue,
44+
trigger,
4045
formState: { errors, isValid, isSubmitting },
4146
} = useForm<z.infer<typeof schema>>({
4247
resolver: zodResolver(schema),
4348
});
49+
const captchaRef = useRef<ReCAPTCHA>(null);
4450
const router = useRouter();
4551
const login = trpc.account.login.useMutation();
4652
const { handleResponse, showResponse } = useFormFeedback();
4753

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

5965
reset();
66+
67+
if (captchaRef.current) {
68+
captchaRef.current.reset();
69+
}
6070
};
6171

6272
return (
6373
<>
64-
<Head title="Login" />
65-
<Panel header="Login">
66-
<Text align="center" margin="10px">
67-
Please enter your account name and your password.
68-
</Text>
69-
<Text align="center" margin="10px">
70-
<Link href="/account/register" text="Create an account " />
71-
if you do not have one yet.
72-
</Text>
74+
<Head title="Log In" />
75+
<Panel header="Log In">
76+
<VStack>
77+
<form onSubmit={handleSubmit(onSubmit)}>
78+
<Container alignContent={"center"} padding={2}>
79+
<VStack spacing={5}>
80+
{fields.map((field) => (
81+
<FormField key={field.name} error={(errors as any)[field.name]?.message} name={field.name} label={field.label}>
82+
<TextInput type={field.type} {...register(field.name as any)} />
83+
</FormField>
84+
))}
7385

74-
<form onSubmit={handleSubmit(onSubmit)}>
75-
<Container alignContent={"center"} padding={2}>
76-
<VStack spacing={5}>
77-
{fields.map((field) => (
78-
<FormField key={field.name} error={(errors as any)[field.name]?.message} name={field.name} label={field.label}>
79-
<TextInput type={field.type} {...register(field.name as any)} />
86+
<FormField error={errors.captcha?.message} name="Captcha" justifyItems="center">
87+
<Captcha
88+
{...register("captcha")}
89+
onChange={(token) => {
90+
setValue("captcha", token ?? "");
91+
trigger("captcha");
92+
}}
93+
ref={captchaRef}
94+
/>
8095
</FormField>
81-
))}
82-
<Wrap spacing={2} padding="10px">
83-
<Button isLoading={isSubmitting} isActive={!isValid} loadingText="Submitting" type="submit" value="Submit" btnColorType="primary" />
84-
<Button value="Lost Account?" btnColorType="danger" href="/account/lost" />
85-
</Wrap>
86-
</VStack>
87-
</Container>
88-
</form>
96+
97+
<Button
98+
isLoading={isSubmitting}
99+
isActive={!isValid}
100+
width="100%"
101+
loadingText="Submitting"
102+
type="submit"
103+
value="Log In"
104+
btnColorType="primary"
105+
/>
106+
107+
<Text align="center">
108+
Don&apos;t have an account? <Link href="/account/register">Register</Link>
109+
</Text>
110+
111+
<Link href="/account/lost">Forgot password?</Link>
112+
</VStack>
113+
</Container>
114+
</form>
115+
</VStack>
89116
</Panel>
90117
</>
91118
);

0 commit comments

Comments
 (0)