-
-
CPU
-
{formatPercentage(cpu ?? 0)}
+ <>
+
+
+
+
+ {node && (
+
+
+
+ {t('nodeDetail')} - {node.name}
+
+
+
+
{t('nodeStatus')}
+
+
+
+ {isOnline ? t('normal') : t('abnormal')}
+
+
+ {t('onlineCount')}: {onlineCount}
+
+ {isOnline && (
+
+ {t('lastUpdated')}: {formatDate(updated_at)}
+
+ )}
+
+
+ {isOnline && (
+
+
+
+ CPU
+ {cpu?.toFixed(1)}%
+
+
+
+
+
+ {t('memory')}
+ {mem?.toFixed(1)}%
+
+
+
+
+
+ {t('disk')}
+ {disk?.toFixed(1)}%
+
+
+
+
+ )}
-
+ {isOnline && onlineCount > 0 && (
+
+
{t('onlineUsers')}
+
+
{
+ const ips = row.original.ips;
+ return (
+
+ {ips.map((ip: string, index: number) => (
+
+ {index === 0 ? (
+
+ ) : (
+
+ )}
+
+ ))}
+
+ );
+ },
+ },
+ {
+ accessorKey: 'user',
+ header: t('user'),
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: 'subscribeName',
+ header: t('subscribeName'),
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: 'subscribeId',
+ header: t('subscribeId'),
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: 'trafficUsage',
+ header: t('trafficUsage'),
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: 'expireTime',
+ header: t('expireTime'),
+ cell: ({ row }) => (
+
+ ),
+ },
+ ]}
+ request={async () => ({
+ list: onlineUsersData,
+ total: onlineUsersData.length,
+ })}
+ />
+
+
+ )}
-
-
- {t('memory')}
- {formatPercentage(mem ?? 0)}
-
-
-
-
-
- {t('disk')}
- {formatPercentage(disk ?? 0)}
-
-
-
- {isOnline && (
-
- {t('lastUpdated')}: {formatDate(updated_at ?? 0)}
-
- )}
-
-
- {isOnline && onlineCount > 0 && (
-
-
- {t('onlineUsers')}
- setOpenItem(value)}
- >
- {Object.entries(online).map(([uid, ips]) => (
-
- {`[UID: ${uid}] - ${ips[0]}`}
-
-
- {ips.map((ip: string) => (
- - {ip}
- ))}
-
-
-
-
- ))}
-
-
-
+
)}
-
-
+
+ >
);
}
diff --git a/apps/admin/app/dashboard/server/node-table.tsx b/apps/admin/app/dashboard/server/node-table.tsx
index 01290df..d21d6d0 100644
--- a/apps/admin/app/dashboard/server/node-table.tsx
+++ b/apps/admin/app/dashboard/server/node-table.tsx
@@ -139,7 +139,7 @@ export default function NodeTable() {
accessorKey: 'status',
header: t('status'),
cell: ({ row }) => {
- return
;
+ return
;
},
},
{
@@ -183,9 +183,6 @@ export default function NodeTable() {
},
]}
params={[
- {
- key: 'search',
- },
{
key: 'group_id',
placeholder: t('nodeGroup'),
@@ -194,6 +191,9 @@ export default function NodeTable() {
value: String(item.id),
})),
},
+ {
+ key: 'search',
+ },
]}
request={async (pagination, filter) => {
const { data } = await getNodeList({
diff --git a/apps/admin/app/dashboard/subscribe/subscribe-table.tsx b/apps/admin/app/dashboard/subscribe/subscribe-table.tsx
index 151435a..7f7d529 100644
--- a/apps/admin/app/dashboard/subscribe/subscribe-table.tsx
+++ b/apps/admin/app/dashboard/subscribe/subscribe-table.tsx
@@ -67,9 +67,6 @@ export default function SubscribeTable() {
),
}}
params={[
- {
- key: 'search',
- },
{
key: 'group_id',
placeholder: t('subscribeGroup'),
@@ -78,6 +75,9 @@ export default function SubscribeTable() {
value: String(item.id),
})),
},
+ {
+ key: 'search',
+ },
]}
request={async (pagination, filters) => {
const { data } = await getSubscribeList({
diff --git a/apps/admin/app/dashboard/system/basic-settings/currency-form.tsx b/apps/admin/app/dashboard/system/basic-settings/currency-form.tsx
new file mode 100644
index 0000000..ef84720
--- /dev/null
+++ b/apps/admin/app/dashboard/system/basic-settings/currency-form.tsx
@@ -0,0 +1,188 @@
+'use client';
+
+import { getCurrencyConfig, updateCurrencyConfig } from '@/services/admin/system';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useQuery } from '@tanstack/react-query';
+import { Button } from '@workspace/ui/components/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@workspace/ui/components/form';
+import { ScrollArea } from '@workspace/ui/components/scroll-area';
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@workspace/ui/components/sheet';
+
+import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
+import { Icon } from '@workspace/ui/custom-components/icon';
+import { useTranslations } from 'next-intl';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { z } from 'zod';
+
+// Constants
+const EXCHANGE_RATE_HOST_URL = 'https://exchangerate.host';
+
+const currencySchema = z.object({
+ access_key: z.string().optional(),
+ currency_unit: z.string().min(1),
+ currency_symbol: z.string().min(1),
+});
+
+type CurrencyFormData = z.infer
;
+
+export default function CurrencyConfig() {
+ const t = useTranslations('system');
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const { data, refetch } = useQuery({
+ queryKey: ['getCurrencyConfig'],
+ queryFn: async () => {
+ const { data } = await getCurrencyConfig();
+ return data.data;
+ },
+ enabled: open, // Only request data when the modal is open
+ });
+
+ const form = useForm({
+ resolver: zodResolver(currencySchema),
+ defaultValues: {
+ access_key: '',
+ currency_unit: 'USD',
+ currency_symbol: '$',
+ },
+ });
+
+ useEffect(() => {
+ if (data) {
+ form.reset(data);
+ }
+ }, [data, form]);
+
+ async function onSubmit(values: CurrencyFormData) {
+ setLoading(true);
+ try {
+ await updateCurrencyConfig(values as API.CurrencyConfig);
+ toast.success(t('common.saveSuccess'));
+ refetch();
+ setOpen(false);
+ } catch (error) {
+ toast.error(t('common.saveFailed'));
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{t('currency.title')}
+
{t('currency.description')}
+
+
+
+
+
+
+
+ {t('currency.title')}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/app/dashboard/system/basic-settings/privacy-policy-form.tsx b/apps/admin/app/dashboard/system/basic-settings/privacy-policy-form.tsx
new file mode 100644
index 0000000..4eb007f
--- /dev/null
+++ b/apps/admin/app/dashboard/system/basic-settings/privacy-policy-form.tsx
@@ -0,0 +1,139 @@
+'use client';
+
+import { getPrivacyPolicyConfig, updatePrivacyPolicyConfig } from '@/services/admin/system';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useQuery } from '@tanstack/react-query';
+import { Button } from '@workspace/ui/components/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@workspace/ui/components/form';
+import { ScrollArea } from '@workspace/ui/components/scroll-area';
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@workspace/ui/components/sheet';
+
+import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
+import { Icon } from '@workspace/ui/custom-components/icon';
+import { useTranslations } from 'next-intl';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { z } from 'zod';
+
+const privacyPolicySchema = z.object({
+ content: z.string().optional(),
+});
+
+type PrivacyPolicyFormData = z.infer;
+
+export default function PrivacyPolicyConfig() {
+ const t = useTranslations('system');
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const { data, refetch } = useQuery({
+ queryKey: ['getPrivacyPolicyConfig'],
+ queryFn: async () => {
+ const { data } = await getPrivacyPolicyConfig();
+ return data.data;
+ },
+ enabled: open,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(privacyPolicySchema),
+ defaultValues: {
+ content: '',
+ },
+ });
+
+ useEffect(() => {
+ if (data) {
+ form.reset({
+ content: data.content || '',
+ });
+ }
+ }, [data, form]);
+
+ async function onSubmit(values: PrivacyPolicyFormData) {
+ setLoading(true);
+ try {
+ await updatePrivacyPolicyConfig(values as API.PrivacyPolicyConfig);
+ toast.success(t('common.saveSuccess'));
+ refetch();
+ setOpen(false);
+ } catch (error) {
+ toast.error(t('common.saveFailed'));
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{t('privacyPolicy.title')}
+
{t('privacyPolicy.description')}
+
+
+
+
+
+
+
+ {t('privacyPolicy.title')}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/app/dashboard/system/basic-settings/site-form.tsx b/apps/admin/app/dashboard/system/basic-settings/site-form.tsx
new file mode 100644
index 0000000..eb4f8a5
--- /dev/null
+++ b/apps/admin/app/dashboard/system/basic-settings/site-form.tsx
@@ -0,0 +1,304 @@
+'use client';
+
+import { getSiteConfig, updateSiteConfig } from '@/services/admin/system';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useQuery } from '@tanstack/react-query';
+import { Button } from '@workspace/ui/components/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@workspace/ui/components/form';
+import { ScrollArea } from '@workspace/ui/components/scroll-area';
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@workspace/ui/components/sheet';
+
+import { Textarea } from '@workspace/ui/components/textarea';
+import { JSONEditor } from '@workspace/ui/custom-components/editor';
+import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
+import { Icon } from '@workspace/ui/custom-components/icon';
+import { UploadImage } from '@workspace/ui/custom-components/upload-image';
+import { useTranslations } from 'next-intl';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { z } from 'zod';
+
+const siteSchema = z.object({
+ site_logo: z.string().optional(),
+ site_name: z.string().min(1),
+ site_desc: z.string().optional(),
+ keywords: z.string().optional(),
+ custom_html: z.string().optional(),
+ host: z.string().optional(),
+ custom_data: z.any().optional(),
+});
+
+type SiteFormData = z.infer;
+
+export default function SiteConfig() {
+ const t = useTranslations('system');
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const { data, refetch } = useQuery({
+ queryKey: ['getSiteConfig'],
+ queryFn: async () => {
+ const { data } = await getSiteConfig();
+ return data.data;
+ },
+ enabled: open,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(siteSchema),
+ defaultValues: {
+ site_logo: '',
+ site_name: '',
+ site_desc: '',
+ keywords: '',
+ custom_html: '',
+ host: '',
+ custom_data: {},
+ },
+ });
+
+ useEffect(() => {
+ if (data) {
+ form.reset(data);
+ }
+ }, [data, form]);
+
+ async function onSubmit(values: SiteFormData) {
+ setLoading(true);
+ try {
+ await updateSiteConfig(values as API.SiteConfig);
+ toast.success(t('common.saveSuccess'));
+ refetch();
+ setOpen(false);
+ } catch (error) {
+ toast.error(t('common.saveFailed'));
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{t('site.title')}
+
{t('site.description')}
+
+
+
+
+
+
+
+ {t('site.title')}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/app/dashboard/system/basic-settings/tos-form.tsx b/apps/admin/app/dashboard/system/basic-settings/tos-form.tsx
new file mode 100644
index 0000000..3aa2207
--- /dev/null
+++ b/apps/admin/app/dashboard/system/basic-settings/tos-form.tsx
@@ -0,0 +1,134 @@
+'use client';
+
+import { getTosConfig, updateTosConfig } from '@/services/admin/system';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useQuery } from '@tanstack/react-query';
+import { Button } from '@workspace/ui/components/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@workspace/ui/components/form';
+import { ScrollArea } from '@workspace/ui/components/scroll-area';
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@workspace/ui/components/sheet';
+import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
+import { Icon } from '@workspace/ui/custom-components/icon';
+import { useTranslations } from 'next-intl';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { z } from 'zod';
+
+const tosSchema = z.object({
+ content: z.string().optional(),
+});
+
+type TosFormData = z.infer;
+
+export default function TosConfig() {
+ const t = useTranslations('system');
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const { data, refetch } = useQuery({
+ queryKey: ['getTosConfig'],
+ queryFn: async () => {
+ const { data } = await getTosConfig();
+ return data.data;
+ },
+ enabled: open,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(tosSchema),
+ defaultValues: {
+ content: '',
+ },
+ });
+
+ useEffect(() => {
+ if (data) {
+ form.reset({
+ content: data.content || '',
+ });
+ }
+ }, [data, form]);
+
+ async function onSubmit(values: TosFormData) {
+ setLoading(true);
+ try {
+ await updateTosConfig(values as API.TosConfig);
+ toast.success(t('common.saveSuccess'));
+ refetch();
+ setOpen(false);
+ } catch (error) {
+ toast.error(t('common.saveFailed'));
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{t('tos.title')}
+
{t('tos.description')}
+
+
+
+
+
+
+
+ {t('tos.title')}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/app/dashboard/system/currency.tsx b/apps/admin/app/dashboard/system/currency.tsx
deleted file mode 100644
index e5689cf..0000000
--- a/apps/admin/app/dashboard/system/currency.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-'use client';
-
-import { getCurrencyConfig, updateCurrencyConfig } from '@/services/admin/system';
-import { useQuery } from '@tanstack/react-query';
-import { Label } from '@workspace/ui/components/label';
-import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
-import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
-import { useTranslations } from 'next-intl';
-import { toast } from 'sonner';
-
-export default function Site() {
- const t = useTranslations('system.currency');
-
- const { data, refetch } = useQuery({
- queryKey: ['getCurrencyConfig'],
- queryFn: async () => {
- const { data } = await getCurrencyConfig();
- return data.data;
- },
- });
-
- async function updateConfig(key: string, value: unknown) {
- if (data?.[key] === value) return;
- try {
- await updateCurrencyConfig({
- ...data,
- [key]: value,
- } as API.CurrencyConfig);
- toast.success(t('saveSuccess'));
- refetch();
- } catch (error) {
- /* empty */
- }
- }
-
- return (
-
-
-
-
-
- {t('accessKeyDescription')}
-
-
- updateConfig('access_key', value)}
- />
-
-
-
-
-
- {t('currencyUnitDescription')}
-
-
- updateConfig('currency_unit', value)}
- />
-
-
-
-
-
- {t('currencySymbolDescription')}
-
-
- updateConfig('currency_symbol', value)}
- />
-
-
-
-
- );
-}
diff --git a/apps/admin/app/dashboard/system/page.tsx b/apps/admin/app/dashboard/system/page.tsx
index dc20de4..601a064 100644
--- a/apps/admin/app/dashboard/system/page.tsx
+++ b/apps/admin/app/dashboard/system/page.tsx
@@ -1,33 +1,62 @@
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
-import { getTranslations } from 'next-intl/server';
-import Currency from './currency';
-import PrivacyPolicy from './privacy-policy';
-import Site from './site';
-import Tos from './tos';
+'use client';
-export default async function Page() {
- const t = await getTranslations('system');
+import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
+import { useTranslations } from 'next-intl';
+import CurrencyForm from './basic-settings/currency-form';
+import PrivacyPolicyForm from './basic-settings/privacy-policy-form';
+import SiteForm from './basic-settings/site-form';
+import TosForm from './basic-settings/tos-form';
+import InviteForm from './user-security/invite-form';
+import RegisterForm from './user-security/register-form';
+import VerifyCodeForm from './user-security/verify-code-form';
+import VerifyForm from './user-security/verify-form';
+
+export default function Page() {
+ const t = useTranslations('system');
+
+ // 定义表单配置
+ const formSections = [
+ {
+ title: t('basicSettings'),
+ forms: [
+ { component: SiteForm },
+ { component: CurrencyForm },
+ { component: TosForm },
+ { component: PrivacyPolicyForm },
+ ],
+ },
+ {
+ title: t('userSecuritySettings'),
+ forms: [
+ { component: RegisterForm },
+ { component: InviteForm },
+ { component: VerifyForm },
+ { component: VerifyCodeForm },
+ ],
+ },
+ ];
return (
-
-
- {t('tabs.site')}
- {t('tabs.currency')}
- {t('tabs.tos')}
- {t('privacy-policy.title')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {formSections.map((section, sectionIndex) => (
+
+
{section.title}
+
+
+ {section.forms.map((form, formIndex) => {
+ const FormComponent = form.component;
+ return (
+
+
+
+
+
+ );
+ })}
+
+
+
+ ))}
+
);
}
diff --git a/apps/admin/app/dashboard/system/privacy-policy.tsx b/apps/admin/app/dashboard/system/privacy-policy.tsx
deleted file mode 100644
index 22ff7b7..0000000
--- a/apps/admin/app/dashboard/system/privacy-policy.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-'use client';
-
-import { getPrivacyPolicyConfig, updatePrivacyPolicyConfig } from '@/services/admin/system';
-import { useQuery } from '@tanstack/react-query';
-import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
-import { useTranslations } from 'next-intl';
-import { toast } from 'sonner';
-
-export default function PrivacyPolicy() {
- const t = useTranslations('system.privacy-policy');
- const { data, refetch, isFetched } = useQuery({
- queryKey: ['getPrivacyPolicyConfig'],
- queryFn: async () => {
- const { data } = await getPrivacyPolicyConfig();
- return data.data;
- },
- });
-
- async function updateConfig(key: string, value: unknown) {
- if (data?.[key] === value) return;
- try {
- await updatePrivacyPolicyConfig({
- ...data,
- [key]: value,
- } as API.PrivacyPolicyConfig);
- toast.success(t('saveSuccess'));
- refetch();
- } catch (error) {
- /* empty */
- }
- }
-
- return (
- isFetched && (
-
- {
- if (data?.privacy_policy !== value) {
- updateConfig('privacy_policy', value);
- }
- }}
- />
-
- )
- );
-}
diff --git a/apps/admin/app/dashboard/system/site.tsx b/apps/admin/app/dashboard/system/site.tsx
deleted file mode 100644
index bf9f6b7..0000000
--- a/apps/admin/app/dashboard/system/site.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-'use client';
-
-import { getSiteConfig, updateSiteConfig } from '@/services/admin/system';
-import { useQuery } from '@tanstack/react-query';
-import { Label } from '@workspace/ui/components/label';
-import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
-import { Textarea } from '@workspace/ui/components/textarea';
-import { JSONEditor } from '@workspace/ui/custom-components/editor';
-import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
-import { UploadImage } from '@workspace/ui/custom-components/upload-image';
-import { useTranslations } from 'next-intl';
-import { useRef } from 'react';
-import { toast } from 'sonner';
-
-export default function Site() {
- const t = useTranslations('system.site');
- const ref = useRef(undefined);
- const { data, refetch } = useQuery({
- queryKey: ['getSiteConfig'],
- queryFn: async () => {
- const { data } = await getSiteConfig();
- ref.current = data.data;
- return data.data;
- },
- });
-
- async function updateConfig(key: string, value: unknown) {
- if (data?.[key] === value) return;
- try {
- await updateSiteConfig({
- ...ref.current,
- [key]: value,
- } as API.SiteConfig);
- toast.success(t('saveSuccess'));
- refetch();
- } catch (error) {
- /* empty */
- }
- }
-
- return (
-
-
-
-
-
- {t('logoDescription')}
-
-
- updateConfig('site_logo', value)}
- suffix={
- {
- updateConfig('site_logo', value);
- }}
- />
- }
- />
-
-
-
-
-
- {t('siteNameDescription')}
-
-
- updateConfig('site_name', value)}
- />
-
-
-
-
-
- {t('siteDescDescription')}
-
-
- updateConfig('site_desc', value)}
- />
-
-
-
-
-
- {t('keywordsDescription')}
-
-
- updateConfig('keywords', value)}
- />
-
-
-
-
-
- {t('customHtmlDescription')}
-
-
-
-
-
-
-
- {t('siteDomainDescription')}
-
-
-
-
-
-
-
- {t('customDataDescription')}
-
-
- updateConfig('custom_data', value)}
- />
-
-
-
-
- );
-}
diff --git a/apps/admin/app/dashboard/system/tos.tsx b/apps/admin/app/dashboard/system/tos.tsx
deleted file mode 100644
index 8e33b76..0000000
--- a/apps/admin/app/dashboard/system/tos.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-'use client';
-
-import { getTosConfig, updateTosConfig } from '@/services/admin/system';
-import { useQuery } from '@tanstack/react-query';
-import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
-import { useTranslations } from 'next-intl';
-import { toast } from 'sonner';
-
-export default function Tos() {
- const t = useTranslations('system.tos');
- const { data, refetch, isFetched } = useQuery({
- queryKey: ['getTosConfig'],
- queryFn: async () => {
- const { data } = await getTosConfig();
-
- return data.data;
- },
- });
-
- async function updateConfig(key: string, value: unknown) {
- if (data?.[key] === value) return;
- try {
- await updateTosConfig({
- ...data,
- [key]: value,
- } as API.TosConfig);
- toast.success(t('saveSuccess'));
- refetch();
- } catch (error) {
- /* empty */
- }
- }
-
- return (
- isFetched && (
-
- {
- if (data?.tos_content !== value) {
- updateConfig('tos_content', value);
- }
- }}
- />
-
- )
- );
-}
diff --git a/apps/admin/app/dashboard/system/user-security/invite-form.tsx b/apps/admin/app/dashboard/system/user-security/invite-form.tsx
new file mode 100644
index 0000000..1db24ce
--- /dev/null
+++ b/apps/admin/app/dashboard/system/user-security/invite-form.tsx
@@ -0,0 +1,188 @@
+'use client';
+
+import { getInviteConfig, updateInviteConfig } from '@/services/admin/system';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useQuery } from '@tanstack/react-query';
+import { Button } from '@workspace/ui/components/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@workspace/ui/components/form';
+import { ScrollArea } from '@workspace/ui/components/scroll-area';
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@workspace/ui/components/sheet';
+import { Switch } from '@workspace/ui/components/switch';
+import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
+import { Icon } from '@workspace/ui/custom-components/icon';
+import { useTranslations } from 'next-intl';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { z } from 'zod';
+
+const inviteSchema = z.object({
+ forced_invite: z.boolean().optional(),
+ referral_percentage: z.number().optional(),
+ only_first_purchase: z.boolean().optional(),
+});
+
+type InviteFormData = z.infer;
+
+export default function InviteConfig() {
+ const t = useTranslations('system.invite');
+ const systemT = useTranslations('system');
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const { data, refetch } = useQuery({
+ queryKey: ['getInviteConfig'],
+ queryFn: async () => {
+ const { data } = await getInviteConfig();
+ return data.data;
+ },
+ enabled: open,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(inviteSchema),
+ defaultValues: {
+ forced_invite: false,
+ referral_percentage: 0,
+ only_first_purchase: false,
+ },
+ });
+
+ useEffect(() => {
+ if (data) {
+ form.reset(data);
+ }
+ }, [data, form]);
+
+ async function onSubmit(values: InviteFormData) {
+ setLoading(true);
+ try {
+ await updateInviteConfig(values as API.InviteConfig);
+ toast.success(t('saveSuccess'));
+ refetch();
+ setOpen(false);
+ } catch (error) {
+ toast.error(t('saveFailed'));
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{t('title')}
+
{t('description')}
+
+
+
+
+
+
+
+ {t('title')}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/app/dashboard/system/user-security/register-form.tsx b/apps/admin/app/dashboard/system/user-security/register-form.tsx
new file mode 100644
index 0000000..6306fbd
--- /dev/null
+++ b/apps/admin/app/dashboard/system/user-security/register-form.tsx
@@ -0,0 +1,304 @@
+'use client';
+
+import { getSubscribeList } from '@/services/admin/subscribe';
+import { getRegisterConfig, updateRegisterConfig } from '@/services/admin/system';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useQuery } from '@tanstack/react-query';
+import { Button } from '@workspace/ui/components/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@workspace/ui/components/form';
+import { ScrollArea } from '@workspace/ui/components/scroll-area';
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@workspace/ui/components/sheet';
+import { Switch } from '@workspace/ui/components/switch';
+import { Combobox } from '@workspace/ui/custom-components/combobox';
+import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
+import { Icon } from '@workspace/ui/custom-components/icon';
+import { useTranslations } from 'next-intl';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { z } from 'zod';
+
+const registerSchema = z.object({
+ stop_register: z.boolean().optional(),
+ enable_ip_register_limit: z.boolean().optional(),
+ ip_register_limit_count: z.number().optional(),
+ ip_register_limit_expire_day: z.number().optional(),
+ trial_flow: z.number().optional(),
+ trial_day: z.number().optional(),
+ default_subscribe_id: z.number().optional(),
+});
+
+type RegisterFormData = z.infer;
+
+export default function RegisterConfig() {
+ const t = useTranslations('system.register');
+ const systemT = useTranslations('system');
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const { data, refetch } = useQuery({
+ queryKey: ['getRegisterConfig'],
+ queryFn: async () => {
+ const { data } = await getRegisterConfig();
+ return data.data;
+ },
+ enabled: open,
+ });
+
+ const { data: subscribe } = useQuery({
+ queryKey: ['getSubscribeList', 'all'],
+ queryFn: async () => {
+ const { data } = await getSubscribeList({
+ page: 1,
+ size: 9999,
+ });
+ return data.data?.list as API.Subscribe[];
+ },
+ enabled: open,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(registerSchema),
+ defaultValues: {
+ stop_register: false,
+ enable_ip_register_limit: false,
+ ip_register_limit_count: 1,
+ ip_register_limit_expire_day: 1,
+ trial_flow: 0,
+ trial_day: 0,
+ default_subscribe_id: undefined,
+ },
+ });
+
+ useEffect(() => {
+ if (data) {
+ form.reset(data);
+ }
+ }, [data, form]);
+
+ async function onSubmit(values: RegisterFormData) {
+ setLoading(true);
+ try {
+ await updateRegisterConfig(values as API.RegisterConfig);
+ toast.success(t('saveSuccess'));
+ refetch();
+ setOpen(false);
+ } catch (error) {
+ toast.error(t('saveFailed'));
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{t('title')}
+
{t('description')}
+
+
+
+
+
+
+
+ {t('title')}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/app/dashboard/system/user-security/verify-code-form.tsx b/apps/admin/app/dashboard/system/user-security/verify-code-form.tsx
new file mode 100644
index 0000000..e19a3f4
--- /dev/null
+++ b/apps/admin/app/dashboard/system/user-security/verify-code-form.tsx
@@ -0,0 +1,192 @@
+'use client';
+
+import { getVerifyCodeConfig, updateVerifyCodeConfig } from '@/services/admin/system';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useQuery } from '@tanstack/react-query';
+import { Button } from '@workspace/ui/components/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@workspace/ui/components/form';
+import { ScrollArea } from '@workspace/ui/components/scroll-area';
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@workspace/ui/components/sheet';
+import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
+import { Icon } from '@workspace/ui/custom-components/icon';
+import { useTranslations } from 'next-intl';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { z } from 'zod';
+
+const verifyCodeSchema = z.object({
+ verify_code_expire_time: z.number().optional(),
+ verify_code_interval: z.number().optional(),
+ verify_code_limit: z.number().optional(),
+});
+
+type VerifyCodeFormData = z.infer;
+
+export default function VerifyCodeConfig() {
+ const t = useTranslations('system.verifyCode');
+ const systemT = useTranslations('system');
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const { data, refetch } = useQuery({
+ queryKey: ['getVerifyCodeConfig'],
+ queryFn: async () => {
+ const { data } = await getVerifyCodeConfig();
+ return data.data;
+ },
+ enabled: open,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(verifyCodeSchema),
+ defaultValues: {
+ verify_code_expire_time: 300,
+ verify_code_interval: 60,
+ verify_code_limit: 10,
+ },
+ });
+
+ useEffect(() => {
+ if (data) {
+ form.reset(data);
+ }
+ }, [data, form]);
+
+ async function onSubmit(values: VerifyCodeFormData) {
+ setLoading(true);
+ try {
+ await updateVerifyCodeConfig(values as API.VerifyCodeConfig);
+ toast.success(t('saveSuccess'));
+ refetch();
+ setOpen(false);
+ } catch (error) {
+ toast.error(t('saveFailed'));
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{t('title')}
+
{t('description')}
+
+
+
+
+
+
+
+ {t('title')}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/app/dashboard/system/user-security/verify-form.tsx b/apps/admin/app/dashboard/system/user-security/verify-form.tsx
new file mode 100644
index 0000000..ed25df9
--- /dev/null
+++ b/apps/admin/app/dashboard/system/user-security/verify-form.tsx
@@ -0,0 +1,227 @@
+'use client';
+
+import { getVerifyConfig, updateVerifyConfig } from '@/services/admin/system';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useQuery } from '@tanstack/react-query';
+import { Button } from '@workspace/ui/components/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@workspace/ui/components/form';
+import { ScrollArea } from '@workspace/ui/components/scroll-area';
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from '@workspace/ui/components/sheet';
+import { Switch } from '@workspace/ui/components/switch';
+import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
+import { Icon } from '@workspace/ui/custom-components/icon';
+import { useTranslations } from 'next-intl';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { z } from 'zod';
+
+const verifySchema = z.object({
+ turnstile_site_key: z.string().optional(),
+ turnstile_secret: z.string().optional(),
+ enable_register_verify: z.boolean().optional(),
+ enable_login_verify: z.boolean().optional(),
+ enable_password_verify: z.boolean().optional(),
+});
+
+type VerifyFormData = z.infer;
+
+export default function VerifyConfig() {
+ const t = useTranslations('system.verify');
+ const systemT = useTranslations('system');
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const { data, refetch } = useQuery({
+ queryKey: ['getVerifyConfig'],
+ queryFn: async () => {
+ const { data } = await getVerifyConfig();
+ return data.data;
+ },
+ enabled: open,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(verifySchema),
+ defaultValues: {
+ turnstile_site_key: '',
+ turnstile_secret: '',
+ enable_register_verify: false,
+ enable_login_verify: false,
+ enable_password_verify: false,
+ },
+ });
+
+ useEffect(() => {
+ if (data) {
+ form.reset(data);
+ }
+ }, [data, form]);
+
+ async function onSubmit(values: VerifyFormData) {
+ setLoading(true);
+ try {
+ await updateVerifyConfig(values as API.VerifyConfig);
+ toast.success(t('saveSuccess'));
+ refetch();
+ setOpen(false);
+ } catch (error) {
+ toast.error(t('saveFailed'));
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{t('title')}
+
{t('description')}
+
+
+
+
+
+
+
+ {t('title')}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/app/dashboard/user/[id]/user-login-history/index.tsx b/apps/admin/app/dashboard/user/[id]/user-login-history/index.tsx
index 3fe4f18..2d8d782 100644
--- a/apps/admin/app/dashboard/user/[id]/user-login-history/index.tsx
+++ b/apps/admin/app/dashboard/user/[id]/user-login-history/index.tsx
@@ -1,5 +1,6 @@
'use client';
+import { IpLink } from '@/components/ip-link';
import { ProTable } from '@/components/pro-table';
import { getUserLoginLogs } from '@/services/admin/user';
import { Badge } from '@workspace/ui/components/badge';
@@ -26,6 +27,7 @@ export default function UserLoginHistory() {
{
accessorKey: 'login_ip',
header: t('loginIp'),
+ cell: ({ row }) => ,
},
{
accessorKey: 'user_agent',
diff --git a/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx b/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx
index 06f112a..94996a0 100644
--- a/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx
+++ b/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx
@@ -1,6 +1,7 @@
'use client';
import { Display } from '@/components/display';
+import { IpLink } from '@/components/ip-link';
import { ProTable } from '@/components/pro-table';
import {
getUserSubscribeDevices,
@@ -64,6 +65,7 @@ export function SubscriptionDetail({
{
accessorKey: 'ip',
header: 'IP',
+ cell: ({ row }) => ,
},
{
accessorKey: 'user_agent',
@@ -156,6 +158,7 @@ export function SubscriptionDetail({
{
accessorKey: 'ip',
header: 'IP',
+ cell: ({ row }) => ,
},
{
accessorKey: 'online',
diff --git a/apps/admin/app/dashboard/user/user-detail.tsx b/apps/admin/app/dashboard/user/user-detail.tsx
index c092906..5d84bdb 100644
--- a/apps/admin/app/dashboard/user/user-detail.tsx
+++ b/apps/admin/app/dashboard/user/user-detail.tsx
@@ -8,7 +8,6 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from '@workspace/ui/com
import { formatBytes, formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
-import { useState } from 'react';
export function UserSubscribeDetail({ id, enabled }: { id: number; enabled: boolean }) {
const t = useTranslations('user');
@@ -118,10 +117,9 @@ export function UserSubscribeDetail({ id, enabled }: { id: number; enabled: bool
export function UserDetail({ id }: { id: number }) {
const t = useTranslations('user');
- const [shouldFetch, setShouldFetch] = useState(false);
const { data } = useQuery({
- enabled: id !== 0 && shouldFetch,
+ enabled: id !== 0,
queryKey: ['getUserDetail', id],
queryFn: async () => {
const { data } = await getUserDetail({ id });
@@ -133,7 +131,7 @@ export function UserDetail({ id }: { id: number }) {
return (
- setShouldFetch(true)}>
+