From bb7f84630549a98a59f023b4cc59ab8fbbce1502 Mon Sep 17 00:00:00 2001 From: alikia2x Date: Sun, 1 Jun 2025 14:36:55 +0800 Subject: [PATCH] improve: the structure of the error handling in sign up page --- packages/backend/src/schema.d.ts | 2 +- .../next/app/[locale]/signup/ErrorDialog.tsx | 22 +++ .../next/app/[locale]/signup/SignUpForm.tsx | 164 +++++------------- packages/next/app/[locale]/signup/request.tsx | 139 +++++++++++++++ packages/next/components/hooks/useCaptcha.ts | 21 ++- packages/next/i18n/strings/zh.json | 15 ++ packages/next/lib/net.ts | 2 +- packages/next/package.json | 2 +- 8 files changed, 228 insertions(+), 139 deletions(-) create mode 100644 packages/next/app/[locale]/signup/ErrorDialog.tsx create mode 100644 packages/next/app/[locale]/signup/request.tsx diff --git a/packages/backend/src/schema.d.ts b/packages/backend/src/schema.d.ts index ab9a7bc..62ad606 100644 --- a/packages/backend/src/schema.d.ts +++ b/packages/backend/src/schema.d.ts @@ -1,4 +1,4 @@ -type ErrorCode = +export type ErrorCode = | "INVALID_QUERY_PARAMS" | "UNKNOWN_ERROR" | "INVALID_PAYLOAD" diff --git a/packages/next/app/[locale]/signup/ErrorDialog.tsx b/packages/next/app/[locale]/signup/ErrorDialog.tsx new file mode 100644 index 0000000..4dbbe6d --- /dev/null +++ b/packages/next/app/[locale]/signup/ErrorDialog.tsx @@ -0,0 +1,22 @@ +import { DialogButton, DialogButtonGroup, DialogHeadline, DialogSupportingText } from "@/components/ui/Dialog"; +import { ErrorCode as ResponseErrorCode } from "@backend/src/schema"; +import { useTranslations } from "next-intl"; + +interface ErrorDialogProps { + children: React.ReactNode; + closeDialog: () => void; + errorCode?: ResponseErrorCode; +} + +export const ErrorDialog: React.FC = ({ children, closeDialog, errorCode }) => { + const t = useTranslations("backend.error_code"); + return ( + <> + {errorCode ? t(errorCode) : "错误"} + {children} + + 关闭 + + + ); +}; diff --git a/packages/next/app/[locale]/signup/SignUpForm.tsx b/packages/next/app/[locale]/signup/SignUpForm.tsx index 01aac95..23497f5 100644 --- a/packages/next/app/[locale]/signup/SignUpForm.tsx +++ b/packages/next/app/[locale]/signup/SignUpForm.tsx @@ -3,16 +3,16 @@ import { useEffect, useState } from "react"; import TextField from "@/components/ui/TextField"; import LoadingSpinner from "@/components/icons/LoadingSpinner"; -import { ApiRequestError } from "@/lib/net"; import { Portal } from "@/components/utils/Portal"; -import { Dialog, DialogButton, DialogButtonGroup, DialogHeadline, DialogSupportingText } from "@/components/ui/Dialog"; -import { FilledButton } from "@/components/ui/Buttons/FilledButton"; -import { string, object, ValidationError } from "yup"; +import { Dialog } from "@/components/ui/Dialog"; import { setLocale } from "yup"; import { useTranslations } from "next-intl"; -import type { ErrorResponse } from "@backend/src/schema"; -import Link from "next/link"; import { useCaptcha } from "@/components/hooks/useCaptcha"; +import useSWRMutation from "swr/mutation"; +import { requestSignUp } from "./request"; +import { FilledButton } from "@/components/ui/Buttons/FilledButton"; +import { ErrorDialog } from "./ErrorDialog"; +import { ApiRequestError } from "@/lib/net"; setLocale({ mixed: { @@ -25,52 +25,30 @@ setLocale({ } }); -interface LocalizedMessage { +export interface LocalizedMessage { key: string; values: { [key: string]: number | string; }; } -const FormSchema = object().shape({ - username: string().required().max(50), - password: string().required().min(4).max(120), - nickname: string().optional().max(30) -}); - interface RegistrationFormProps { backendURL: string; } -interface ErrorDialogProps { - children: React.ReactNode; - closeDialog: () => void; -} - -const ErrorDialog: React.FC = ({ children, closeDialog }) => { - return ( - <> - 错误 - {children} - - 关闭 - - - ); -}; - const SignUpForm: React.FC = ({ backendURL }) => { const [usernameInput, setUsername] = useState(""); const [passwordInput, setPassword] = useState(""); const [nicknameInput, setNickname] = useState(""); - const [loading, setLoading] = useState(false); const [showDialog, setShowDialog] = useState(false); const [dialogContent, setDialogContent] = useState(<>); + const [isLoading, setLoading] = useState(false); const t = useTranslations(""); - const { startCaptcha, captchaResult } = useCaptcha({ + const { startCaptcha, captchaResult, captchaUsed, setCaptchaUsedState, captchaError } = useCaptcha({ backendURL, route: "POST-/user" }); + const { trigger } = useSWRMutation(`${backendURL}/user`, requestSignUp); const translateErrorMessage = (item: LocalizedMessage | string, path?: string) => { if (typeof item === "string") { @@ -80,115 +58,51 @@ const SignUpForm: React.FC = ({ backendURL }) => { }; const register = async () => { - let username: string | undefined; - let password: string | undefined; - let nickname: string | undefined; try { - const formData = await FormSchema.validate( - { + if (captchaUsed || !captchaResult) { + await startCaptcha(); + } + + trigger({ + data: { username: usernameInput, password: passwordInput, nickname: nicknameInput }, - { abortEarly: false } - ); - username = formData.username; - password = formData.password; - nickname = formData.nickname; - } catch (e) { - if (!(e instanceof ValidationError)) { - return; - } - console.log(JSON.parse(JSON.stringify(e))); - setShowDialog(true); - setDialogContent( - setShowDialog(false)}> -

注册信息填写有误,请检查后重新提交。

- 错误信息: -
-
    - {e.errors.map((item, i) => { - return
  1. {translateErrorMessage(item, e.inner[i].path)}
  2. ; - })} -
-
- ); - return; - } - - setLoading(true); - try { - if (!captchaResult || !captchaResult.token) { - await startCaptcha(); - } - // Proceed with user registration using username, password, and nickname - const registrationUrl = new URL(`${backendURL}/user`); - const registrationResponse = await fetch(registrationUrl.toString(), { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${captchaResult!.token}` - }, - body: JSON.stringify({ - username: username, - password: password, - nickname: nickname - }) + setShowDialog, + captchaResult, + setCaptchaUsedState, + translateErrorMessage, + setDialogContent, + t }); - - if (registrationResponse.ok) { - console.log("Registration successful!"); - // Optionally redirect the user or show a success message - //router.push("/login"); // Example redirection - } else { - const res: ErrorResponse = await registrationResponse.json(); - setShowDialog(true); - setDialogContent( - setShowDialog(false)}> -

无法为你注册账户。

-

错误码: {res.code}

-

- 错误信息:
- {res.i18n ? t(res.i18n.key, { ...res.i18n.values }) : res.message} -

-
- ); - } - } catch (error) { - if (error instanceof ApiRequestError) { - const res = error.response as ErrorResponse; - setShowDialog(true); - setDialogContent( - setShowDialog(false)}> -

无法为你注册账户。

-

错误码: {res.code}

-

- 错误信息:
- {res.i18n - ? t.rich(res.i18n.key, { - ...res.i18n.values, - support: (chunks) => {chunks} - }) - : res.message} -

-
- ); - } } finally { setLoading(false); } }; useEffect(() => { - if (startCaptcha) { - startCaptcha(); + if (!captchaError || captchaError === undefined) return; + const err = captchaError as ApiRequestError; + setShowDialog(true); + if (err.code && err.code == -1) { + setDialogContent( + setShowDialog(false)}> +

无法连接到服务器,请检查你的网络连接后重试。

+
+ ); } - }, [startCaptcha]); + }, [captchaError]); + + useEffect(() => { + startCaptcha(); + }, []); return (
{ + setLoading(true); e.preventDefault(); await register(); }} @@ -215,8 +129,8 @@ const SignUpForm: React.FC = ({ backendURL }) => { supportingText="昵称可以重复。" maxChar={30} /> - - {!loading ? 注册 : } + + {isLoading ? : 注册} {dialogContent} diff --git a/packages/next/app/[locale]/signup/request.tsx b/packages/next/app/[locale]/signup/request.tsx new file mode 100644 index 0000000..e7b2613 --- /dev/null +++ b/packages/next/app/[locale]/signup/request.tsx @@ -0,0 +1,139 @@ +import { Dispatch, JSX, SetStateAction } from "react"; +import { ApiRequestError, fetcher } from "@/lib/net"; +import type { CaptchaVerificationRawResponse, ErrorResponse } from "@backend/src/schema"; +import Link from "next/link"; +import { LocalizedMessage } from "./SignUpForm"; +import { ErrorDialog } from "./ErrorDialog"; +import { string, object, ValidationError } from "yup"; + +interface SignUpFormData { + username: string; + password: string; + nickname?: string; +} + +const FormSchema = object().shape({ + username: string().required().max(50), + password: string().required().min(4).max(120), + nickname: string().optional().max(30) +}); + +const validateForm = async ( + data: SignUpFormData, + setShowDialog: Dispatch>, + setDialogContent: Dispatch>, + translateErrorMessage: (item: LocalizedMessage | string, path?: string) => string +): Promise => { + const { username: usernameInput, password: passwordInput, nickname: nicknameInput } = data; + try { + const formData = await FormSchema.validate( + { + username: usernameInput, + password: passwordInput, + nickname: nicknameInput + }, + { abortEarly: false } + ); + return { + username: formData.username, + password: formData.password, + nickname: formData.nickname + }; + } catch (e) { + if (!(e instanceof ValidationError)) { + return null; + } + setShowDialog(true); + setDialogContent( + setShowDialog(false)}> +

注册信息填写有误,请检查后重新提交。

+ 错误信息: +
+
    + {e.errors.map((item, i) => { + return
  1. {translateErrorMessage(item, e.inner[i].path)}
  2. ; + })} +
+
+ ); + return null; + } +}; + +interface RequestSignUpArgs { + data: SignUpFormData; + setShowDialog: Dispatch>; + setDialogContent: Dispatch>; + translateErrorMessage: (item: LocalizedMessage | string, path?: string) => string; + setCaptchaUsedState: Dispatch>; + captchaResult: CaptchaVerificationRawResponse | undefined; + t: any; +} + +export const requestSignUp = async (url: string, { arg }: { arg: RequestSignUpArgs }) => { + const { data, setShowDialog, setDialogContent, translateErrorMessage, setCaptchaUsedState, captchaResult, t } = arg; + const res = await validateForm(data, setShowDialog, setDialogContent, translateErrorMessage); + if (!res) { + return; + } + const { username, nickname, password } = res; + + try { + if (!captchaResult) { + const err = new ApiRequestError("Cannot get captcha result"); + err.response = { + code: "UNKNOWN_ERROR", + message: "Cannot get captch verifiction result", + i18n: { + key: "captcha_failed_to_get" + } + } as ErrorResponse; + throw err; + } + setCaptchaUsedState(true); + const registrationResponse = await fetcher(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${captchaResult!.token}` + }, + data: { + username: username, + password: password, + nickname: nickname + } + }); + return registrationResponse; + } catch (error) { + if (error instanceof ApiRequestError) { + const res = error.response as ErrorResponse; + setShowDialog(true); + setDialogContent( + setShowDialog(false)} errorCode={res.code}> +

无法为你注册账户。

+

+ 错误信息:
+ {res.i18n + ? t.rich(res.i18n.key, { + ...res.i18n.values, + support: (chunks: string) => {chunks} + }) + : res.message} +

+
+ ); + } else if (error instanceof Error) { + setShowDialog(true); + setDialogContent( + setShowDialog(false)}> +

无法为你注册账户。

+

+ 错误信息: +
+ {error.message} +

+
+ ); + } + } +}; diff --git a/packages/next/components/hooks/useCaptcha.ts b/packages/next/components/hooks/useCaptcha.ts index baf04d0..60e5986 100644 --- a/packages/next/components/hooks/useCaptcha.ts +++ b/packages/next/components/hooks/useCaptcha.ts @@ -1,5 +1,6 @@ import useSWRMutation from "swr/mutation"; -import type { ErrorResponse, CaptchaSessionResponse, CaptchaVerificationRawResponse } from "@backend/src/schema"; +import { useState } from "react"; +import type { CaptchaVerificationRawResponse, CaptchaSessionRawResponse } from "@backend/src/schema"; import { fetcher } from "@/lib/net"; import { computeVdfInWorker } from "@/lib/vdf"; @@ -8,17 +9,16 @@ interface UseCaptchaOptions { route: string; } -function isErrResponse(res: ErrorResponse | object): res is ErrorResponse { - return (res as ErrorResponse).errors !== undefined; -} - export function useCaptcha({ backendURL, route }: UseCaptchaOptions) { const fullUrl = `${backendURL}/captcha/session`; + const [isUsed, setIsUsed] = useState(false); const { trigger, data, isMutating, error } = useSWRMutation( fullUrl, async (url: string) => { - const sessionRes = await fetcher(url, { + setIsUsed(false); + + const sessionRes = await fetcher(url, { method: "POST", headers: { "Content-Type": "application/json" @@ -26,10 +26,6 @@ export function useCaptcha({ backendURL, route }: UseCaptchaOptions) { data: { route } }); - if (isErrResponse(sessionRes)) { - throw new Error(sessionRes.message || "Failed to get captcha session"); - } - const { g, n, t, id } = sessionRes; if (!g || !n || !t || !id) { throw new Error("Missing required CAPTCHA parameters"); @@ -41,6 +37,7 @@ export function useCaptcha({ backendURL, route }: UseCaptchaOptions) { resultUrl.searchParams.set("ans", ans.result.toString()); const result = await fetcher(resultUrl.toString()); + return result; } ); @@ -49,6 +46,8 @@ export function useCaptcha({ backendURL, route }: UseCaptchaOptions) { startCaptcha: trigger, captchaResult: data, isLoadingCaptcha: isMutating, - captchaError: error + captchaError: error, + captchaUsed: isUsed, + setCaptchaUsedState: setIsUsed }; } diff --git a/packages/next/i18n/strings/zh.json b/packages/next/i18n/strings/zh.json index cf13140..7a921c5 100644 --- a/packages/next/i18n/strings/zh.json +++ b/packages/next/i18n/strings/zh.json @@ -9,9 +9,24 @@ "nickname": "昵称", "backend": { "error": { + "captcha_failed": "无法完成安全验证。", "user_exists": "用户名 “{username}” 已被占用。", "user_not_found_after_register": "我们的服务器出现错误:找不到名为'{username}'的用户。请联系我们的支持团队,反馈此问题。", "captcha_not_found": "无法完成安全验证。你可以刷新页面并重试。如果此问题反复出现,你可以联系我们的支持团队,反馈此问题。" + }, + "error_code": { + "INVALID_QUERY_PARAMS": "查询无效", + "UNKNOWN_ERROR": "未知错误", + "INVALID_PAYLOAD": "请求格式错误", + "INVALID_FORMAT": "数据格式错误", + "INVALID_HEADER": "请求头无效", + "BODY_TOO_LARGE": "请求体过大", + "UNAUTHORIZED": "未授权", + "INVALID_CREDENTIALS": "凭证无效", + "ENTITY_NOT_FOUND": "实体不存在", + "SERVER_ERROR": "服务器错误", + "RATE_LIMIT_EXCEEDED": "请求过于频繁", + "ENTITY_EXISTS": "实体已存在" } } } diff --git a/packages/next/lib/net.ts b/packages/next/lib/net.ts index bcc8b91..87cfbe5 100644 --- a/packages/next/lib/net.ts +++ b/packages/next/lib/net.ts @@ -49,7 +49,7 @@ export async function fetcher( const { status, data } = axiosError.response; throw new ApiRequestError(`HTTP error! status: ${status}`, data, status); } else if (axiosError.request) { - throw new ApiRequestError("No response received."); + throw new ApiRequestError("No response received", undefined, -1); } else { throw new ApiRequestError(axiosError.message || "Unknown error"); } diff --git a/packages/next/package.json b/packages/next/package.json index 85c6298..128d1f5 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "next dev --turbopack -p 7400", "build": "next build", - "start": "next start", + "start": "next start -p 7400", "lint": "next lint", "format": "prettier --write ." },