✨ feat: Authentication verification code
This commit is contained in:
@@ -10,12 +10,13 @@ import {
|
||||
import { Input } from "@workspace/ui/components/input";
|
||||
import { Icon } from "@workspace/ui/composed/icon";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useRef } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
import CloudFlareTurnstile, { type TurnstileRef } from "../turnstile";
|
||||
import LocalCaptcha, { type LocalCaptchaRef } from "../local-captcha";
|
||||
|
||||
export default function LoginForm({
|
||||
loading,
|
||||
@@ -33,14 +34,25 @@ export default function LoginForm({
|
||||
const { t } = useTranslation("auth");
|
||||
const { common } = useGlobalStore();
|
||||
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({
|
||||
email: z.email(t("login.email", "Email")),
|
||||
email: z
|
||||
.string()
|
||||
.email(t("login.email", "Please enter a valid email address")),
|
||||
password: z.string(),
|
||||
cf_token:
|
||||
verify.enable_login_verify && verify.turnstile_site_key
|
||||
captchaEnabled && isTurnstile && verify.turnstile_site_key
|
||||
? z.string()
|
||||
: 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>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -48,11 +60,17 @@ export default function LoginForm({
|
||||
});
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const localCaptcha = useRef<LocalCaptchaRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
// Add captcha_id for local captcha
|
||||
if (isLocal && captchaEnabled) {
|
||||
(data as any).captcha_id = captchaId;
|
||||
}
|
||||
onSubmit(data);
|
||||
} catch (_error) {
|
||||
turnstile.current?.reset();
|
||||
localCaptcha.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -98,7 +116,7 @@ export default function LoginForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{verify.enable_login_verify && (
|
||||
{captchaEnabled && isTurnstile && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
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">
|
||||
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
||||
{t("login.title", "Login")}
|
||||
|
||||
@@ -10,13 +10,14 @@ import {
|
||||
import { Input } from "@workspace/ui/components/input";
|
||||
import { Icon } from "@workspace/ui/composed/icon";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useRef } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
import SendCode from "../send-code";
|
||||
import CloudFlareTurnstile, { type TurnstileRef } from "../turnstile";
|
||||
import LocalCaptcha, { type LocalCaptchaRef } from "../local-captcha";
|
||||
|
||||
export default function ResetForm({
|
||||
loading,
|
||||
@@ -35,15 +36,26 @@ export default function ResetForm({
|
||||
|
||||
const { common } = useGlobalStore();
|
||||
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({
|
||||
email: z.email(t("reset.email", "Email")),
|
||||
email: z
|
||||
.string()
|
||||
.email(t("reset.email", "Please enter a valid email address")),
|
||||
password: z.string(),
|
||||
code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
|
||||
cf_token:
|
||||
verify.enable_register_verify && verify.turnstile_site_key
|
||||
captchaEnabled && isTurnstile && verify.turnstile_site_key
|
||||
? z.string()
|
||||
: 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>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -51,11 +63,17 @@ export default function ResetForm({
|
||||
});
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const localCaptcha = useRef<LocalCaptchaRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
// Add captcha_id for local captcha
|
||||
if (isLocal && captchaEnabled) {
|
||||
(data as any).captcha_id = captchaId;
|
||||
}
|
||||
onSubmit(data);
|
||||
} catch (_error) {
|
||||
turnstile.current?.reset();
|
||||
localCaptcha.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -128,7 +146,7 @@ export default function ResetForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{verify.enable_reset_password_verify && (
|
||||
{captchaEnabled && isTurnstile && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
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">
|
||||
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
|
||||
{t("reset.title", "Reset Password")}
|
||||
|
||||
@@ -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,
|
||||
} from "@workspace/ui/components/form";
|
||||
import { ScrollArea } from "@workspace/ui/components/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@workspace/ui/components/select";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -33,11 +40,13 @@ import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const verifySchema = z.object({
|
||||
captcha_type: z.string().optional(),
|
||||
turnstile_site_key: z.string().optional(),
|
||||
turnstile_secret: z.string().optional(),
|
||||
enable_register_verify: z.boolean().optional(),
|
||||
enable_login_verify: z.boolean().optional(),
|
||||
enable_reset_password_verify: z.boolean().optional(),
|
||||
enable_user_login_captcha: z.boolean().optional(),
|
||||
enable_user_register_captcha: z.boolean().optional(),
|
||||
enable_admin_login_captcha: z.boolean().optional(),
|
||||
enable_user_reset_password_captcha: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type VerifyFormData = z.infer<typeof verifySchema>;
|
||||
@@ -59,11 +68,13 @@ export default function VerifyConfig() {
|
||||
const form = useForm<VerifyFormData>({
|
||||
resolver: zodResolver(verifySchema),
|
||||
defaultValues: {
|
||||
captcha_type: "local",
|
||||
turnstile_site_key: "",
|
||||
turnstile_secret: "",
|
||||
enable_register_verify: false,
|
||||
enable_login_verify: false,
|
||||
enable_reset_password_verify: false,
|
||||
enable_user_login_captcha: false,
|
||||
enable_user_register_captcha: false,
|
||||
enable_admin_login_captcha: false,
|
||||
enable_user_reset_password_captcha: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -126,26 +137,42 @@ export default function VerifyConfig() {
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="turnstile_site_key"
|
||||
name="captcha_type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("verify.turnstileSiteKey", "Turnstile Site Key")}
|
||||
{t("verify.captchaType", "Captcha Type")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t(
|
||||
"verify.turnstileSiteKeyPlaceholder",
|
||||
"Enter Turnstile site key"
|
||||
)}
|
||||
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>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"verify.turnstileSiteKeyDescription",
|
||||
"Cloudflare Turnstile site key for frontend verification"
|
||||
"verify.captchaTypeDescription",
|
||||
"Choose between local image captcha (offline) or Cloudflare Turnstile"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -153,45 +180,78 @@ export default function VerifyConfig() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
{form.watch("captcha_type") === "turnstile" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="turnstile_site_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("verify.turnstileSiteKey", "Turnstile Site Key")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t(
|
||||
"verify.turnstileSiteKeyPlaceholder",
|
||||
"Enter Turnstile site key"
|
||||
)}
|
||||
value={field.value}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"verify.turnstileSiteKeyDescription",
|
||||
"Cloudflare Turnstile site key for frontend verification"
|
||||
)}
|
||||
</FormDescription>
|
||||
<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
|
||||
control={form.control}
|
||||
name="enable_register_verify"
|
||||
name="enable_user_login_captcha"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"verify.enableRegisterVerify",
|
||||
"Enable Verification on Registration"
|
||||
"verify.enableUserLoginCaptcha",
|
||||
"Enable User Login Captcha"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
@@ -203,8 +263,8 @@ export default function VerifyConfig() {
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"verify.enableRegisterVerifyDescription",
|
||||
"When enabled, users must pass human verification during registration"
|
||||
"verify.enableUserLoginCaptchaDescription",
|
||||
"When enabled, users must pass captcha verification during login"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -214,13 +274,13 @@ export default function VerifyConfig() {
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enable_login_verify"
|
||||
name="enable_user_register_captcha"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"verify.enableLoginVerify",
|
||||
"Enable Verification on Login"
|
||||
"verify.enableUserRegisterCaptcha",
|
||||
"Enable User Registration Captcha"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
@@ -232,8 +292,8 @@ export default function VerifyConfig() {
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"verify.enableLoginVerifyDescription",
|
||||
"When enabled, users must pass human verification during login"
|
||||
"verify.enableUserRegisterCaptchaDescription",
|
||||
"When enabled, users must pass captcha verification during registration"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -243,13 +303,13 @@ export default function VerifyConfig() {
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enable_reset_password_verify"
|
||||
name="enable_user_reset_password_captcha"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"verify.enablePasswordVerify",
|
||||
"Enable Verification on Password Reset"
|
||||
"verify.enableUserResetPasswordCaptcha",
|
||||
"Enable User Password Reset Captcha"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
@@ -261,8 +321,37 @@ export default function VerifyConfig() {
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"verify.enablePasswordVerifyDescription",
|
||||
"When enabled, users must pass human verification during password reset"
|
||||
"verify.enableUserResetPasswordCaptchaDescription",
|
||||
"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>
|
||||
<FormMessage />
|
||||
|
||||
@@ -477,7 +477,12 @@ function PreviewNodesDialog({ userId }: { userId: number }) {
|
||||
{previewData.node_groups.map((group) => (
|
||||
<div key={group.id}>
|
||||
<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>
|
||||
{group.nodes && group.nodes.length > 0 ? (
|
||||
<table className="w-full text-sm">
|
||||
|
||||
@@ -49,9 +49,14 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
||||
},
|
||||
verify: {
|
||||
turnstile_site_key: "",
|
||||
captcha_type: "turnstile",
|
||||
enable_login_verify: false,
|
||||
enable_register_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: {
|
||||
mobile: {
|
||||
|
||||
Reference in New Issue
Block a user