✨ feat: Authentication verification code
This commit is contained in:
parent
4b4edd48e3
commit
eac7b27f60
@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^10.0.0",
|
"@faker-js/faker": "^10.0.0",
|
||||||
"@lottiefiles/dotlottie-react": "^0.17.7",
|
"@lottiefiles/dotlottie-react": "^0.17.15",
|
||||||
"@noble/curves": "^2.0.1",
|
"@noble/curves": "^2.0.1",
|
||||||
"@stripe/react-stripe-js": "^5.4.0",
|
"@stripe/react-stripe-js": "^5.4.0",
|
||||||
"@stripe/stripe-js": "^8.5.2",
|
"@stripe/stripe-js": "^8.5.2",
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"captcha": {
|
||||||
|
"clickToRefresh": "Click to refresh",
|
||||||
|
"noImage": "No Image",
|
||||||
|
"placeholder": "Enter captcha code...",
|
||||||
|
"refresh": "Refresh captcha",
|
||||||
|
"required": "Please enter captcha code"
|
||||||
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"description": "Verify your identity",
|
"description": "Verify your identity",
|
||||||
"title": "Verify"
|
"title": "Verify"
|
||||||
|
|||||||
@ -104,13 +104,20 @@
|
|||||||
},
|
},
|
||||||
"userSecuritySettings": "User & Security",
|
"userSecuritySettings": "User & Security",
|
||||||
"verify": {
|
"verify": {
|
||||||
"description": "Configure Turnstile CAPTCHA and verification settings",
|
"captchaType": "Captcha Type",
|
||||||
"enableLoginVerify": "Enable Verification on Login",
|
"captchaTypeDescription": "Choose between local image captcha (offline) or Cloudflare Turnstile",
|
||||||
"enableLoginVerifyDescription": "When enabled, users must pass human verification during login",
|
"captchaTypeLocal": "Local Image Captcha",
|
||||||
"enablePasswordVerify": "Enable Verification on Password Reset",
|
"captchaTypePlaceholder": "Select captcha type",
|
||||||
"enablePasswordVerifyDescription": "When enabled, users must pass human verification during password reset",
|
"captchaTypeTurnstile": "Cloudflare Turnstile",
|
||||||
"enableRegisterVerify": "Enable Verification on Registration",
|
"description": "Configure captcha type and verification settings",
|
||||||
"enableRegisterVerifyDescription": "When enabled, users must pass human verification during registration",
|
"enableAdminLoginCaptcha": "Enable Admin Authentication Captcha",
|
||||||
|
"enableAdminLoginCaptchaDescription": "When enabled, administrators must pass captcha verification during login or password reset",
|
||||||
|
"enableUserLoginCaptcha": "Enable User Login Captcha",
|
||||||
|
"enableUserLoginCaptchaDescription": "When enabled, users must pass captcha verification during login",
|
||||||
|
"enableUserRegisterCaptcha": "Enable User Registration Captcha",
|
||||||
|
"enableUserRegisterCaptchaDescription": "When enabled, users must pass captcha verification during registration",
|
||||||
|
"enableUserResetPasswordCaptcha": "Enable User Password Reset Captcha",
|
||||||
|
"enableUserResetPasswordCaptchaDescription": "When enabled, users must pass captcha verification during password reset",
|
||||||
"saveFailed": "Save Failed",
|
"saveFailed": "Save Failed",
|
||||||
"saveSuccess": "Save Successful",
|
"saveSuccess": "Save Successful",
|
||||||
"title": "Security Verification",
|
"title": "Security Verification",
|
||||||
|
|||||||
@ -147,5 +147,6 @@
|
|||||||
"address": "Address",
|
"address": "Address",
|
||||||
"noNodesAvailable": "No nodes available",
|
"noNodesAvailable": "No nodes available",
|
||||||
"nodeGroup": "Node Group",
|
"nodeGroup": "Node Group",
|
||||||
"publicNodes": "Public Nodes"
|
"publicNodes": "Public Nodes",
|
||||||
|
"subscriptionNodes": "Subscription Nodes"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"captcha": {
|
||||||
|
"clickToRefresh": "点击刷新",
|
||||||
|
"noImage": "无图片",
|
||||||
|
"placeholder": "请输入验证码...",
|
||||||
|
"refresh": "刷新验证码",
|
||||||
|
"required": "请输入验证码"
|
||||||
|
},
|
||||||
"check": {
|
"check": {
|
||||||
"description": "验证您的身份",
|
"description": "验证您的身份",
|
||||||
"title": "验证"
|
"title": "验证"
|
||||||
|
|||||||
@ -104,13 +104,20 @@
|
|||||||
},
|
},
|
||||||
"userSecuritySettings": "用户与安全",
|
"userSecuritySettings": "用户与安全",
|
||||||
"verify": {
|
"verify": {
|
||||||
"description": "配置 Turnstile 验证码和验证设置",
|
"captchaType": "验证码类型",
|
||||||
"enableLoginVerify": "登录验证",
|
"captchaTypeDescription": "选择本地图形验证码(离线)或 Cloudflare Turnstile",
|
||||||
"enableLoginVerifyDescription": "启用后,用户登录时必须通过人机验证",
|
"captchaTypeLocal": "本地图形验证码",
|
||||||
"enablePasswordVerify": "密码重置验证",
|
"captchaTypePlaceholder": "选择验证码类型",
|
||||||
"enablePasswordVerifyDescription": "启用后,用户重置密码时必须通过人机验证",
|
"captchaTypeTurnstile": "Cloudflare Turnstile",
|
||||||
"enableRegisterVerify": "注册验证",
|
"description": "配置验证码类型和验证设置",
|
||||||
"enableRegisterVerifyDescription": "启用后,用户注册时必须通过人机验证",
|
"enableAdminLoginCaptcha": "启用管理端认证验证码",
|
||||||
|
"enableAdminLoginCaptchaDescription": "启用后,管理员登录或重置密码时必须通过验证码验证",
|
||||||
|
"enableUserLoginCaptcha": "启用用户端登录验证码",
|
||||||
|
"enableUserLoginCaptchaDescription": "启用后,用户登录时必须通过验证码验证",
|
||||||
|
"enableUserRegisterCaptcha": "启用用户端注册验证码",
|
||||||
|
"enableUserRegisterCaptchaDescription": "启用后,用户注册时必须通过验证码验证",
|
||||||
|
"enableUserResetPasswordCaptcha": "启用用户重置密码验证码",
|
||||||
|
"enableUserResetPasswordCaptchaDescription": "启用后,用户重置密码时必须通过验证码验证",
|
||||||
"saveFailed": "保存失败",
|
"saveFailed": "保存失败",
|
||||||
"saveSuccess": "保存成功",
|
"saveSuccess": "保存成功",
|
||||||
"title": "安全验证",
|
"title": "安全验证",
|
||||||
|
|||||||
@ -147,5 +147,6 @@
|
|||||||
"address": "地址",
|
"address": "地址",
|
||||||
"noNodesAvailable": "无可用节点",
|
"noNodesAvailable": "无可用节点",
|
||||||
"nodeGroup": "节点组",
|
"nodeGroup": "节点组",
|
||||||
"publicNodes": "公共节点"
|
"publicNodes": "公共节点",
|
||||||
|
"subscriptionNodes": "套餐节点"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,12 +10,13 @@ import {
|
|||||||
import { Input } from "@workspace/ui/components/input";
|
import { Input } from "@workspace/ui/components/input";
|
||||||
import { Icon } from "@workspace/ui/composed/icon";
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useGlobalStore } from "@/stores/global";
|
import { useGlobalStore } from "@/stores/global";
|
||||||
import CloudFlareTurnstile, { type TurnstileRef } from "../turnstile";
|
import CloudFlareTurnstile, { type TurnstileRef } from "../turnstile";
|
||||||
|
import LocalCaptcha, { type LocalCaptchaRef } from "../local-captcha";
|
||||||
|
|
||||||
export default function LoginForm({
|
export default function LoginForm({
|
||||||
loading,
|
loading,
|
||||||
@ -33,14 +34,25 @@ export default function LoginForm({
|
|||||||
const { t } = useTranslation("auth");
|
const { t } = useTranslation("auth");
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify } = common;
|
const { verify } = common;
|
||||||
|
const [captchaId, setCaptchaId] = useState("");
|
||||||
|
|
||||||
|
const isTurnstile = verify.captcha_type === "turnstile";
|
||||||
|
const isLocal = verify.captcha_type === "local";
|
||||||
|
const captchaEnabled = verify.enable_admin_login_captcha;
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.email(t("login.email", "Email")),
|
email: z
|
||||||
|
.string()
|
||||||
|
.email(t("login.email", "Please enter a valid email address")),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_login_verify && verify.turnstile_site_key
|
captchaEnabled && isTurnstile && verify.turnstile_site_key
|
||||||
? z.string()
|
? z.string()
|
||||||
: z.string().optional(),
|
: z.string().optional(),
|
||||||
|
captcha_code:
|
||||||
|
captchaEnabled && isLocal
|
||||||
|
? z.string().min(1, t("captcha.required", "Please enter captcha code"))
|
||||||
|
: z.string().optional(),
|
||||||
});
|
});
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@ -48,11 +60,17 @@ export default function LoginForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const turnstile = useRef<TurnstileRef>(null);
|
const turnstile = useRef<TurnstileRef>(null);
|
||||||
|
const localCaptcha = useRef<LocalCaptchaRef>(null);
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
const handleSubmit = form.handleSubmit((data) => {
|
||||||
try {
|
try {
|
||||||
|
// Add captcha_id for local captcha
|
||||||
|
if (isLocal && captchaEnabled) {
|
||||||
|
(data as any).captcha_id = captchaId;
|
||||||
|
}
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
turnstile.current?.reset();
|
turnstile.current?.reset();
|
||||||
|
localCaptcha.current?.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -98,7 +116,7 @@ export default function LoginForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{verify.enable_login_verify && (
|
{captchaEnabled && isTurnstile && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="cf_token"
|
name="cf_token"
|
||||||
@ -116,6 +134,24 @@ export default function LoginForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{captchaEnabled && isLocal && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="captcha_code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<LocalCaptcha
|
||||||
|
{...field}
|
||||||
|
ref={localCaptcha}
|
||||||
|
onCaptchaIdChange={setCaptchaId}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button disabled={loading} type="submit">
|
<Button disabled={loading} type="submit">
|
||||||
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
||||||
{t("login.title", "Login")}
|
{t("login.title", "Login")}
|
||||||
|
|||||||
@ -10,13 +10,14 @@ import {
|
|||||||
import { Input } from "@workspace/ui/components/input";
|
import { Input } from "@workspace/ui/components/input";
|
||||||
import { Icon } from "@workspace/ui/composed/icon";
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useGlobalStore } from "@/stores/global";
|
import { useGlobalStore } from "@/stores/global";
|
||||||
import SendCode from "../send-code";
|
import SendCode from "../send-code";
|
||||||
import CloudFlareTurnstile, { type TurnstileRef } from "../turnstile";
|
import CloudFlareTurnstile, { type TurnstileRef } from "../turnstile";
|
||||||
|
import LocalCaptcha, { type LocalCaptchaRef } from "../local-captcha";
|
||||||
|
|
||||||
export default function ResetForm({
|
export default function ResetForm({
|
||||||
loading,
|
loading,
|
||||||
@ -35,15 +36,26 @@ export default function ResetForm({
|
|||||||
|
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify, auth } = common;
|
const { verify, auth } = common;
|
||||||
|
const [captchaId, setCaptchaId] = useState("");
|
||||||
|
|
||||||
|
const isTurnstile = verify.captcha_type === "turnstile";
|
||||||
|
const isLocal = verify.captcha_type === "local";
|
||||||
|
const captchaEnabled = verify.enable_user_reset_password_captcha;
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.email(t("reset.email", "Email")),
|
email: z
|
||||||
|
.string()
|
||||||
|
.email(t("reset.email", "Please enter a valid email address")),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
|
code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_register_verify && verify.turnstile_site_key
|
captchaEnabled && isTurnstile && verify.turnstile_site_key
|
||||||
? z.string()
|
? z.string()
|
||||||
: z.string().nullish(),
|
: z.string().nullish(),
|
||||||
|
captcha_code:
|
||||||
|
captchaEnabled && isLocal
|
||||||
|
? z.string().min(1, t("captcha.required", "Please enter captcha code"))
|
||||||
|
: z.string().nullish(),
|
||||||
});
|
});
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@ -51,11 +63,17 @@ export default function ResetForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const turnstile = useRef<TurnstileRef>(null);
|
const turnstile = useRef<TurnstileRef>(null);
|
||||||
|
const localCaptcha = useRef<LocalCaptchaRef>(null);
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
const handleSubmit = form.handleSubmit((data) => {
|
||||||
try {
|
try {
|
||||||
|
// Add captcha_id for local captcha
|
||||||
|
if (isLocal && captchaEnabled) {
|
||||||
|
(data as any).captcha_id = captchaId;
|
||||||
|
}
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
turnstile.current?.reset();
|
turnstile.current?.reset();
|
||||||
|
localCaptcha.current?.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -128,7 +146,7 @@ export default function ResetForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{verify.enable_reset_password_verify && (
|
{captchaEnabled && isTurnstile && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="cf_token"
|
name="cf_token"
|
||||||
@ -146,6 +164,24 @@ export default function ResetForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{captchaEnabled && isLocal && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="captcha_code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<LocalCaptcha
|
||||||
|
{...field}
|
||||||
|
ref={localCaptcha}
|
||||||
|
onCaptchaIdChange={setCaptchaId}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button disabled={loading} type="submit">
|
<Button disabled={loading} type="submit">
|
||||||
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
||||||
{t("reset.title", "Reset Password")}
|
{t("reset.title", "Reset Password")}
|
||||||
|
|||||||
95
apps/admin/src/sections/auth/local-captcha.tsx
Normal file
95
apps/admin/src/sections/auth/local-captcha.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { Button } from "@workspace/ui/components/button";
|
||||||
|
import { Input } from "@workspace/ui/components/input";
|
||||||
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
|
import { adminGenerateCaptcha } from "@workspace/ui/services/admin/auth";
|
||||||
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export interface LocalCaptchaRef {
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocalCaptchaProps {
|
||||||
|
value?: string | null;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onCaptchaIdChange?: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocalCaptcha = forwardRef<LocalCaptchaRef, LocalCaptchaProps>(
|
||||||
|
({ value, onChange, onCaptchaIdChange }, ref) => {
|
||||||
|
const { t } = useTranslation("auth");
|
||||||
|
const [captchaImage, setCaptchaImage] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const fetchCaptcha = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await adminGenerateCaptcha();
|
||||||
|
const captchaData = res.data?.data;
|
||||||
|
if (captchaData) {
|
||||||
|
setCaptchaImage(captchaData.image);
|
||||||
|
onCaptchaIdChange?.(captchaData.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to generate captcha:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCaptcha();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
reset: () => {
|
||||||
|
onChange?.("");
|
||||||
|
fetchCaptcha();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder={t("captcha.placeholder", "Enter captcha code...")}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<div className="relative h-10 w-32 flex-shrink-0">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex h-full items-center justify-center bg-muted">
|
||||||
|
<Icon className="animate-spin" icon="mdi:loading" />
|
||||||
|
</div>
|
||||||
|
) : captchaImage ? (
|
||||||
|
<img
|
||||||
|
src={captchaImage}
|
||||||
|
alt="captcha"
|
||||||
|
className="h-full w-full cursor-pointer object-contain"
|
||||||
|
onClick={fetchCaptcha}
|
||||||
|
title={t("captcha.clickToRefresh", "Click to refresh")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center bg-muted text-xs text-muted-foreground">
|
||||||
|
{t("captcha.noImage", "No Image")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={fetchCaptcha}
|
||||||
|
disabled={loading}
|
||||||
|
title={t("captcha.refresh", "Refresh captcha")}
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:refresh" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
LocalCaptcha.displayName = "LocalCaptcha";
|
||||||
|
|
||||||
|
export default LocalCaptcha;
|
||||||
@ -11,6 +11,13 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@workspace/ui/components/form";
|
} from "@workspace/ui/components/form";
|
||||||
import { ScrollArea } from "@workspace/ui/components/scroll-area";
|
import { ScrollArea } from "@workspace/ui/components/scroll-area";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@workspace/ui/components/select";
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
@ -33,11 +40,13 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const verifySchema = z.object({
|
const verifySchema = z.object({
|
||||||
|
captcha_type: z.string().optional(),
|
||||||
turnstile_site_key: z.string().optional(),
|
turnstile_site_key: z.string().optional(),
|
||||||
turnstile_secret: z.string().optional(),
|
turnstile_secret: z.string().optional(),
|
||||||
enable_register_verify: z.boolean().optional(),
|
enable_user_login_captcha: z.boolean().optional(),
|
||||||
enable_login_verify: z.boolean().optional(),
|
enable_user_register_captcha: z.boolean().optional(),
|
||||||
enable_reset_password_verify: z.boolean().optional(),
|
enable_admin_login_captcha: z.boolean().optional(),
|
||||||
|
enable_user_reset_password_captcha: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type VerifyFormData = z.infer<typeof verifySchema>;
|
type VerifyFormData = z.infer<typeof verifySchema>;
|
||||||
@ -59,11 +68,13 @@ export default function VerifyConfig() {
|
|||||||
const form = useForm<VerifyFormData>({
|
const form = useForm<VerifyFormData>({
|
||||||
resolver: zodResolver(verifySchema),
|
resolver: zodResolver(verifySchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
captcha_type: "local",
|
||||||
turnstile_site_key: "",
|
turnstile_site_key: "",
|
||||||
turnstile_secret: "",
|
turnstile_secret: "",
|
||||||
enable_register_verify: false,
|
enable_user_login_captcha: false,
|
||||||
enable_login_verify: false,
|
enable_user_register_captcha: false,
|
||||||
enable_reset_password_verify: false,
|
enable_admin_login_captcha: false,
|
||||||
|
enable_user_reset_password_captcha: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -126,26 +137,42 @@ export default function VerifyConfig() {
|
|||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="turnstile_site_key"
|
name="captcha_type"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("verify.turnstileSiteKey", "Turnstile Site Key")}
|
{t("verify.captchaType", "Captcha Type")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<EnhancedInput
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
placeholder={t(
|
|
||||||
"verify.turnstileSiteKeyPlaceholder",
|
|
||||||
"Enter Turnstile site key"
|
|
||||||
)}
|
|
||||||
value={field.value}
|
value={field.value}
|
||||||
/>
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"verify.captchaTypePlaceholder",
|
||||||
|
"Select captcha type"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="local">
|
||||||
|
{t("verify.captchaTypeLocal", "Local Image Captcha")}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="turnstile">
|
||||||
|
{t(
|
||||||
|
"verify.captchaTypeTurnstile",
|
||||||
|
"Cloudflare Turnstile"
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"verify.turnstileSiteKeyDescription",
|
"verify.captchaTypeDescription",
|
||||||
"Cloudflare Turnstile site key for frontend verification"
|
"Choose between local image captcha (offline) or Cloudflare Turnstile"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -153,45 +180,78 @@ export default function VerifyConfig() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{form.watch("captcha_type") === "turnstile" && (
|
||||||
control={form.control}
|
<>
|
||||||
name="turnstile_secret"
|
<FormField
|
||||||
render={({ field }) => (
|
control={form.control}
|
||||||
<FormItem>
|
name="turnstile_site_key"
|
||||||
<FormLabel>
|
render={({ field }) => (
|
||||||
{t("verify.turnstileSecret", "Turnstile Secret Key")}
|
<FormItem>
|
||||||
</FormLabel>
|
<FormLabel>
|
||||||
<FormControl>
|
{t("verify.turnstileSiteKey", "Turnstile Site Key")}
|
||||||
<EnhancedInput
|
</FormLabel>
|
||||||
onValueChange={field.onChange}
|
<FormControl>
|
||||||
placeholder={t(
|
<EnhancedInput
|
||||||
"verify.turnstileSecretPlaceholder",
|
onValueChange={field.onChange}
|
||||||
"Enter Turnstile secret key"
|
placeholder={t(
|
||||||
)}
|
"verify.turnstileSiteKeyPlaceholder",
|
||||||
type="password"
|
"Enter Turnstile site key"
|
||||||
value={field.value}
|
)}
|
||||||
/>
|
value={field.value}
|
||||||
</FormControl>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
{t(
|
<FormDescription>
|
||||||
"verify.turnstileSecretDescription",
|
{t(
|
||||||
"Cloudflare Turnstile secret key for backend verification"
|
"verify.turnstileSiteKeyDescription",
|
||||||
)}
|
"Cloudflare Turnstile site key for frontend verification"
|
||||||
</FormDescription>
|
)}
|
||||||
<FormMessage />
|
</FormDescription>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="turnstile_secret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("verify.turnstileSecret", "Turnstile Secret Key")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
placeholder={t(
|
||||||
|
"verify.turnstileSecretPlaceholder",
|
||||||
|
"Enter Turnstile secret key"
|
||||||
|
)}
|
||||||
|
type="password"
|
||||||
|
value={field.value}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"verify.turnstileSecretDescription",
|
||||||
|
"Cloudflare Turnstile secret key for backend verification"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enable_register_verify"
|
name="enable_user_login_captcha"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t(
|
{t(
|
||||||
"verify.enableRegisterVerify",
|
"verify.enableUserLoginCaptcha",
|
||||||
"Enable Verification on Registration"
|
"Enable User Login Captcha"
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -203,8 +263,8 @@ export default function VerifyConfig() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"verify.enableRegisterVerifyDescription",
|
"verify.enableUserLoginCaptchaDescription",
|
||||||
"When enabled, users must pass human verification during registration"
|
"When enabled, users must pass captcha verification during login"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -214,13 +274,13 @@ export default function VerifyConfig() {
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enable_login_verify"
|
name="enable_user_register_captcha"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t(
|
{t(
|
||||||
"verify.enableLoginVerify",
|
"verify.enableUserRegisterCaptcha",
|
||||||
"Enable Verification on Login"
|
"Enable User Registration Captcha"
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -232,8 +292,8 @@ export default function VerifyConfig() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"verify.enableLoginVerifyDescription",
|
"verify.enableUserRegisterCaptchaDescription",
|
||||||
"When enabled, users must pass human verification during login"
|
"When enabled, users must pass captcha verification during registration"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -243,13 +303,13 @@ export default function VerifyConfig() {
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enable_reset_password_verify"
|
name="enable_user_reset_password_captcha"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t(
|
{t(
|
||||||
"verify.enablePasswordVerify",
|
"verify.enableUserResetPasswordCaptcha",
|
||||||
"Enable Verification on Password Reset"
|
"Enable User Password Reset Captcha"
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -261,8 +321,37 @@ export default function VerifyConfig() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"verify.enablePasswordVerifyDescription",
|
"verify.enableUserResetPasswordCaptchaDescription",
|
||||||
"When enabled, users must pass human verification during password reset"
|
"When enabled, users must pass captcha verification during password reset"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enable_admin_login_captcha"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"verify.enableAdminLoginCaptcha",
|
||||||
|
"Enable Admin Authentication Captcha"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
className="!mt-0 float-end"
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"verify.enableAdminLoginCaptchaDescription",
|
||||||
|
"When enabled, administrators must pass captcha verification during login"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@ -477,7 +477,12 @@ function PreviewNodesDialog({ userId }: { userId: number }) {
|
|||||||
{previewData.node_groups.map((group) => (
|
{previewData.node_groups.map((group) => (
|
||||||
<div key={group.id}>
|
<div key={group.id}>
|
||||||
<h4 className="text-sm font-semibold mb-2">
|
<h4 className="text-sm font-semibold mb-2">
|
||||||
{group.name || (group.id === 0 ? t("publicNodes", "Public Nodes") : `${t("nodeGroup", "Node Group")} ${group.id}`)}
|
{group.name ||
|
||||||
|
(group.id === -1
|
||||||
|
? t("subscriptionNodes", "Subscription Nodes")
|
||||||
|
: group.id === 0
|
||||||
|
? t("publicNodes", "Public Nodes")
|
||||||
|
: `${t("nodeGroup", "Node Group")} ${group.id}`)}
|
||||||
</h4>
|
</h4>
|
||||||
{group.nodes && group.nodes.length > 0 ? (
|
{group.nodes && group.nodes.length > 0 ? (
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
|
|||||||
@ -49,9 +49,14 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
|||||||
},
|
},
|
||||||
verify: {
|
verify: {
|
||||||
turnstile_site_key: "",
|
turnstile_site_key: "",
|
||||||
|
captcha_type: "turnstile",
|
||||||
enable_login_verify: false,
|
enable_login_verify: false,
|
||||||
enable_register_verify: false,
|
enable_register_verify: false,
|
||||||
enable_reset_password_verify: false,
|
enable_reset_password_verify: false,
|
||||||
|
enable_user_login_captcha: false,
|
||||||
|
enable_user_register_captcha: false,
|
||||||
|
enable_user_reset_password_captcha: false,
|
||||||
|
enable_admin_login_captcha: false,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
mobile: {
|
mobile: {
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^10.0.0",
|
"@faker-js/faker": "^10.0.0",
|
||||||
"@lottiefiles/dotlottie-react": "^0.17.7",
|
"@lottiefiles/dotlottie-react": "^0.17.15",
|
||||||
"@stripe/react-stripe-js": "^5.4.0",
|
"@stripe/react-stripe-js": "^5.4.0",
|
||||||
"@stripe/stripe-js": "^8.5.2",
|
"@stripe/stripe-js": "^8.5.2",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
|
|||||||
BIN
apps/user/public/assets/locales.zip
Normal file
BIN
apps/user/public/assets/locales.zip
Normal file
Binary file not shown.
@ -1,12 +1,21 @@
|
|||||||
{
|
{
|
||||||
"authenticating": "Authenticating...",
|
"authenticating": "Authenticating...",
|
||||||
"binding": "Binding account...",
|
"binding": "Binding account...",
|
||||||
|
"captcha": {
|
||||||
|
"clickToRefresh": "Click to refresh",
|
||||||
|
"noImage": "No Image",
|
||||||
|
"placeholder": "Enter captcha code...",
|
||||||
|
"refresh": "Refresh captcha",
|
||||||
|
"required": "Please enter captcha code"
|
||||||
|
},
|
||||||
"get": "Get Code",
|
"get": "Get Code",
|
||||||
"login": {
|
"login": {
|
||||||
"codeLogin": "Login with Code",
|
"codeLogin": "Login with Code",
|
||||||
"email": "Please enter a valid email address",
|
"email": "Please enter a valid email address",
|
||||||
|
"emailPlaceholder": "Enter your email...",
|
||||||
"forgotPassword": "Forgot Password?",
|
"forgotPassword": "Forgot Password?",
|
||||||
"passwordLogin": "Login with Password",
|
"passwordLogin": "Login with Password",
|
||||||
|
"passwordPlaceholder": "Enter your password...",
|
||||||
"registerAccount": "Register Account",
|
"registerAccount": "Register Account",
|
||||||
"success": "Login successful!",
|
"success": "Login successful!",
|
||||||
"title": "Login"
|
"title": "Login"
|
||||||
@ -17,19 +26,28 @@
|
|||||||
},
|
},
|
||||||
"privacyPolicy": "Privacy Policy",
|
"privacyPolicy": "Privacy Policy",
|
||||||
"register": {
|
"register": {
|
||||||
|
"areaCodePlaceholder": "Area code...",
|
||||||
|
"codePlaceholder": "Enter code...",
|
||||||
"email": "Please enter a valid email address",
|
"email": "Please enter a valid email address",
|
||||||
|
"emailPlaceholder": "Enter your email...",
|
||||||
"existingAccount": "Already have an account?",
|
"existingAccount": "Already have an account?",
|
||||||
"invite": "Invitation Code (Optional)",
|
"invite": "Invitation Code (Optional)",
|
||||||
"message": "Registration is currently disabled",
|
"message": "Registration is currently disabled",
|
||||||
"passwordMismatch": "Passwords do not match",
|
"passwordMismatch": "Passwords do not match",
|
||||||
|
"passwordPlaceholder": "Enter your password...",
|
||||||
|
"repeatPasswordPlaceholder": "Enter password again...",
|
||||||
"success": "Registration successful!",
|
"success": "Registration successful!",
|
||||||
"switchToLogin": "Login",
|
"switchToLogin": "Login",
|
||||||
|
"telephonePlaceholder": "Enter your telephone...",
|
||||||
"title": "Register",
|
"title": "Register",
|
||||||
"whitelist": "This email domain is not in the whitelist"
|
"whitelist": "This email domain is not in the whitelist"
|
||||||
},
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
|
"codePlaceholder": "Enter code...",
|
||||||
"email": "Please enter a valid email address",
|
"email": "Please enter a valid email address",
|
||||||
|
"emailPlaceholder": "Enter your email...",
|
||||||
"existingAccount": "Remember your password?",
|
"existingAccount": "Remember your password?",
|
||||||
|
"passwordPlaceholder": "Enter your new password...",
|
||||||
"success": "Password reset successful!",
|
"success": "Password reset successful!",
|
||||||
"switchToLogin": "Login",
|
"switchToLogin": "Login",
|
||||||
"title": "Reset Password"
|
"title": "Reset Password"
|
||||||
|
|||||||
@ -1,12 +1,21 @@
|
|||||||
{
|
{
|
||||||
"authenticating": "正在认证...",
|
"authenticating": "正在认证...",
|
||||||
"binding": "正在绑定账号...",
|
"binding": "正在绑定账号...",
|
||||||
|
"captcha": {
|
||||||
|
"clickToRefresh": "点击刷新",
|
||||||
|
"noImage": "无图片",
|
||||||
|
"placeholder": "请输入验证码...",
|
||||||
|
"refresh": "刷新验证码",
|
||||||
|
"required": "请输入验证码"
|
||||||
|
},
|
||||||
"get": "获取验证码",
|
"get": "获取验证码",
|
||||||
"login": {
|
"login": {
|
||||||
"codeLogin": "验证码登录",
|
"codeLogin": "验证码登录",
|
||||||
"email": "请输入有效的邮箱地址",
|
"email": "请输入有效的邮箱地址",
|
||||||
|
"emailPlaceholder": "请输入邮箱...",
|
||||||
"forgotPassword": "忘记密码?",
|
"forgotPassword": "忘记密码?",
|
||||||
"passwordLogin": "密码登录",
|
"passwordLogin": "密码登录",
|
||||||
|
"passwordPlaceholder": "请输入密码...",
|
||||||
"registerAccount": "注册账号",
|
"registerAccount": "注册账号",
|
||||||
"success": "登录成功!",
|
"success": "登录成功!",
|
||||||
"title": "登录"
|
"title": "登录"
|
||||||
@ -17,19 +26,28 @@
|
|||||||
},
|
},
|
||||||
"privacyPolicy": "隐私政策",
|
"privacyPolicy": "隐私政策",
|
||||||
"register": {
|
"register": {
|
||||||
|
"areaCodePlaceholder": "区号...",
|
||||||
|
"codePlaceholder": "请输入验证码...",
|
||||||
"email": "请输入有效的邮箱地址",
|
"email": "请输入有效的邮箱地址",
|
||||||
|
"emailPlaceholder": "请输入邮箱...",
|
||||||
"existingAccount": "已有账号?",
|
"existingAccount": "已有账号?",
|
||||||
"invite": "邀请码(可选)",
|
"invite": "邀请码(可选)",
|
||||||
"message": "注册功能暂时不可用",
|
"message": "注册功能暂时不可用",
|
||||||
"passwordMismatch": "两次密码输入不一致",
|
"passwordMismatch": "两次密码输入不一致",
|
||||||
|
"passwordPlaceholder": "请输入密码...",
|
||||||
|
"repeatPasswordPlaceholder": "请再次输入密码...",
|
||||||
"success": "注册成功!",
|
"success": "注册成功!",
|
||||||
"switchToLogin": "登录",
|
"switchToLogin": "登录",
|
||||||
|
"telephonePlaceholder": "请输入手机号...",
|
||||||
"title": "注册",
|
"title": "注册",
|
||||||
"whitelist": "该邮箱域名不在白名单中"
|
"whitelist": "该邮箱域名不在白名单中"
|
||||||
},
|
},
|
||||||
"reset": {
|
"reset": {
|
||||||
|
"codePlaceholder": "请输入验证码...",
|
||||||
"email": "请输入有效的邮箱地址",
|
"email": "请输入有效的邮箱地址",
|
||||||
|
"emailPlaceholder": "请输入邮箱...",
|
||||||
"existingAccount": "记得密码了?",
|
"existingAccount": "记得密码了?",
|
||||||
|
"passwordPlaceholder": "请输入新密码...",
|
||||||
"success": "密码重置成功!",
|
"success": "密码重置成功!",
|
||||||
"switchToLogin": "登录",
|
"switchToLogin": "登录",
|
||||||
"title": "重置密码"
|
"title": "重置密码"
|
||||||
|
|||||||
@ -10,13 +10,14 @@ import {
|
|||||||
import { Input } from "@workspace/ui/components/input";
|
import { Input } from "@workspace/ui/components/input";
|
||||||
import { Icon } from "@workspace/ui/composed/icon";
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useGlobalStore } from "@/stores/global";
|
import { useGlobalStore } from "@/stores/global";
|
||||||
import type { TurnstileRef } from "../turnstile";
|
import type { TurnstileRef } from "../turnstile";
|
||||||
import CloudFlareTurnstile from "../turnstile";
|
import CloudFlareTurnstile from "../turnstile";
|
||||||
|
import LocalCaptcha, { type LocalCaptchaRef } from "../local-captcha";
|
||||||
|
|
||||||
export default function LoginForm({
|
export default function LoginForm({
|
||||||
loading,
|
loading,
|
||||||
@ -34,14 +35,23 @@ export default function LoginForm({
|
|||||||
const { t } = useTranslation("auth");
|
const { t } = useTranslation("auth");
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify } = common;
|
const { verify } = common;
|
||||||
|
const [captchaId, setCaptchaId] = useState("");
|
||||||
|
|
||||||
|
const isTurnstile = verify.captcha_type === "turnstile";
|
||||||
|
const isLocal = verify.captcha_type === "local";
|
||||||
|
const captchaEnabled = verify.enable_user_login_captcha;
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z.email(t("login.email", "Please enter a valid email address")),
|
email: z.email(t("login.email", "Please enter a valid email address")),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_login_verify && verify.turnstile_site_key
|
captchaEnabled && isTurnstile && verify.turnstile_site_key
|
||||||
? z.string()
|
? z.string()
|
||||||
: z.string().optional(),
|
: z.string().optional(),
|
||||||
|
captcha_code:
|
||||||
|
captchaEnabled && isLocal
|
||||||
|
? z.string().min(1, t("captcha.required", "Please enter captcha code"))
|
||||||
|
: z.string().optional(),
|
||||||
});
|
});
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@ -49,11 +59,17 @@ export default function LoginForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const turnstile = useRef<TurnstileRef>(null);
|
const turnstile = useRef<TurnstileRef>(null);
|
||||||
|
const localCaptcha = useRef<LocalCaptchaRef>(null);
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
const handleSubmit = form.handleSubmit((data) => {
|
||||||
try {
|
try {
|
||||||
|
// Add captcha_id for local captcha
|
||||||
|
if (isLocal && captchaEnabled) {
|
||||||
|
(data as any).captcha_id = captchaId;
|
||||||
|
}
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
turnstile.current?.reset();
|
turnstile.current?.reset();
|
||||||
|
localCaptcha.current?.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -68,7 +84,7 @@ export default function LoginForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter your email..."
|
placeholder={t("login.emailPlaceholder", "Enter your email...")}
|
||||||
type="email"
|
type="email"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -84,7 +100,7 @@ export default function LoginForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter your password..."
|
placeholder={t("login.passwordPlaceholder", "Enter your password...")}
|
||||||
type="password"
|
type="password"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -93,7 +109,7 @@ export default function LoginForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{verify.enable_login_verify && (
|
{captchaEnabled && isTurnstile && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="cf_token"
|
name="cf_token"
|
||||||
@ -111,6 +127,24 @@ export default function LoginForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{captchaEnabled && isLocal && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="captcha_code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<LocalCaptcha
|
||||||
|
{...field}
|
||||||
|
ref={localCaptcha}
|
||||||
|
onCaptchaIdChange={setCaptchaId}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button disabled={loading} type="submit">
|
<Button disabled={loading} type="submit">
|
||||||
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
||||||
{t("login.title", "Login")}
|
{t("login.title", "Login")}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { Input } from "@workspace/ui/components/input";
|
|||||||
import { Icon } from "@workspace/ui/composed/icon";
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
import { Markdown } from "@workspace/ui/composed/markdown";
|
import { Markdown } from "@workspace/ui/composed/markdown";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -19,6 +19,7 @@ import { useGlobalStore } from "@/stores/global";
|
|||||||
import SendCode from "../send-code";
|
import SendCode from "../send-code";
|
||||||
import type { TurnstileRef } from "../turnstile";
|
import type { TurnstileRef } from "../turnstile";
|
||||||
import CloudFlareTurnstile from "../turnstile";
|
import CloudFlareTurnstile from "../turnstile";
|
||||||
|
import LocalCaptcha, { type LocalCaptchaRef } from "../local-captcha";
|
||||||
|
|
||||||
export default function RegisterForm({
|
export default function RegisterForm({
|
||||||
loading,
|
loading,
|
||||||
@ -36,6 +37,11 @@ export default function RegisterForm({
|
|||||||
const { t } = useTranslation("auth");
|
const { t } = useTranslation("auth");
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify, auth, invite } = common;
|
const { verify, auth, invite } = common;
|
||||||
|
const [captchaId, setCaptchaId] = useState("");
|
||||||
|
|
||||||
|
const isTurnstile = verify.captcha_type === "turnstile";
|
||||||
|
const isLocal = verify.captcha_type === "local";
|
||||||
|
const captchaEnabled = verify.enable_user_register_captcha;
|
||||||
|
|
||||||
const handleCheckUser = async (email: string) => {
|
const handleCheckUser = async (email: string) => {
|
||||||
try {
|
try {
|
||||||
@ -67,9 +73,13 @@ export default function RegisterForm({
|
|||||||
code: auth.email.enable_verify ? z.string() : z.string().nullish(),
|
code: auth.email.enable_verify ? z.string() : z.string().nullish(),
|
||||||
invite: invite.forced_invite ? z.string().min(1) : z.string().nullish(),
|
invite: invite.forced_invite ? z.string().min(1) : z.string().nullish(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_register_verify && verify.turnstile_site_key
|
captchaEnabled && isTurnstile && verify.turnstile_site_key
|
||||||
? z.string()
|
? z.string()
|
||||||
: z.string().nullish(),
|
: z.string().nullish(),
|
||||||
|
captcha_code:
|
||||||
|
captchaEnabled && isLocal
|
||||||
|
? z.string().min(1, t("captcha.required", "Please enter captcha code"))
|
||||||
|
: z.string().nullish(),
|
||||||
})
|
})
|
||||||
.superRefine(({ password, repeat_password }, ctx) => {
|
.superRefine(({ password, repeat_password }, ctx) => {
|
||||||
if (password !== repeat_password) {
|
if (password !== repeat_password) {
|
||||||
@ -90,11 +100,17 @@ export default function RegisterForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const turnstile = useRef<TurnstileRef>(null);
|
const turnstile = useRef<TurnstileRef>(null);
|
||||||
|
const localCaptcha = useRef<LocalCaptchaRef>(null);
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
const handleSubmit = form.handleSubmit((data) => {
|
||||||
try {
|
try {
|
||||||
|
// Add captcha_id for local captcha
|
||||||
|
if (isLocal && captchaEnabled) {
|
||||||
|
(data as any).captcha_id = captchaId;
|
||||||
|
}
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
turnstile.current?.reset();
|
turnstile.current?.reset();
|
||||||
|
localCaptcha.current?.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,7 +130,7 @@ export default function RegisterForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter your email..."
|
placeholder={t("register.emailPlaceholder", "Enter your email...")}
|
||||||
type="email"
|
type="email"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -130,7 +146,7 @@ export default function RegisterForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter your password..."
|
placeholder={t("register.passwordPlaceholder", "Enter your password...")}
|
||||||
type="password"
|
type="password"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -147,7 +163,7 @@ export default function RegisterForm({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
placeholder="Enter password again..."
|
placeholder={t("register.repeatPasswordPlaceholder", "Enter password again...")}
|
||||||
type="password"
|
type="password"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -166,7 +182,7 @@ export default function RegisterForm({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
placeholder="Enter code..."
|
placeholder={t("register.codePlaceholder", "Enter code...")}
|
||||||
type="text"
|
type="text"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value as string}
|
value={field.value as string}
|
||||||
@ -205,7 +221,7 @@ export default function RegisterForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{verify.enable_register_verify && (
|
{captchaEnabled && isTurnstile && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="cf_token"
|
name="cf_token"
|
||||||
@ -223,6 +239,24 @@ export default function RegisterForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{captchaEnabled && isLocal && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="captcha_code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<LocalCaptcha
|
||||||
|
{...field}
|
||||||
|
ref={localCaptcha}
|
||||||
|
onCaptchaIdChange={setCaptchaId}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button disabled={loading} type="submit">
|
<Button disabled={loading} type="submit">
|
||||||
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
||||||
{t("register.title", "Register")}
|
{t("register.title", "Register")}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
import { Input } from "@workspace/ui/components/input";
|
import { Input } from "@workspace/ui/components/input";
|
||||||
import { Icon } from "@workspace/ui/composed/icon";
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -18,6 +18,7 @@ import { useGlobalStore } from "@/stores/global";
|
|||||||
import SendCode from "../send-code";
|
import SendCode from "../send-code";
|
||||||
import type { TurnstileRef } from "../turnstile";
|
import type { TurnstileRef } from "../turnstile";
|
||||||
import CloudFlareTurnstile from "../turnstile";
|
import CloudFlareTurnstile from "../turnstile";
|
||||||
|
import LocalCaptcha, { type LocalCaptchaRef } from "../local-captcha";
|
||||||
|
|
||||||
export default function ResetForm({
|
export default function ResetForm({
|
||||||
loading,
|
loading,
|
||||||
@ -36,6 +37,11 @@ export default function ResetForm({
|
|||||||
|
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify, auth } = common;
|
const { verify, auth } = common;
|
||||||
|
const [captchaId, setCaptchaId] = useState("");
|
||||||
|
|
||||||
|
const isTurnstile = verify.captcha_type === "turnstile";
|
||||||
|
const isLocal = verify.captcha_type === "local";
|
||||||
|
const captchaEnabled = verify.enable_user_reset_password_captcha;
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
email: z
|
email: z
|
||||||
@ -44,9 +50,13 @@ export default function ResetForm({
|
|||||||
password: z.string(),
|
password: z.string(),
|
||||||
code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
|
code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_register_verify && verify.turnstile_site_key
|
captchaEnabled && isTurnstile && verify.turnstile_site_key
|
||||||
? z.string()
|
? z.string()
|
||||||
: z.string().nullish(),
|
: z.string().nullish(),
|
||||||
|
captcha_code:
|
||||||
|
captchaEnabled && isLocal
|
||||||
|
? z.string().min(1, t("captcha.required", "Please enter captcha code"))
|
||||||
|
: z.string().nullish(),
|
||||||
});
|
});
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@ -54,11 +64,17 @@ export default function ResetForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const turnstile = useRef<TurnstileRef>(null);
|
const turnstile = useRef<TurnstileRef>(null);
|
||||||
|
const localCaptcha = useRef<LocalCaptchaRef>(null);
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
const handleSubmit = form.handleSubmit((data) => {
|
||||||
try {
|
try {
|
||||||
|
// Add captcha_id for local captcha
|
||||||
|
if (isLocal && captchaEnabled) {
|
||||||
|
(data as any).captcha_id = captchaId;
|
||||||
|
}
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
turnstile.current?.reset();
|
turnstile.current?.reset();
|
||||||
|
localCaptcha.current?.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,7 +89,7 @@ export default function ResetForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter your email..."
|
placeholder={t("reset.emailPlaceholder", "Enter your email...")}
|
||||||
type="email"
|
type="email"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -91,7 +107,7 @@ export default function ResetForm({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
placeholder="Enter code..."
|
placeholder={t("reset.codePlaceholder", "Enter code...")}
|
||||||
type="text"
|
type="text"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value as string}
|
value={field.value as string}
|
||||||
@ -116,7 +132,7 @@ export default function ResetForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter your new password..."
|
placeholder={t("reset.passwordPlaceholder", "Enter your new password...")}
|
||||||
type="password"
|
type="password"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -125,7 +141,7 @@ export default function ResetForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{verify.enable_reset_password_verify && (
|
{captchaEnabled && isTurnstile && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="cf_token"
|
name="cf_token"
|
||||||
@ -143,6 +159,24 @@ export default function ResetForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{captchaEnabled && isLocal && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="captcha_code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<LocalCaptcha
|
||||||
|
{...field}
|
||||||
|
ref={localCaptcha}
|
||||||
|
onCaptchaIdChange={setCaptchaId}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button disabled={loading} type="submit">
|
<Button disabled={loading} type="submit">
|
||||||
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
||||||
{t("reset.title", "Reset Password")}
|
{t("reset.title", "Reset Password")}
|
||||||
|
|||||||
95
apps/user/src/sections/auth/local-captcha.tsx
Normal file
95
apps/user/src/sections/auth/local-captcha.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { Button } from "@workspace/ui/components/button";
|
||||||
|
import { Input } from "@workspace/ui/components/input";
|
||||||
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
|
import { generateCaptcha } from "@workspace/ui/services/common/auth";
|
||||||
|
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export interface LocalCaptchaRef {
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocalCaptchaProps {
|
||||||
|
value?: string | null;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onCaptchaIdChange?: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocalCaptcha = forwardRef<LocalCaptchaRef, LocalCaptchaProps>(
|
||||||
|
({ value, onChange, onCaptchaIdChange }, ref) => {
|
||||||
|
const { t } = useTranslation("auth");
|
||||||
|
const [captchaImage, setCaptchaImage] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const fetchCaptcha = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await generateCaptcha();
|
||||||
|
const captchaData = res.data?.data;
|
||||||
|
if (captchaData) {
|
||||||
|
setCaptchaImage(captchaData.image);
|
||||||
|
onCaptchaIdChange?.(captchaData.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to generate captcha:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCaptcha();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
reset: () => {
|
||||||
|
onChange?.("");
|
||||||
|
fetchCaptcha();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder={t("captcha.placeholder", "Enter captcha code...")}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<div className="relative h-10 w-32 flex-shrink-0">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex h-full items-center justify-center bg-muted">
|
||||||
|
<Icon className="animate-spin" icon="mdi:loading" />
|
||||||
|
</div>
|
||||||
|
) : captchaImage ? (
|
||||||
|
<img
|
||||||
|
src={captchaImage}
|
||||||
|
alt="captcha"
|
||||||
|
className="h-full w-full cursor-pointer object-contain"
|
||||||
|
onClick={fetchCaptcha}
|
||||||
|
title={t("captcha.clickToRefresh", "Click to refresh")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center bg-muted text-xs text-muted-foreground">
|
||||||
|
{t("captcha.noImage", "No Image")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={fetchCaptcha}
|
||||||
|
disabled={loading}
|
||||||
|
title={t("captcha.refresh", "Refresh captcha")}
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:refresh" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
LocalCaptcha.displayName = "LocalCaptcha";
|
||||||
|
|
||||||
|
export default LocalCaptcha;
|
||||||
@ -19,6 +19,7 @@ import { useGlobalStore } from "@/stores/global";
|
|||||||
import SendCode from "../send-code";
|
import SendCode from "../send-code";
|
||||||
import type { TurnstileRef } from "../turnstile";
|
import type { TurnstileRef } from "../turnstile";
|
||||||
import CloudFlareTurnstile from "../turnstile";
|
import CloudFlareTurnstile from "../turnstile";
|
||||||
|
import LocalCaptcha, { type LocalCaptchaRef } from "../local-captcha";
|
||||||
|
|
||||||
export default function LoginForm({
|
export default function LoginForm({
|
||||||
loading,
|
loading,
|
||||||
@ -34,6 +35,11 @@ export default function LoginForm({
|
|||||||
const { t } = useTranslation("auth");
|
const { t } = useTranslation("auth");
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify } = common;
|
const { verify } = common;
|
||||||
|
const [captchaId, setCaptchaId] = useState("");
|
||||||
|
|
||||||
|
const isTurnstile = verify.captcha_type === "turnstile";
|
||||||
|
const isLocal = verify.captcha_type === "local";
|
||||||
|
const captchaEnabled = verify.enable_user_login_captcha;
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
telephone_area_code: z.string(),
|
telephone_area_code: z.string(),
|
||||||
@ -41,9 +47,13 @@ export default function LoginForm({
|
|||||||
telephone_code: z.string().optional(),
|
telephone_code: z.string().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_login_verify && verify.turnstile_site_key
|
captchaEnabled && isTurnstile && verify.turnstile_site_key
|
||||||
? z.string()
|
? z.string()
|
||||||
: z.string().optional(),
|
: z.string().optional(),
|
||||||
|
captcha_code:
|
||||||
|
captchaEnabled && isLocal
|
||||||
|
? z.string().min(1, t("captcha.required", "Please enter captcha code"))
|
||||||
|
: z.string().optional(),
|
||||||
});
|
});
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@ -53,11 +63,17 @@ export default function LoginForm({
|
|||||||
const [mode, setMode] = useState<"password" | "code">("password");
|
const [mode, setMode] = useState<"password" | "code">("password");
|
||||||
|
|
||||||
const turnstile = useRef<TurnstileRef>(null);
|
const turnstile = useRef<TurnstileRef>(null);
|
||||||
|
const localCaptcha = useRef<LocalCaptchaRef>(null);
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
const handleSubmit = form.handleSubmit((data) => {
|
||||||
try {
|
try {
|
||||||
|
// Add captcha_id for local captcha
|
||||||
|
if (isLocal && captchaEnabled) {
|
||||||
|
(data as any).captcha_id = captchaId;
|
||||||
|
}
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
turnstile.current?.reset();
|
turnstile.current?.reset();
|
||||||
|
localCaptcha.current?.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -88,7 +104,7 @@ export default function LoginForm({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="Area code..."
|
placeholder={t("register.areaCodePlaceholder", "Area code...")}
|
||||||
simple
|
simple
|
||||||
value={field.value}
|
value={field.value}
|
||||||
/>
|
/>
|
||||||
@ -99,7 +115,7 @@ export default function LoginForm({
|
|||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
className="rounded-l-none"
|
className="rounded-l-none"
|
||||||
placeholder="Enter your telephone..."
|
placeholder={t("register.telephonePlaceholder", "Enter your telephone...")}
|
||||||
type="tel"
|
type="tel"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -119,7 +135,9 @@ export default function LoginForm({
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder={
|
placeholder={
|
||||||
mode === "code" ? "Enter code..." : "Enter password..."
|
mode === "code"
|
||||||
|
? t("register.codePlaceholder", "Enter code...")
|
||||||
|
: t("login.passwordPlaceholder", "Enter your password...")
|
||||||
}
|
}
|
||||||
type={mode === "code" ? "text" : "password"}
|
type={mode === "code" ? "text" : "password"}
|
||||||
{...field}
|
{...field}
|
||||||
@ -157,7 +175,7 @@ export default function LoginForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{verify.enable_login_verify && (
|
{captchaEnabled && isTurnstile && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="cf_token"
|
name="cf_token"
|
||||||
@ -175,6 +193,24 @@ export default function LoginForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{captchaEnabled && isLocal && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="captcha_code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<LocalCaptcha
|
||||||
|
{...field}
|
||||||
|
ref={localCaptcha}
|
||||||
|
onCaptchaIdChange={setCaptchaId}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button disabled={loading} type="submit">
|
<Button disabled={loading} type="submit">
|
||||||
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
||||||
{t("login.title", "Login")}
|
{t("login.title", "Login")}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { AreaCodeSelect } from "@workspace/ui/composed/area-code-select";
|
|||||||
import { Icon } from "@workspace/ui/composed/icon";
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
import { Markdown } from "@workspace/ui/composed/markdown";
|
import { Markdown } from "@workspace/ui/composed/markdown";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -20,6 +20,7 @@ import { useGlobalStore } from "@/stores/global";
|
|||||||
import SendCode from "../send-code";
|
import SendCode from "../send-code";
|
||||||
import type { TurnstileRef } from "../turnstile";
|
import type { TurnstileRef } from "../turnstile";
|
||||||
import CloudFlareTurnstile from "../turnstile";
|
import CloudFlareTurnstile from "../turnstile";
|
||||||
|
import LocalCaptcha, { type LocalCaptchaRef } from "../local-captcha";
|
||||||
|
|
||||||
export default function RegisterForm({
|
export default function RegisterForm({
|
||||||
loading,
|
loading,
|
||||||
@ -36,6 +37,11 @@ export default function RegisterForm({
|
|||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify, auth, invite } = common;
|
const { verify, auth, invite } = common;
|
||||||
const { enable_whitelist, whitelist } = auth.mobile;
|
const { enable_whitelist, whitelist } = auth.mobile;
|
||||||
|
const [captchaId, setCaptchaId] = useState("");
|
||||||
|
|
||||||
|
const isTurnstile = verify.captcha_type === "turnstile";
|
||||||
|
const isLocal = verify.captcha_type === "local";
|
||||||
|
const captchaEnabled = verify.enable_user_register_captcha;
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
@ -46,9 +52,13 @@ export default function RegisterForm({
|
|||||||
code: z.string(),
|
code: z.string(),
|
||||||
invite: invite.forced_invite ? z.string().min(1) : z.string().nullish(),
|
invite: invite.forced_invite ? z.string().min(1) : z.string().nullish(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_register_verify && verify.turnstile_site_key
|
captchaEnabled && isTurnstile && verify.turnstile_site_key
|
||||||
? z.string()
|
? z.string()
|
||||||
: z.string().nullish(),
|
: z.string().nullish(),
|
||||||
|
captcha_code:
|
||||||
|
captchaEnabled && isLocal
|
||||||
|
? z.string().min(1, t("captcha.required", "Please enter captcha code"))
|
||||||
|
: z.string().nullish(),
|
||||||
})
|
})
|
||||||
.superRefine(({ password, repeat_password }, ctx) => {
|
.superRefine(({ password, repeat_password }, ctx) => {
|
||||||
if (password !== repeat_password) {
|
if (password !== repeat_password) {
|
||||||
@ -70,11 +80,17 @@ export default function RegisterForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const turnstile = useRef<TurnstileRef>(null);
|
const turnstile = useRef<TurnstileRef>(null);
|
||||||
|
const localCaptcha = useRef<LocalCaptchaRef>(null);
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
const handleSubmit = form.handleSubmit((data) => {
|
||||||
try {
|
try {
|
||||||
|
// Add captcha_id for local captcha
|
||||||
|
if (isLocal && captchaEnabled) {
|
||||||
|
(data as any).captcha_id = captchaId;
|
||||||
|
}
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
turnstile.current?.reset();
|
turnstile.current?.reset();
|
||||||
|
localCaptcha.current?.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -110,7 +126,7 @@ export default function RegisterForm({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="Area code..."
|
placeholder={t("register.areaCodePlaceholder", "Area code...")}
|
||||||
simple
|
simple
|
||||||
value={field.value}
|
value={field.value}
|
||||||
whitelist={enable_whitelist ? whitelist : []}
|
whitelist={enable_whitelist ? whitelist : []}
|
||||||
@ -122,7 +138,7 @@ export default function RegisterForm({
|
|||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
className="rounded-l-none"
|
className="rounded-l-none"
|
||||||
placeholder="Enter your telephone..."
|
placeholder={t("register.telephonePlaceholder", "Enter your telephone...")}
|
||||||
type="tel"
|
type="tel"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -139,7 +155,7 @@ export default function RegisterForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter your password..."
|
placeholder={t("register.passwordPlaceholder", "Enter your password...")}
|
||||||
type="password"
|
type="password"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -156,7 +172,7 @@ export default function RegisterForm({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
placeholder="Enter password again..."
|
placeholder={t("register.repeatPasswordPlaceholder", "Enter password again...")}
|
||||||
type="password"
|
type="password"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -174,7 +190,7 @@ export default function RegisterForm({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
placeholder="Enter code..."
|
placeholder={t("register.codePlaceholder", "Enter code...")}
|
||||||
type="text"
|
type="text"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value as string}
|
value={field.value as string}
|
||||||
@ -216,7 +232,7 @@ export default function RegisterForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{verify.enable_register_verify && (
|
{captchaEnabled && isTurnstile && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="cf_token"
|
name="cf_token"
|
||||||
@ -234,6 +250,24 @@ export default function RegisterForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{captchaEnabled && isLocal && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="captcha_code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<LocalCaptcha
|
||||||
|
{...field}
|
||||||
|
ref={localCaptcha}
|
||||||
|
onCaptchaIdChange={setCaptchaId}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button disabled={loading} type="submit">
|
<Button disabled={loading} type="submit">
|
||||||
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
||||||
{t("register.title", "Register")}
|
{t("register.title", "Register")}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { Input } from "@workspace/ui/components/input";
|
|||||||
import { AreaCodeSelect } from "@workspace/ui/composed/area-code-select";
|
import { AreaCodeSelect } from "@workspace/ui/composed/area-code-select";
|
||||||
import { Icon } from "@workspace/ui/composed/icon";
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -19,6 +19,7 @@ import { useGlobalStore } from "@/stores/global";
|
|||||||
import SendCode from "../send-code";
|
import SendCode from "../send-code";
|
||||||
import type { TurnstileRef } from "../turnstile";
|
import type { TurnstileRef } from "../turnstile";
|
||||||
import CloudFlareTurnstile from "../turnstile";
|
import CloudFlareTurnstile from "../turnstile";
|
||||||
|
import LocalCaptcha, { type LocalCaptchaRef } from "../local-captcha";
|
||||||
|
|
||||||
export default function ResetForm({
|
export default function ResetForm({
|
||||||
loading,
|
loading,
|
||||||
@ -36,6 +37,11 @@ export default function ResetForm({
|
|||||||
|
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { verify, auth } = common;
|
const { verify, auth } = common;
|
||||||
|
const [captchaId, setCaptchaId] = useState("");
|
||||||
|
|
||||||
|
const isTurnstile = verify.captcha_type === "turnstile";
|
||||||
|
const isLocal = verify.captcha_type === "local";
|
||||||
|
const captchaEnabled = verify.enable_user_reset_password_captcha;
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
telephone_area_code: z.string(),
|
telephone_area_code: z.string(),
|
||||||
@ -43,9 +49,13 @@ export default function ResetForm({
|
|||||||
password: z.string(),
|
password: z.string(),
|
||||||
code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
|
code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
|
||||||
cf_token:
|
cf_token:
|
||||||
verify.enable_register_verify && verify.turnstile_site_key
|
captchaEnabled && isTurnstile && verify.turnstile_site_key
|
||||||
? z.string()
|
? z.string()
|
||||||
: z.string().nullish(),
|
: z.string().nullish(),
|
||||||
|
captcha_code:
|
||||||
|
captchaEnabled && isLocal
|
||||||
|
? z.string().min(1, t("captcha.required", "Please enter captcha code"))
|
||||||
|
: z.string().nullish(),
|
||||||
});
|
});
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@ -53,11 +63,17 @@ export default function ResetForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const turnstile = useRef<TurnstileRef>(null);
|
const turnstile = useRef<TurnstileRef>(null);
|
||||||
|
const localCaptcha = useRef<LocalCaptchaRef>(null);
|
||||||
const handleSubmit = form.handleSubmit((data) => {
|
const handleSubmit = form.handleSubmit((data) => {
|
||||||
try {
|
try {
|
||||||
|
// Add captcha_id for local captcha
|
||||||
|
if (isLocal && captchaEnabled) {
|
||||||
|
(data as any).captcha_id = captchaId;
|
||||||
|
}
|
||||||
onSubmit(data);
|
onSubmit(data);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
turnstile.current?.reset();
|
turnstile.current?.reset();
|
||||||
|
localCaptcha.current?.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -88,7 +104,7 @@ export default function ResetForm({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="Area code..."
|
placeholder={t("register.areaCodePlaceholder", "Area code...")}
|
||||||
simple
|
simple
|
||||||
value={field.value}
|
value={field.value}
|
||||||
/>
|
/>
|
||||||
@ -99,7 +115,7 @@ export default function ResetForm({
|
|||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
className="rounded-l-none"
|
className="rounded-l-none"
|
||||||
placeholder="Enter your telephone..."
|
placeholder={t("register.telephonePlaceholder", "Enter your telephone...")}
|
||||||
type="tel"
|
type="tel"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -117,7 +133,7 @@ export default function ResetForm({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter code..."
|
placeholder={t("register.codePlaceholder", "Enter code...")}
|
||||||
type="text"
|
type="text"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value as string}
|
value={field.value as string}
|
||||||
@ -143,7 +159,7 @@ export default function ResetForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter your new password..."
|
placeholder={t("reset.passwordPlaceholder", "Enter your new password...")}
|
||||||
type="password"
|
type="password"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -152,7 +168,7 @@ export default function ResetForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{verify.enable_reset_password_verify && (
|
{captchaEnabled && isTurnstile && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="cf_token"
|
name="cf_token"
|
||||||
@ -170,6 +186,24 @@ export default function ResetForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{captchaEnabled && isLocal && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="captcha_code"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<LocalCaptcha
|
||||||
|
{...field}
|
||||||
|
ref={localCaptcha}
|
||||||
|
onCaptchaIdChange={setCaptchaId}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button disabled={loading} type="submit">
|
<Button disabled={loading} type="submit">
|
||||||
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
||||||
{t("reset.title", "Reset Password")}
|
{t("reset.title", "Reset Password")}
|
||||||
|
|||||||
@ -49,9 +49,14 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
|||||||
},
|
},
|
||||||
verify: {
|
verify: {
|
||||||
turnstile_site_key: "",
|
turnstile_site_key: "",
|
||||||
|
captcha_type: "turnstile",
|
||||||
enable_login_verify: false,
|
enable_login_verify: false,
|
||||||
enable_register_verify: false,
|
enable_register_verify: false,
|
||||||
enable_reset_password_verify: false,
|
enable_reset_password_verify: false,
|
||||||
|
enable_user_login_captcha: false,
|
||||||
|
enable_user_register_captcha: false,
|
||||||
|
enable_user_reset_password_captcha: false,
|
||||||
|
enable_admin_login_captcha: false,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
mobile: {
|
mobile: {
|
||||||
|
|||||||
9
bun.lock
9
bun.lock
@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 0,
|
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "shadcn-ui-monorepo",
|
"name": "shadcn-ui-monorepo",
|
||||||
@ -30,7 +29,7 @@
|
|||||||
"name": "ppanel-admin-web",
|
"name": "ppanel-admin-web",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^10.0.0",
|
"@faker-js/faker": "^10.0.0",
|
||||||
"@lottiefiles/dotlottie-react": "^0.17.7",
|
"@lottiefiles/dotlottie-react": "^0.17.15",
|
||||||
"@noble/curves": "^2.0.1",
|
"@noble/curves": "^2.0.1",
|
||||||
"@stripe/react-stripe-js": "^5.4.0",
|
"@stripe/react-stripe-js": "^5.4.0",
|
||||||
"@stripe/stripe-js": "^8.5.2",
|
"@stripe/stripe-js": "^8.5.2",
|
||||||
@ -70,7 +69,7 @@
|
|||||||
"name": "ppanel-user-web",
|
"name": "ppanel-user-web",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^10.0.0",
|
"@faker-js/faker": "^10.0.0",
|
||||||
"@lottiefiles/dotlottie-react": "^0.17.7",
|
"@lottiefiles/dotlottie-react": "^0.17.15",
|
||||||
"@stripe/react-stripe-js": "^5.4.0",
|
"@stripe/react-stripe-js": "^5.4.0",
|
||||||
"@stripe/stripe-js": "^8.5.2",
|
"@stripe/stripe-js": "^8.5.2",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
@ -548,9 +547,9 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.4" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.4" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||||
|
|
||||||
"@lottiefiles/dotlottie-react": ["@lottiefiles/dotlottie-react@0.17.7", "", { "dependencies": { "@lottiefiles/dotlottie-web": "0.56.0" }, "peerDependencies": { "react": "^17 || ^18 || ^19" } }, "sha512-A6wO3zqkDx/t0ULfctcr1Bmb1f1hc4zUV3NcbKQOsBGAOIx1vABV/fRabFYElvbJl9lmOR24yMh//Z0fvvJV+Q=="],
|
"@lottiefiles/dotlottie-react": ["@lottiefiles/dotlottie-react@0.17.15", "", { "dependencies": { "@lottiefiles/dotlottie-web": "0.63.0" }, "peerDependencies": { "react": "^17 || ^18 || ^19" } }, "sha512-4wYAjsJhM28eUvJ/gT3KRM6fcyT7EM9n7PDrP71LaBTacc6bSN43qFTSJc1Li3QxUiraz23p0Q8EJBzXo8DsRw=="],
|
||||||
|
|
||||||
"@lottiefiles/dotlottie-web": ["@lottiefiles/dotlottie-web@0.56.0", "", {}, "sha512-bWHRIGzjZs3Hjkz0JRsCMX2ya9a1tGU4atdrlfM3UoN0iamsDE64kSCMfGuchCwGAxg0xEh84CkF+SVV1NU9ow=="],
|
"@lottiefiles/dotlottie-web": ["@lottiefiles/dotlottie-web@0.63.0", "", {}, "sha512-oYIkvu6E4n8fZH7ciQsVqamlUDeBnd6JbNYa1UWC/npkNzEHqM5saL3vk/nNorqdfjYwdcdmhLtYbnuwVy+3/Q=="],
|
||||||
|
|
||||||
"@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="],
|
"@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="],
|
||||||
|
|
||||||
|
|||||||
17
packages/ui/src/services/admin/auth.ts
Normal file
17
packages/ui/src/services/admin/auth.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/* eslint-disable */
|
||||||
|
import request from "@workspace/ui/lib/request";
|
||||||
|
|
||||||
|
/** Generate captcha POST /v1/auth/admin/captcha/generate */
|
||||||
|
export async function adminGenerateCaptcha(options?: { [key: string]: any }) {
|
||||||
|
return request<API.Response & { data?: API.GenerateCaptchaResponse }>(
|
||||||
|
`${import.meta.env.VITE_API_PREFIX || ""}/v1/auth/admin/captcha/generate`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
...(options || {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
import * as ads from "./ads";
|
import * as ads from "./ads";
|
||||||
import * as announcement from "./announcement";
|
import * as announcement from "./announcement";
|
||||||
import * as application from "./application";
|
import * as application from "./application";
|
||||||
|
import * as auth from "./auth";
|
||||||
import * as authMethod from "./authMethod";
|
import * as authMethod from "./authMethod";
|
||||||
import * as console from "./console";
|
import * as console from "./console";
|
||||||
import * as coupon from "./coupon";
|
import * as coupon from "./coupon";
|
||||||
@ -24,6 +25,7 @@ export default {
|
|||||||
ads,
|
ads,
|
||||||
announcement,
|
announcement,
|
||||||
application,
|
application,
|
||||||
|
auth,
|
||||||
authMethod,
|
authMethod,
|
||||||
console,
|
console,
|
||||||
coupon,
|
coupon,
|
||||||
|
|||||||
@ -163,3 +163,17 @@ export async function telephoneResetPassword(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Generate captcha POST /v1/auth/captcha/generate */
|
||||||
|
export async function generateCaptcha(options?: { [key: string]: any }) {
|
||||||
|
return request<API.Response & { data?: API.GenerateCaptchaResponse }>(
|
||||||
|
`${import.meta.env.VITE_API_PREFIX || ""}/v1/auth/captcha/generate`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
...(options || {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
5
packages/ui/src/services/common/typings.d.ts
vendored
5
packages/ui/src/services/common/typings.d.ts
vendored
@ -1111,9 +1111,14 @@ declare namespace API {
|
|||||||
|
|
||||||
type VeifyConfig = {
|
type VeifyConfig = {
|
||||||
turnstile_site_key: string;
|
turnstile_site_key: string;
|
||||||
|
captcha_type: string;
|
||||||
enable_login_verify: boolean;
|
enable_login_verify: boolean;
|
||||||
enable_register_verify: boolean;
|
enable_register_verify: boolean;
|
||||||
enable_reset_password_verify: boolean;
|
enable_reset_password_verify: boolean;
|
||||||
|
enable_user_login_captcha: boolean;
|
||||||
|
enable_user_register_captcha: boolean;
|
||||||
|
enable_user_reset_password_captcha: boolean;
|
||||||
|
enable_admin_login_captcha: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VerifyCodeConfig = {
|
type VerifyCodeConfig = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user