♻️ refactor(view): System and Auth Control

This commit is contained in:
web 2025-08-04 08:58:57 -07:00 committed by speakeloudest
parent 9ba3ec40d6
commit 97aa24ba8e
333 changed files with 13905 additions and 9399 deletions

View File

@ -1,37 +1,34 @@
<a name="readme-top"></a>
# Changelog
## [1.1.5](https://github.com/perfect-panel/ppanel-web/compare/v1.1.4...v1.1.5) (2025-07-26)
### 🐛 Bug Fixes
* **subscribe**: Filter out items that are not marked as visible in subscription list ([32253e3](https://github.com/perfect-panel/ppanel-web/commit/32253e3))
- **subscribe**: Filter out items that are not marked as visible in subscription list ([32253e3](https://github.com/perfect-panel/ppanel-web/commit/32253e3))
## [1.1.4](https://github.com/perfect-panel/ppanel-web/compare/v1.1.3...v1.1.4) (2025-07-25)
### 🐛 Bug Fixes
* **locales**: Simplify "show" label in subscription localization files ([d53a006](https://github.com/perfect-panel/ppanel-web/commit/d53a006))
* **order**: Preserve last successful order on error during order creation ([2fb98be](https://github.com/perfect-panel/ppanel-web/commit/2fb98be))
* **subscribe**: Filter out hidden items in subscription list display ([634be37](https://github.com/perfect-panel/ppanel-web/commit/634be37))
- **locales**: Simplify "show" label in subscription localization files ([d53a006](https://github.com/perfect-panel/ppanel-web/commit/d53a006))
- **order**: Preserve last successful order on error during order creation ([2fb98be](https://github.com/perfect-panel/ppanel-web/commit/2fb98be))
- **subscribe**: Filter out hidden items in subscription list display ([634be37](https://github.com/perfect-panel/ppanel-web/commit/634be37))
## [1.1.3](https://github.com/perfect-panel/ppanel-web/compare/v1.1.2...v1.1.3) (2025-07-24)
### 🐛 Bug Fixes
* **auth**: Implement user redirection to dashboard upon authentication ([f84f98c](https://github.com/perfect-panel/ppanel-web/commit/f84f98c))
- **auth**: Implement user redirection to dashboard upon authentication ([f84f98c](https://github.com/perfect-panel/ppanel-web/commit/f84f98c))
## [1.1.2](https://github.com/perfect-panel/ppanel-web/compare/v1.1.1...v1.1.2) (2025-07-24)
### 🐛 Bug Fixes
* **billing**: Add display for gift amount in subscription billing ([04af2f9](https://github.com/perfect-panel/ppanel-web/commit/04af2f9))
* **order**: Update subscription cell to display name and quantity ([96eba17](https://github.com/perfect-panel/ppanel-web/commit/96eba17))
* **tool**: Added API for obtaining version, updated version information display logic ([2675034](https://github.com/perfect-panel/ppanel-web/commit/2675034))
- **billing**: Add display for gift amount in subscription billing ([04af2f9](https://github.com/perfect-panel/ppanel-web/commit/04af2f9))
- **order**: Update subscription cell to display name and quantity ([96eba17](https://github.com/perfect-panel/ppanel-web/commit/96eba17))
- **tool**: Added API for obtaining version, updated version information display logic ([2675034](https://github.com/perfect-panel/ppanel-web/commit/2675034))
<a name="readme-top"></a>

View File

@ -46,9 +46,6 @@ export default function Page() {
),
}}
params={[
{
key: 'search',
},
{
key: 'status',
placeholder: t('status'),
@ -57,6 +54,9 @@ export default function Page() {
{ label: t('disabled'), value: '0' },
],
},
{
key: 'search',
},
]}
request={async (pagination, filters) => {
const { data } = await getAdsList({

View File

@ -81,9 +81,13 @@ export default function AnnouncementForm<T extends Record<string, any>>({
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100vh-48px-36px-36px-env(safe-area-inset-top))]'>
<ScrollArea className='-mx-6 h-[calc(100vh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-4 px-6 pt-4'>
<form
id='notice-form'
onSubmit={form.handleSubmit(handleSubmit)}
className='space-y-4 pt-4'
>
<FormField
control={form.control}
name='title'

View File

@ -1,148 +0,0 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Textarea } from '@workspace/ui/components/textarea';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function Page() {
const t = useTranslations('apple');
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'apple'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'apple',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try {
await updateAuthMethodConfig({
...data,
[key]: value,
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('teamId')}</Label>
<p className='text-muted-foreground text-xs'>{t('teamIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='ABCDE1FGHI'
value={data?.config?.team_id}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
team_id: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('keyId')}</Label>
<p className='text-muted-foreground text-xs'>{t('keyIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='ABC1234567'
value={data?.config?.key_id}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
key_id: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientId')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='com.your.app.service'
value={data?.config?.client_id}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_id: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('clientSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
className='h-20'
placeholder={`-----BEGIN PRIVATE KEY-----\nMIGTAgEA...\n-----END PRIVATE KEY-----`}
defaultValue={data?.config?.client_secret}
onBlur={(e) => {
updateConfig('config', {
...data?.config,
client_secret: e.target.value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('redirectUri')}</Label>
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='https://your-domain.com'
value={data?.config.redirect_url}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
redirect_url: value,
})
}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}

View File

@ -1,141 +0,0 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { DicesIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { uid } from 'radash';
import { toast } from 'sonner';
export default function Page() {
const t = useTranslations('device');
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'device'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'device',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try {
await updateAuthMethodConfig({
...data,
[key]: value,
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('showAds')}</Label>
<p className='text-muted-foreground text-xs'>{t('showAdsDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.config?.show_ads}
onCheckedChange={(checked) => {
updateConfig('config', {
...data?.config,
show_ads: checked,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('blockVirtualMachine')}</Label>
<p className='text-muted-foreground text-xs'>{t('blockVirtualMachineDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.config?.only_real_device}
onCheckedChange={(checked) => {
updateConfig('config', {
...data?.config,
only_real_device: checked,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('enableSecurity')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableSecurityDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.config?.enable_security}
onCheckedChange={(checked) => {
updateConfig('config', {
...data?.config,
enable_security: checked,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('communicationKey')}</Label>
<p className='text-muted-foreground text-xs'>{t('communicationKeyDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.security_secret}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
security_secret: value,
});
}}
suffix={
<div className='bg-muted flex h-9 items-center text-nowrap px-3'>
<DicesIcon
onClick={() => {
const id = uid(32).toLowerCase();
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
updateConfig('config', {
...data?.config,
security_secret: formatted,
});
}}
className='cursor-pointer'
/>
</div>
}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}

View File

@ -1,325 +0,0 @@
'use client';
import {
getAuthMethodConfig,
testEmailSend,
updateAuthMethodConfig,
} from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@workspace/ui/components/card';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { Textarea } from '@workspace/ui/components/textarea';
import { HTMLEditor } from '@workspace/ui/custom-components/editor';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
import { toast } from 'sonner';
import { LogsTable } from '../log';
export default function Page() {
const t = useTranslations('email');
const ref = useRef<Partial<API.AuthMethodConfig>>({});
const [email, setEmail] = useState<string>();
const { data, refetch, isFetching } = useQuery({
queryKey: ['getAuthMethodConfig', 'email'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'email',
});
ref.current = data.data as API.AuthMethodConfig;
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateAuthMethodConfig({
...ref.current,
[key]: value,
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Tabs defaultValue='settings'>
<TabsList className='h-full flex-wrap'>
<TabsTrigger value='settings'>{t('settings')}</TabsTrigger>
<TabsTrigger value='template'>{t('template')}</TabsTrigger>
<TabsTrigger value='logs'>{t('logs')}</TabsTrigger>
</TabsList>
<TabsContent value='settings'>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('emailVerification')}</Label>
<p className='text-muted-foreground text-xs'>{t('emailVerificationDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.config?.enable_verify}
onCheckedChange={(checked) =>
updateConfig('config', { ...data?.config, enable_verify: checked })
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('emailSuffixWhitelist')}</Label>
<p className='text-muted-foreground text-xs'>
{t('emailSuffixWhitelistDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.config?.enable_domain_suffix}
onCheckedChange={(checked) =>
updateConfig('config', { ...data?.config, enable_domain_suffix: checked })
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('whitelistSuffixes')}</Label>
<p className='text-muted-foreground text-xs'>{t('whitelistSuffixesDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
className='h-32'
placeholder={t('whitelistSuffixesPlaceholder')}
defaultValue={data?.config?.domain_suffix_list}
onBlur={(e) =>
updateConfig('config', { ...data?.config, domain_suffix_list: e.target.value })
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('smtpServerAddress')}</Label>
<p className='text-muted-foreground text-xs'>{t('smtpServerAddressDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.config?.platform_config?.host}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.platform_config,
host: value,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('smtpServerPort')}</Label>
<p className='text-muted-foreground text-xs'>{t('smtpServerPortDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.config?.platform_config?.port}
type='number'
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...ref.current?.config?.platform_config,
port: value,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('smtpEncryptionMethod')}</Label>
<p className='text-muted-foreground text-xs'>
{t('smtpEncryptionMethodDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.config?.platform_config?.ssl}
onCheckedChange={(checked) =>
updateConfig('config', {
...data?.config,
platform_config: {
...ref.current?.config?.platform_config,
ssl: checked,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('smtpAccount')}</Label>
<p className='text-muted-foreground text-xs'>{t('smtpAccountDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.config?.platform_config?.user}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...ref.current?.config?.platform_config,
user: value,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('smtpPassword')}</Label>
<p className='text-muted-foreground text-xs'>{t('smtpPasswordDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.config?.platform_config?.pass}
type='password'
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...ref.current?.config?.platform_config,
pass: value,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('senderAddress')}</Label>
<p className='text-muted-foreground text-xs'>{t('senderAddressDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.config?.platform_config?.from}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...ref.current?.config?.platform_config,
from: value,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('sendTestEmail')}</Label>
<p className='text-muted-foreground text-xs'>{t('sendTestEmailDescription')}</p>
</TableCell>
<TableCell className='flex items-center gap-2 text-right'>
<EnhancedInput
placeholder='test@example.com'
value={email}
type='email'
onValueChange={(value) => setEmail(value as string)}
/>
<Button
disabled={!email || isFetching}
onClick={async () => {
if (!email) return;
try {
await testEmailSend({ email });
toast.success(t('sendSuccess'));
} catch {
toast.error(t('sendFailure'));
}
}}
>
{t('sendTestEmail')}
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
<TabsContent value='template'>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
{[
'verify_email_template',
'expiration_email_template',
'maintenance_email_template',
'traffic_exceed_email_template',
].map((templateKey) => (
<Card key={templateKey}>
<CardHeader>
<CardTitle>{t(`${templateKey}`)}</CardTitle>
<CardDescription>
{t(`${templateKey}Description`, { after: '{{', before: '}}' })}
</CardDescription>
</CardHeader>
<CardContent>
<HTMLEditor
placeholder={t('inputPlaceholder')}
value={data?.config?.[templateKey] as string}
onBlur={(value) =>
updateConfig('config', {
...data?.config,
[templateKey]: value,
})
}
/>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value='logs'>
<LogsTable type='email' />
</TabsContent>
</Tabs>
);
}

View File

@ -1,92 +0,0 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
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 Page() {
const t = useTranslations('facebook');
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'facebook'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'facebook',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try {
await updateAuthMethodConfig({
...data,
[key]: value,
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientId')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='1234567890123456'
value={data?.config?.client_id}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_id: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('clientSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='1234567890abcdef1234567890abcdef'
value={data?.config?.client_secret}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_secret: value,
});
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}

View File

@ -0,0 +1,268 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
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 { Textarea } from '@workspace/ui/components/textarea';
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 appleSchema = z.object({
enabled: z.boolean(),
config: z
.object({
team_id: z.string().optional(),
key_id: z.string().optional(),
client_id: z.string().optional(),
client_secret: z.string().optional(),
redirect_url: z.string().optional(),
})
.optional(),
});
type AppleFormData = z.infer<typeof appleSchema>;
export default function AppleForm() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'apple'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'apple',
});
return data.data;
},
});
const form = useForm<AppleFormData>({
resolver: zodResolver(appleSchema),
defaultValues: {
enabled: false,
config: {
team_id: '',
key_id: '',
client_id: '',
client_secret: '',
redirect_url: '',
},
},
});
useEffect(() => {
if (data) {
form.reset({
enabled: data.enabled || false,
config: {
team_id: data.config?.team_id || '',
key_id: data.config?.key_id || '',
client_id: data.config?.client_id || '',
client_secret: data.config?.client_secret || '',
redirect_url: data.config?.redirect_url || '',
},
});
}
}, [data, form]);
async function onSubmit(values: AppleFormData) {
setLoading(true);
try {
await updateAuthMethodConfig({
...data,
enabled: values.enabled,
config: {
...data?.config,
...values.config,
},
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('common.saveSuccess'));
refetch();
setOpen(false);
} catch (error) {
toast.error(t('common.saveFailed'));
} finally {
setLoading(false);
}
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:apple' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('apple.title')}</p>
<p className='text-muted-foreground text-sm'>{t('apple.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('apple.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form id='apple-form' onSubmit={form.handleSubmit(onSubmit)} className='space-y-2 pt-4'>
<FormField
control={form.control}
name='enabled'
render={({ field }) => (
<FormItem>
<FormLabel>{t('apple.enable')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('apple.enableDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.team_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('apple.teamId')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='ABCDE1FGHI'
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('apple.teamIdDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.key_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('apple.keyId')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='ABC1234567'
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('apple.keyIdDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.client_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('apple.clientId')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='com.your.app.service'
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('apple.clientIdDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.client_secret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('apple.clientSecret')}</FormLabel>
<FormControl>
<Textarea
className='h-20'
placeholder={`-----BEGIN PRIVATE KEY-----\nMIGTAgEA...\n-----END PRIVATE KEY-----`}
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('apple.clientSecretDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.redirect_url'
render={({ field }) => (
<FormItem>
<FormLabel>{t('apple.redirectUri')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='https://your-domain.com'
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('apple.redirectUriDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='apple-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,253 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
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 { uid } from 'radash';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
const deviceSchema = z.object({
id: z.number(),
method: z.string().default('device'),
enabled: z.boolean().default(false),
config: z
.object({
show_ads: z.boolean().optional(),
only_real_device: z.boolean().optional(),
enable_security: z.boolean().optional(),
security_secret: z.string().optional(),
})
.optional(),
});
type DeviceFormData = z.infer<typeof deviceSchema>;
export default function DeviceForm() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'device'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'device',
});
return data.data;
},
enabled: open,
});
const form = useForm<DeviceFormData>({
resolver: zodResolver(deviceSchema),
defaultValues: {
id: 0,
method: 'device',
enabled: false,
config: {
show_ads: false,
only_real_device: false,
enable_security: false,
security_secret: '',
},
},
});
useEffect(() => {
if (data) {
form.reset(data);
}
}, [data, form]);
async function onSubmit(values: DeviceFormData) {
setLoading(true);
try {
await updateAuthMethodConfig(values as API.UpdateAuthMethodConfigRequest);
toast.success(t('common.saveSuccess'));
refetch();
setOpen(false);
} catch (error) {
toast.error(t('common.saveFailed'));
} finally {
setLoading(false);
}
}
function generateSecurityKey() {
const id = uid(32).toLowerCase();
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
form.setValue('config.security_secret', formatted);
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:devices' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('device.title')}</p>
<p className='text-muted-foreground text-sm'>{t('device.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('device.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='device-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='enabled'
render={({ field }) => (
<FormItem>
<FormLabel>{t('device.enable')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('device.enableDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.show_ads'
render={({ field }) => (
<FormItem>
<FormLabel>{t('device.showAds')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('device.showAdsDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.only_real_device'
render={({ field }) => (
<FormItem>
<FormLabel>{t('device.blockVirtualMachine')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('device.blockVirtualMachineDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.enable_security'
render={({ field }) => (
<FormItem>
<FormLabel>{t('device.enableSecurity')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('device.enableSecurityDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.security_secret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('device.communicationKey')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='e.g., 12345678-1234-1234-1234-123456789abc'
value={field.value}
onValueChange={field.onChange}
suffix={
<div className='bg-muted flex h-9 items-center text-nowrap px-3'>
<Icon
icon='mdi:dice-multiple'
onClick={generateSecurityKey}
className='size-4 cursor-pointer'
/>
</div>
}
/>
</FormControl>
<FormDescription>{t('device.communicationKeyDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='device-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,48 @@
'use client';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { LogsTable } from '../log';
export default function EmailLogsTable() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:email-newsletter' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('email.logs')}</p>
<p className='text-muted-foreground text-sm'>{t('email.logsDescription')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[800px] max-w-full md:max-w-screen-lg'>
<SheetHeader>
<SheetTitle>{t('email.logs')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<div className='px-6 pt-4'>
<LogsTable type='email' />
</div>
</ScrollArea>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,627 @@
'use client';
import {
getAuthMethodConfig,
testEmailSend,
updateAuthMethodConfig,
} from '@/services/admin/authMethod';
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { Textarea } from '@workspace/ui/components/textarea';
import { HTMLEditor } from '@workspace/ui/custom-components/editor';
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 emailSettingsSchema = z.object({
id: z.number(),
method: z.string().default('email'),
enabled: z.boolean().default(false),
config: z
.object({
enable_verify: z.boolean().default(false),
enable_domain_suffix: z.boolean().default(false),
domain_suffix_list: z.string().optional(),
verify_email_template: z.string().optional(),
expiration_email_template: z.string().optional(),
maintenance_email_template: z.string().optional(),
traffic_exceed_email_template: z.string().optional(),
platform: z.string().default('smtp'),
platform_config: z
.object({
host: z.string().optional(),
port: z.coerce.number().optional(),
ssl: z.boolean().default(false),
user: z.string().optional(),
pass: z.string().optional(),
from: z.string().optional(),
})
.optional(),
})
.optional(),
});
type EmailSettingsFormData = z.infer<typeof emailSettingsSchema>;
export default function EmailSettingsForm() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [testEmail, setTestEmail] = useState<string>();
const { data, refetch, isFetching } = useQuery({
queryKey: ['getAuthMethodConfig', 'email'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'email',
});
return data.data;
},
enabled: open,
});
const form = useForm<EmailSettingsFormData>({
resolver: zodResolver(emailSettingsSchema),
defaultValues: {
id: 0,
method: 'email',
enabled: false,
config: {
enable_verify: false,
enable_domain_suffix: false,
domain_suffix_list: '',
verify_email_template: '',
expiration_email_template: '',
maintenance_email_template: '',
traffic_exceed_email_template: '',
platform: 'smtp',
platform_config: {
host: '',
port: 587,
ssl: false,
user: '',
pass: '',
from: '',
},
},
},
});
useEffect(() => {
if (data) {
form.reset(data);
}
}, [data, form]);
async function onSubmit(values: EmailSettingsFormData) {
setLoading(true);
try {
await updateAuthMethodConfig({
...values,
config: {
...values.config,
platform: 'smtp',
},
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('common.saveSuccess'));
refetch();
setOpen(false);
} catch (error) {
toast.error(t('common.saveFailed'));
} finally {
setLoading(false);
}
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:email-outline' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('email.title')}</p>
<p className='text-muted-foreground text-sm'>{t('email.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('email.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='email-settings-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<Tabs defaultValue='basic' className='space-y-2'>
<TabsList className='flex h-full w-full flex-wrap *:flex-auto md:flex-nowrap'>
<TabsTrigger value='basic'>{t('email.basicSettings')}</TabsTrigger>
<TabsTrigger value='smtp'>{t('email.smtpSettings')}</TabsTrigger>
<TabsTrigger value='verify'>{t('email.verifyTemplate')}</TabsTrigger>
<TabsTrigger value='expiration'>{t('email.expirationTemplate')}</TabsTrigger>
<TabsTrigger value='maintenance'>{t('email.maintenanceTemplate')}</TabsTrigger>
<TabsTrigger value='traffic'>{t('email.trafficTemplate')}</TabsTrigger>
</TabsList>
<TabsContent value='basic' className='space-y-2'>
<FormField
control={form.control}
name='enabled'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.enable')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('email.enableDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.enable_verify'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.emailVerification')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('email.emailVerificationDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.enable_domain_suffix'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.emailSuffixWhitelist')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>
{t('email.emailSuffixWhitelistDescription')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.domain_suffix_list'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.whitelistSuffixes')}</FormLabel>
<FormControl>
<Textarea
className='h-32'
placeholder={t('email.whitelistSuffixesPlaceholder')}
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('email.whitelistSuffixesDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent value='smtp' className='space-y-2'>
<FormField
control={form.control}
name='config.platform_config.host'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.smtpServerAddress')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('email.inputPlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('email.smtpServerAddressDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.platform_config.port'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.smtpServerPort')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
placeholder='587'
value={field.value?.toString()}
onValueChange={(value) => field.onChange(Number(value))}
/>
</FormControl>
<FormDescription>{t('email.smtpServerPortDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.platform_config.ssl'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.smtpEncryptionMethod')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>
{t('email.smtpEncryptionMethodDescription')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.platform_config.user'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.smtpAccount')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('email.inputPlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('email.smtpAccountDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.platform_config.pass'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.smtpPassword')}</FormLabel>
<FormControl>
<EnhancedInput
type='password'
placeholder={t('email.inputPlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('email.smtpPasswordDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.platform_config.from'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.senderAddress')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('email.inputPlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('email.senderAddressDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className='space-y-2 border-t pt-4'>
<FormLabel>{t('email.sendTestEmail')}</FormLabel>
<div className='flex items-center gap-2'>
<EnhancedInput
placeholder='test@example.com'
type='email'
value={testEmail}
onValueChange={(value) => setTestEmail(value as string)}
/>
<Button
type='button'
disabled={!testEmail || isFetching}
onClick={async () => {
if (!testEmail) return;
try {
await testEmailSend({ email: testEmail });
toast.success(t('email.sendSuccess'));
} catch {
toast.error(t('email.sendFailure'));
}
}}
>
{t('email.sendTestEmail')}
</Button>
</div>
<p className='text-muted-foreground text-xs'>
{t('email.sendTestEmailDescription')}
</p>
</div>
</TabsContent>
<TabsContent value='verify' className='space-y-2'>
<FormField
control={form.control}
name='config.verify_email_template'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.verifyEmailTemplate')}</FormLabel>
<FormControl>
<HTMLEditor
placeholder={t('email.inputPlaceholder')}
value={field.value}
onBlur={field.onChange}
/>
</FormControl>
<div className='mt-4 space-y-2 border-t pt-4'>
<p className='text-muted-foreground text-sm font-medium'>
{t('email.templateVariables.title')}
</p>
<div className='text-muted-foreground space-y-2 text-xs'>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.Type}}'}
</code>
<span>{t('email.templateVariables.type.description')}</span>
</div>
<div className='pl-6 text-orange-600 dark:text-orange-400'>
💡 {t('email.templateVariables.type.conditionalSyntax')}
<br />
<code className='rounded bg-orange-50 px-1 text-xs dark:bg-orange-900/20'>
{'{{if eq .Type 1}}...{{else}}...{{end}}'}
</code>
</div>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.SiteLogo}}'}
</code>
<span>{t('email.templateVariables.siteLogo.description')}</span>
</div>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.SiteName}}'}
</code>
<span>{t('email.templateVariables.siteName.description')}</span>
</div>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.Expire}}'}
</code>
<span>{t('email.templateVariables.expire.description')}</span>
</div>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.Code}}'}
</code>
<span>{t('email.templateVariables.code.description')}</span>
</div>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent value='expiration' className='space-y-2'>
<FormField
control={form.control}
name='config.expiration_email_template'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.expirationEmailTemplate')}</FormLabel>
<FormControl>
<HTMLEditor
placeholder={t('email.inputPlaceholder')}
value={field.value}
onBlur={field.onChange}
/>
</FormControl>
<div className='mt-4 space-y-2 border-t pt-4'>
<p className='text-muted-foreground text-sm font-medium'>
{t('email.templateVariables.title')}
</p>
<div className='text-muted-foreground space-y-2 text-xs'>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.SiteLogo}}'}
</code>
<span>{t('email.templateVariables.siteLogo.description')}</span>
</div>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.SiteName}}'}
</code>
<span>{t('email.templateVariables.siteName.description')}</span>
</div>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.ExpireDate}}'}
</code>
<span>{t('email.templateVariables.expireDate.description')}</span>
</div>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent value='maintenance' className='space-y-2'>
<FormField
control={form.control}
name='config.maintenance_email_template'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.maintenanceEmailTemplate')}</FormLabel>
<FormControl>
<HTMLEditor
placeholder={t('email.inputPlaceholder')}
value={field.value}
onBlur={field.onChange}
/>
</FormControl>
<div className='mt-4 space-y-2 border-t pt-4'>
<p className='text-muted-foreground text-sm font-medium'>
{t('email.templateVariables.title')}
</p>
<div className='text-muted-foreground space-y-2 text-xs'>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.SiteLogo}}'}
</code>
<span>{t('email.templateVariables.siteLogo.description')}</span>
</div>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.SiteName}}'}
</code>
<span>{t('email.templateVariables.siteName.description')}</span>
</div>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.MaintenanceDate}}'}
</code>
<span>
{t('email.templateVariables.maintenanceDate.description')}
</span>
</div>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.MaintenanceTime}}'}
</code>
<span>
{t('email.templateVariables.maintenanceTime.description')}
</span>
</div>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent value='traffic' className='space-y-2'>
<FormField
control={form.control}
name='config.traffic_exceed_email_template'
render={({ field }) => (
<FormItem>
<FormLabel>{t('email.trafficExceedEmailTemplate')}</FormLabel>
<FormControl>
<HTMLEditor
placeholder={t('email.inputPlaceholder')}
value={field.value}
onBlur={field.onChange}
/>
</FormControl>
<div className='mt-4 space-y-2 border-t pt-4'>
<p className='text-muted-foreground text-sm font-medium'>
{t('email.templateVariables.title')}
</p>
<div className='text-muted-foreground space-y-2 text-xs'>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.SiteLogo}}'}
</code>
<span>{t('email.templateVariables.siteLogo.description')}</span>
</div>
<div className='flex items-center gap-2'>
<code className='bg-muted text-foreground rounded px-1.5 py-0.5 font-mono'>
{'{{.SiteName}}'}
</code>
<span>{t('email.templateVariables.siteName.description')}</span>
</div>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
</Tabs>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='email-settings-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,198 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
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 facebookSchema = z.object({
enabled: z.boolean(),
client_id: z.string().optional(),
client_secret: z.string().optional(),
});
type FacebookFormData = z.infer<typeof facebookSchema>;
export default function FacebookForm() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch, isFetching } = useQuery({
queryKey: ['getAuthMethodConfig', 'facebook'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'facebook',
});
return data.data;
},
// 移除 enabled: open现在默认加载数据
});
const form = useForm<FacebookFormData>({
resolver: zodResolver(facebookSchema),
defaultValues: {
enabled: false,
client_id: '',
client_secret: '',
},
});
useEffect(() => {
if (data) {
form.reset({
enabled: data.enabled || false,
client_id: data.config?.client_id || '',
client_secret: data.config?.client_secret || '',
});
}
}, [data, form]);
async function onSubmit(values: FacebookFormData) {
setLoading(true);
try {
await updateAuthMethodConfig({
...data,
enabled: values.enabled,
config: {
...data?.config,
client_id: values.client_id,
client_secret: values.client_secret,
},
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('common.saveSuccess'));
refetch();
setOpen(false);
} catch (error) {
toast.error(t('common.saveFailed'));
} finally {
setLoading(false);
}
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:facebook' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('facebook.title')}</p>
<p className='text-muted-foreground text-sm'>{t('facebook.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('facebook.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='facebook-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='enabled'
render={({ field }) => (
<FormItem>
<FormLabel>{t('facebook.enable')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('facebook.enableDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='client_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('facebook.clientId')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='1234567890123456'
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('facebook.clientIdDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='client_secret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('facebook.clientSecret')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='1234567890abcdef1234567890abcdef'
value={field.value}
onValueChange={field.onChange}
type='password'
/>
</FormControl>
<FormDescription>{t('facebook.clientSecretDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='facebook-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,198 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
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 githubSchema = z.object({
enabled: z.boolean(),
client_id: z.string().optional(),
client_secret: z.string().optional(),
});
type GithubFormData = z.infer<typeof githubSchema>;
export default function GithubForm() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'github'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'github',
});
return data.data;
},
});
const form = useForm<GithubFormData>({
resolver: zodResolver(githubSchema),
defaultValues: {
enabled: false,
client_id: '',
client_secret: '',
},
});
useEffect(() => {
if (data) {
form.reset({
enabled: data.enabled || false,
client_id: data.config?.client_id || '',
client_secret: data.config?.client_secret || '',
});
}
}, [data, form]);
async function onSubmit(values: GithubFormData) {
setLoading(true);
try {
await updateAuthMethodConfig({
...data,
enabled: values.enabled,
config: {
...data?.config,
client_id: values.client_id,
client_secret: values.client_secret,
},
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('common.saveSuccess'));
refetch();
setOpen(false);
} catch (error) {
toast.error(t('common.saveFailed'));
} finally {
setLoading(false);
}
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:github' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('github.title')}</p>
<p className='text-muted-foreground text-sm'>{t('github.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('github.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='github-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='enabled'
render={({ field }) => (
<FormItem>
<FormLabel>{t('github.enable')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('github.enableDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='client_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('github.clientId')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='e.g., Iv1.1234567890abcdef'
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('github.clientIdDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='client_secret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('github.clientSecret')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='e.g., 1234567890abcdef1234567890abcdef12345678'
value={field.value}
onValueChange={field.onChange}
type='password'
/>
</FormControl>
<FormDescription>{t('github.clientSecretDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='github-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,196 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
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 googleSchema = z.object({
id: z.number(),
method: z.string().default('google'),
enabled: z.boolean().default(false),
config: z
.object({
client_id: z.string().optional(),
client_secret: z.string().optional(),
})
.optional(),
});
type GoogleFormData = z.infer<typeof googleSchema>;
export default function GoogleForm() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch, isFetching } = useQuery({
queryKey: ['getAuthMethodConfig', 'google'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'google',
});
return data.data;
},
enabled: open,
});
const form = useForm<GoogleFormData>({
resolver: zodResolver(googleSchema),
defaultValues: {
id: 0,
method: 'google',
enabled: false,
config: {
client_id: '',
client_secret: '',
},
},
});
useEffect(() => {
if (data) {
form.reset(data);
}
}, [data, form]);
async function onSubmit(values: GoogleFormData) {
setLoading(true);
try {
await updateAuthMethodConfig(values as API.UpdateAuthMethodConfigRequest);
toast.success(t('common.saveSuccess'));
refetch();
setOpen(false);
} catch (error) {
toast.error(t('common.saveFailed'));
} finally {
setLoading(false);
}
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:google' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('google.title')}</p>
<p className='text-muted-foreground text-sm'>{t('google.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('google.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='google-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='enabled'
render={({ field }) => (
<FormItem>
<FormLabel>{t('google.enable')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('google.enableDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.client_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('google.clientId')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='123456789-abc123def456.apps.googleusercontent.com'
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('google.clientIdDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.client_secret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('google.clientSecret')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
value={field.value}
onValueChange={field.onChange}
type='password'
/>
</FormControl>
<FormDescription>{t('google.clientSecretDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='google-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,48 @@
'use client';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { LogsTable } from '../log';
export default function PhoneLogsTable() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:phone-log' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('phone.logs')}</p>
<p className='text-muted-foreground text-sm'>{t('phone.logsDescription')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[800px] max-w-full md:max-w-screen-lg'>
<SheetHeader>
<SheetTitle>{t('phone.logs')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<div className='px-6 pt-4'>
<LogsTable type='mobile' />
</div>
</ScrollArea>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,505 @@
'use client';
import {
getAuthMethodConfig,
getSmsPlatform,
testSmsSend,
updateAuthMethodConfig,
} from '@/services/admin/authMethod';
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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/select';
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { Switch } from '@workspace/ui/components/switch';
import { Textarea } from '@workspace/ui/components/textarea';
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import TagInput from '@workspace/ui/custom-components/tag-input';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
const phoneSettingsSchema = z.object({
id: z.number(),
method: z.string().default('mobile'),
enabled: z.boolean().default(false),
config: z
.object({
enable_whitelist: z.boolean().optional(),
whitelist: z.array(z.string()).optional(),
platform: z.string().optional(),
platform_config: z
.object({
access: z.string().optional(),
endpoint: z.string().optional(),
secret: z.string().optional(),
template_code: z.string().optional(),
sign_name: z.string().optional(),
phone_number: z.string().optional(),
template: z.string().optional(),
})
.optional(),
})
.optional(),
});
type PhoneSettingsFormData = z.infer<typeof phoneSettingsSchema>;
export default function PhoneSettingsForm() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [testParams, setTestParams] = useState<API.TestSmsSendRequest>({
telephone: '',
area_code: '1',
});
const { data, refetch, isFetching } = useQuery({
queryKey: ['getAuthMethodConfig', 'mobile'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'mobile',
});
return data.data;
},
enabled: open,
});
const { data: platforms } = useQuery({
queryKey: ['getSmsPlatform'],
queryFn: async () => {
const { data } = await getSmsPlatform();
return data.data?.list;
},
enabled: open,
});
const form = useForm<PhoneSettingsFormData>({
resolver: zodResolver(phoneSettingsSchema),
defaultValues: {
id: 0,
method: 'mobile',
enabled: false,
config: {
enable_whitelist: false,
whitelist: [],
platform: '',
platform_config: {
access: '',
endpoint: '',
secret: '',
template_code: 'code',
sign_name: '',
phone_number: '',
template: '',
},
},
},
});
const selectedPlatform = platforms?.find(
(platform) => platform.platform === form.watch('config.platform'),
);
const { platform_url, platform_field_description: platformConfig } = selectedPlatform ?? {};
useEffect(() => {
if (data) {
form.reset(data);
}
}, [data, form]);
async function onSubmit(values: PhoneSettingsFormData) {
setLoading(true);
try {
await updateAuthMethodConfig(values as API.UpdateAuthMethodConfigRequest);
toast.success(t('common.saveSuccess'));
refetch();
setOpen(false);
} catch (error) {
toast.error(t('common.saveFailed'));
} finally {
setLoading(false);
}
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:phone-settings' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('phone.title')}</p>
<p className='text-muted-foreground text-sm'>{t('phone.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('phone.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='phone-settings-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='enabled'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.enable')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={isFetching}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('phone.enableTip')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.enable_whitelist'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.whitelistValidation')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('phone.whitelistValidationTip')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.whitelist'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.whitelistAreaCode')}</FormLabel>
<FormControl>
<TagInput
placeholder='1, 852, 886, 888'
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('phone.whitelistAreaCodeTip')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.platform'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.platform')}</FormLabel>
<div className='flex items-center gap-1'>
<FormControl>
<Select
value={field.value}
onValueChange={field.onChange}
disabled={isFetching}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{platforms?.map((item) => (
<SelectItem key={item.platform} value={item.platform}>
{item.platform}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
{platform_url && (
<Button size='sm' asChild>
<Link href={platform_url} target='_blank'>
{t('phone.applyPlatform')}
</Link>
</Button>
)}
</div>
<FormDescription>{t('phone.platformTip')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.platform_config.access'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.accessLabel')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={field.onChange}
disabled={isFetching}
placeholder={t('phone.platformConfigTip', { key: platformConfig?.access })}
/>
</FormControl>
<FormDescription>
{t('phone.platformConfigTip', { key: platformConfig?.access })}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{platformConfig?.endpoint && (
<FormField
control={form.control}
name='config.platform_config.endpoint'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.endpointLabel')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={field.onChange}
disabled={isFetching}
placeholder={t('phone.platformConfigTip', {
key: platformConfig?.endpoint,
})}
/>
</FormControl>
<FormDescription>
{t('phone.platformConfigTip', { key: platformConfig?.endpoint })}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name='config.platform_config.secret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.secretLabel')}</FormLabel>
<FormControl>
<EnhancedInput
type='password'
value={field.value}
onValueChange={field.onChange}
disabled={isFetching}
placeholder={t('phone.platformConfigTip', { key: platformConfig?.secret })}
/>
</FormControl>
<FormDescription>
{t('phone.platformConfigTip', { key: platformConfig?.secret })}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{platformConfig?.template_code && (
<FormField
control={form.control}
name='config.platform_config.template_code'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.templateCodeLabel')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={field.onChange}
disabled={isFetching}
placeholder={t('phone.platformConfigTip', {
key: platformConfig?.template_code,
})}
/>
</FormControl>
<FormDescription>
{t('phone.platformConfigTip', { key: platformConfig?.template_code })}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{platformConfig?.sign_name && (
<FormField
control={form.control}
name='config.platform_config.sign_name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.signNameLabel')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={field.onChange}
disabled={isFetching}
placeholder={t('phone.platformConfigTip', {
key: platformConfig?.sign_name,
})}
/>
</FormControl>
<FormDescription>
{t('phone.platformConfigTip', { key: platformConfig?.sign_name })}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{platformConfig?.phone_number && (
<FormField
control={form.control}
name='config.platform_config.phone_number'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.phoneNumberLabel')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={field.onChange}
disabled={isFetching}
placeholder={t('phone.platformConfigTip', {
key: platformConfig?.phone_number,
})}
/>
</FormControl>
<FormDescription>
{t('phone.platformConfigTip', { key: platformConfig?.phone_number })}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{platformConfig?.code_variable && (
<FormField
control={form.control}
name='config.platform_config.template'
render={({ field }) => (
<FormItem>
<FormLabel>{t('phone.template')}</FormLabel>
<FormControl>
<Textarea
value={field.value}
onChange={field.onChange}
disabled={isFetching}
placeholder={t('phone.placeholders.template', {
code: platformConfig?.code_variable,
})}
/>
</FormControl>
<FormDescription>
{t('phone.templateTip', { code: platformConfig?.code_variable })}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<div className='space-y-4 border-t pt-4'>
<div>
<FormLabel>{t('phone.testSms')}</FormLabel>
<p className='text-muted-foreground mb-3 text-sm'>{t('phone.testSmsTip')}</p>
<div className='flex items-center gap-2'>
<AreaCodeSelect
value={testParams.area_code}
onChange={(value) => {
if (value.phone) {
setTestParams((prev) => ({ ...prev, area_code: value.phone! }));
}
}}
/>
<EnhancedInput
placeholder={t('phone.testSmsPhone')}
value={testParams.telephone}
onValueChange={(value) => {
setTestParams((prev) => ({ ...prev, telephone: value as string }));
}}
/>
<Button
type='button'
disabled={!testParams.telephone || !testParams.area_code || isFetching}
onClick={async () => {
if (isFetching || !testParams.telephone || !testParams.area_code) return;
try {
await testSmsSend(testParams);
toast.success(t('phone.sendSuccess'));
} catch {
toast.error(t('phone.sendFailed'));
}
}}
>
{t('phone.testSms')}
</Button>
</div>
</div>
</div>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='phone-settings-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,198 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
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 telegramSchema = z.object({
enabled: z.boolean(),
bot: z.string().optional(),
bot_token: z.string().optional(),
});
type TelegramFormData = z.infer<typeof telegramSchema>;
export default function TelegramForm() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'telegram'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'telegram',
});
return data.data;
},
});
const form = useForm<TelegramFormData>({
resolver: zodResolver(telegramSchema),
defaultValues: {
enabled: false,
bot: '',
bot_token: '',
},
});
useEffect(() => {
if (data) {
form.reset({
enabled: data.enabled || false,
bot: data.config?.bot || '',
bot_token: data.config?.bot_token || '',
});
}
}, [data, form]);
async function onSubmit(values: TelegramFormData) {
setLoading(true);
try {
await updateAuthMethodConfig({
...data,
enabled: values.enabled,
config: {
...data?.config,
bot: values.bot,
bot_token: values.bot_token,
},
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('common.saveSuccess'));
refetch();
setOpen(false);
} catch (error) {
toast.error(t('common.saveFailed'));
} finally {
setLoading(false);
}
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:telegram' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('telegram.title')}</p>
<p className='text-muted-foreground text-sm'>{t('telegram.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('telegram.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='telegram-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='enabled'
render={({ field }) => (
<FormItem>
<FormLabel>{t('telegram.enable')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('telegram.enableDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='bot'
render={({ field }) => (
<FormItem>
<FormLabel>{t('telegram.clientId')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='6123456789'
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('telegram.clientIdDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='bot_token'
render={({ field }) => (
<FormItem>
<FormLabel>{t('telegram.clientSecret')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='6123456789:AAHn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
value={field.value}
onValueChange={field.onChange}
type='password'
/>
</FormControl>
<FormDescription>{t('telegram.clientSecretDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='telegram-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -1,135 +0,0 @@
'use client';
import { getInviteConfig, updateInviteConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
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 function Invite() {
const t = useTranslations('auth-control.invite');
const { data, refetch } = useQuery({
queryKey: ['getInviteConfig'],
queryFn: async () => {
const { data } = await getInviteConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateInviteConfig({
...data,
[key]: value,
} as API.InviteConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<Card>
<CardHeader>
<CardTitle>{t('inviteSettings')}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enableForcedInvite')}</Label>
<p className='text-muted-foreground text-xs'>
{t('enableForcedInviteDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.forced_invite}
onCheckedChange={(checked) => {
updateConfig('forced_invite', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label></Label>
<p className='text-muted-foreground text-xs'></p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.first_yearly_purchase_percentage}
type='number'
min={0}
max={100}
suffix='%'
onValueBlur={(value) => updateConfig('first_yearly_purchase_percentage', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label></Label>
<p className='text-muted-foreground text-xs'></p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.first_purchase_percentage}
type='number'
min={0}
max={100}
suffix='%'
onValueBlur={(value) => updateConfig('first_purchase_percentage', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label></Label>
<p className='text-muted-foreground text-xs'></p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.non_first_purchase_percentage}
type='number'
min={0}
max={100}
suffix='%'
onValueBlur={(value) => updateConfig('non_first_purchase_percentage', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('commissionFirstTimeOnly')}</Label>
<p className='text-muted-foreground text-xs'>
{t('commissionFirstTimeOnlyDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.only_first_purchase}
onCheckedChange={(checked) => {
updateConfig('only_first_purchase', checked);
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
);
}

View File

@ -1,17 +0,0 @@
'use client';
import { Invite } from './invite';
import { Register } from './register';
import { Verify } from './verify';
import { VerifyCode } from './verify-code';
export default function Page() {
return (
<div className='space-y-3'>
<Invite />
<Register />
<VerifyCode />
<Verify />
</div>
);
}

View File

@ -1,201 +0,0 @@
'use client';
import { getSubscribeList } from '@/services/admin/subscribe';
import { getRegisterConfig, updateRegisterConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Label } from '@workspace/ui/components/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/select';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Combobox } from '@workspace/ui/custom-components/combobox';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export function Register() {
const t = useTranslations('auth-control.register');
const { data, refetch } = useQuery({
queryKey: ['getRegisterConfig'],
queryFn: async () => {
const { data } = await getRegisterConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
await updateRegisterConfig({
...data,
[key]: value,
} as API.RegisterConfig);
toast.success(t('saveSuccess'));
refetch();
}
const { data: subscribe } = useQuery({
queryKey: ['getSubscribeList', 'all'],
queryFn: async () => {
const { data } = await getSubscribeList({
page: 1,
size: 9999,
});
return data.data?.list as API.Subscribe[];
},
});
return (
<Card>
<CardHeader>
<CardTitle>{t('registerSettings')}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('stopNewUserRegistration')}</Label>
<p className='text-muted-foreground text-xs'>
{t('stopNewUserRegistrationDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.stop_register}
onCheckedChange={(checked) => {
updateConfig('stop_register', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('ipRegistrationLimit')}</Label>
<p className='text-muted-foreground text-xs'>
{t('ipRegistrationLimitDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_ip_register_limit}
onCheckedChange={(checked) => {
updateConfig('enable_ip_register_limit', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('registrationLimitCount')}</Label>
<p className='text-muted-foreground text-xs'>
{t('registrationLimitCountDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
min={0}
value={data?.ip_register_limit}
onValueBlur={(value) => updateConfig('ip_register_limit', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('penaltyTime')}</Label>
<p className='text-muted-foreground text-xs'>{t('penaltyTimeDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
min={0}
value={data?.ip_register_limit_duration}
onValueBlur={(value) => updateConfig('ip_register_limit_duration', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('trialRegistration')}</Label>
<p className='text-muted-foreground text-xs'>{t('trialRegistrationDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_trial}
onCheckedChange={(checked) => {
updateConfig('enable_trial', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow className='flex flex-col'>
<TableCell className='w-full'>
<Label>{t('trialSubscribePlan')}</Label>
<p className='text-muted-foreground text-xs'>
{t('trialSubscribePlanDescription')}
</p>
</TableCell>
<TableCell className='max-w-96'>
<EnhancedInput
placeholder={t('trialDuration')}
type='number'
min={0}
value={data?.trial_time}
onValueBlur={(value) => updateConfig('trial_time', value)}
prefix={
<Select
value={String(data?.trial_subscribe)}
onValueChange={(value) => updateConfig('trial_subscribe', Number(value))}
>
<SelectTrigger className='bg-secondary rounded-r-none'>
{data?.trial_subscribe ? (
<SelectValue placeholder='Select Subscribe' />
) : (
'Select Subscribe'
)}
</SelectTrigger>
<SelectContent>
{subscribe?.map((item) => (
<SelectItem key={item.id} value={String(item.id)}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
}
suffix={
<Combobox
className='bg-secondary rounded-l-none'
value={data?.trial_time_unit}
onChange={(value) => {
if (value) {
updateConfig('trial_time_unit', value);
}
}}
options={[
{ label: t('noLimit'), value: 'NoLimit' },
{ label: t('year'), value: 'Year' },
{ label: t('month'), value: 'Month' },
{ label: t('day'), value: 'Day' },
{ label: t('hour'), value: 'Hour' },
{ label: t('minute'), value: 'Minute' },
]}
/>
}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
);
}

View File

@ -1,95 +0,0 @@
'use client';
import { getVerifyCodeConfig, updateVerifyCodeConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
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 function VerifyCode() {
const t = useTranslations('auth-control.verify-code');
const { data, refetch } = useQuery({
queryKey: ['getVerifyCodeConfig'],
queryFn: async () => {
const { data } = await getVerifyCodeConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateVerifyCodeConfig({
...data,
[key]: value,
} as API.VerifyCodeConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<Card className='mb-6'>
<CardHeader>
<CardTitle>{t('verifyCodeSettings')}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('expireTime')}</Label>
<p className='text-muted-foreground text-xs'>{t('expireTimeDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
placeholder='300'
value={data?.verify_code_expire_time}
onValueBlur={(value) => updateConfig('verify_code_expire_time', Number(value))}
suffix={t('second')}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('interval')}</Label>
<p className='text-muted-foreground text-xs'>{t('intervalDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
placeholder='60'
value={data?.verify_code_interval}
onValueBlur={(value) => updateConfig('verify_code_interval', Number(value))}
suffix={t('second')}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('dailyLimit')}</Label>
<p className='text-muted-foreground text-xs'>{t('dailyLimitDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
placeholder='15'
value={data?.verify_code_limit}
onValueBlur={(value) => updateConfig('verify_code_limit', Number(value))}
suffix={t('times')}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
);
}

View File

@ -1,125 +0,0 @@
'use client';
import { getVerifyConfig, updateVerifyConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
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 function Verify() {
const t = useTranslations('auth-control.verify');
const { data, refetch } = useQuery({
queryKey: ['getVerifyConfig'],
queryFn: async () => {
const { data } = await getVerifyConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateVerifyConfig({
...data,
[key]: value,
} as API.VerifyConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<Card className='mb-6'>
<CardHeader>
<CardTitle>{t('verifySettings')}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>Turnstile Site Key</Label>
<p className='text-muted-foreground text-xs'>{t('turnstileSiteKeyDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.turnstile_site_key}
onValueBlur={(value) => updateConfig('turnstile_site_key', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>Turnstile Site Secret</Label>
<p className='text-muted-foreground text-xs'>{t('turnstileSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.turnstile_secret}
onValueBlur={(value) => updateConfig('turnstile_secret', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('registrationVerificationCode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('registrationVerificationCodeDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_register_verify}
onCheckedChange={(checked) => {
updateConfig('enable_register_verify', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('loginVerificationCode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('loginVerificationCodeDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_login_verify}
onCheckedChange={(checked) => {
updateConfig('enable_login_verify', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('resetPasswordVerificationCode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('resetPasswordVerificationCodeDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_reset_password_verify}
onCheckedChange={(checked) => {
updateConfig('enable_reset_password_verify', checked);
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
);
}

View File

@ -1,91 +0,0 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
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 Page() {
const t = useTranslations('github');
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'github'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'github',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try {
await updateAuthMethodConfig({
...data,
[key]: value,
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientId')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='e.g., Iv1.1234567890abcdef'
value={data?.config?.client_id}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
client_id: value,
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('clientSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='e.g., 1234567890abcdef1234567890abcdef12345678'
value={data?.config?.client_secret}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_secret: value,
});
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}

View File

@ -1,92 +0,0 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
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 Page() {
const t = useTranslations('google');
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'google'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'google',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try {
await updateAuthMethodConfig({
...data,
[key]: value,
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientId')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='123456789-abc123def456.apps.googleusercontent.com'
value={data?.config?.client_id}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_id: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('clientSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
value={data?.config?.client_secret}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
client_secret: value,
});
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}

View File

@ -1,29 +0,0 @@
'use client';
import { AuthControl } from '@/config/navs';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface AuthControlLayoutProps {
children: React.ReactNode;
}
export default function AuthControlLayout({ children }: Readonly<AuthControlLayoutProps>) {
const pathname = usePathname();
const t = useTranslations('menu');
if (!pathname) return null;
return (
<Tabs value={pathname}>
<TabsList className='h-full flex-wrap'>
{AuthControl.map((item) => (
<TabsTrigger key={item.url} value={item.url} asChild>
<Link href={item.url}>{t(item.title)}</Link>
</TabsTrigger>
))}
</TabsList>
<TabsContent value={pathname}>{children}</TabsContent>
</Tabs>
);
}

View File

@ -66,10 +66,6 @@ export function LogsTable({ type }: { type: 'email' | 'mobile' }) {
},
]}
params={[
// {
// key: 'platform',
// placeholder: t('platform'),
// },
{
key: 'to',
placeholder: t('to'),

View File

@ -1,5 +1,69 @@
import { redirect } from 'next/navigation';
'use client';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { useTranslations } from 'next-intl';
import AppleForm from './forms/apple-form';
import DeviceForm from './forms/device-form';
import EmailLogsTable from './forms/email-logs-table';
import EmailSettingsForm from './forms/email-settings-form';
import FacebookForm from './forms/facebook-form';
import GithubForm from './forms/github-form';
import GoogleForm from './forms/google-form';
import PhoneLogsTable from './forms/phone-logs-table';
import PhoneSettingsForm from './forms/phone-settings-form';
import TelegramForm from './forms/telegram-form';
export default function Page() {
return redirect('/dashboard/auth-control/general');
const t = useTranslations('auth-control');
// 定义表单配置
const formSections = [
{
title: t('communicationMethods'),
forms: [
{ component: EmailSettingsForm },
{ component: EmailLogsTable },
{ component: PhoneSettingsForm },
{ component: PhoneLogsTable },
],
},
{
title: t('socialAuthMethods'),
forms: [
{ component: AppleForm },
{ component: GoogleForm },
{ component: FacebookForm },
{ component: GithubForm },
{ component: TelegramForm },
],
},
{
title: t('deviceAuthMethods'),
forms: [{ component: DeviceForm }],
},
];
return (
<div className='space-y-8'>
{formSections.map((section, sectionIndex) => (
<div key={sectionIndex}>
<h2 className='mb-4 text-lg font-semibold'>{section.title}</h2>
<Table>
<TableBody>
{section.forms.map((form, formIndex) => {
const FormComponent = form.component;
return (
<TableRow key={formIndex}>
<TableCell>
<FormComponent />
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
))}
</div>
);
}

View File

@ -1,395 +0,0 @@
'use client';
import {
getAuthMethodConfig,
getSmsPlatform,
testSmsSend,
updateAuthMethodConfig,
} from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import { Label } from '@workspace/ui/components/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/select';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { Textarea } from '@workspace/ui/components/textarea';
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import TagInput from '@workspace/ui/custom-components/tag-input';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useState } from 'react';
import { toast } from 'sonner';
import { LogsTable } from '../log';
export default function Page() {
const t = useTranslations('phone');
const { data, refetch, isFetching } = useQuery({
queryKey: ['getAuthMethodConfig', 'mobile'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'mobile',
});
return data.data;
},
});
const { data: platforms } = useQuery({
queryKey: ['getSmsPlatform'],
queryFn: async () => {
const { data } = await getSmsPlatform();
return data.data?.list;
},
});
const selectedPlatform = platforms?.find(
(platform) => platform.platform === data?.config?.platform,
);
const { platform_url, platform_field_description: platformConfig } = selectedPlatform ?? {};
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateAuthMethodConfig({
...data,
[key]: value,
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('updateSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
const [params, setParams] = useState<API.TestSmsSendRequest>({
telephone: '',
area_code: '1',
});
return (
<Tabs defaultValue='settings' className='w-full'>
<TabsList>
<TabsTrigger value='settings'>{t('settings')}</TabsTrigger>
<TabsTrigger value='logs'>{t('logs')}</TabsTrigger>
</TabsList>
<TabsContent value='settings'>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableTip')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
disabled={isFetching}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('whitelistValidation')}</Label>
<p className='text-muted-foreground text-xs'>{t('whitelistValidationTip')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
defaultValue={data?.config?.enable_whitelist}
onCheckedChange={(checked) =>
updateConfig('config', { ...data?.config, enable_whitelist: checked })
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('whitelistAreaCode')}</Label>
<p className='text-muted-foreground text-xs'>{t('whitelistAreaCodeTip')}</p>
</TableCell>
<TableCell className='w-1/2 text-right'>
<TagInput
placeholder='1, 852, 886, 888'
value={data?.config?.whitelist || []}
onChange={(value) =>
updateConfig('config', { ...data?.config, whitelist: value })
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('platform')}</Label>
<p className='text-muted-foreground text-xs'>{t('platformTip')}</p>
</TableCell>
<TableCell className='flex items-center gap-1 text-right'>
<Select
value={data?.config?.platform}
onValueChange={(value) =>
updateConfig('config', {
...data?.config,
platform: value,
})
}
disabled={isFetching}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{platforms?.map((item) => (
<SelectItem key={item.platform} value={item.platform}>
{item.platform}
</SelectItem>
))}
</SelectContent>
</Select>
{platform_url && (
<Button size='sm' asChild>
<Link href={platform_url} target='_blank'>
{t('applyPlatform')}
</Link>
</Button>
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('accessLabel')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.access })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.platform_config.access ?? ''}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
access: value,
},
})
}
disabled={isFetching}
placeholder={t('platformConfigTip', { key: platformConfig?.access })}
/>
</TableCell>
</TableRow>
{platformConfig?.endpoint && (
<TableRow>
<TableCell>
<Label>{t('endpointLabel')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.endpoint })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.platform_config.endpoint ?? ''}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
endpoint: value,
},
})
}
disabled={isFetching}
placeholder={t('platformConfigTip', { key: platformConfig?.endpoint })}
/>
</TableCell>
</TableRow>
)}
<TableRow>
<TableCell>
<Label>{t('secretLabel')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.secret })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.platform_config?.secret ?? ''}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
secret: value,
},
})
}
disabled={isFetching}
type='password'
placeholder={t('platformConfigTip', { key: platformConfig?.secret })}
/>
</TableCell>
</TableRow>
{platformConfig?.template_code && (
<TableRow>
<TableCell>
<Label>{t('templateCodeLabel')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.template_code })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.platform_config?.template_code ?? 'code'}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
template_code: value,
},
})
}
disabled={isFetching}
placeholder={t('platformConfigTip', { key: platformConfig?.template_code })}
/>
</TableCell>
</TableRow>
)}
{platformConfig?.sign_name && (
<TableRow>
<TableCell>
<Label>{t('signNameLabel')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sign_name })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.platform_config?.sign_name ?? ''}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
sign_name: value,
},
})
}
disabled={isFetching}
placeholder={t('platformConfigTip', {
key: platformConfig?.sign_name,
})}
/>
</TableCell>
</TableRow>
)}
{platformConfig?.phone_number && (
<TableRow>
<TableCell>
<Label>{t('phoneNumberLabel')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.phone_number })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.platform_config?.phone_number ?? ''}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
phone_number: value,
},
})
}
disabled={isFetching}
placeholder={t('platformConfigTip', {
key: platformConfig?.phone_number,
})}
/>
</TableCell>
</TableRow>
)}
{platformConfig?.code_variable && (
<TableRow>
<TableCell>
<Label>{t('template')}</Label>
<p className='text-muted-foreground text-xs'>
{t('templateTip', { code: platformConfig?.code_variable })}
</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
defaultValue={data?.config?.platform_config?.template ?? ''}
onBlur={(e) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
template: e.target.value,
},
})
}
disabled={isFetching}
placeholder={t('placeholders.template', {
code: platformConfig?.code_variable,
})}
/>
</TableCell>
</TableRow>
)}
<TableRow>
<TableCell>
<Label>{t('testSms')}</Label>
<p className='text-muted-foreground text-xs'>{t('testSmsTip')}</p>
</TableCell>
<TableCell className='flex items-center gap-2 text-right'>
<AreaCodeSelect
value={params.area_code}
onChange={(value) => {
if (value.phone) {
setParams((prev) => ({ ...prev, area_code: value.phone! }));
}
}}
/>
<EnhancedInput
placeholder={t('testSmsPhone')}
value={params.telephone}
onValueChange={(value) => {
setParams((prev) => ({ ...prev, telephone: value as string }));
}}
/>
<Button
disabled={!params.telephone || !params.area_code}
onClick={async () => {
if (isFetching || !params.telephone || !params.area_code) return;
try {
await testSmsSend(params);
toast.success(t('sendSuccess'));
} catch {
toast.error(t('sendFailed'));
}
}}
>
{t('testSms')}
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
<TabsContent value='logs'>
<LogsTable type='mobile' />
</TabsContent>
</Tabs>
);
}

View File

@ -1,92 +0,0 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
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 Page() {
const t = useTranslations('telegram');
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'telegram'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'telegram',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try {
await updateAuthMethodConfig({
...data,
[key]: value,
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientId')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientIdDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='6123456789'
value={data?.config?.bot}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
bot: value,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('clientSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('clientSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='6123456789:AAHn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
value={data?.config?.bot_token}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
bot_token: value,
});
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}

View File

@ -64,9 +64,6 @@ export default function Page() {
),
}}
params={[
{
key: 'search',
},
{
key: 'subscribe',
placeholder: t('subscribe'),
@ -75,6 +72,9 @@ export default function Page() {
value: String(item.id),
})),
},
{
key: 'search',
},
]}
request={async (pagination, filters) => {
const { data } = await getCouponList({

View File

@ -165,7 +165,6 @@ export default function Page(props: any) {
},
]}
params={[
{ key: 'search' },
{
key: 'status',
placeholder: t('status.0'),
@ -182,6 +181,7 @@ export default function Page(props: any) {
value: String(item.id),
})),
},
{ key: 'search' },
].concat(
props.userId
? []

View File

@ -90,7 +90,7 @@ export default function GroupForm<T extends Record<string, any>>({
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('group.form.name')}</FormLabel>
<FormLabel>{t('groupForm.name')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -108,7 +108,7 @@ export default function GroupForm<T extends Record<string, any>>({
name='description'
render={({ field }) => (
<FormItem>
<FormLabel>{t('group.form.description')}</FormLabel>
<FormLabel>{t('groupForm.description')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -132,11 +132,11 @@ export default function GroupForm<T extends Record<string, any>>({
setOpen(false);
}}
>
{t('group.form.cancel')}
{t('groupForm.cancel')}
</Button>
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
{t('group.form.confirm')}
{t('groupForm.confirm')}
</Button>
</SheetFooter>
</SheetContent>

View File

@ -0,0 +1,251 @@
'use client';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { ProTable } from '@/components/pro-table';
import { getUserSubscribeById } from '@/services/admin/user';
import { useQuery } from '@tanstack/react-query';
import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button';
import { Progress } from '@workspace/ui/components/progress';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { formatBytes, formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
interface NodeDetailDialogProps {
node: API.Server;
children?: React.ReactNode;
trigger?: React.ReactNode;
}
// 统一的用户订阅信息组件
function UserSubscribeInfo({
userId,
type,
}: {
userId: number;
type: 'account' | 'subscribeName' | 'subscribeId' | 'trafficUsage' | 'expireTime';
}) {
const { data } = useQuery({
enabled: userId !== 0,
queryKey: ['getUserSubscribeById', userId],
queryFn: async () => {
const { data } = await getUserSubscribeById({ id: userId });
return data.data;
},
});
if (!data) return <span className='text-muted-foreground'>--</span>;
switch (type) {
case 'account':
if (!data.user_id) return <span className='text-muted-foreground'>--</span>;
return <UserDetail id={data.user_id} />;
case 'subscribeName':
if (!data.subscribe?.name) return <span className='text-muted-foreground'>--</span>;
return <span className='text-sm'>{data.subscribe.name}</span>;
case 'subscribeId':
if (!data.id) return <span className='text-muted-foreground'>--</span>;
return <span className='font-mono text-sm'>{data.id}</span>;
case 'trafficUsage': {
const usedTraffic = data.upload + data.download;
const totalTraffic = data.traffic || 0;
return (
<div className='min-w-0 text-sm'>
<div className='break-words'>
{formatBytes(usedTraffic)} / {totalTraffic > 0 ? formatBytes(totalTraffic) : '无限制'}
</div>
</div>
);
}
case 'expireTime': {
if (!data.expire_time) return <span className='text-muted-foreground'>--</span>;
const isExpired = data.expire_time < Date.now() / 1000;
return (
<div className='flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2'>
<span className='text-sm'>{formatDate(data.expire_time)}</span>
{isExpired && (
<Badge variant='destructive' className='w-fit px-1 py-0 text-xs'>
</Badge>
)}
</div>
);
}
default:
return <span className='text-muted-foreground'>--</span>;
}
}
export function NodeDetailDialog({ node, children, trigger }: NodeDetailDialogProps) {
const t = useTranslations('server.node');
const [open, setOpen] = useState(false);
const { status } = node;
const { online, cpu, mem, disk, updated_at } = status || {
online: {},
cpu: 0,
mem: 0,
disk: 0,
updated_at: 0,
};
const isOnline = updated_at > 0;
const onlineCount = (online && Object.keys(online).length) || 0;
// 转换在线用户数据为ProTable需要的格式
const onlineUsersData = Object.entries(online || {}).map(([uid, ips]) => ({
uid,
ips: ips as string[],
primaryIp: ips[0] || '',
allIps: (ips as string[]).join(', '),
}));
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
{trigger || (
<Button variant='outline' size='sm'>
{t('detail')}
</Button>
)}
</SheetTrigger>
<SheetContent className='w-full max-w-full sm:w-[600px] sm:max-w-screen-md'>
<SheetHeader>
<SheetTitle>
{t('nodeDetail')} - {node.name}
</SheetTitle>
</SheetHeader>
<div className='-mx-6 h-[calc(100dvh-48px-16px-env(safe-area-inset-top))] space-y-2 overflow-y-auto px-6 py-4'>
<h3 className='text-base font-medium'>{t('nodeStatus')}</h3>
<div className='space-y-3'>
<div className='flex w-full flex-col gap-2 text-sm sm:flex-row sm:items-center sm:gap-3'>
<Badge variant={isOnline ? 'default' : 'destructive'} className='w-fit text-xs'>
{isOnline ? t('normal') : t('abnormal')}
</Badge>
<span className='text-muted-foreground'>
{t('onlineCount')}: {onlineCount}
</span>
{isOnline && (
<span className='text-muted-foreground text-xs sm:text-sm'>
{t('lastUpdated')}: {formatDate(updated_at)}
</span>
)}
</div>
{isOnline && (
<div className='grid grid-cols-1 gap-3 sm:grid-cols-3'>
<div className='space-y-1'>
<div className='flex justify-between text-xs'>
<span>CPU</span>
<span>{cpu?.toFixed(1)}%</span>
</div>
<Progress value={cpu ?? 0} className='h-1.5' max={100} />
</div>
<div className='space-y-1'>
<div className='flex justify-between text-xs'>
<span>{t('memory')}</span>
<span>{mem?.toFixed(1)}%</span>
</div>
<Progress value={mem ?? 0} className='h-1.5' max={100} />
</div>
<div className='space-y-1'>
<div className='flex justify-between text-xs'>
<span>{t('disk')}</span>
<span>{disk?.toFixed(1)}%</span>
</div>
<Progress value={disk ?? 0} className='h-1.5' max={100} />
</div>
</div>
)}
</div>
{isOnline && onlineCount > 0 && (
<div>
<h3 className='mb-3 text-lg font-medium'>{t('onlineUsers')}</h3>
<div className='overflow-x-auto'>
<ProTable
header={{
hidden: true,
}}
columns={[
{
accessorKey: 'allIps',
header: t('ipAddresses'),
cell: ({ row }) => {
const ips = row.original.ips;
return (
<div className='flex min-w-0 flex-col gap-1'>
{ips.map((ip: string, index: number) => (
<div key={ip} className='whitespace-nowrap text-sm'>
{index === 0 ? (
<span className='font-medium'>{ip}</span>
) : (
<span className='text-muted-foreground'>{ip}</span>
)}
</div>
))}
</div>
);
},
},
{
accessorKey: 'user',
header: t('user'),
cell: ({ row }) => (
<UserSubscribeInfo userId={Number(row.original.uid)} type='account' />
),
},
{
accessorKey: 'subscribeName',
header: t('subscribeName'),
cell: ({ row }) => (
<UserSubscribeInfo userId={Number(row.original.uid)} type='subscribeName' />
),
},
{
accessorKey: 'subscribeId',
header: t('subscribeId'),
cell: ({ row }) => (
<UserSubscribeInfo userId={Number(row.original.uid)} type='subscribeId' />
),
},
{
accessorKey: 'trafficUsage',
header: t('trafficUsage'),
cell: ({ row }) => (
<UserSubscribeInfo userId={Number(row.original.uid)} type='trafficUsage' />
),
},
{
accessorKey: 'expireTime',
header: t('expireTime'),
cell: ({ row }) => (
<UserSubscribeInfo userId={Number(row.original.uid)} type='expireTime' />
),
},
]}
request={async () => ({
list: onlineUsersData,
total: onlineUsersData.length,
})}
/>
</div>
</div>
)}
</div>
</SheetContent>
</Sheet>
);
}

View File

@ -58,7 +58,10 @@ export default function NodeForm<T extends { [x: string]: any }>({
trigger,
title,
}: Readonly<NodeFormProps<T>>) {
const t = useTranslations('server.node');
const t = useTranslations('server');
const tf = useTranslations('server.nodeForm');
const trs = useTranslations('server.relayModeOptions');
const tsc = useTranslations('server.securityConfig');
const [open, setOpen] = useState(false);
const form = useForm({
@ -123,7 +126,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.name')}</FormLabel>
<FormLabel>{t('nodeForm.name')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -141,10 +144,10 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='group_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.groupId')}</FormLabel>
<FormLabel>{t('nodeForm.groupId')}</FormLabel>
<FormControl>
<Combobox<number, false>
placeholder={t('form.selectNodeGroup')}
placeholder={t('nodeForm.selectNodeGroup')}
{...field}
options={groups?.map((item) => ({
value: item.id,
@ -166,10 +169,10 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='tags'
render={({ field }) => (
<FormItem className='col-span-3'>
<FormLabel>{t('form.tags')}</FormLabel>
<FormLabel>{t('nodeForm.tags')}</FormLabel>
<FormControl>
<TagInput
placeholder={t('form.tagsPlaceholder')}
placeholder={t('nodeForm.tagsPlaceholder')}
value={field.value || []}
onChange={(value) => form.setValue(field.name, value)}
/>
@ -183,7 +186,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='country'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.country')}</FormLabel>
<FormLabel>{t('nodeForm.country')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -201,7 +204,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='city'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.city')}</FormLabel>
<FormLabel>{t('nodeForm.city')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -221,7 +224,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='server_addr'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.serverAddr')}</FormLabel>
<FormLabel>{t('nodeForm.serverAddr')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -239,12 +242,12 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='speed_limit'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.speedLimit')}</FormLabel>
<FormLabel>{t('nodeForm.speedLimit')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
{...field}
placeholder={t('form.speedLimitPlaceholder')}
placeholder={t('nodeForm.speedLimitPlaceholder')}
formatInput={(value) => unitConversion('bitsToMb', value)}
formatOutput={(value) => unitConversion('mbToBits', value)}
onValueChange={(value) => {
@ -262,7 +265,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='traffic_ratio'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.trafficRatio')}</FormLabel>
<FormLabel>{t('nodeForm.trafficRatio')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -284,7 +287,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='protocol'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.protocol')}</FormLabel>
<FormLabel>{t('nodeForm.protocol')}</FormLabel>
<FormControl>
<Tabs
value={field.value}
@ -316,7 +319,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.method'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.encryptionMethod')}</FormLabel>
<FormLabel>{t('nodeForm.encryptionMethod')}</FormLabel>
<FormControl>
<Select
onValueChange={(value) => {
@ -326,7 +329,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.selectEncryptionMethod')} />
<SelectValue placeholder={t('nodeForm.selectEncryptionMethod')} />
</SelectTrigger>
</FormControl>
<SelectContent>
@ -357,7 +360,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.port'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.port')}</FormLabel>
<FormLabel>{t('nodeForm.port')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -385,7 +388,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.server_key'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.serverKey')}</FormLabel>
<FormLabel>{t('nodeForm.serverKey')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -414,7 +417,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.port'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.port')}</FormLabel>
<FormLabel>{t('nodeForm.port')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -438,7 +441,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.flow'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.flow')}</FormLabel>
<FormLabel>{t('nodeForm.flow')}</FormLabel>
<FormControl>
<Select
value={field.value}
@ -448,7 +451,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.pleaseSelect')} />
<SelectValue placeholder={t('nodeForm.pleaseSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
@ -469,11 +472,11 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.obfs_password'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.obfsPassword')}</FormLabel>
<FormLabel>{t('nodeForm.obfsPassword')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t('form.obfsPasswordPlaceholder')}
placeholder={t('nodeForm.obfsPasswordPlaceholder')}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
@ -488,10 +491,10 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.hop_ports'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.hopPorts')}</FormLabel>
<FormLabel>{t('nodeForm.hopPorts')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.hopPortsPlaceholder')}
placeholder={t('nodeForm.hopPortsPlaceholder')}
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
@ -507,7 +510,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.hop_interval'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.hopInterval')}</FormLabel>
<FormLabel>{t('nodeForm.hopInterval')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -531,7 +534,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.udp_relay_mode'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.udpRelayMode')}</FormLabel>
<FormLabel>{t('nodeForm.udpRelayMode')}</FormLabel>
<FormControl>
<Select
value={field.value}
@ -541,7 +544,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.pleaseSelect')} />
<SelectValue placeholder={t('nodeForm.pleaseSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
@ -559,7 +562,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.congestion_controller'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.congestionController')}</FormLabel>
<FormLabel>{t('nodeForm.congestionController')}</FormLabel>
<FormControl>
<Select
value={field.value}
@ -569,7 +572,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.pleaseSelect')} />
<SelectValue placeholder={t('nodeForm.pleaseSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
@ -589,7 +592,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.disable_sni'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.disableSni')}</FormLabel>
<FormLabel>{t('nodeForm.disableSni')}</FormLabel>
<FormControl>
<div className='pt-2'>
<Switch
@ -609,7 +612,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.reduce_rtt'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.reduceRtt')}</FormLabel>
<FormLabel>{t('nodeForm.reduceRtt')}</FormLabel>
<FormControl>
<div className='pt-2'>
<Switch
@ -631,7 +634,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
{['vmess', 'vless', 'trojan'].includes(protocol) && (
<Card>
<CardHeader className='flex flex-row items-center justify-between p-3'>
<CardTitle>{t('form.transportConfig')}</CardTitle>
<CardTitle>{t('nodeForm.transportConfig')}</CardTitle>
<FormField
control={form.control}
name='config.transport'
@ -646,7 +649,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.pleaseSelect')} />
<SelectValue placeholder={t('nodeForm.pleaseSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
@ -737,7 +740,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
['anytls', 'tuic', 'hysteria2'].includes(protocol)) && (
<Card>
<CardHeader className='flex flex-row items-center justify-between p-3'>
<CardTitle>{t('form.securityConfig')}</CardTitle>
<CardTitle>{t('nodeForm.securityConfig')}</CardTitle>
{['vmess', 'vless', 'trojan'].includes(protocol) && (
<FormField
control={form.control}
@ -752,7 +755,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.pleaseSelect')} />
<SelectValue placeholder={t('nodeForm.pleaseSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
@ -802,7 +805,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.security_config.reality_server_addr'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.serverAddress')}</FormLabel>
<FormLabel>{t('securityConfig.serverAddress')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -823,7 +826,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.security_config.reality_server_port'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.serverPort')}</FormLabel>
<FormLabel>{t('securityConfig.serverPort')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -847,7 +850,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.security_config.reality_private_key'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.privateKey')}</FormLabel>
<FormLabel>{t('securityConfig.privateKey')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
@ -868,11 +871,11 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.security_config.reality_public_key'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.publicKey')}</FormLabel>
<FormLabel>{t('securityConfig.publicKey')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t('form.security_config.publicKeyPlaceholder')}
placeholder={t('securityConfig.publicKeyPlaceholder')}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
@ -887,11 +890,11 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.security_config.reality_short_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.shortId')}</FormLabel>
<FormLabel>{t('securityConfig.shortId')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t('form.security_config.shortIdPlaceholder')}
placeholder={t('securityConfig.shortIdPlaceholder')}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
@ -910,7 +913,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
name='config.security_config.fingerprint'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.fingerprint')}</FormLabel>
<FormLabel>{t('securityConfig.fingerprint')}</FormLabel>
<Select
value={field.value}
onValueChange={(value) => {
@ -919,7 +922,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.pleaseSelect')} />
<SelectValue placeholder={t('nodeForm.pleaseSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
@ -968,7 +971,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
<Card>
<CardHeader className='flex flex-row items-center justify-between p-3'>
<CardTitle>{t('form.relayMode')}</CardTitle>
<CardTitle>{t('nodeForm.relayMode')}</CardTitle>
<FormField
control={form.control}
name='relay_mode'
@ -983,17 +986,13 @@ export default function NodeForm<T extends { [x: string]: any }>({
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.selectRelayMode')} />
<SelectValue placeholder={t('nodeForm.selectRelayMode')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='none'>
{t('form.relayModeOptions.none')}
</SelectItem>
<SelectItem value='all'>{t('form.relayModeOptions.all')}</SelectItem>
<SelectItem value='random'>
{t('form.relayModeOptions.random')}
</SelectItem>
<SelectItem value='none'>{t('relayModeOptions.none')}</SelectItem>
<SelectItem value='all'>{t('relayModeOptions.all')}</SelectItem>
<SelectItem value='random'>{t('relayModeOptions.random')}</SelectItem>
</SelectContent>
</Select>
</FormControl>
@ -1015,7 +1014,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
{
name: 'host',
type: 'text',
placeholder: t('form.relayHost'),
placeholder: t('nodeForm.relayHost'),
},
{
name: 'port',
@ -1023,12 +1022,12 @@ export default function NodeForm<T extends { [x: string]: any }>({
step: 1,
min: 1,
max: 65535,
placeholder: t('form.relayPort'),
placeholder: t('nodeForm.relayPort'),
},
{
name: 'prefix',
type: 'text',
placeholder: t('form.relayPrefix'),
placeholder: t('nodeForm.relayPrefix'),
},
]}
value={field.value}
@ -1055,7 +1054,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
setOpen(false);
}}
>
{t('form.cancel')}
{t('node.cancel')}
</Button>
<Button
disabled={loading}
@ -1069,7 +1068,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
})}
>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
{t('form.confirm')}
{t('node.confirm')}
</Button>
</SheetFooter>
</SheetContent>

View File

@ -1,32 +1,94 @@
'use client';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@workspace/ui/components/accordion';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { IpLink } from '@/components/ip-link';
import { ProTable } from '@/components/pro-table';
import { getUserSubscribeById } from '@/services/admin/user';
import { useQuery } from '@tanstack/react-query';
import { Badge } from '@workspace/ui/components/badge';
import { Progress } from '@workspace/ui/components/progress';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@workspace/ui/components/tooltip';
import { formatDate } from '@workspace/ui/utils';
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { formatBytes, formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { UserSubscribeDetail } from '../user/user-detail';
export function formatPercentage(value: number): string {
return `${value.toFixed(1)}%`;
}
export function NodeStatusCell({ status }: { status: API.NodeStatus }) {
// 统一的用户订阅信息组件
function UserSubscribeInfo({
userId,
type,
}: {
userId: number;
type: 'account' | 'subscribeName' | 'subscribeId' | 'trafficUsage' | 'expireTime';
}) {
const { data } = useQuery({
enabled: userId !== 0,
queryKey: ['getUserSubscribeById', userId],
queryFn: async () => {
const { data } = await getUserSubscribeById({ id: userId });
return data.data;
},
});
if (!data) return <span className='text-muted-foreground'>--</span>;
switch (type) {
case 'account':
if (!data.user_id) return <span className='text-muted-foreground'>--</span>;
return <UserDetail id={data.user_id} />;
case 'subscribeName':
if (!data.subscribe?.name) return <span className='text-muted-foreground'>--</span>;
return <span className='text-sm'>{data.subscribe.name}</span>;
case 'subscribeId':
if (!data.id) return <span className='text-muted-foreground'>--</span>;
return <span className='font-mono text-sm'>{data.id}</span>;
case 'trafficUsage': {
const usedTraffic = data.upload + data.download;
const totalTraffic = data.traffic || 0;
return (
<div className='min-w-0 text-sm'>
<div className='break-words'>
{formatBytes(usedTraffic)} / {totalTraffic > 0 ? formatBytes(totalTraffic) : '无限制'}
</div>
</div>
);
}
case 'expireTime': {
if (!data.expire_time) return <span className='text-muted-foreground'>--</span>;
const isExpired = data.expire_time < Date.now() / 1000;
return (
<div className='flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2'>
<span className='text-sm'>{formatDate(data.expire_time)}</span>
{isExpired && (
<Badge variant='destructive' className='w-fit px-1 py-0 text-xs'>
</Badge>
)}
</div>
);
}
default:
return <span className='text-muted-foreground'>--</span>;
}
}
export function NodeStatusCell({ status, node }: { status: API.NodeStatus; node?: API.Server }) {
const t = useTranslations('server.node');
const [openItem, setOpenItem] = useState<string | null>(null);
const [open, setOpen] = useState(false);
const { online, cpu, mem, disk, updated_at } = status || {
online: {},
@ -35,78 +97,170 @@ export function NodeStatusCell({ status }: { status: API.NodeStatus }) {
disk: 0,
updated_at: 0,
};
const isOnline = updated_at > 0;
const badgeVariant = isOnline ? 'default' : 'destructive';
const badgeText = isOnline ? t('normal') : t('abnormal');
const onlineCount = (online && Object.keys(online).length) || 0;
// 转换在线用户数据为ProTable需要的格式
const onlineUsersData = Object.entries(online || {}).map(([uid, ips]) => ({
uid,
ips: ips as string[],
primaryIp: ips[0] || '',
allIps: (ips as string[]).join(', '),
}));
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='flex items-center gap-2 text-xs *:flex-1'>
<div className='flex items-center space-x-1'>
<Badge variant={badgeVariant}>{badgeText}</Badge>
<span className='font-medium'>
{t('onlineCount')}: {onlineCount}
</span>
</div>
<div className='flex flex-col space-y-1'>
<div className='flex justify-between'>
<span>CPU</span>
<span>{formatPercentage(cpu ?? 0)}</span>
<>
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<button className='hover:text-foreground flex cursor-pointer items-center gap-2 border-none bg-transparent p-0 text-left text-sm transition-colors'>
<Badge variant={badgeVariant}>{badgeText}</Badge>
<span className='text-muted-foreground'>
{t('onlineCount')}: {onlineCount}
</span>
</button>
</SheetTrigger>
{node && (
<SheetContent className='h-screen w-screen max-w-none sm:h-auto sm:w-[800px] sm:max-w-[90vw]'>
<SheetHeader>
<SheetTitle>
{t('nodeDetail')} - {node.name}
</SheetTitle>
</SheetHeader>
<div className='-mx-6 h-[calc(100vh-48px-16px)] space-y-2 overflow-y-auto px-6 py-4 sm:h-[calc(100dvh-48px-16px-env(safe-area-inset-top))]'>
<h3 className='text-base font-medium'>{t('nodeStatus')}</h3>
<div className='space-y-3'>
<div className='flex w-full flex-col gap-2 text-sm sm:flex-row sm:items-center sm:gap-3'>
<Badge variant={isOnline ? 'default' : 'destructive'} className='w-fit text-xs'>
{isOnline ? t('normal') : t('abnormal')}
</Badge>
<span className='text-muted-foreground'>
{t('onlineCount')}: {onlineCount}
</span>
{isOnline && (
<span className='text-muted-foreground text-xs sm:text-sm'>
{t('lastUpdated')}: {formatDate(updated_at)}
</span>
)}
</div>
{isOnline && (
<div className='grid grid-cols-1 gap-3 sm:grid-cols-3'>
<div className='space-y-1'>
<div className='flex justify-between text-xs'>
<span>CPU</span>
<span>{cpu?.toFixed(1)}%</span>
</div>
<Progress value={cpu ?? 0} className='h-1.5' max={100} />
</div>
<div className='space-y-1'>
<div className='flex justify-between text-xs'>
<span>{t('memory')}</span>
<span>{mem?.toFixed(1)}%</span>
</div>
<Progress value={mem ?? 0} className='h-1.5' max={100} />
</div>
<div className='space-y-1'>
<div className='flex justify-between text-xs'>
<span>{t('disk')}</span>
<span>{disk?.toFixed(1)}%</span>
</div>
<Progress value={disk ?? 0} className='h-1.5' max={100} />
</div>
</div>
)}
</div>
<Progress value={cpu ?? 0} className='h-2' max={100} />
{isOnline && onlineCount > 0 && (
<div>
<h3 className='mb-3 text-lg font-medium'>{t('onlineUsers')}</h3>
<div className='overflow-x-auto'>
<ProTable
header={{
hidden: true,
}}
columns={[
{
accessorKey: 'allIps',
header: t('ipAddresses'),
cell: ({ row }) => {
const ips = row.original.ips;
return (
<div className='flex min-w-0 flex-col gap-1'>
{ips.map((ip: string, index: number) => (
<div key={ip} className='whitespace-nowrap text-sm'>
{index === 0 ? (
<IpLink ip={ip} className='font-medium' />
) : (
<IpLink ip={ip} className='text-muted-foreground' />
)}
</div>
))}
</div>
);
},
},
{
accessorKey: 'user',
header: t('user'),
cell: ({ row }) => (
<UserSubscribeInfo userId={Number(row.original.uid)} type='account' />
),
},
{
accessorKey: 'subscribeName',
header: t('subscribeName'),
cell: ({ row }) => (
<UserSubscribeInfo
userId={Number(row.original.uid)}
type='subscribeName'
/>
),
},
{
accessorKey: 'subscribeId',
header: t('subscribeId'),
cell: ({ row }) => (
<UserSubscribeInfo
userId={Number(row.original.uid)}
type='subscribeId'
/>
),
},
{
accessorKey: 'trafficUsage',
header: t('trafficUsage'),
cell: ({ row }) => (
<UserSubscribeInfo
userId={Number(row.original.uid)}
type='trafficUsage'
/>
),
},
{
accessorKey: 'expireTime',
header: t('expireTime'),
cell: ({ row }) => (
<UserSubscribeInfo
userId={Number(row.original.uid)}
type='expireTime'
/>
),
},
]}
request={async () => ({
list: onlineUsersData,
total: onlineUsersData.length,
})}
/>
</div>
</div>
)}
</div>
<div className='flex flex-col space-y-1'>
<div className='flex justify-between'>
<span>{t('memory')}</span>
<span>{formatPercentage(mem ?? 0)}</span>
</div>
<Progress value={mem ?? 0} className='h-2' max={100} />
</div>
<div className='flex flex-col space-y-1'>
<div className='flex justify-between'>
<span>{t('disk')}</span>
<span>{formatPercentage(disk ?? 0)}</span>
</div>
<Progress value={disk ?? 0} className='h-2' max={100} />
</div>
{isOnline && (
<div>
{t('lastUpdated')}: {formatDate(updated_at ?? 0)}
</div>
)}
</div>
</TooltipTrigger>
{isOnline && onlineCount > 0 && (
<TooltipContent className='bg-card text-foreground w-96'>
<ScrollArea className='h-[540px] rounded-md border px-4 py-2'>
<h4 className='py-1 text-sm font-semibold'>{t('onlineUsers')}</h4>
<Accordion
type='single'
collapsible
className='w-full'
onValueChange={(value) => setOpenItem(value)}
>
{Object.entries(online).map(([uid, ips]) => (
<AccordionItem key={uid} value={uid}>
<AccordionTrigger>{`[UID: ${uid}] - ${ips[0]}`}</AccordionTrigger>
<AccordionContent>
<ul>
{ips.map((ip: string) => (
<li key={ip}>{ip}</li>
))}
</ul>
<UserSubscribeDetail id={Number(uid)} enabled={openItem === uid} />
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</ScrollArea>
</TooltipContent>
</SheetContent>
)}
</Tooltip>
</TooltipProvider>
</Sheet>
</>
);
}

View File

@ -139,7 +139,7 @@ export default function NodeTable() {
accessorKey: 'status',
header: t('status'),
cell: ({ row }) => {
return <NodeStatusCell status={row.original?.status} />;
return <NodeStatusCell status={row.original?.status} node={row.original} />;
},
},
{
@ -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({

View File

@ -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({

View File

@ -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<typeof currencySchema>;
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<CurrencyFormData>({
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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:currency-usd' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('currency.title')}</p>
<p className='text-muted-foreground text-sm'>{t('currency.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('currency.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='currency-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='access_key'
render={({ field }) => (
<FormItem>
<FormLabel>{t('currency.accessKey')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('currency.accessKeyPlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t('currency.accessKeyDescription', { url: EXCHANGE_RATE_HOST_URL })}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='currency_unit'
render={({ field }) => (
<FormItem>
<FormLabel>{t('currency.currencyUnit')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('currency.currencyUnitPlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('currency.currencyUnitDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='currency_symbol'
render={({ field }) => (
<FormItem>
<FormLabel>{t('currency.currencySymbol')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('currency.currencySymbolPlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('currency.currencySymbolDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='currency-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -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<typeof privacyPolicySchema>;
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<PrivacyPolicyFormData>({
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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:shield-account-outline' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('privacyPolicy.title')}</p>
<p className='text-muted-foreground text-sm'>{t('privacyPolicy.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('privacyPolicy.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='privacy-policy-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='content'
render={({ field }) => (
<FormItem>
<FormLabel>{t('privacyPolicy.title')}</FormLabel>
<FormControl>
<MarkdownEditor value={field.value || ''} onChange={field.onChange} />
</FormControl>
<FormDescription>{t('privacyPolicy.description')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='privacy-policy-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -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<typeof siteSchema>;
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<SiteFormData>({
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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:web' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('site.title')}</p>
<p className='text-muted-foreground text-sm'>{t('site.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('site.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form id='site-form' onSubmit={form.handleSubmit(onSubmit)} className='space-y-2 pt-4'>
<FormField
control={form.control}
name='site_logo'
render={({ field }) => (
<FormItem>
<FormLabel>{t('site.logo')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('site.logoPlaceholder')}
value={field.value}
onValueChange={field.onChange}
suffix={
<UploadImage
className='bg-muted h-9 rounded-none border-none px-2'
onChange={(value) => {
field.onChange(value);
}}
/>
}
/>
</FormControl>
<FormDescription>{t('site.logoDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='site_name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('site.siteName')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('site.siteNamePlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('site.siteNameDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='site_desc'
render={({ field }) => (
<FormItem>
<FormLabel>{t('site.siteDesc')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('site.siteDescPlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('site.siteDescDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='keywords'
render={({ field }) => (
<FormItem>
<FormLabel>{t('site.keywords')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('site.keywordsPlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('site.keywordsDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='custom_html'
render={({ field }) => (
<FormItem>
<FormLabel>{t('site.customHtml')}</FormLabel>
<FormControl>
<Textarea
className='h-32'
placeholder={t('site.customHtmlDescription')}
{...field}
/>
</FormControl>
<FormDescription>{t('site.customHtmlDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='host'
render={({ field }) => (
<FormItem>
<FormLabel>{t('site.siteDomain')}</FormLabel>
<FormControl>
<Textarea
className='h-32'
placeholder={`${t('site.siteDomainPlaceholder')}\nexample.com\nwww.example.com`}
{...field}
/>
</FormControl>
<FormDescription>{t('site.siteDomainDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='custom_data'
render={({ field }) => (
<FormItem>
<FormLabel>{t('site.customData')}</FormLabel>
<FormControl>
<JSONEditor
schema={{
type: 'object',
additionalProperties: true,
properties: {
website: { type: 'string', title: 'Website' },
contacts: {
type: 'object',
title: 'Contacts',
additionalProperties: true,
properties: {
email: { type: 'string', title: 'Email' },
telephone: { type: 'string', title: 'Telephone' },
address: { type: 'string', title: 'Address' },
},
},
community: {
type: 'object',
title: 'Community',
additionalProperties: true,
properties: {
telegram: { type: 'string', title: 'Telegram' },
twitter: { type: 'string', title: 'Twitter' },
discord: { type: 'string', title: 'Discord' },
instagram: { type: 'string', title: 'Instagram' },
linkedin: { type: 'string', title: 'Linkedin' },
facebook: { type: 'string', title: 'Facebook' },
github: { type: 'string', title: 'Github' },
},
},
},
}}
value={field.value}
onBlur={(value) => field.onChange(value)}
/>
</FormControl>
<FormDescription>{t('site.customDataDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='site-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -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<typeof tosSchema>;
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<TosFormData>({
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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:file-document-outline' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('tos.title')}</p>
<p className='text-muted-foreground text-sm'>{t('tos.description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('tos.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form id='tos-form' onSubmit={form.handleSubmit(onSubmit)} className='space-y-2 pt-4'>
<FormField
control={form.control}
name='content'
render={({ field }) => (
<FormItem>
<FormLabel>{t('tos.title')}</FormLabel>
<FormControl>
<MarkdownEditor value={field.value || ''} onChange={field.onChange} />
</FormControl>
<FormDescription>{t('tos.description')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='tos-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -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 (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('accessKey')}</Label>
<p className='text-muted-foreground text-xs'>{t('accessKeyDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.access_key}
onValueBlur={(value) => updateConfig('access_key', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('currencyUnit')}</Label>
<p className='text-muted-foreground text-xs'>{t('currencyUnitDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='USD'
value={data?.currency_unit}
onValueBlur={(value) => updateConfig('currency_unit', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('currencySymbol')}</Label>
<p className='text-muted-foreground text-xs'>{t('currencySymbolDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='$'
value={data?.currency_symbol}
onValueBlur={(value) => updateConfig('currency_symbol', value)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}

View File

@ -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 (
<Tabs defaultValue='site'>
<TabsList className='h-full flex-wrap'>
<TabsTrigger value='site'>{t('tabs.site')}</TabsTrigger>
<TabsTrigger value='currency'>{t('tabs.currency')}</TabsTrigger>
<TabsTrigger value='tos'>{t('tabs.tos')}</TabsTrigger>
<TabsTrigger value='privacy-policy'>{t('privacy-policy.title')}</TabsTrigger>
</TabsList>
<TabsContent value='site'>
<Site />
</TabsContent>
<TabsContent value='currency'>
<Currency />
</TabsContent>
<TabsContent value='tos'>
<Tos />
</TabsContent>
<TabsContent value='privacy-policy'>
<PrivacyPolicy />
</TabsContent>
</Tabs>
<div className='space-y-8'>
{formSections.map((section, sectionIndex) => (
<div key={sectionIndex}>
<h2 className='mb-4 text-lg font-semibold'>{section.title}</h2>
<Table>
<TableBody>
{section.forms.map((form, formIndex) => {
const FormComponent = form.component;
return (
<TableRow key={formIndex}>
<TableCell>
<FormComponent />
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
))}
</div>
);
}

View File

@ -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 && (
<div className='h-[calc(100dvh-132px-env(safe-area-inset-top))] overflow-hidden'>
<MarkdownEditor
title={t('title')}
value={data?.privacy_policy}
onBlur={(value) => {
if (data?.privacy_policy !== value) {
updateConfig('privacy_policy', value);
}
}}
/>
</div>
)
);
}

View File

@ -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<API.SiteConfig | undefined>(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 (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('logo')}</Label>
<p className='text-muted-foreground text-xs'>{t('logoDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('logoPlaceholder')}
value={data?.site_logo}
onValueBlur={(value) => updateConfig('site_logo', value)}
suffix={
<UploadImage
className='bg-muted h-9 rounded-none border-none px-2'
onChange={(value) => {
updateConfig('site_logo', value);
}}
/>
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('siteName')}</Label>
<p className='text-muted-foreground text-xs'>{t('siteNameDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('siteNamePlaceholder')}
value={data?.site_name}
onValueBlur={(value) => updateConfig('site_name', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('siteDesc')}</Label>
<p className='text-muted-foreground text-xs'>{t('siteDescDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('siteDescPlaceholder')}
value={data?.site_desc}
onValueBlur={(value) => updateConfig('site_desc', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('keywords')}</Label>
<p className='text-muted-foreground text-xs'>{t('keywordsDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('keywordsDescription')}
value={data?.keywords}
onValueBlur={(value) => updateConfig('keywords', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('customHtml')}</Label>
<p className='text-muted-foreground text-xs'>{t('customHtmlDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
className='h-52'
placeholder={t('customHtmlDescription')}
defaultValue={data?.custom_html}
onBlur={(e) => {
updateConfig('custom_html', e.target.value);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('siteDomain')}</Label>
<p className='text-muted-foreground text-xs'>{t('siteDomainDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
className='h-52'
placeholder={`${t('siteDomainPlaceholder')}\nexample.com\nwww.example.com`}
defaultValue={data?.host}
onBlur={(e) => {
updateConfig('host', e.target.value);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('customData')}</Label>
<p className='text-muted-foreground text-xs'>{t('customDataDescription')}</p>
</TableCell>
<TableCell className='w-1/2 text-right'>
<JSONEditor
schema={{
type: 'object',
additionalProperties: true,
properties: {
website: { type: 'string', title: 'Website' },
contacts: {
type: 'object',
title: 'Contacts',
additionalProperties: true,
properties: {
email: { type: 'string', title: 'Email' },
telephone: { type: 'string', title: 'Telephone' },
address: { type: 'string', title: 'Address' },
},
},
community: {
type: 'object',
title: 'Community',
additionalProperties: true,
properties: {
telegram: { type: 'string', title: 'Telegram' },
twitter: { type: 'string', title: 'Twitter' },
discord: { type: 'string', title: 'Discord' },
instagram: { type: 'string', title: 'Instagram' },
linkedin: { type: 'string', title: 'Linkedin' },
facebook: { type: 'string', title: 'Facebook' },
github: { type: 'string', title: 'Github' },
},
},
},
}}
value={data?.custom_data}
onBlur={(value) => updateConfig('custom_data', value)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}

View File

@ -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 && (
<div className='h-[calc(100dvh-132px-env(safe-area-inset-top))] overflow-hidden'>
<MarkdownEditor
title={t('title')}
value={data?.tos_content}
onBlur={(value) => {
if (data?.tos_content !== value) {
updateConfig('tos_content', value);
}
}}
/>
</div>
)
);
}

View File

@ -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<typeof inviteSchema>;
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<InviteFormData>({
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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:account-multiple-plus-outline' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('title')}</p>
<p className='text-muted-foreground text-sm'>{t('description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='invite-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='forced_invite'
render={({ field }) => (
<FormItem>
<FormLabel>{t('forcedInvite')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('forcedInviteDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='referral_percentage'
render={({ field }) => (
<FormItem>
<FormLabel>{t('referralPercentage')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={field.value}
type='number'
min={0}
max={100}
suffix='%'
onValueBlur={(value) => field.onChange(Number(value))}
/>
</FormControl>
<FormDescription>{t('referralPercentageDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='only_first_purchase'
render={({ field }) => (
<FormItem>
<FormLabel>{t('onlyFirstPurchase')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('onlyFirstPurchaseDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{systemT('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='invite-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{systemT('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -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<typeof registerSchema>;
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<RegisterFormData>({
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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:account-plus-outline' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('title')}</p>
<p className='text-muted-foreground text-sm'>{t('description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='register-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='stop_register'
render={({ field }) => (
<FormItem>
<FormLabel>{t('stopNewUserRegistration')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('stopNewUserRegistrationDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='enable_ip_register_limit'
render={({ field }) => (
<FormItem>
<FormLabel>{t('ipRegistrationLimit')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('ipRegistrationLimitDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='ip_register_limit_count'
render={({ field }) => (
<FormItem>
<FormLabel>{t('registrationLimitCount')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={field.value}
type='number'
min={1}
onValueBlur={(value) => field.onChange(Number(value))}
/>
</FormControl>
<FormDescription>{t('registrationLimitCountDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='ip_register_limit_expire_day'
render={({ field }) => (
<FormItem>
<FormLabel>{t('registrationLimitExpire')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={field.value}
type='number'
min={1}
suffix={t('day')}
onValueBlur={(value) => field.onChange(Number(value))}
/>
</FormControl>
<FormDescription>{t('registrationLimitExpireDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='trial_flow'
render={({ field }) => (
<FormItem>
<FormLabel>{t('trialFlow')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={field.value}
type='number'
min={0}
suffix='GB'
onValueBlur={(value) => field.onChange(Number(value))}
/>
</FormControl>
<FormDescription>{t('trialFlowDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='trial_day'
render={({ field }) => (
<FormItem>
<FormLabel>{t('trialDay')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={field.value}
type='number'
min={0}
suffix={t('day')}
onValueBlur={(value) => field.onChange(Number(value))}
/>
</FormControl>
<FormDescription>{t('trialDayDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='default_subscribe_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('defaultSubscribe')}</FormLabel>
<FormControl>
<Combobox
placeholder={t('selectPlaceholder')}
value={field.value}
onChange={(value: number) => {
if (value) {
field.onChange(value);
}
}}
options={
subscribe?.map((item) => ({
label: item.name,
value: item.id,
})) || []
}
/>
</FormControl>
<FormDescription>{t('defaultSubscribeDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{systemT('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='register-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{systemT('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -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<typeof verifyCodeSchema>;
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<VerifyCodeFormData>({
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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:message-text-clock-outline' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('title')}</p>
<p className='text-muted-foreground text-sm'>{t('description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='verify-code-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='verify_code_expire_time'
render={({ field }) => (
<FormItem>
<FormLabel>{t('expireTime')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={field.value}
type='number'
min={60}
suffix={t('seconds')}
onValueBlur={(value) => field.onChange(Number(value))}
/>
</FormControl>
<FormDescription>{t('expireTimeDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='verify_code_interval'
render={({ field }) => (
<FormItem>
<FormLabel>{t('interval')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={field.value}
type='number'
min={30}
suffix={t('seconds')}
onValueBlur={(value) => field.onChange(Number(value))}
/>
</FormControl>
<FormDescription>{t('intervalDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='verify_code_limit'
render={({ field }) => (
<FormItem>
<FormLabel>{t('dailyLimit')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={field.value}
type='number'
min={1}
suffix={t('times')}
onValueBlur={(value) => field.onChange(Number(value))}
/>
</FormControl>
<FormDescription>{t('dailyLimitDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{systemT('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='verify-code-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{systemT('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -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<typeof verifySchema>;
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<VerifyFormData>({
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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:shield-check-outline' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('title')}</p>
<p className='text-muted-foreground text-sm'>{t('description')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='verify-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-2 pt-4'
>
<FormField
control={form.control}
name='turnstile_site_key'
render={({ field }) => (
<FormItem>
<FormLabel>{t('turnstileSiteKey')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('turnstileSiteKeyPlaceholder')}
value={field.value}
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('turnstileSiteKeyDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='turnstile_secret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('turnstileSecret')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('turnstileSecretPlaceholder')}
value={field.value}
type='password'
onValueChange={field.onChange}
/>
</FormControl>
<FormDescription>{t('turnstileSecretDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='enable_register_verify'
render={({ field }) => (
<FormItem>
<FormLabel>{t('enableRegisterVerify')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('enableRegisterVerifyDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='enable_login_verify'
render={({ field }) => (
<FormItem>
<FormLabel>{t('enableLoginVerify')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('enableLoginVerifyDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='enable_password_verify'
render={({ field }) => (
<FormItem>
<FormLabel>{t('enablePasswordVerify')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
className='float-end !mt-0'
/>
</FormControl>
<FormDescription>{t('enablePasswordVerifyDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{systemT('common.cancel')}
</Button>
<Button disabled={loading} type='submit' form='verify-form'>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{systemT('common.save')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -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 }) => <IpLink ip={row.getValue('login_ip')} />,
},
{
accessorKey: 'user_agent',

View File

@ -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 }) => <IpLink ip={row.getValue('ip')} />,
},
{
accessorKey: 'user_agent',
@ -156,6 +158,7 @@ export function SubscriptionDetail({
{
accessorKey: 'ip',
header: 'IP',
cell: ({ row }) => <IpLink ip={row.getValue('ip')} />,
},
{
accessorKey: 'online',

View File

@ -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 (
<HoverCard>
<HoverCardTrigger asChild onMouseEnter={() => setShouldFetch(true)}>
<HoverCardTrigger asChild>
<Button variant='link' className='p-0' asChild>
<Link href={`/dashboard/user/${id}`}>
{data?.auth_methods[0]?.auth_identifier || t('loading')}

View File

@ -0,0 +1,27 @@
'use client';
import { ExternalLink } from 'lucide-react';
import React from 'react';
interface IpLinkProps {
ip: string;
children?: React.ReactNode;
className?: string;
target?: '_blank' | '_self';
}
export function IpLink({ ip, children, className = '', target = '_blank' }: IpLinkProps) {
const url = `https://ip.sb/ip/${ip}`;
return (
<a
href={url}
target={target}
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
className={`text-primary hover:text-primary/80 inline-flex items-center gap-1 font-mono transition-colors hover:underline ${className}`}
>
{children || ip}
<ExternalLink className='h-3 w-3' />
</a>
);
}

View File

@ -1,8 +1,4 @@
export const AuthControl = [
{
title: 'General',
url: '/dashboard/auth-control/general',
},
{
title: 'Email',
url: '/dashboard/auth-control/email',

View File

@ -1,16 +0,0 @@
{
"clientId": "ID služby",
"clientIdDescription": "Apple Service ID, který můžete získat z Apple Developer Portálu",
"clientSecret": "Soukromý klíč",
"clientSecretDescription": "Obsah soukromého klíče (.p8 soubor) používaný pro autentizaci s Apple",
"enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého Apple ID",
"keyId": "ID klíče",
"keyIdDescription": "ID vašeho soukromého klíče z Apple Developer Portálu",
"redirectUri": "Přesměrovací URL",
"redirectUriDescription": "Prosím vyplňte adresu API přesměrované URL po úspěšném ověření pomocí Apple. Nepoužívejte / na konci.",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné",
"teamId": "ID týmu",
"teamIdDescription": "ID týmu Apple Developer"
}

View File

@ -1,14 +1,149 @@
{
"invite": {
"commissionFirstTimeOnly": "Provize pouze za první nákup",
"commissionFirstTimeOnlyDescription": "Pokud je povoleno, provize se generuje pouze při první platbě pozývatele; jednotlivé uživatele můžete konfigurovat v uživatelské správě",
"enableForcedInvite": "Povolit nucené pozvání",
"enableForcedInviteDescription": "Pokud je povoleno, mohou se registrovat pouze pozvaní uživatelé",
"inputPlaceholder": "Zadejte",
"inviteCommissionPercentage": "Procento provize za pozvání",
"inviteCommissionPercentageDescription": "Výchozí globální poměr rozdělení provize; jednotlivé poměry můžete konfigurovat v uživatelské správě",
"inviteSettings": "Nastavení pozvání",
"saveSuccess": "Uložení úspěšné"
"apple": {
"clientId": "ID služby",
"clientIdDescription": "Apple Service ID, který můžete získat z Apple Developer Portálu",
"clientSecret": "Soukromý klíč",
"clientSecretDescription": "Obsah soukromého klíče (.p8 soubor) používaný pro autentizaci s Apple",
"description": "Ověřte uživatele pomocí účtů Apple",
"enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého Apple ID",
"keyId": "ID klíče",
"keyIdDescription": "ID vašeho soukromého klíče z Apple Developer Portálu",
"redirectUri": "Přesměrovací URL",
"redirectUriDescription": "Prosím vyplňte adresu API přesměrované URL po úspěšném ověření pomocí Apple. Nepoužívejte / na konci.",
"teamId": "ID týmu",
"teamIdDescription": "ID týmu Apple Developer",
"title": "Přihlášení pomocí Apple"
},
"common": {
"cancel": "Zrušit",
"save": "Uložit",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Úspěšně uloženo"
},
"communicationMethods": "Communication Methods",
"device": {
"blockVirtualMachine": "Blokovat virtuální stroj",
"blockVirtualMachineDescription": "Pokud je povoleno, zařízení nebudou moci běžet na virtuálních strojích nebo emulátorech",
"communicationKey": "Komunikační klíč",
"communicationKeyDescription": "Komunikační klíč se používá pro bezpečnou komunikaci mezi zařízeními a servery",
"description": "Ověřte uživatele pomocí identifikátorů zařízení",
"enable": "Povolit",
"enableDescription": "Po povolení jsou podporovány různé identifikátory zařízení, jako jsou IMEI/IDFA/IDFV/AndroidID/Mac adresa pro přihlášení a registraci",
"enableSecurity": "Povolit šifrování komunikace",
"enableSecurityDescription": "Pokud je povoleno, komunikace mezi zařízeními a servery bude šifrována",
"showAds": "Zobrazit reklamy",
"showAdsDescription": "Pokud je povoleno, na zařízeních se budou zobrazovat reklamy",
"title": "Ověření zařízení"
},
"deviceAuthMethods": "Device Authentication Methods",
"email": {
"basicSettings": "Základní nastavení",
"description": "Ověřte uživatele pomocí e-mailových adres",
"emailSuffixWhitelist": "Seznam povolených přípon e-mailů",
"emailSuffixWhitelistDescription": "Když je povoleno, mohou se registrovat pouze e-maily s příponami v seznamu",
"emailVerification": "Ověření e-mailu",
"emailVerificationDescription": "Když je povoleno, uživatelé budou muset ověřit svůj e-mail",
"enable": "Povolit",
"enableDescription": "Po aktivaci budou povoleny funkce registrace e-mailem, přihlášení, propojení a odpojení.",
"expirationEmailTemplate": "Šablona oznámení o vypršení platnosti",
"expirationTemplate": "Oznámení o vypršení platnosti",
"inputPlaceholder": "Zadejte hodnotu...",
"logs": "Protokoly",
"logsDescription": "Zobrazit historii odeslaných e-mailů a jejich stav",
"maintenanceEmailTemplate": "Šablona oznámení o údržbě",
"maintenanceTemplate": "Oznámení o údržbě",
"sendFailure": "Nepodařilo se odeslat testovací e-mail, zkontrolujte prosím konfiguraci.",
"sendSuccess": "Testovací e-mail byl úspěšně odeslán.",
"sendTestEmail": "Odeslat testovací e-mail",
"sendTestEmailDescription": "Odeslat testovací e-mail pro ověření konfigurace.",
"senderAddress": "Adresa odesílatele",
"senderAddressDescription": "Výchozí e-mailová adresa používaná pro odesílání e-mailů.",
"smtpAccount": "SMTP účet",
"smtpAccountDescription": "E-mailový účet používaný pro ověřování.",
"smtpEncryptionMethod": "Metoda šifrování SMTP",
"smtpEncryptionMethodDescription": "Vyberte, zda povolit šifrování SSL/TLS.",
"smtpPassword": "SMTP heslo",
"smtpPasswordDescription": "Heslo pro účet SMTP.",
"smtpServerAddress": "Adresa SMTP serveru",
"smtpServerAddressDescription": "Zadejte adresu serveru používanou pro odesílání e-mailů.",
"smtpServerPort": "Port SMTP serveru",
"smtpServerPortDescription": "Zadejte port používaný k připojení k SMTP serveru.",
"smtpSettings": "Nastavení SMTP",
"templateVariables": {
"code": {
"description": "6místný ověřovací kód",
"title": "Ověřovací kód"
},
"expire": {
"description": "Doba platnosti ověřovacího kódu (minuty)",
"title": "Doba platnosti"
},
"expireDate": {
"description": "Datum vypršení platnosti služby, připomíná uživatelům, kdy služba vyprší",
"title": "Datum vypršení platnosti"
},
"maintenanceDate": {
"description": "Datum údržby systému, zobrazuje datum zahájení údržby",
"title": "Datum údržby"
},
"maintenanceTime": {
"description": "Odhadovaná doba údržby, zobrazuje časové období nebo trvání údržby",
"title": "Doba údržby"
},
"siteLogo": {
"description": "URL obrázku loga webu",
"title": "Logo webu"
},
"siteName": {
"description": "Aktuální název webu",
"title": "Název webu"
},
"title": "Proměnné šablony e-mailu",
"type": {
"conditionalSyntax": "Podporuje podmínkovou syntaxi pro přepínání obsahu na základě typu",
"description": "Identifikátor typu e-mailu, 1 pro ověřovací kód registrace, ostatní pro ověřovací kód pro resetování hesla",
"title": "Typ e-mailu"
}
},
"title": "Ověření e-mailem",
"trafficExceedEmailTemplate": "Šablona oznámení o překročení limitu provozu",
"trafficTemplate": "Limit provozu",
"verifyEmailTemplate": "Šablona ověřovacího e-mailu",
"verifyTemplate": "Ověřovací e-mail",
"whitelistSuffixes": "Povolené přípony",
"whitelistSuffixesDescription": "Používá se pro ověření e-mailu během registrace; jeden na řádek",
"whitelistSuffixesPlaceholder": "Zadejte přípony e-mailů, každou na samostatný řádek"
},
"facebook": {
"clientId": "ID klienta",
"clientIdDescription": "ID aplikace Facebook z konzole pro vývojáře Facebooku",
"clientSecret": "Klientský tajný klíč",
"clientSecretDescription": "Facebook App Secret z Facebook Developers Console",
"description": "Ověřte uživatele pomocí účtů Facebook",
"enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu na Facebooku",
"title": "Přihlášení pomocí Facebooku"
},
"github": {
"clientId": "ID klienta GitHub",
"clientIdDescription": "ID klienta z nastavení vaší GitHub OAuth aplikace",
"clientSecret": "GitHub klientský tajný klíč",
"clientSecretDescription": "Tajný klíč klienta z nastavení vaší GitHub OAuth aplikace",
"description": "Ověřte uživatele pomocí účtů GitHub",
"enable": "Povolit ověřování GitHub",
"enableDescription": "Povolit uživatelům přihlásit se pomocí jejich účtů na GitHubu",
"title": "Přihlášení pomocí GitHubu"
},
"google": {
"clientId": "ID klienta",
"clientIdDescription": "ID klienta Google OAuth 2.0 z Google Cloud Console",
"clientSecret": "Klientský tajný klíč",
"clientSecretDescription": "Tajný klíč klienta Google OAuth 2.0 z Google Cloud Console",
"description": "Ověřte uživatele pomocí účtů Google",
"enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu Google",
"title": "Přihlášení pomocí Google"
},
"log": {
"content": "Obsah",
@ -23,52 +158,49 @@
"to": "Příjemce",
"updatedAt": "Aktualizováno"
},
"register": {
"day": "Den",
"hour": "Hodina",
"ipRegistrationLimit": "Omezení registrace podle IP",
"ipRegistrationLimitDescription": "Pokud je povoleno, IP adresy splňující pravidla budou omezeny v registraci; mějte na paměti, že určení IP může způsobit problémy kvůli CDN nebo proxy serverům",
"minute": "Minuta",
"month": "Měsíc",
"noLimit": "Bez omezení",
"penaltyTime": "Doba trestu (minuty)",
"penaltyTimeDescription": "Uživatelé musí počkat, až doba trestu vyprší, než se znovu zaregistrují",
"registerSettings": "Nastavení registrace",
"registrationLimitCount": "Počet omezení registrace",
"registrationLimitCountDescription": "Povolit trest po dosažení registračního limitu",
"saveSuccess": "Uložení úspěšné",
"stopNewUserRegistration": "Zastavit registraci nových uživatelů",
"stopNewUserRegistrationDescription": "Pokud je povoleno, nikdo se nemůže registrovat",
"trialDuration": "Doba zkušební verze",
"trialRegistration": "Zkušební registrace",
"trialRegistrationDescription": "Povolit zkušební registraci; nejprve upravte zkušební balíček a dobu trvání",
"trialSubscribePlan": "Plán zkušebního předplatného",
"trialSubscribePlanDescription": "Vyberte plán zkušebního předplatného",
"year": "Rok"
"phone": {
"accessLabel": "Přístup",
"applyPlatform": "Použít platformu",
"description": "Ověřte uživatele pomocí telefonních čísel",
"enable": "Povolit",
"enableTip": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
"endpointLabel": "Koncový bod",
"logs": "Protokoly",
"logsDescription": "Zobrazit historii odeslaných SMS zpráv a jejich stav",
"phoneNumberLabel": "Telefonní číslo",
"placeholders": {
"template": "Váš ověřovací kód je {code}, platný po dobu 5 minut"
},
"platform": "SMS platforma",
"platformConfigTip": "Vyplňte prosím poskytnutou konfiguraci {key}",
"platformTip": "Vyberte prosím platformu pro SMS",
"secretLabel": "Tajný klíč",
"sendFailed": "Odeslání se nezdařilo",
"sendSuccess": "Úspěšně odesláno",
"settings": "Nastavení",
"signNameLabel": "Název podpisu",
"template": "Šablona SMS",
"templateCodeLabel": "Kód šablony",
"templateTip": "Prosím vyplňte SMS šablonu, ponechte {code} uprostřed, jinak SMS funkce nebude fungovat",
"testSms": "Odeslat testovací SMS",
"testSmsPhone": "Zadejte telefonní číslo",
"testSmsTip": "Odeslat testovací SMS pro ověření vaší konfigurace",
"title": "Ověření telefonem",
"updateSuccess": "Aktualizace úspěšná",
"whitelistAreaCode": "Kód oblasti v bílém seznamu",
"whitelistAreaCodeTip": "Zadejte kódy oblastí v bílém seznamu, např. 1, 852, 886, 888",
"whitelistValidation": "Ověření bílého seznamu",
"whitelistValidationTip": "Pokud je povoleno, mohou SMS odesílat pouze kódy oblastí v bílém seznamu"
},
"verify": {
"inputPlaceholder": "Zadejte",
"loginVerificationCode": "Ověřovací kód pro přihlášení",
"loginVerificationCodeDescription": "Ověření člověka během přihlášení",
"registrationVerificationCode": "Ověřovací kód pro registraci",
"registrationVerificationCodeDescription": "Ověření člověka během registrace",
"resetPasswordVerificationCode": "Ověřovací kód pro resetování hesla",
"resetPasswordVerificationCodeDescription": "Ověření člověka během resetování hesla",
"saveSuccess": "Uložení úspěšné",
"turnstileSecretDescription": "Tajný klíč turniketu poskytovaný Cloudflare",
"turnstileSiteKeyDescription": "Klíč pro web turniketu poskytovaný Cloudflare",
"verifySettings": "Nastavení ověření"
},
"verify-code": {
"dailyLimit": "Denní limit",
"dailyLimitDescription": "Maximální počet ověřovacích kódů, které lze odeslat za den",
"expireTime": "Čas vypršení",
"expireTimeDescription": "Doba platnosti ověřovacího kódu (vteřiny)",
"interval": "Interval odesílání",
"intervalDescription": "Minimální interval mezi odesíláním ověřovacích kódů (sekundy)",
"saveSuccess": "Uložení úspěšné",
"second": "sekundy",
"times": "krát",
"verifyCodeSettings": "Nastavení ověřovacího kódu"
"socialAuthMethods": "Social Authentication Methods",
"telegram": {
"clientId": "ID robota",
"clientIdDescription": "ID Telegram bota, které můžete získat od @BotFather",
"clientSecret": "Token robota",
"clientSecretDescription": "Telegram Bot Token, který můžete získat od @BotFather",
"description": "Ověřte uživatele pomocí účtů Telegram",
"enable": "Povolit",
"enableDescription": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
"title": "Přihlášení pomocí Telegramu"
}
}

View File

@ -1,14 +0,0 @@
{
"blockVirtualMachine": "Blokovat virtuální stroj",
"blockVirtualMachineDescription": "Pokud je povoleno, zařízení nebudou moci běžet na virtuálních strojích nebo emulátorech",
"communicationKey": "Komunikační klíč",
"communicationKeyDescription": "Komunikační klíč se používá pro bezpečnou komunikaci mezi zařízeními a servery",
"enable": "Povolit",
"enableDescription": "Po povolení jsou podporovány různé identifikátory zařízení, jako jsou IMEI/IDFA/IDFV/AndroidID/Mac adresa pro přihlášení a registraci",
"enableSecurity": "Povolit šifrování komunikace",
"enableSecurityDescription": "Pokud je povoleno, komunikace mezi zařízeními a servery bude šifrována",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné",
"showAds": "Zobrazit reklamy",
"showAdsDescription": "Pokud je povoleno, na zařízeních se budou zobrazovat reklamy"
}

View File

@ -1,49 +0,0 @@
{
"emailBasicConfigDescription": "Nakonfigurujte nastavení SMTP serveru a možnosti ověření e-mailu",
"emailLogsDescription": "Zobrazit historii odeslaných e-mailů a jejich stav",
"emailSuffixWhitelist": "Seznam povolených přípon e-mailů",
"emailSuffixWhitelistDescription": "Když je povoleno, mohou se registrovat pouze e-maily s příponami v seznamu",
"emailVerification": "Ověření e-mailu",
"emailVerificationDescription": "Když je povoleno, uživatelé budou muset ověřit svůj e-mail",
"enable": "Povolit",
"enableDescription": "Po aktivaci budou povoleny funkce registrace e-mailem, přihlášení, propojení a odpojení.",
"expiration_email_template": "Šablona oznámení o vypršení platnosti",
"expiration_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
"failed": "Neúspěch",
"inputPlaceholder": "Zadejte hodnotu...",
"logs": "Protokoly",
"maintenance_email_template": "Šablona oznámení o údržbě",
"maintenance_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
"recipient": "Příjemce",
"saveFailed": "Nepodařilo se uložit konfiguraci",
"saveSuccess": "Konfigurace byla úspěšně uložena.",
"sendFailure": "Nepodařilo se odeslat testovací e-mail, zkontrolujte prosím konfiguraci.",
"sendSuccess": "Testovací e-mail byl úspěšně odeslán.",
"sendTestEmail": "Odeslat testovací e-mail",
"sendTestEmailDescription": "Odeslat testovací e-mail pro ověření konfigurace.",
"senderAddress": "Adresa odesílatele",
"senderAddressDescription": "Výchozí e-mailová adresa používaná pro odesílání e-mailů.",
"sent": "Odesláno",
"sentAt": "Odesláno",
"settings": "Nastavení",
"smtpAccount": "SMTP účet",
"smtpAccountDescription": "E-mailový účet používaný pro ověřování.",
"smtpEncryptionMethod": "Metoda šifrování SMTP",
"smtpEncryptionMethodDescription": "Vyberte, zda povolit šifrování SSL/TLS.",
"smtpPassword": "SMTP heslo",
"smtpPasswordDescription": "Heslo pro účet SMTP.",
"smtpServerAddress": "Adresa SMTP serveru",
"smtpServerAddressDescription": "Zadejte adresu serveru používanou pro odesílání e-mailů.",
"smtpServerPort": "Port SMTP serveru",
"smtpServerPortDescription": "Zadejte port používaný k připojení k SMTP serveru.",
"status": "Stav",
"subject": "Předmět",
"template": "Šablona",
"traffic_exceed_email_template": "Šablona oznámení o překročení provozu",
"traffic_exceed_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
"verify_email_template": "Šablona ověřovacího e-mailu",
"verify_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
"whitelistSuffixes": "Povolené přípony",
"whitelistSuffixesDescription": "Používá se pro ověření e-mailu během registrace; jeden na řádek",
"whitelistSuffixesPlaceholder": "Zadejte přípony e-mailů, každou na samostatný řádek"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "ID klienta",
"clientIdDescription": "ID aplikace Facebook z konzole pro vývojáře Facebooku",
"clientSecret": "Klientský tajný klíč",
"clientSecretDescription": "Facebook App Secret z Facebook Developers Console",
"enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu na Facebooku",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "ID klienta GitHub",
"clientIdDescription": "ID klienta z nastavení vaší GitHub OAuth aplikace",
"clientSecret": "GitHub klientský tajný klíč",
"clientSecretDescription": "Tajný klíč klienta z nastavení vaší GitHub OAuth aplikace",
"enable": "Povolit ověřování GitHub",
"enableDescription": "Povolit uživatelům přihlásit se pomocí jejich účtů na GitHubu",
"saveFailed": "Nepodařilo se uložit nastavení GitHubu",
"saveSuccess": "Nastavení GitHubu bylo úspěšně uloženo"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "ID klienta",
"clientIdDescription": "ID klienta Google OAuth 2.0 z Google Cloud Console",
"clientSecret": "Klientský tajný klíč",
"clientSecretDescription": "Tajný klíč klienta Google OAuth 2.0 z Google Cloud Console",
"enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu Google",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné"
}

View File

@ -1,32 +0,0 @@
{
"accessLabel": "Přístup",
"applyPlatform": "Použít platformu",
"enable": "Povolit",
"enableTip": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
"endpointLabel": "Koncový bod",
"logs": "Protokoly",
"phoneNumberLabel": "Telefonní číslo",
"placeholders": {
"template": "Váš ověřovací kód je {code}, platný po dobu 5 minut",
"templateCode": "Zadejte kód šablony"
},
"platform": "SMS platforma",
"platformConfigTip": "Vyplňte prosím poskytnutou konfiguraci {key}",
"platformTip": "Vyberte prosím platformu pro SMS",
"secretLabel": "Tajný klíč",
"sendFailed": "Odeslání se nezdařilo",
"sendSuccess": "Úspěšně odesláno",
"settings": "Nastavení",
"signNameLabel": "Název podpisu",
"template": "Šablona SMS",
"templateCodeLabel": "Kód šablony",
"templateTip": "Prosím vyplňte SMS šablonu, ponechte {code} uprostřed, jinak SMS funkce nebude fungovat",
"testSms": "Odeslat testovací SMS",
"testSmsPhone": "Zadejte telefonní číslo",
"testSmsTip": "Odeslat testovací SMS pro ověření vaší konfigurace",
"updateSuccess": "Aktualizace úspěšná",
"whitelistAreaCode": "Kód oblasti v bílém seznamu",
"whitelistAreaCodeTip": "Zadejte kódy oblastí v bílém seznamu, např. 1, 852, 886, 888",
"whitelistValidation": "Ověření bílého seznamu",
"whitelistValidationTip": "Pokud je povoleno, mohou SMS odesílat pouze kódy oblastí v bílém seznamu"
}

View File

@ -8,9 +8,12 @@
"createRule": "Přidat pravidlo",
"createSuccess": "Pravidlo bylo úspěšně vytvořeno",
"createdAt": "Vytvořeno dne",
"default": "Výchozí",
"defaultRule": "Výchozí pravidlo",
"delete": "Smazat",
"deleteSuccess": "Pravidlo bylo úspěšně smazáno",
"deleteWarning": "Opravdu chcete smazat toto pravidlo? Tato akce není vratná.",
"direct": "Přímý",
"downloadTemplate": "Stáhnout šablonu",
"edit": "Upravit",
"editRule": "Upravit pravidlo",
@ -29,11 +32,14 @@
"noValidRules": "Nebyly nalezeny žádné platné pravidla",
"pleaseUploadFile": "Prosím, nahrajte soubor YAML",
"preview": "Náhled",
"reject": "Odmítnout",
"rulesFormat": "Formát pravidla: typ pravidla, obsah shody, [politika], kde politika je volitelná.\nPokud politika není specifikována, bude automaticky použito aktuální jméno skupiny pravidel. Příklady:",
"rulesLabel": "Obsah pravidla",
"searchRule": "Hledat název pravidla",
"selectTags": "Vybrat značky uzlu",
"selectType": "Vyberte typ pravidla",
"tags": "Značky uzlu",
"tagsLabel": "Značky uzlu",
"type": "Typ pravidla",
"updateSuccess": "Pravidlo bylo úspěšně aktualizováno"
}

View File

@ -33,21 +33,22 @@
"description": "Popis",
"edit": "Upravit",
"editNodeGroup": "Upravit skupinu uzlů",
"form": {
"cancel": "Zrušit",
"confirm": "Potvrdit",
"description": "Popis",
"name": "Název"
},
"name": "Název",
"title": "Seznam skupin uzlů",
"updatedAt": "Aktualizováno"
},
"groupForm": {
"cancel": "Zrušit",
"confirm": "Potvrdit",
"description": "Popis",
"name": "Název"
},
"node": {
"abnormal": "Abnormální",
"actions": "Akce",
"address": "Adresa",
"all": "Vše",
"basicInfo": "Základní informace",
"cancel": "Zrušit",
"confirm": "Potvrdit",
"confirmDelete": "Opravdu chcete smazat?",
@ -59,96 +60,120 @@
"delete": "Smazat",
"deleteSuccess": "Úspěšně smazáno",
"deleteWarning": "Po smazání nebude možné data obnovit. Prosím, buďte opatrní.",
"detail": "Detail",
"disabled": "Zakázáno",
"disk": "Disk",
"edit": "Upravit",
"editNode": "Upravit uzel",
"enable": "Povolit",
"form": {
"allowInsecure": "Povolit nezabezpečené",
"cancel": "Zrušit",
"city": "Město",
"confirm": "Potvrdit",
"country": "Země",
"edit": "Upravit",
"editSecurity": "Upravit nastavení zabezpečení",
"enableTLS": "Povolit TLS",
"encryptionMethod": "Metoda šifrování",
"flow": "Algoritmus řízení toku",
"groupId": "Skupina uzlů",
"hopInterval": "Interval skoků",
"hopPorts": "Porty skoků",
"hopPortsPlaceholder": "Více portů oddělených čárkou",
"name": "Název",
"obfsPassword": "Heslo pro zmatení",
"obfsPasswordPlaceholder": "Nechte prázdné, pokud nechcete zmást",
"path": "Cesta",
"pleaseSelect": "Prosím vyberte",
"port": "Port služby",
"protocol": "Protokol",
"relayHost": "Adresa přenosu",
"relayMode": "Režim relé",
"relayModeOptions": {
"all": "Vše",
"none": "Žádný",
"random": "Náhodný"
},
"relayPort": "Port přenosu",
"relayPrefix": "Předpona relé",
"remarks": "Poznámky",
"security": "Zabezpečení",
"securityConfig": "Nastavení zabezpečení",
"security_config": {
"fingerprint": "FingerPrint",
"privateKey": "Soukromý klíč",
"privateKeyPlaceholder": "Nechte prázdné pro automatické generování",
"publicKey": "Veřejný klíč",
"publicKeyPlaceholder": "Nechte prázdné pro automatické generování",
"serverAddress": "Adresa serveru",
"serverAddressPlaceholder": "Cílová adresa REALITY, výchozí je SNI",
"serverName": "Název serveru (SNI)",
"serverNamePlaceholder": "REALITY je povinné, musí být shodné s backendem",
"serverPort": "Port serveru",
"serverPortPlaceholder": "Cílový port REALITY, výchozí 443",
"shortId": "ShortId",
"shortIdPlaceholder": "Nechte prázdné pro automatické generování",
"sni": "Indikace názvu serveru (SNI)"
},
"selectEncryptionMethod": "Vyberte metodu šifrování",
"selectNodeGroup": "Vyberte skupinu uzlů",
"selectProtocol": "Vyberte protokol",
"selectRelayMode": "Vyberte režim relé",
"serverAddr": "Adresa serveru",
"serverKey": "Klíč serveru",
"serverName": "Název služby",
"speedLimit": "Omezení rychlosti",
"speedLimitPlaceholder": "Bez omezení",
"tags": "Štítky",
"tagsPlaceholder": "Použijte Enter nebo čárku (,) pro zadání více štítků",
"trafficRatio": "Poměr provozu",
"transport": "Přenosový protokol",
"transportConfig": "Nastavení přenosového protokolu",
"transportHost": "Adresa přenosové služby",
"transportPath": "Cesta přenosu",
"transportServerName": "Název přenosové služby"
},
"enabled": "Povoleno",
"expireTime": "Čas vypršení",
"hide": "Skrýt",
"id": "ID",
"ipAddresses": "IP adresy",
"lastUpdated": "Poslední aktualizace",
"location": "Umístění",
"memory": "Paměť",
"name": "Název",
"noData": "--",
"node": "Uzel",
"nodeDetail": "Detail uzlu",
"nodeGroup": "Skupina uzlů",
"nodeStatus": "Stav uzlu",
"normal": "Normální",
"onlineCount": "Počet online",
"onlineUsers": "Online uživatelé",
"protocol": "Protokol",
"rate": "Rychlost",
"relay": "Přenos",
"serverAddr": "Adresa serveru",
"speedLimit": "Omezení rychlosti",
"status": "Stav",
"subscribeId": "ID předplatného",
"subscribeName": "Název předplatného",
"subscription": "Předplatné",
"tags": "Štítky",
"trafficRatio": "Poměr provozu",
"trafficUsage": "Využití provozu",
"type": "Typ",
"updateSuccess": "Úspěšně aktualizováno",
"updatedAt": "Čas aktualizace"
"updatedAt": "Čas aktualizace",
"userAccount": "Uživatelský účet",
"userDetail": "Detail uživatele",
"userId": "ID uživatele"
},
"nodeForm": {
"allowInsecure": "Povolit nezabezpečené",
"cancel": "Zrušit",
"city": "Město",
"confirm": "Potvrdit",
"congestionController": "Ovladač přetížení",
"country": "Země",
"disableSni": "Zakázat SNI",
"edit": "Upravit",
"editSecurity": "Upravit bezpečnostní konfiguraci",
"enableTLS": "Povolit TLS",
"encryptionMethod": "Metoda šifrování",
"fingerprint": "Otisk",
"flow": "Algoritmus řízení toku",
"groupId": "Skupina uzlů",
"hopInterval": "Interval skoků",
"hopPorts": "Porty skoků",
"hopPortsPlaceholder": "Oddělte více portů čárkami",
"name": "Název",
"obfsPassword": "Heslo pro obfuskování",
"obfsPasswordPlaceholder": "Nechte prázdné pro žádné obfuskování",
"path": "Cesta",
"pleaseSelect": "Prosím vyberte",
"port": "Port serveru",
"protocol": "Protokol",
"reduceRtt": "Snížit RTT",
"relayHost": "Hostitel relé",
"relayMode": "Režim relé",
"relayPort": "Port relé",
"relayPrefix": "Předpona relé",
"remarks": "Poznámky",
"security": "Bezpečnost",
"securityConfig": "Bezpečnostní konfigurace",
"selectEncryptionMethod": "Vyberte metodu šifrování",
"selectNodeGroup": "Vyberte skupinu uzlů",
"selectProtocol": "Vyberte protokol",
"selectRelayMode": "Vyberte režim relé",
"serverAddr": "Adresa serveru",
"serverKey": "Klíč serveru",
"serverName": "Název služby",
"speedLimit": "Rychlostní limit",
"speedLimitPlaceholder": "Neomezeno",
"tags": "Štítky",
"tagsPlaceholder": "Použijte Enter nebo čárku (,) pro zadání více štítků",
"trafficRatio": "Poměr provozu",
"transport": "Konfigurace transportního protokolu",
"transportConfig": "Konfigurace transportního protokolu",
"transportHost": "Adresa transportního serveru",
"transportPath": "Transportní cesta",
"transportServerName": "Název transportního serveru",
"udpRelayMode": "Režim UDP relé"
},
"relayModeOptions": {
"all": "Vše",
"none": "Žádné",
"random": "Náhodně"
},
"securityConfig": {
"fingerprint": "Otisk",
"privateKey": "Soukromý klíč",
"privateKeyPlaceholder": "Nechte prázdné pro automatické vygenerování",
"publicKey": "Veřejný klíč",
"publicKeyPlaceholder": "Nechte prázdné pro automatické vygenerování",
"serverAddress": "Adresa serveru",
"serverAddressPlaceholder": "Cílová adresa REALITY, výchozí použití SNI",
"serverName": "Název serveru (SNI)",
"serverNamePlaceholder": "REALITY vyžaduje, konzistentní s backendem",
"serverPort": "Port serveru",
"serverPortPlaceholder": "Cílový port REALITY, výchozí 443",
"shortId": "Krátké ID",
"shortIdPlaceholder": "Nechte prázdné pro automatické vygenerování",
"sni": "Indikace názvu serveru (SNI)"
},
"tabs": {
"node": "Uzlu",

View File

@ -1,28 +1,77 @@
{
"authSettings": "Nastavení ověřování",
"basicSettings": "Základní nastavení",
"common": {
"cancel": "Zrušit",
"save": "Uložit nastavení",
"saveFailed": "Uložení selhalo",
"saveSuccess": "Uložení úspěšné"
},
"currency": {
"accessKey": "Klíč",
"accessKeyDescription": "https://exchangerate.host poskytuje zdarma klíč k API směnných kurzů",
"accessKeyDescription": "{url} poskytuje zdarma klíč k API směnných kurzů",
"accessKeyPlaceholder": "Zadejte API klíč",
"currencySymbol": "Symbol měny",
"currencySymbolDescription": "Pouze pro zobrazení, po změně se změní všechny měnové jednotky v systému",
"currencySymbolPlaceholder": "Kč",
"currencyUnit": "Měnová jednotka",
"currencyUnitDescription": "Pouze pro zobrazení, po změně se změní všechny měnové jednotky v systému",
"saveSuccess": "Úspěšně uloženo"
"currencyUnitPlaceholder": "CZK",
"description": "Nastavte jednotky měny, symboly a nastavení API pro směnné kurzy",
"title": "Nastavení měny"
},
"privacy-policy": {
"saveSuccess": "Uložení bylo úspěšné",
"invite": {
"description": "Nastavte pozvání uživatelů a odměny za doporučení",
"forcedInvite": "Vyžadovat pozvánku k registraci",
"forcedInviteDescription": "Pokud je povoleno, uživatelé se musí registrovat prostřednictvím pozvánkového odkazu",
"inputPlaceholder": "Prosím zadejte",
"onlyFirstPurchase": "Odměna pouze za první nákup",
"onlyFirstPurchaseDescription": "Pokud je povoleno, doporučující obdrží odměny pouze za první nákup doporučených uživatelů",
"referralPercentage": "Procento odměny za doporučení",
"referralPercentageDescription": "Procento odměny udělené doporučujícím",
"saveFailed": "Uložení selhalo",
"saveSuccess": "Uložení úspěšné",
"title": "Nastavení pozvánek"
},
"privacyPolicy": {
"description": "Upravte a spravujte obsah zásad ochrany osobních údajů",
"title": "Zásady ochrany osobních údajů"
},
"register": {
"day": "den(dny)",
"defaultSubscribe": "Výchozí předplatné",
"defaultSubscribeDescription": "Výchozí plán předplatného pro nové uživatele",
"description": "Nastavte související nastavení registrace uživatelů",
"inputPlaceholder": "Prosím zadejte",
"ipRegistrationLimit": "Limit registrace podle IP",
"ipRegistrationLimitDescription": "Omezení počtu registrací z jedné IP adresy",
"registrationLimitCount": "Počet registrací v limitu",
"registrationLimitCountDescription": "Počet registrací povolených na IP během limitního období",
"registrationLimitExpire": "Limitní období",
"registrationLimitExpireDescription": "Doba pro limit registrace podle IP",
"saveFailed": "Uložení selhalo",
"saveSuccess": "Uložení úspěšné",
"selectPlaceholder": "Prosím vyberte",
"stopNewUserRegistration": "Zastavit registraci nových uživatelů",
"stopNewUserRegistrationDescription": "Pokud je povoleno, registrace nových uživatelů bude zakázána",
"title": "Nastavení registrace",
"trialDay": "Zkušební dny",
"trialDayDescription": "Zkušební dny poskytnuté novým uživatelům při registraci",
"trialFlow": "Zkušební provoz",
"trialFlowDescription": "Zkušební provoz poskytnutý novým uživatelům při registraci"
},
"site": {
"customData": "Vlastní data",
"customDataDescription": "Vlastní data, používaná pro vlastní data webu",
"customHtml": "Vlastní HTML",
"customHtmlDescription": "Vlastní HTML kód, který bude vložen na konec tagu body na stránce.",
"description": "Nastavte základní informace o webu, logo, doménu a další nastavení",
"keywords": "Klíčová slova",
"keywordsDescription": "Používá se pro SEO účely",
"keywordsPlaceholder": "klíčové slovo1, klíčové slovo2, klíčové slovo3",
"logo": "LOGO",
"logoDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit LOGO",
"logoPlaceholder": "Zadejte URL adresu LOGA, nekončete '/'",
"saveSuccess": "Uložení bylo úspěšné",
"siteDesc": "Popis webu",
"siteDescDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit popis webu",
"siteDescPlaceholder": "Zadejte popis webu",
@ -31,15 +80,47 @@
"siteDomainPlaceholder": "Zadejte adresu domény, pro více domén použijte jeden řádek pro každou",
"siteName": "Název webu",
"siteNameDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit název webu",
"siteNamePlaceholder": "Zadejte název webu"
},
"tabs": {
"currency": "Měna",
"site": "Stránka",
"tos": "Podmínky služby"
"siteNamePlaceholder": "Zadejte název webu",
"title": "Nastavení webu"
},
"siteSettings": "Nastavení webu",
"tos": {
"saveSuccess": "Uložení bylo úspěšné",
"description": "Upravte a spravujte obsah podmínek služby",
"title": "Podmínky služby"
},
"userSecuritySettings": "Uživatel a zabezpečení",
"verify": {
"description": "Nastavte Turnstile CAPTCHA a ověřovací nastavení",
"enableLoginVerify": "Povolit ověření při přihlášení",
"enableLoginVerifyDescription": "Pokud je povoleno, uživatelé musí projít ověřením člověka při přihlášení",
"enablePasswordVerify": "Povolit ověření při resetování hesla",
"enablePasswordVerifyDescription": "Pokud je povoleno, uživatelé musí projít ověřením člověka při resetování hesla",
"enableRegisterVerify": "Povolit ověření při registraci",
"enableRegisterVerifyDescription": "Pokud je povoleno, uživatelé musí projít ověřením člověka při registraci",
"inputPlaceholder": "Prosím zadejte",
"saveFailed": "Uložení selhalo",
"saveSuccess": "Uložení úspěšné",
"title": "Ověření zabezpečení",
"turnstileSecret": "Tajný klíč Turnstile",
"turnstileSecretDescription": "Tajný klíč Cloudflare Turnstile pro ověření na backendu",
"turnstileSecretPlaceholder": "Zadejte tajný klíč Turnstile",
"turnstileSiteKey": "Klíč webu Turnstile",
"turnstileSiteKeyDescription": "Klíč webu Cloudflare Turnstile pro ověření na frontendu",
"turnstileSiteKeyPlaceholder": "Zadejte klíč webu Turnstile"
},
"verifyCode": {
"dailyLimit": "Denní limit odesílání",
"dailyLimitDescription": "Maximální počet ověřovacích kódů, které může každý uživatel odeslat za den",
"description": "Nastavte pravidla a limity pro odesílání ověřovacích kódů e-mailem",
"expireTime": "Platnost ověřovacího kódu",
"expireTimeDescription": "Doba platnosti ověřovacích kódů (vteřiny)",
"inputPlaceholder": "Prosím zadejte",
"interval": "Interval odesílání",
"intervalDescription": "Minimální interval mezi dvěma odesláními ověřovacího kódu (vteřiny)",
"saveFailed": "Uložení selhalo",
"saveSuccess": "Uložení úspěšné",
"seconds": "vteřiny",
"times": "krát",
"title": "Nastavení ověřovacího kódu"
}
}

View File

@ -1,10 +0,0 @@
{
"clientId": "ID robota",
"clientIdDescription": "ID Telegram bota, které můžete získat od @BotFather",
"clientSecret": "Token robota",
"clientSecretDescription": "Telegram Bot Token, který můžete získat od @BotFather",
"enable": "Povolit",
"enableDescription": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné"
}

View File

@ -1,16 +0,0 @@
{
"clientId": "Dienst-ID",
"clientIdDescription": "Apple-Dienst-ID, die Sie im Apple Developer Portal erhalten können",
"clientSecret": "Privater Schlüssel",
"clientSecretDescription": "Der private Schlüsselinhalt (.p8-Datei), der für die Authentifizierung bei Apple verwendet wird",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrer Apple-ID anmelden",
"keyId": "Schlüssel-ID",
"keyIdDescription": "Die ID Ihres privaten Schlüssels aus dem Apple Developer Portal",
"redirectUri": "Weiterleitungs-URL",
"redirectUriDescription": "Bitte geben Sie die API-Adresse der umgeleiteten URL ein, nachdem die Apple-Authentifizierung erfolgreich abgeschlossen wurde. Verwenden Sie kein / am Ende.",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert",
"teamId": "Team-ID",
"teamIdDescription": "Apple Developer-Team-ID"
}

View File

@ -1,15 +1,150 @@
{
"invite": {
"commissionFirstTimeOnly": "Provision nur beim ersten Kauf",
"commissionFirstTimeOnlyDescription": "Wenn aktiviert, wird die Provision nur bei der ersten Zahlung des Einladers generiert; individuelle Benutzer können in der Benutzerverwaltung konfiguriert werden",
"enableForcedInvite": "Erzwinge Einladung",
"enableForcedInviteDescription": "Wenn aktiviert, können sich nur eingeladene Benutzer registrieren",
"inputPlaceholder": "Eingeben",
"inviteCommissionPercentage": "Einladungsprovisionssatz",
"inviteCommissionPercentageDescription": "Standardmäßiges globales Provisionsverteilungsschema; individuelle Sätze können in der Benutzerverwaltung konfiguriert werden",
"inviteSettings": "Einladungseinstellungen",
"apple": {
"clientId": "Dienst-ID",
"clientIdDescription": "Apple-Dienst-ID, die Sie im Apple Developer Portal erhalten können",
"clientSecret": "Privater Schlüssel",
"clientSecretDescription": "Der private Schlüsselinhalt (.p8-Datei), der für die Authentifizierung bei Apple verwendet wird",
"description": "Benutzer mit Apple-Konten authentifizieren",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrer Apple-ID anmelden",
"keyId": "Schlüssel-ID",
"keyIdDescription": "Die ID Ihres privaten Schlüssels aus dem Apple Developer Portal",
"redirectUri": "Weiterleitungs-URL",
"redirectUriDescription": "Bitte geben Sie die API-Adresse der umgeleiteten URL ein, nachdem die Apple-Authentifizierung erfolgreich abgeschlossen wurde. Verwenden Sie kein / am Ende.",
"teamId": "Team-ID",
"teamIdDescription": "Apple Developer-Team-ID",
"title": "Apple-Anmeldung"
},
"common": {
"cancel": "Abbrechen",
"save": "Speichern",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert"
},
"communicationMethods": "Communication Methods",
"device": {
"blockVirtualMachine": "Virtuelle Maschine blockieren",
"blockVirtualMachineDescription": "Wenn aktiviert, wird verhindert, dass Geräte auf virtuellen Maschinen oder Emulatoren ausgeführt werden.",
"communicationKey": "Kommunikationsschlüssel",
"communicationKeyDescription": "Der Kommunikationsschlüssel wird für die sichere Kommunikation zwischen Geräten und Servern verwendet.",
"description": "Benutzer mit Gerätekennungen authentifizieren",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden mehrere Geräteidentifikatoren wie IMEI/IDFA/IDFV/AndroidID/Mac-Adresse für die Anmeldung und Registrierung unterstützt.",
"enableSecurity": "Kommunikationsverschlüsselung aktivieren",
"enableSecurityDescription": "Wenn aktiviert, wird die Kommunikation zwischen Geräten und Servern verschlüsselt.",
"showAds": "Werbung anzeigen",
"showAdsDescription": "Wenn aktiviert, werden auf den Geräten Anzeigen angezeigt.",
"title": "Geräteauthentifizierung"
},
"deviceAuthMethods": "Device Authentication Methods",
"email": {
"basicSettings": "Grundeinstellungen",
"description": "Benutzer mit E-Mail-Adressen authentifizieren",
"emailSuffixWhitelist": "E-Mail-Suffix-Whitelist",
"emailSuffixWhitelistDescription": "Wenn aktiviert, können sich nur E-Mails mit Suffixen aus der Liste registrieren",
"emailVerification": "E-Mail-Verifizierung",
"emailVerificationDescription": "Wenn aktiviert, müssen Benutzer ihre E-Mail-Adresse verifizieren",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden die Funktionen zur E-Mail-Registrierung, Anmeldung, Bindung und Entbindung aktiviert.",
"expirationEmailTemplate": "Vorlage für Ablaufbenachrichtigung",
"expirationTemplate": "Ablaufbenachrichtigung",
"inputPlaceholder": "Wert eingeben...",
"logs": "Protokolle",
"logsDescription": "Verlauf der gesendeten E-Mails und deren Status anzeigen",
"maintenanceEmailTemplate": "Vorlage für Wartungsbenachrichtigung",
"maintenanceTemplate": "Wartungsbenachrichtigung",
"sendFailure": "Senden der Test-E-Mail fehlgeschlagen, bitte überprüfen Sie die Konfiguration.",
"sendSuccess": "Test-E-Mail erfolgreich gesendet.",
"sendTestEmail": "Test-E-Mail senden",
"sendTestEmailDescription": "Senden Sie eine Test-E-Mail, um die Konfiguration zu überprüfen.",
"senderAddress": "Absenderadresse",
"senderAddressDescription": "Die standardmäßige E-Mail-Adresse, die zum Versenden von E-Mails verwendet wird.",
"smtpAccount": "SMTP-Konto",
"smtpAccountDescription": "Das E-Mail-Konto, das für die Authentifizierung verwendet wird.",
"smtpEncryptionMethod": "SMTP-Verschlüsselungsmethode",
"smtpEncryptionMethodDescription": "Wählen Sie, ob SSL/TLS-Verschlüsselung aktiviert werden soll.",
"smtpPassword": "SMTP-Passwort",
"smtpPasswordDescription": "Das Passwort für das SMTP-Konto.",
"smtpServerAddress": "SMTP-Server-Adresse",
"smtpServerAddressDescription": "Geben Sie die Serveradresse an, die zum Senden von E-Mails verwendet wird.",
"smtpServerPort": "SMTP-Server-Port",
"smtpServerPortDescription": "Geben Sie den Port an, der für die Verbindung zum SMTP-Server verwendet wird.",
"smtpSettings": "SMTP-Einstellungen",
"templateVariables": {
"code": {
"description": "6-stelliger Bestätigungscode",
"title": "Bestätigungscode"
},
"expire": {
"description": "Gültigkeitszeit des Bestätigungscodes (Minuten)",
"title": "Gültigkeitsdauer"
},
"expireDate": {
"description": "Ablaufdatum des Dienstes, erinnert Benutzer daran, wann der Dienst abläuft",
"title": "Ablaufdatum"
},
"maintenanceDate": {
"description": "Datum der Systemwartung, zeigt das Startdatum der Wartung an",
"title": "Wartungsdatum"
},
"maintenanceTime": {
"description": "Geschätzte Wartungszeit, zeigt den Wartungszeitraum oder die Dauer an",
"title": "Wartungsdauer"
},
"siteLogo": {
"description": "URL des Website-Logo-Bildes",
"title": "Website-Logo"
},
"siteName": {
"description": "Aktueller Website-Name",
"title": "Website-Name"
},
"title": "E-Mail-Vorlagenvariablen",
"type": {
"conditionalSyntax": "Unterstützt bedingte Syntax zum Wechseln des Inhalts basierend auf dem Typ",
"description": "E-Mail-Typ-Identifikator, 1 für Registrierungsbestätigungscode, andere für Passwortzurücksetzungsbestätigungscode",
"title": "E-Mail-Typ"
}
},
"title": "E-Mail-Authentifizierung",
"trafficExceedEmailTemplate": "Vorlage für Verkehrsüberschreitungsbenachrichtigung",
"trafficTemplate": "Verkehrsgrenze",
"verifyEmailTemplate": "Vorlage für Bestätigungs-E-Mail",
"verifyTemplate": "Bestätigungs-E-Mail",
"whitelistSuffixes": "Whitelist-Suffixe",
"whitelistSuffixesDescription": "Wird zur E-Mail-Verifizierung während der Registrierung verwendet; eine pro Zeile",
"whitelistSuffixesPlaceholder": "Geben Sie E-Mail-Suffixe ein, eines pro Zeile"
},
"facebook": {
"clientId": "Kunden-ID",
"clientIdDescription": "Facebook-App-ID aus der Facebook-Entwicklerkonsole",
"clientSecret": "Client-Geheimnis",
"clientSecretDescription": "Facebook-App-Geheimnis aus der Facebook-Entwicklerkonsole",
"description": "Benutzer mit Facebook-Konten authentifizieren",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Facebook-Konto anmelden",
"title": "Facebook-Anmeldung"
},
"github": {
"clientId": "GitHub-Client-ID",
"clientIdDescription": "Die Client-ID aus den Einstellungen Ihrer GitHub-OAuth-Anwendung",
"clientSecret": "GitHub-Clientgeheimnis",
"clientSecretDescription": "Das Client-Geheimnis aus den Einstellungen Ihrer GitHub-OAuth-Anwendung",
"description": "Benutzer mit GitHub-Konten authentifizieren",
"enable": "GitHub-Authentifizierung aktivieren",
"enableDescription": "Ermöglichen Sie Benutzern, sich mit ihren GitHub-Konten anzumelden",
"title": "GitHub-Anmeldung"
},
"google": {
"clientId": "Kunden-ID",
"clientIdDescription": "Google OAuth 2.0-Client-ID aus der Google Cloud Console",
"clientSecret": "Client-Geheimnis",
"clientSecretDescription": "Google OAuth 2.0-Client-Geheimnis aus der Google Cloud Console",
"description": "Benutzer mit Google-Konten authentifizieren",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Google-Konto anmelden",
"title": "Google-Anmeldung"
},
"log": {
"content": "Inhalt",
"createdAt": "Erstellt am",
@ -23,52 +158,49 @@
"to": "Empfänger",
"updatedAt": "Aktualisiert am"
},
"register": {
"day": "Tag",
"hour": "Stunde",
"ipRegistrationLimit": "IP-Registrierungsgrenze",
"ipRegistrationLimitDescription": "Wenn aktiviert, werden IPs, die die Regelanforderungen erfüllen, von der Registrierung ausgeschlossen; beachten Sie, dass die IP-Bestimmung aufgrund von CDNs oder Frontend-Proxys Probleme verursachen kann",
"minute": "Minute",
"month": "Monat",
"noLimit": "Keine Begrenzung",
"penaltyTime": "Strafzeit (Minuten)",
"penaltyTimeDescription": "Benutzer müssen warten, bis die Strafzeit abgelaufen ist, bevor sie sich erneut registrieren",
"registerSettings": "Registrierungseinstellungen",
"registrationLimitCount": "Registrierungsgrenzanzahl",
"registrationLimitCountDescription": "Strafe aktivieren, nachdem die Registrierungsgrenze erreicht wurde",
"saveSuccess": "Erfolgreich gespeichert",
"stopNewUserRegistration": "Neue Benutzerregistrierung stoppen",
"stopNewUserRegistrationDescription": "Wenn aktiviert, kann sich niemand registrieren",
"trialDuration": "Testdauer",
"trialRegistration": "Testregistrierung",
"trialRegistrationDescription": "Testregistrierung aktivieren; Testpaket und -dauer zuerst ändern",
"trialSubscribePlan": "Testabonnement-Plan",
"trialSubscribePlanDescription": "Wählen Sie einen Testabonnement-Plan aus",
"year": "Jahr"
"phone": {
"accessLabel": "Zugriff",
"applyPlatform": "Plattform anwenden",
"description": "Benutzer mit Telefonnummern authentifizieren",
"enable": "Aktivieren",
"enableTip": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
"endpointLabel": "Endpunkt",
"logs": "Protokolle",
"logsDescription": "Verlauf der gesendeten SMS-Nachrichten und deren Status anzeigen",
"phoneNumberLabel": "Telefonnummer",
"placeholders": {
"template": "Ihr Bestätigungscode ist {code}, gültig für 5 Minuten"
},
"platform": "SMS-Plattform",
"platformConfigTip": "Bitte füllen Sie die bereitgestellte {key}-Konfiguration aus",
"platformTip": "Bitte wählen Sie die SMS-Plattform aus",
"secretLabel": "Geheimnis",
"sendFailed": "Senden fehlgeschlagen",
"sendSuccess": "Erfolgreich gesendet",
"settings": "Einstellungen",
"signNameLabel": "Signaturname",
"template": "SMS-Vorlage",
"templateCodeLabel": "Vorlagen-Code",
"templateTip": "Bitte füllen Sie die SMS-Vorlage aus, lassen Sie {code} in der Mitte, andernfalls funktioniert die SMS-Funktion nicht.",
"testSms": "Test-SMS senden",
"testSmsPhone": "Telefonnummer eingeben",
"testSmsTip": "Senden Sie eine Test-SMS, um Ihre Konfiguration zu überprüfen",
"title": "Telefon-Authentifizierung",
"updateSuccess": "Aktualisierung erfolgreich",
"whitelistAreaCode": "Whitelist Bereichscode",
"whitelistAreaCodeTip": "Bitte geben Sie die Whitelist Bereichscodes ein, z.B. 1, 852, 886, 888",
"whitelistValidation": "Whitelist Überprüfung",
"whitelistValidationTip": "Wenn aktiviert, können nur Bereichscodes in der Whitelist SMS senden"
},
"verify": {
"inputPlaceholder": "Eingeben",
"loginVerificationCode": "Anmeldebestätigungscode",
"loginVerificationCodeDescription": "Menschliche Überprüfung während der Anmeldung",
"registrationVerificationCode": "Registrierungsbestätigungscode",
"registrationVerificationCodeDescription": "Menschliche Überprüfung während der Registrierung",
"resetPasswordVerificationCode": "Bestätigungscode zum Zurücksetzen des Passworts",
"resetPasswordVerificationCodeDescription": "Menschliche Überprüfung während der Passwortzurücksetzung",
"saveSuccess": "Erfolgreich gespeichert",
"turnstileSecretDescription": "Turnstile-Geheimschlüssel, bereitgestellt von Cloudflare",
"turnstileSiteKeyDescription": "Turnstile-Seitenschlüssel, bereitgestellt von Cloudflare",
"verifySettings": "Überprüfungseinstellungen"
},
"verify-code": {
"dailyLimit": "Tägliches Limit",
"dailyLimitDescription": "Maximale Anzahl von Bestätigungscodes, die pro Tag gesendet werden können",
"expireTime": "Ablaufzeit",
"expireTimeDescription": "Ablaufzeit des Bestätigungscodes (Sekunden)",
"interval": "Sendeintervall",
"intervalDescription": "Minimales Intervall zwischen dem Versenden von Bestätigungscodes (Sekunden)",
"saveSuccess": "Erfolgreich gespeichert",
"second": "Sekunden",
"times": "Mal",
"verifyCodeSettings": "Einstellungen für den Bestätigungscode"
"socialAuthMethods": "Social Authentication Methods",
"telegram": {
"clientId": "Bot-ID",
"clientIdDescription": "Telegram-Bot-ID, die Sie von @BotFather erhalten können",
"clientSecret": "Bot-Token",
"clientSecretDescription": "Telegram-Bot-Token, den Sie von @BotFather erhalten können",
"description": "Benutzer mit Telegram-Konten authentifizieren",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
"title": "Telegram-Anmeldung"
}
}

View File

@ -1,14 +0,0 @@
{
"blockVirtualMachine": "Virtuelle Maschine blockieren",
"blockVirtualMachineDescription": "Wenn aktiviert, wird verhindert, dass Geräte auf virtuellen Maschinen oder Emulatoren ausgeführt werden.",
"communicationKey": "Kommunikationsschlüssel",
"communicationKeyDescription": "Der Kommunikationsschlüssel wird für die sichere Kommunikation zwischen Geräten und Servern verwendet.",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden mehrere Geräteidentifikatoren wie IMEI/IDFA/IDFV/AndroidID/Mac-Adresse für die Anmeldung und Registrierung unterstützt.",
"enableSecurity": "Kommunikationsverschlüsselung aktivieren",
"enableSecurityDescription": "Wenn aktiviert, wird die Kommunikation zwischen Geräten und Servern verschlüsselt.",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Speichern erfolgreich",
"showAds": "Werbung anzeigen",
"showAdsDescription": "Wenn aktiviert, werden auf den Geräten Anzeigen angezeigt."
}

View File

@ -1,49 +0,0 @@
{
"emailBasicConfigDescription": "Konfigurieren Sie die SMTP-Servereinstellungen und E-Mail-Verifizierungsoptionen",
"emailLogsDescription": "Sehen Sie sich den Verlauf gesendeter E-Mails und deren Status an",
"emailSuffixWhitelist": "E-Mail-Suffix-Whitelist",
"emailSuffixWhitelistDescription": "Wenn aktiviert, können sich nur E-Mails mit Suffixen aus der Liste registrieren",
"emailVerification": "E-Mail-Verifizierung",
"emailVerificationDescription": "Wenn aktiviert, müssen Benutzer ihre E-Mail-Adresse verifizieren",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden die Funktionen zur E-Mail-Registrierung, Anmeldung, Bindung und Entbindung aktiviert.",
"expiration_email_template": "Vorlage für Ablaufbenachrichtigung",
"expiration_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.",
"failed": "Fehlgeschlagen",
"inputPlaceholder": "Wert eingeben...",
"logs": "Protokolle",
"maintenance_email_template": "Wartungshinweis Vorlage",
"maintenance_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.",
"recipient": "Empfänger",
"saveFailed": "Konfiguration konnte nicht gespeichert werden",
"saveSuccess": "Konfiguration erfolgreich gespeichert.",
"sendFailure": "Senden der Test-E-Mail fehlgeschlagen, bitte überprüfen Sie die Konfiguration.",
"sendSuccess": "Test-E-Mail erfolgreich gesendet.",
"sendTestEmail": "Test-E-Mail senden",
"sendTestEmailDescription": "Senden Sie eine Test-E-Mail, um die Konfiguration zu überprüfen.",
"senderAddress": "Absenderadresse",
"senderAddressDescription": "Die standardmäßige E-Mail-Adresse, die zum Versenden von E-Mails verwendet wird.",
"sent": "Gesendet",
"sentAt": "Gesendet am",
"settings": "Einstellungen",
"smtpAccount": "SMTP-Konto",
"smtpAccountDescription": "Das E-Mail-Konto, das für die Authentifizierung verwendet wird.",
"smtpEncryptionMethod": "SMTP-Verschlüsselungsmethode",
"smtpEncryptionMethodDescription": "Wählen Sie, ob SSL/TLS-Verschlüsselung aktiviert werden soll.",
"smtpPassword": "SMTP-Passwort",
"smtpPasswordDescription": "Das Passwort für das SMTP-Konto.",
"smtpServerAddress": "SMTP-Server-Adresse",
"smtpServerAddressDescription": "Geben Sie die Serveradresse an, die zum Senden von E-Mails verwendet wird.",
"smtpServerPort": "SMTP-Server-Port",
"smtpServerPortDescription": "Geben Sie den Port an, der für die Verbindung zum SMTP-Server verwendet wird.",
"status": "Status",
"subject": "Betreff",
"template": "Vorlage",
"traffic_exceed_email_template": "Benachrichtigung über überschreitenden Verkehr",
"traffic_exceed_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.",
"verify_email_template": "E-Mail-Bestätigungsvorlage",
"verify_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass diese Variablen beibehalten werden.",
"whitelistSuffixes": "Whitelist-Suffixe",
"whitelistSuffixesDescription": "Wird zur E-Mail-Verifizierung während der Registrierung verwendet; eine pro Zeile",
"whitelistSuffixesPlaceholder": "Geben Sie E-Mail-Suffixe ein, eines pro Zeile"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "Kunden-ID",
"clientIdDescription": "Facebook-App-ID aus der Facebook-Entwicklerkonsole",
"clientSecret": "Client-Geheimnis",
"clientSecretDescription": "Facebook-App-Geheimnis aus der Facebook-Entwicklerkonsole",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Facebook-Konto anmelden",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "GitHub-Client-ID",
"clientIdDescription": "Die Client-ID aus den Einstellungen Ihrer GitHub-OAuth-Anwendung",
"clientSecret": "GitHub-Clientgeheimnis",
"clientSecretDescription": "Das Client-Geheimnis aus den Einstellungen Ihrer GitHub-OAuth-Anwendung",
"enable": "GitHub-Authentifizierung aktivieren",
"enableDescription": "Ermöglichen Sie Benutzern, sich mit ihren GitHub-Konten anzumelden",
"saveFailed": "Speichern der GitHub-Einstellungen fehlgeschlagen",
"saveSuccess": "GitHub-Einstellungen erfolgreich gespeichert"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "Kunden-ID",
"clientIdDescription": "Google OAuth 2.0-Client-ID aus der Google Cloud Console",
"clientSecret": "Client-Geheimnis",
"clientSecretDescription": "Google OAuth 2.0-Client-Geheimnis aus der Google Cloud Console",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Google-Konto anmelden",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert"
}

View File

@ -1,32 +0,0 @@
{
"accessLabel": "Zugriff",
"applyPlatform": "Plattform anwenden",
"enable": "Aktivieren",
"enableTip": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
"endpointLabel": "Endpunkt",
"logs": "Protokolle",
"phoneNumberLabel": "Telefonnummer",
"placeholders": {
"template": "Ihr Bestätigungscode ist {code}, gültig für 5 Minuten",
"templateCode": "Geben Sie den Vorlagen-Code ein"
},
"platform": "SMS-Plattform",
"platformConfigTip": "Bitte füllen Sie die bereitgestellte {key}-Konfiguration aus",
"platformTip": "Bitte wählen Sie die SMS-Plattform aus",
"secretLabel": "Geheimnis",
"sendFailed": "Senden fehlgeschlagen",
"sendSuccess": "Erfolgreich gesendet",
"settings": "Einstellungen",
"signNameLabel": "Signaturname",
"template": "SMS-Vorlage",
"templateCodeLabel": "Vorlagen-Code",
"templateTip": "Bitte füllen Sie die SMS-Vorlage aus, lassen Sie {code} in der Mitte, andernfalls funktioniert die SMS-Funktion nicht.",
"testSms": "Test-SMS senden",
"testSmsPhone": "Telefonnummer eingeben",
"testSmsTip": "Senden Sie eine Test-SMS, um Ihre Konfiguration zu überprüfen",
"updateSuccess": "Aktualisierung erfolgreich",
"whitelistAreaCode": "Whitelist Bereichscode",
"whitelistAreaCodeTip": "Bitte geben Sie die Whitelist Bereichscodes ein, z.B. 1, 852, 886, 888",
"whitelistValidation": "Whitelist Überprüfung",
"whitelistValidationTip": "Wenn aktiviert, können nur Bereichscodes in der Whitelist SMS senden"
}

View File

@ -8,9 +8,12 @@
"createRule": "Regel hinzufügen",
"createSuccess": "Regel erfolgreich erstellt",
"createdAt": "Erstellt am",
"default": "Standard",
"defaultRule": "Standardregel",
"delete": "Löschen",
"deleteSuccess": "Regel erfolgreich gelöscht",
"deleteWarning": "Sind Sie sicher, dass Sie diese Regel löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"direct": "Direkt",
"downloadTemplate": "Vorlage herunterladen",
"edit": "Bearbeiten",
"editRule": "Regel bearbeiten",
@ -29,11 +32,14 @@
"noValidRules": "Keine gültigen Regeln gefunden",
"pleaseUploadFile": "Bitte laden Sie eine YAML-Datei hoch",
"preview": "Vorschau",
"reject": "Ablehnen",
"rulesFormat": "Regelformat: Regeltyp, Übereinstimmungsinhalt, [Richtlinie], wobei die Richtlinie optional ist.\nWenn keine Richtlinie angegeben ist, wird automatisch der aktuelle Regelgruppenname verwendet. Beispiele:",
"rulesLabel": "Regelinhalt",
"searchRule": "Regelname suchen",
"selectTags": "Knoten-Tags auswählen",
"selectType": "Regeltyp auswählen",
"tags": "Knoten-Tags",
"tagsLabel": "Knoten-Tags",
"type": "Regeltyp",
"updateSuccess": "Regel erfolgreich aktualisiert"
}

View File

@ -33,21 +33,22 @@
"description": "Beschreibung",
"edit": "Bearbeiten",
"editNodeGroup": "Knoten-Gruppe bearbeiten",
"form": {
"cancel": "Abbrechen",
"confirm": "Bestätigen",
"description": "Beschreibung",
"name": "Name"
},
"name": "Name",
"title": "Liste der Knoten-Gruppen",
"updatedAt": "Aktualisierungszeit"
},
"groupForm": {
"cancel": "Abbrechen",
"confirm": "Bestätigen",
"description": "Beschreibung",
"name": "Name"
},
"node": {
"abnormal": "Abnormalität",
"actions": "Aktionen",
"address": "Adresse",
"all": "Alle",
"basicInfo": "Grundlegende Informationen",
"cancel": "Abbrechen",
"confirm": "Bestätigen",
"confirmDelete": "Möchten Sie wirklich löschen?",
@ -59,96 +60,120 @@
"delete": "Löschen",
"deleteSuccess": "Erfolgreich gelöscht",
"deleteWarning": "Nach dem Löschen können die Daten nicht wiederhergestellt werden. Bitte vorsichtig vorgehen.",
"detail": "Einzelheiten",
"disabled": "Deaktiviert",
"disk": "Festplatte",
"edit": "Bearbeiten",
"editNode": "Knoten bearbeiten",
"enable": "Aktivieren",
"form": {
"allowInsecure": "Unsicher zulassen",
"cancel": "Abbrechen",
"city": "Stadt",
"confirm": "Bestätigen",
"country": "Land",
"edit": "Bearbeiten",
"editSecurity": "Sicherheitskonfiguration bearbeiten",
"enableTLS": "TLS aktivieren",
"encryptionMethod": "Verschlüsselungsmethode",
"flow": "Flusskontrollalgorithmus",
"groupId": "Knotengruppe",
"hopInterval": "Sprungintervall",
"hopPorts": "Sprungports",
"hopPortsPlaceholder": "Mehrere Ports durch Kommas trennen",
"name": "Name",
"obfsPassword": "Verschleierungspasswort",
"obfsPasswordPlaceholder": "Leer lassen für keine Verschleierung",
"path": "Pfad",
"pleaseSelect": "Bitte auswählen",
"port": "Serverport",
"protocol": "Protokoll",
"relayHost": "Relay-Adresse",
"relayMode": "Relaismodus",
"relayModeOptions": {
"all": "Alle",
"none": "Keine",
"random": "Zufällig"
},
"relayPort": "Relay-Port",
"relayPrefix": "Relaispräfix",
"remarks": "Bemerkungen",
"security": "Sicherheit",
"securityConfig": "Sicherheitskonfiguration",
"security_config": {
"fingerprint": "Fingerabdruck",
"privateKey": "Privater Schlüssel",
"privateKeyPlaceholder": "Leer lassen für automatische Generierung",
"publicKey": "Öffentlicher Schlüssel",
"publicKeyPlaceholder": "Leer lassen für automatische Generierung",
"serverAddress": "Serveradresse",
"serverAddressPlaceholder": "REALITY-Zieladresse, standardmäßig SNI verwenden",
"serverName": "Servername (SNI)",
"serverNamePlaceholder": "REALITY erforderlich, mit Backend übereinstimmen",
"serverPort": "Serverport",
"serverPortPlaceholder": "REALITY-Zielport, standardmäßig 443",
"shortId": "Kurz-ID",
"shortIdPlaceholder": "Leer lassen für automatische Generierung",
"sni": "Server Name Indication (SNI)"
},
"selectEncryptionMethod": "Verschlüsselungsmethode auswählen",
"selectNodeGroup": "Knotengruppe auswählen",
"selectProtocol": "Protokoll auswählen",
"selectRelayMode": "Relaismodus auswählen",
"serverAddr": "Serveradresse",
"serverKey": "Server-Schlüssel",
"serverName": "Dienstname",
"speedLimit": "Geschwindigkeitsbegrenzung",
"speedLimitPlaceholder": "Keine Begrenzung",
"tags": "Tags",
"tagsPlaceholder": "Verwenden Sie die Eingabetaste oder das Komma (,), um mehrere Tags einzugeben",
"trafficRatio": "Verkehrsrate",
"transport": "Transportprotokoll",
"transportConfig": "Transportprotokollkonfiguration",
"transportHost": "Transportdienstadresse",
"transportPath": "Transportpfad",
"transportServerName": "Transportdienstname"
},
"enabled": "Aktiviert",
"expireTime": "Ablaufzeit",
"hide": "Ausblenden",
"id": "ID",
"ipAddresses": "IP-Adressen",
"lastUpdated": "Zuletzt aktualisiert",
"location": "Standort",
"memory": "Speicher",
"name": "Name",
"noData": "--",
"node": "Knoten",
"nodeDetail": "Knotendetails",
"nodeGroup": "Knotengruppe",
"nodeStatus": "Knotenstatus",
"normal": "Normal",
"onlineCount": "Online-Anzahl",
"onlineUsers": "Online-Benutzer",
"protocol": "Protokoll",
"rate": "Rate",
"relay": "Relais",
"serverAddr": "Serveradresse",
"speedLimit": "Geschwindigkeitsbegrenzung",
"status": "Status",
"subscribeId": "Abonnieren ID",
"subscribeName": "Abonnieren Name",
"subscription": "Abonnement",
"tags": "Stichwörter",
"trafficRatio": "Verkehrsrate",
"trafficUsage": "Datenverbrauch",
"type": "Typ",
"updateSuccess": "Erfolgreich aktualisiert",
"updatedAt": "Aktualisierungszeit"
"updatedAt": "Aktualisierungszeit",
"userAccount": "Benutzerkonto",
"userDetail": "Benutzerdetails",
"userId": "Benutzer-ID"
},
"nodeForm": {
"allowInsecure": "Unsichere Verbindungen erlauben",
"cancel": "Abbrechen",
"city": "Stadt",
"confirm": "Bestätigen",
"congestionController": "Staukontrolle",
"country": "Land",
"disableSni": "SNI deaktivieren",
"edit": "Bearbeiten",
"editSecurity": "Sicherheitskonfiguration bearbeiten",
"enableTLS": "TLS aktivieren",
"encryptionMethod": "Verschlüsselungsmethode",
"fingerprint": "Fingerprint",
"flow": "Flusskontrollalgorithmus",
"groupId": "Knotengruppe",
"hopInterval": "Hop-Intervall",
"hopPorts": "Hop-Ports",
"hopPortsPlaceholder": "Mehrere Ports mit Kommas trennen",
"name": "Name",
"obfsPassword": "Obfuskationspasswort",
"obfsPasswordPlaceholder": "Für keine Obfuskation leer lassen",
"path": "Pfad",
"pleaseSelect": "Bitte auswählen",
"port": "Serverport",
"protocol": "Protokoll",
"reduceRtt": "RTT reduzieren",
"relayHost": "Relay-Host",
"relayMode": "Relay-Modus",
"relayPort": "Relay-Port",
"relayPrefix": "Relay-Präfix",
"remarks": "Bemerkungen",
"security": "Sicherheit",
"securityConfig": "Sicherheitskonfiguration",
"selectEncryptionMethod": "Verschlüsselungsmethode auswählen",
"selectNodeGroup": "Knotengruppe auswählen",
"selectProtocol": "Protokoll auswählen",
"selectRelayMode": "Relay-Modus auswählen",
"serverAddr": "Serveradresse",
"serverKey": "Server-Schlüssel",
"serverName": "Dienstname",
"speedLimit": "Geschwindigkeitsbegrenzung",
"speedLimitPlaceholder": "Unbegrenzt",
"tags": "Tags",
"tagsPlaceholder": "Verwenden Sie Enter oder Komma (,) um mehrere Tags einzugeben",
"trafficRatio": "Verkehrsrate",
"transport": "Transportprotokollkonfiguration",
"transportConfig": "Transportprotokollkonfiguration",
"transportHost": "Transport-Serveradresse",
"transportPath": "Transportpfad",
"transportServerName": "Transport-Servername",
"udpRelayMode": "UDP-Relay-Modus"
},
"relayModeOptions": {
"all": "Alle",
"none": "Keine",
"random": "Zufällig"
},
"securityConfig": {
"fingerprint": "Fingerprint",
"privateKey": "Privater Schlüssel",
"privateKeyPlaceholder": "Für automatische Generierung leer lassen",
"publicKey": "Öffentlicher Schlüssel",
"publicKeyPlaceholder": "Für automatische Generierung leer lassen",
"serverAddress": "Serveradresse",
"serverAddressPlaceholder": "REALITY-Zieladresse, standardmäßig SNI verwenden",
"serverName": "Servername (SNI)",
"serverNamePlaceholder": "REALITY erforderlich, konsistent mit Backend",
"serverPort": "Serverport",
"serverPortPlaceholder": "REALITY-Zielport, standardmäßig 443",
"shortId": "Kurze ID",
"shortIdPlaceholder": "Für automatische Generierung leer lassen",
"sni": "Server Name Indication (SNI)"
},
"tabs": {
"node": "Knoten",

View File

@ -1,28 +1,77 @@
{
"authSettings": "Authentifizierungseinstellungen",
"basicSettings": "Grundeinstellungen",
"common": {
"cancel": "Abbrechen",
"save": "Einstellungen speichern",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Speichern erfolgreich"
},
"currency": {
"accessKey": "Zugangsschlüssel",
"accessKeyDescription": "https://exchangerate.host bietet kostenlos bereitgestellte API-Schlüssel für Wechselkurse an",
"accessKeyDescription": "{url} bietet kostenlos bereitgestellte API-Schlüssel für Wechselkurse an",
"accessKeyPlaceholder": "API-Schlüssel eingeben",
"currencySymbol": "Währungssymbol",
"currencySymbolDescription": "Nur zur Anzeige verwendet, nach der Änderung werden alle Währungseinheiten im System geändert",
"currencySymbolPlaceholder": "€",
"currencyUnit": "Währungseinheit",
"currencyUnitDescription": "Nur zur Anzeige verwendet, nach der Änderung werden alle Währungseinheiten im System geändert",
"saveSuccess": "Erfolgreich gespeichert"
"currencyUnitPlaceholder": "EUR",
"description": "Konfigurieren Sie Währungseinheiten, Symbole und API-Einstellungen für Wechselkurse",
"title": "Währungsconfiguration"
},
"privacy-policy": {
"saveSuccess": "Erfolgreich gespeichert",
"invite": {
"description": "Konfigurieren Sie die Einstellungen für Benutzer einladungen und Empfehlungsbelohnungen",
"forcedInvite": "Einladung zur Registrierung erforderlich",
"forcedInviteDescription": "Wenn aktiviert, müssen sich Benutzer über einen Einladungslink registrieren",
"inputPlaceholder": "Bitte eingeben",
"onlyFirstPurchase": "Nur Belohnung für den ersten Kauf",
"onlyFirstPurchaseDescription": "Wenn aktiviert, erhalten Empfehlende nur Belohnungen für den ersten Kauf von geworbenen Benutzern",
"referralPercentage": "Empfehlungsbelohnungsprozentsatz",
"referralPercentageDescription": "Prozentsatz der Belohnung, die an Empfehlende vergeben wird",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Speichern erfolgreich",
"title": "Einladungseinstellungen"
},
"privacyPolicy": {
"description": "Bearbeiten und verwalten Sie den Inhalt der Datenschutzrichtlinie",
"title": "Datenschutzrichtlinie"
},
"register": {
"day": "Tag(e)",
"defaultSubscribe": "Standardabonnement",
"defaultSubscribeDescription": "Standardabonnement für neue Benutzer",
"description": "Konfigurieren Sie die Einstellungen zur Benutzerregistrierung",
"inputPlaceholder": "Bitte eingeben",
"ipRegistrationLimit": "IP-Registrierungsgrenze",
"ipRegistrationLimitDescription": "Begrenzen Sie die Anzahl der Registrierungen von einer einzelnen IP-Adresse",
"registrationLimitCount": "Registrierungsgrenze",
"registrationLimitCountDescription": "Anzahl der Registrierungen, die pro IP innerhalb des Begrenzungszeitraums erlaubt sind",
"registrationLimitExpire": "Begrenzungszeitraum",
"registrationLimitExpireDescription": "Dauer für die IP-Registrierungsgrenze",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Speichern erfolgreich",
"selectPlaceholder": "Bitte auswählen",
"stopNewUserRegistration": "Neue Benutzerregistrierung stoppen",
"stopNewUserRegistrationDescription": "Wenn aktiviert, wird die Registrierung neuer Benutzer deaktiviert",
"title": "Registrierungseinstellungen",
"trialDay": "Testtage",
"trialDayDescription": "Testtage, die neuen Benutzern bei der Registrierung gewährt werden",
"trialFlow": "Testverkehr",
"trialFlowDescription": "Testverkehr, der neuen Benutzern bei der Registrierung gewährt wird"
},
"site": {
"customData": "Benutzerdefinierte Daten",
"customDataDescription": "Benutzerdefinierte Daten, die für benutzerdefinierte Daten der Website verwendet werden",
"customHtml": "Benutzerdefiniertes HTML",
"customHtmlDescription": "Benutzerdefinierter HTML-Code, der am Ende des body-Tags der Seite eingefügt werden soll.",
"description": "Konfigurieren Sie grundlegende Informationen zur Website, Logo, Domain und andere Einstellungen",
"keywords": "Schlüsselwörter",
"keywordsDescription": "Für SEO-Zwecke verwendet",
"keywordsPlaceholder": "stichwort1, stichwort2, stichwort3",
"logo": "LOGO",
"logoDescription": "Position zur Anzeige des LOGOs",
"logoPlaceholder": "Bitte geben Sie die URL-Adresse des LOGOs ein, ohne '/' am Ende",
"saveSuccess": "Erfolgreich gespeichert",
"siteDesc": "Seitenbeschreibung",
"siteDescDescription": "Position zur Anzeige der Seitenbeschreibung",
"siteDescPlaceholder": "Bitte geben Sie die Seitenbeschreibung ein",
@ -31,15 +80,47 @@
"siteDomainPlaceholder": "Bitte geben Sie die Domain-Adresse ein, bei mehreren Domains bitte eine pro Zeile",
"siteName": "Seitenname",
"siteNameDescription": "Position zur Anzeige des Seitennamens",
"siteNamePlaceholder": "Bitte geben Sie den Seitennamen ein"
},
"tabs": {
"currency": "Währung",
"site": "Seite",
"tos": "Nutzungsbedingungen"
"siteNamePlaceholder": "Bitte geben Sie den Seitennamen ein",
"title": "Website-Konfiguration"
},
"siteSettings": "Website-Einstellungen",
"tos": {
"saveSuccess": "Erfolgreich gespeichert",
"description": "Bearbeiten und verwalten Sie den Inhalt der Nutzungsbedingungen",
"title": "Nutzungsbedingungen"
},
"userSecuritySettings": "Benutzer & Sicherheit",
"verify": {
"description": "Konfigurieren Sie Turnstile CAPTCHA und Überprüfungseinstellungen",
"enableLoginVerify": "Überprüfung bei der Anmeldung aktivieren",
"enableLoginVerifyDescription": "Wenn aktiviert, müssen Benutzer während der Anmeldung eine menschliche Überprüfung bestehen",
"enablePasswordVerify": "Überprüfung bei der Passwortzurücksetzung aktivieren",
"enablePasswordVerifyDescription": "Wenn aktiviert, müssen Benutzer während der Passwortzurücksetzung eine menschliche Überprüfung bestehen",
"enableRegisterVerify": "Überprüfung bei der Registrierung aktivieren",
"enableRegisterVerifyDescription": "Wenn aktiviert, müssen Benutzer während der Registrierung eine menschliche Überprüfung bestehen",
"inputPlaceholder": "Bitte eingeben",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Speichern erfolgreich",
"title": "Sicherheitsüberprüfung",
"turnstileSecret": "Turnstile-Geheimschlüssel",
"turnstileSecretDescription": "Cloudflare Turnstile-Geheimschlüssel für die Backend-Überprüfung",
"turnstileSecretPlaceholder": "Turnstile-Geheimschlüssel eingeben",
"turnstileSiteKey": "Turnstile-Site-Schlüssel",
"turnstileSiteKeyDescription": "Cloudflare Turnstile-Site-Schlüssel für die Frontend-Überprüfung",
"turnstileSiteKeyPlaceholder": "Turnstile-Site-Schlüssel eingeben"
},
"verifyCode": {
"dailyLimit": "Tägliches Versandlimit",
"dailyLimitDescription": "Maximale Anzahl von Bestätigungscodes, die jeder Benutzer pro Tag senden kann",
"description": "Konfigurieren Sie die Regeln und Grenzen für das Versenden von Bestätigungscodes per E-Mail",
"expireTime": "Gültigkeit des Bestätigungscodes",
"expireTimeDescription": "Gültigkeitsdauer der Bestätigungscodes (Sekunden)",
"inputPlaceholder": "Bitte eingeben",
"interval": "Versandintervall",
"intervalDescription": "Minimales Intervall zwischen zwei Versendungen von Bestätigungscodes (Sekunden)",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Speichern erfolgreich",
"seconds": "Sekunden",
"times": "Mal(e)",
"title": "Einstellungen für den Bestätigungscode"
}
}

View File

@ -1,10 +0,0 @@
{
"clientId": "Bot-ID",
"clientIdDescription": "Telegram-Bot-ID, die Sie von @BotFather erhalten können",
"clientSecret": "Bot-Token",
"clientSecretDescription": "Telegram-Bot-Token, den Sie von @BotFather erhalten können",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert"
}

View File

@ -1,16 +0,0 @@
{
"clientId": "Service ID",
"clientIdDescription": "Apple Service ID, you can get it from Apple Developer Portal",
"clientSecret": "Private Key",
"clientSecretDescription": "The private key content (.p8 file) used for authentication with Apple",
"enable": "Enable",
"enableDescription": "After enabling, users can sign in with their Apple ID",
"keyId": "Key ID",
"keyIdDescription": "The ID of your private key from Apple Developer Portal",
"redirectUri": "Redirect URL",
"redirectUriDescription": "Please fill in the API address of the redirected URL after successfully passing Apple authentication. Do not use / at the end.",
"saveFailed": "Save failed",
"saveSuccess": "Save successful",
"teamId": "Team ID",
"teamIdDescription": "Apple Developer Team ID"
}

View File

@ -1,74 +1,206 @@
{
"invite": {
"commissionFirstTimeOnly": "Commission for First Purchase Only",
"commissionFirstTimeOnlyDescription": "When enabled, commission is generated only on the inviter's first payment; you can configure individual users in user management",
"enableForcedInvite": "Enable Forced Invite",
"enableForcedInviteDescription": "When enabled, only invited users can register",
"inputPlaceholder": "Enter",
"inviteCommissionPercentage": "Invite Commission Percentage",
"inviteCommissionPercentageDescription": "Default global commission distribution ratio; you can configure individual ratios in user management",
"inviteSettings": "Invite Settings",
"saveSuccess": "Save Successful"
"apple": {
"title": "Apple Sign-In",
"description": "Authenticate users with Apple accounts",
"clientId": "Service ID",
"clientIdDescription": "Apple Service ID, available from Apple Developer Portal",
"clientSecret": "Private Key",
"clientSecretDescription": "Private key content (.p8 file) for authenticating with Apple",
"enable": "Enable",
"enableDescription": "When enabled, users can sign in with their Apple ID",
"keyId": "Key ID",
"keyIdDescription": "Your private key ID from Apple Developer Portal",
"redirectUri": "Redirect URL",
"redirectUriDescription": "API address for redirect URL after successful Apple authentication. Do not end with /",
"teamId": "Team ID",
"teamIdDescription": "Apple Developer Team ID"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"saveSuccess": "Saved successfully",
"saveFailed": "Save failed"
},
"communicationMethods": "Communication Methods",
"device": {
"title": "Device Authentication",
"description": "Authenticate users with device identifiers",
"blockVirtualMachine": "Block Virtual Machines",
"blockVirtualMachineDescription": "When enabled, prevents devices from running on virtual machines or emulators",
"communicationKey": "Communication Key",
"communicationKeyDescription": "Secure communication key between devices and servers",
"enable": "Enable",
"enableDescription": "When enabled, supports login and registration using multiple device identifiers like IMEI/IDFA/IDFV/AndroidID/MAC address",
"enableSecurity": "Enable Communication Encryption",
"enableSecurityDescription": "When enabled, communication between devices and servers will be encrypted",
"showAds": "Show Advertisements",
"showAdsDescription": "When enabled, advertisements will be displayed on devices"
},
"deviceAuthMethods": "Device Authentication Methods",
"email": {
"title": "Email Authentication",
"description": "Authenticate users with email addresses",
"basicSettings": "Basic Settings",
"emailVerification": "Email Verification",
"emailVerificationDescription": "When enabled, users need to verify their email addresses",
"emailSuffixWhitelist": "Email Suffix Whitelist",
"emailSuffixWhitelistDescription": "When enabled, only emails with suffixes in the list can register",
"whitelistSuffixes": "Whitelist Suffixes",
"whitelistSuffixesPlaceholder": "Enter email suffixes, one per line (e.g., example.com)",
"whitelistSuffixesDescription": "Enter email suffixes, one per line (e.g., example.com)",
"enable": "Enable",
"enableDescription": "When enabled, enables email registration, login, binding, and unbinding functions",
"inputPlaceholder": "Enter value...",
"logs": "Email Logs",
"logsDescription": "View history of sent emails and their status",
"sendFailure": "Test email send failed, please check configuration.",
"sendSuccess": "Test email sent successfully.",
"sendTestEmail": "Send Test Email",
"sendTestEmailDescription": "Send a test email to verify configuration.",
"senderAddress": "Sender Address",
"senderAddressDescription": "Default email address used for sending emails.",
"smtpAccount": "SMTP Account",
"smtpAccountDescription": "Email account used for authentication.",
"smtpEncryptionMethod": "SMTP Encryption Method",
"smtpEncryptionMethodDescription": "Choose whether to enable SSL/TLS encryption.",
"smtpPassword": "SMTP Password",
"smtpPasswordDescription": "Password for the SMTP account.",
"smtpServerAddress": "SMTP Server Address",
"smtpServerAddressDescription": "Specify the server address used for sending emails.",
"smtpServerPort": "SMTP Server Port",
"smtpServerPortDescription": "Specify the port used to connect to the SMTP server.",
"smtpSettings": "SMTP Settings",
"verifyEmailTemplate": "Verification Email Template",
"verifyTemplate": "Verification Email",
"expirationEmailTemplate": "Expiration Notice Template",
"expirationTemplate": "Expiration Notice",
"maintenanceEmailTemplate": "Maintenance Notice Template",
"maintenanceTemplate": "Maintenance Notice",
"trafficExceedEmailTemplate": "Traffic Limit Notice Template",
"trafficTemplate": "Traffic Limit",
"templateVariables": {
"title": "Email Template Variables",
"type": {
"title": "Email Type",
"description": "Email type identifier, 1 for registration verification code, others for password reset verification code",
"conditionalSyntax": "Supports conditional syntax for switching content based on type"
},
"siteLogo": {
"title": "Site Logo",
"description": "Website logo image URL"
},
"siteName": {
"title": "Site Name",
"description": "Current website name"
},
"expire": {
"title": "Validity Period",
"description": "Verification code validity time (minutes)"
},
"code": {
"title": "Verification Code",
"description": "6-digit verification code"
},
"expireDate": {
"title": "Expiration Date",
"description": "Service expiration date, reminds users when service expires"
},
"maintenanceDate": {
"title": "Maintenance Date",
"description": "System maintenance date, displays maintenance start date"
},
"maintenanceTime": {
"title": "Maintenance Duration",
"description": "Estimated maintenance time, displays maintenance time period or duration"
}
}
},
"facebook": {
"title": "Facebook Sign-In",
"description": "Authenticate users with Facebook accounts",
"clientId": "Client ID",
"clientIdDescription": "Facebook App ID from Facebook Developer Console",
"clientSecret": "Client Secret",
"clientSecretDescription": "Facebook App Secret from Facebook Developer Console",
"enable": "Enable",
"enableDescription": "When enabled, users can sign in with their Facebook account"
},
"github": {
"title": "GitHub Sign-In",
"description": "Authenticate users with GitHub accounts",
"clientId": "GitHub Client ID",
"clientIdDescription": "Client ID from your GitHub OAuth application settings",
"clientSecret": "GitHub Client Secret",
"clientSecretDescription": "Client secret from your GitHub OAuth application settings",
"enable": "Enable GitHub Authentication",
"enableDescription": "Allow users to sign in with their GitHub accounts"
},
"google": {
"title": "Google Sign-In",
"description": "Authenticate users with Google accounts",
"clientId": "Client ID",
"clientIdDescription": "Google OAuth 2.0 Client ID from Google Cloud Console",
"clientSecret": "Client Secret",
"clientSecretDescription": "Google OAuth 2.0 Client Secret from Google Cloud Console",
"enable": "Enable",
"enableDescription": "When enabled, users can sign in with their Google account"
},
"log": {
"content": "Content",
"createdAt": "Created At",
"emailLog": "Email Log",
"mobileLog": "Mobile Log",
"mobileLog": "SMS Log",
"platform": "Platform",
"sendFailed": "Failed",
"sendSuccess": "Success",
"status": "Status",
"subject": "Subject",
"to": "Recipient",
"subject": "Subject",
"content": "Content",
"status": "Status",
"sendSuccess": "Sent Successfully",
"sendFailed": "Send Failed",
"createdAt": "Created At",
"updatedAt": "Updated At"
},
"register": {
"day": "Day",
"hour": "Hour",
"ipRegistrationLimit": "IP Registration Limit",
"ipRegistrationLimitDescription": "When enabled, IPs that meet the rule requirements will be restricted from registering; note that IP determination may cause issues due to CDNs or frontend proxies",
"minute": "Minute",
"month": "Month",
"noLimit": "No Limit",
"penaltyTime": "Penalty Time (minutes)",
"penaltyTimeDescription": "Users must wait for the penalty time to expire before registering again",
"registerSettings": "Register Settings",
"registrationLimitCount": "Registration Limit Count",
"registrationLimitCountDescription": "Enable penalty after reaching registration limit",
"saveSuccess": "Save Successful",
"stopNewUserRegistration": "Stop New User Registration",
"stopNewUserRegistrationDescription": "When enabled, no one can register",
"trialDuration": "Trial Duration",
"trialRegistration": "Trial Registration",
"trialRegistrationDescription": "Enable trial registration; modify trial package and duration first",
"trialSubscribePlan": "Trial Subscription Plan",
"trialSubscribePlanDescription": "Select trial subscription plan",
"year": "Year"
"phone": {
"title": "Phone Authentication",
"description": "Authenticate users with phone numbers",
"enable": "Enable",
"enableTip": "When enabled, enables phone registration, login, binding, and unbinding functions",
"whitelistValidation": "Whitelist Validation",
"whitelistValidationTip": "When enabled, only area codes in the whitelist can send SMS",
"whitelistAreaCode": "Whitelist Area Codes",
"whitelistAreaCodeTip": "Enter whitelist area codes, e.g., 1, 852, 886, 888",
"platform": "SMS Platform",
"platformTip": "Please select SMS platform",
"platformConfigTip": "Please fill in the provided {key} configuration",
"applyPlatform": "Apply Platform",
"accessLabel": "Access",
"endpointLabel": "Endpoint",
"secretLabel": "Secret",
"templateCodeLabel": "Template Code",
"signNameLabel": "Sign Name",
"phoneNumberLabel": "Phone Number",
"template": "SMS Template",
"templateTip": "Please fill in the SMS template, keep {code} in the middle, otherwise SMS function will not work properly",
"placeholders": {
"template": "Your verification code is {code}, valid for 5 minutes"
},
"testSms": "Send Test SMS",
"testSmsTip": "Send a test SMS to verify your configuration",
"testSmsPhone": "Enter phone number",
"sendSuccess": "Sent successfully",
"sendFailed": "Send failed",
"updateSuccess": "Updated successfully",
"settings": "Settings",
"logs": "SMS Logs",
"logsDescription": "View history of sent SMS messages and their status"
},
"verify": {
"inputPlaceholder": "Enter",
"loginVerificationCode": "Login Verification Code",
"loginVerificationCodeDescription": "Human verification during login",
"registrationVerificationCode": "Registration Verification Code",
"registrationVerificationCodeDescription": "Human verification during registration",
"resetPasswordVerificationCode": "Reset Password Verification Code",
"resetPasswordVerificationCodeDescription": "Human verification during password reset",
"saveSuccess": "Save Successful",
"turnstileSecretDescription": "Turnstile secret key provided by Cloudflare",
"turnstileSiteKeyDescription": "Turnstile site key provided by Cloudflare",
"verifySettings": "Verify Settings"
},
"verify-code": {
"dailyLimit": "Daily Limit",
"dailyLimitDescription": "Maximum number of verification codes that can be sent per day",
"expireTime": "Expire Time",
"expireTimeDescription": "Verification code expiration time (seconds)",
"interval": "Send Interval",
"intervalDescription": "Minimum interval between sending verification codes (seconds)",
"saveSuccess": "Save Successful",
"second": "seconds",
"times": "times",
"verifyCodeSettings": "Verification Code Settings"
"socialAuthMethods": "Social Authentication Methods",
"telegram": {
"title": "Telegram Sign-In",
"description": "Authenticate users with Telegram accounts",
"clientId": "Bot ID",
"clientIdDescription": "Telegram Bot ID, available from @BotFather",
"clientSecret": "Bot Token",
"clientSecretDescription": "Telegram Bot Token, available from @BotFather",
"enable": "Enable",
"enableDescription": "When enabled, users can sign in with their Telegram account"
}
}

View File

@ -1,14 +0,0 @@
{
"blockVirtualMachine": "Block Virtual Machine",
"blockVirtualMachineDescription": "When enabled, devices will be prevented from running on virtual machines or emulators",
"communicationKey": "Communication Key",
"communicationKeyDescription": "Communication key is used for secure communication between devices and servers",
"enable": "Enable",
"enableDescription": "After enabling, multiple device identifiers such as IMEI/IDFA/IDFV/AndroidID/Mac address are supported for login and registration",
"enableSecurity": "Enable Communication Encryption",
"enableSecurityDescription": "When enabled, the communication between devices and servers will be encrypted",
"saveFailed": "Save failed",
"saveSuccess": "Save successful",
"showAds": "Show Advertisements",
"showAdsDescription": "When enabled, advertisements will be displayed on devices"
}

View File

@ -1,49 +0,0 @@
{
"emailBasicConfigDescription": "Configure SMTP server settings and email verification options",
"emailLogsDescription": "View the history of sent emails and their status",
"emailSuffixWhitelist": "Email Suffix Whitelist",
"emailSuffixWhitelistDescription": "When enabled, only emails with suffixes in the list can register",
"emailVerification": "Email Verification",
"emailVerificationDescription": "When enabled, users will need to verify their email",
"enable": "Enable",
"enableDescription": "After enabling, email registration, login, binding, and unbinding functions will be enabled",
"expiration_email_template": "Expiration Notice Template",
"expiration_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"failed": "Failed",
"inputPlaceholder": "Enter value...",
"logs": "Logs",
"maintenance_email_template": "Maintenance Notice Template",
"maintenance_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"recipient": "Recipient",
"saveFailed": "Failed to save configuration",
"saveSuccess": "Configuration saved successfully.",
"sendFailure": "Failed to send test email, please check the configuration.",
"sendSuccess": "Test email sent successfully.",
"sendTestEmail": "Send Test Email",
"sendTestEmailDescription": "Send a test email to verify the configuration.",
"senderAddress": "Sender Address",
"senderAddressDescription": "The default email address used for sending emails.",
"sent": "Sent",
"sentAt": "Sent At",
"settings": "Settings",
"smtpAccount": "SMTP Account",
"smtpAccountDescription": "The email account used for authentication.",
"smtpEncryptionMethod": "SMTP Encryption Method",
"smtpEncryptionMethodDescription": "Choose whether to enable SSL/TLS encryption.",
"smtpPassword": "SMTP Password",
"smtpPasswordDescription": "The password for the SMTP account.",
"smtpServerAddress": "SMTP Server Address",
"smtpServerAddressDescription": "Specify the server address used for sending emails.",
"smtpServerPort": "SMTP Server Port",
"smtpServerPortDescription": "Specify the port used to connect to the SMTP server.",
"status": "Status",
"subject": "Subject",
"template": "Template",
"traffic_exceed_email_template": "Traffic Exceed Notice Template",
"traffic_exceed_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"verify_email_template": "Verification Email Template",
"verify_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"whitelistSuffixes": "Whitelist Suffixes",
"whitelistSuffixesDescription": "Used for email verification during registration; one per line",
"whitelistSuffixesPlaceholder": "Enter email suffixes, one per line"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "Client ID",
"clientIdDescription": "Facebook App ID from Facebook Developers Console",
"clientSecret": "Client Secret",
"clientSecretDescription": "Facebook App Secret from Facebook Developers Console",
"enable": "Enable",
"enableDescription": "After enabling, users can sign in with their Facebook account",
"saveFailed": "Save failed",
"saveSuccess": "Save successful"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "GitHub Client ID",
"clientIdDescription": "The client ID from your GitHub OAuth application settings",
"clientSecret": "GitHub Client Secret",
"clientSecretDescription": "The client secret from your GitHub OAuth application settings",
"enable": "Enable GitHub Authentication",
"enableDescription": "Allow users to sign in with their GitHub accounts",
"saveFailed": "Failed to save GitHub settings",
"saveSuccess": "GitHub settings saved successfully"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "Client ID",
"clientIdDescription": "Google OAuth 2.0 Client ID from Google Cloud Console",
"clientSecret": "Client Secret",
"clientSecretDescription": "Google OAuth 2.0 Client Secret from Google Cloud Console",
"enable": "Enable",
"enableDescription": "After enabling, users can sign in with their Google account",
"saveFailed": "Save failed",
"saveSuccess": "Save successful"
}

View File

@ -1,32 +0,0 @@
{
"accessLabel": "Access",
"applyPlatform": "Apply Platform",
"enable": "Enable",
"enableTip": "After enabling, mobile phone registration, login, binding, and unbinding functions will be enabled",
"endpointLabel": "Endpoint",
"logs": "Logs",
"phoneNumberLabel": "Phone Number",
"placeholders": {
"template": "Your verification code is {code}, valid for 5 minutes",
"templateCode": "Enter template code"
},
"platform": "SMS Platform",
"platformConfigTip": "Please fill in the provided {key} configuration",
"platformTip": "Please select SMS platform",
"secretLabel": "Secret",
"sendFailed": "Send Failed",
"sendSuccess": "Send Success",
"settings": "Settings",
"signNameLabel": "Sign Name",
"template": "SMS Template",
"templateCodeLabel": "Template Code",
"templateTip": "Please fill in the SMS template, keep {code} in the middle, otherwise SMS function will not work",
"testSms": "Send Test SMS",
"testSmsPhone": "Enter phone number",
"testSmsTip": "Send a test message to verify your configuration",
"updateSuccess": "Update Success",
"whitelistAreaCode": "Whitelist Area Code",
"whitelistAreaCodeTip": "Please enter whitelist area codes, e.g., 1, 852, 886, 888",
"whitelistValidation": "Whitelist Verification",
"whitelistValidationTip": "When enabled, only area codes in the whitelist can send SMS"
}

View File

@ -33,21 +33,22 @@
"description": "Description",
"edit": "Edit",
"editNodeGroup": "Edit Node Group",
"form": {
"cancel": "Cancel",
"confirm": "Confirm",
"description": "Description",
"name": "Name"
},
"name": "Name",
"title": "Node Group List",
"updatedAt": "Updated At"
},
"groupForm": {
"cancel": "Cancel",
"confirm": "Confirm",
"description": "Description",
"name": "Name"
},
"node": {
"abnormal": "Abnormal",
"actions": "Actions",
"address": "Address",
"all": "All",
"basicInfo": "Basic Info",
"cancel": "Cancel",
"confirm": "Confirm",
"confirmDelete": "Are you sure you want to delete?",
@ -59,103 +60,120 @@
"delete": "Delete",
"deleteSuccess": "Deleted successfully",
"deleteWarning": "Data will not be recoverable after deletion. Please proceed with caution.",
"detail": "Detail",
"disabled": "Disabled",
"disk": "Disk",
"edit": "Edit",
"editNode": "Edit Node",
"enable": "Enable",
"enabled": "Enabled",
"hide": "Hide",
"id": "ID",
"form": {
"allowInsecure": "Allow Insecure",
"cancel": "Cancel",
"city": "City",
"confirm": "Confirm",
"congestionController": "Congestion Controller",
"country": "Country",
"disableSni": "Disable SNI",
"edit": "Edit",
"editSecurity": "Edit Security Configuration",
"enableTLS": "Enable TLS",
"encryptionMethod": "Encryption Method",
"fingerprint": "Fingerprint",
"flow": "Flow Control Algorithm",
"groupId": "Node Group",
"hopInterval": "Hop Interval",
"hopPorts": "Hop Ports",
"hopPortsPlaceholder": "Separate multiple ports with commas",
"name": "Name",
"obfsPassword": "Obfuscation Password",
"obfsPasswordPlaceholder": "Leave blank for no obfuscation",
"path": "Path",
"pleaseSelect": "Please Select",
"port": "Server Port",
"protocol": "Protocol",
"reduceRtt": "Reduce RTT",
"relayHost": "Relay Host",
"relayMode": "Relay Mode",
"relayModeOptions": {
"all": "All",
"none": "None",
"random": "Random"
},
"relayPort": "Relay Port",
"relayPrefix": "Relay Prefix",
"remarks": "Remarks",
"security": "Security",
"securityConfig": "Security Configuration",
"security_config": {
"fingerprint": "Fingerprint",
"privateKey": "Private Key",
"privateKeyPlaceholder": "Leave blank for auto-generation",
"publicKey": "Public Key",
"publicKeyPlaceholder": "Leave blank for auto-generation",
"serverAddress": "Server Address",
"serverAddressPlaceholder": "REALITY target address, default using SNI",
"serverName": "Server Name (SNI)",
"serverNamePlaceholder": "REALITY required, consistent with backend",
"serverPort": "Server Port",
"serverPortPlaceholder": "REALITY target port, default 443",
"shortId": "Short ID",
"shortIdPlaceholder": "Leave blank for auto-generation",
"sni": "Server Name Indication (SNI)"
},
"selectEncryptionMethod": "Select Encryption Method",
"selectNodeGroup": "Select Node Group",
"selectProtocol": "Select Protocol",
"selectRelayMode": "Select Relay Mode",
"serverAddr": "Server Address",
"serverKey": "Server Key",
"serverName": "Service Name",
"speedLimit": "Speed Limit",
"speedLimitPlaceholder": "Unlimited",
"tags": "Tags",
"tagsPlaceholder": "Use Enter or comma (,) to enter multiple tags",
"trafficRatio": "Traffic Rate",
"transport": "Transport Protocol Configuration",
"transportConfig": "Transport Protocol Configuration",
"transportHost": "Transport Server Address",
"udpRelayMode": "UDP Relay Mode",
"transportPath": "Transport Path",
"transportServerName": "Transport Server Name"
},
"ipAddresses": "IP Addresses",
"lastUpdated": "Last Updated",
"location": "Location",
"memory": "Memory",
"name": "Name",
"node": "Node",
"nodeDetail": "Node Detail",
"nodeGroup": "Node Group",
"nodeStatus": "Node Status",
"noData": "--",
"normal": "Normal",
"onlineCount": "Online users",
"onlineUsers": "Online Users",
"protocol": "Protocol",
"rate": "Rate",
"relay": "Relay",
"serverAddr": "Server Address",
"speedLimit": "Speed Limit",
"status": "Status",
"subscription": "Subscription",
"subscribeName": "Subscribe Name",
"subscribeId": "Subscribe ID",
"tags": "Tags",
"trafficRatio": "Traffic Rate",
"trafficUsage": "Traffic Usage",
"type": "Type",
"updateSuccess": "Update successful",
"updatedAt": "Updated At"
"updatedAt": "Updated At",
"userAccount": "User Account",
"userDetail": "User Detail",
"userId": "User ID",
"expireTime": "Expire Time"
},
"nodeForm": {
"allowInsecure": "Allow Insecure",
"cancel": "Cancel",
"city": "City",
"confirm": "Confirm",
"congestionController": "Congestion Controller",
"country": "Country",
"disableSni": "Disable SNI",
"edit": "Edit",
"editSecurity": "Edit Security Configuration",
"enableTLS": "Enable TLS",
"encryptionMethod": "Encryption Method",
"fingerprint": "Fingerprint",
"flow": "Flow Control Algorithm",
"groupId": "Node Group",
"hopInterval": "Hop Interval",
"hopPorts": "Hop Ports",
"hopPortsPlaceholder": "Separate multiple ports with commas",
"name": "Name",
"obfsPassword": "Obfuscation Password",
"obfsPasswordPlaceholder": "Leave blank for no obfuscation",
"path": "Path",
"pleaseSelect": "Please Select",
"port": "Server Port",
"protocol": "Protocol",
"reduceRtt": "Reduce RTT",
"relayHost": "Relay Host",
"relayMode": "Relay Mode",
"relayPort": "Relay Port",
"relayPrefix": "Relay Prefix",
"remarks": "Remarks",
"security": "Security",
"securityConfig": "Security Configuration",
"selectEncryptionMethod": "Select Encryption Method",
"selectNodeGroup": "Select Node Group",
"selectProtocol": "Select Protocol",
"selectRelayMode": "Select Relay Mode",
"serverAddr": "Server Address",
"serverKey": "Server Key",
"serverName": "Service Name",
"speedLimit": "Speed Limit",
"speedLimitPlaceholder": "Unlimited",
"tags": "Tags",
"tagsPlaceholder": "Use Enter or comma (,) to enter multiple tags",
"trafficRatio": "Traffic Rate",
"transport": "Transport Protocol Configuration",
"transportConfig": "Transport Protocol Configuration",
"transportHost": "Transport Server Address",
"udpRelayMode": "UDP Relay Mode",
"transportPath": "Transport Path",
"transportServerName": "Transport Server Name"
},
"relayModeOptions": {
"all": "All",
"none": "None",
"random": "Random"
},
"securityConfig": {
"fingerprint": "Fingerprint",
"privateKey": "Private Key",
"privateKeyPlaceholder": "Leave blank for auto-generation",
"publicKey": "Public Key",
"publicKeyPlaceholder": "Leave blank for auto-generation",
"serverAddress": "Server Address",
"serverAddressPlaceholder": "REALITY target address, default using SNI",
"serverName": "Server Name (SNI)",
"serverNamePlaceholder": "REALITY required, consistent with backend",
"serverPort": "Server Port",
"serverPortPlaceholder": "REALITY target port, default 443",
"shortId": "Short ID",
"shortIdPlaceholder": "Leave blank for auto-generation",
"sni": "Server Name Indication (SNI)"
},
"tabs": {
"node": "Node",

View File

@ -1,28 +1,78 @@
{
"authSettings": "Authentication Settings",
"basicSettings": "Basic Settings",
"common": {
"cancel": "Cancel",
"save": "Save Settings",
"saveSuccess": "Save Successful",
"saveFailed": "Save Failed"
},
"currency": {
"accessKey": "Access Key",
"accessKeyDescription": "API key provided for free by https://exchangerate.host",
"title": "Currency Configuration",
"description": "Configure currency units, symbols, and exchange rate API settings",
"accessKey": "API Key",
"accessKeyDescription": "Free exchange rate API key provided by {url}",
"accessKeyPlaceholder": "Enter API key",
"currencySymbol": "Currency Symbol",
"currencySymbolDescription": "Used for display purposes only; changing this will affect all currency units in the system",
"currencySymbolPlaceholder": "$",
"currencyUnit": "Currency Unit",
"currencyUnitDescription": "Used for display purposes only; changing this will affect all currency units in the system",
"saveSuccess": "Save Successful"
"currencyUnitPlaceholder": "USD"
},
"privacy-policy": {
"invite": {
"title": "Invitation Settings",
"description": "Configure user invitation and referral reward settings",
"forcedInvite": "Require Invitation to Register",
"forcedInviteDescription": "When enabled, users must register through an invitation link",
"referralPercentage": "Referral Reward Percentage",
"referralPercentageDescription": "Percentage of reward given to referrers",
"onlyFirstPurchase": "First Purchase Reward Only",
"onlyFirstPurchaseDescription": "When enabled, referrers only receive rewards for the first purchase by referred users",
"inputPlaceholder": "Please enter",
"saveSuccess": "Save Successful",
"title": "Privacy Policy"
"saveFailed": "Save Failed"
},
"privacyPolicy": {
"title": "Privacy Policy",
"description": "Edit and manage privacy policy content"
},
"register": {
"title": "Registration Settings",
"description": "Configure user registration related settings",
"stopNewUserRegistration": "Stop New User Registration",
"stopNewUserRegistrationDescription": "When enabled, new user registration will be disabled",
"ipRegistrationLimit": "IP Registration Limit",
"ipRegistrationLimitDescription": "Limit the number of registrations from a single IP address",
"registrationLimitCount": "Registration Limit Count",
"registrationLimitCountDescription": "Number of registrations allowed per IP within the limit period",
"registrationLimitExpire": "Limit Period",
"registrationLimitExpireDescription": "Duration for IP registration limit",
"trialFlow": "Trial Traffic",
"trialFlowDescription": "Trial traffic given to new users upon registration",
"trialDay": "Trial Days",
"trialDayDescription": "Trial days given to new users upon registration",
"defaultSubscribe": "Default Subscription",
"defaultSubscribeDescription": "Default subscription plan for new users",
"inputPlaceholder": "Please enter",
"selectPlaceholder": "Please select",
"day": "day(s)",
"saveSuccess": "Save Successful",
"saveFailed": "Save Failed"
},
"site": {
"title": "Site Configuration",
"description": "Configure basic site information, logo, domain and other settings",
"customData": "Custom Data",
"customDataDescription": "Custom Data, used for custom data of the site",
"customDataDescription": "Custom data for website customization",
"customHtml": "Custom HTML",
"customHtmlDescription": "Custom HTML code to be injected into the bottom of the site's body tag.",
"customHtmlDescription": "Custom HTML code to be injected into the bottom of the site's body tag",
"keywords": "Keywords",
"keywordsDescription": "Used for SEO purposes",
"logo": "Logo",
"keywordsPlaceholder": "keyword1, keyword2, keyword3",
"logo": "Site Logo",
"logoDescription": "Used for displaying the logo in designated locations",
"logoPlaceholder": "Enter the URL of the logo, without ending with '/'",
"saveSuccess": "Save Successful",
"siteDesc": "Site Description",
"siteDescDescription": "Used for displaying the site description in designated locations",
"siteDescPlaceholder": "Enter site description",
@ -33,13 +83,44 @@
"siteNameDescription": "Used for displaying the site name in designated locations",
"siteNamePlaceholder": "Enter site name"
},
"tabs": {
"currency": "Currency",
"site": "Site",
"tos": "Terms of Service"
},
"siteSettings": "Site Settings",
"tos": {
"title": "Terms of Service",
"description": "Edit and manage terms of service content"
},
"userSecuritySettings": "User & Security",
"verify": {
"title": "Security Verification",
"description": "Configure Turnstile CAPTCHA and verification settings",
"turnstileSiteKey": "Turnstile Site Key",
"turnstileSiteKeyDescription": "Cloudflare Turnstile site key for frontend verification",
"turnstileSiteKeyPlaceholder": "Enter Turnstile site key",
"turnstileSecret": "Turnstile Secret Key",
"turnstileSecretDescription": "Cloudflare Turnstile secret key for backend verification",
"turnstileSecretPlaceholder": "Enter Turnstile secret key",
"enableRegisterVerify": "Enable Verification on Registration",
"enableRegisterVerifyDescription": "When enabled, users must pass human verification during registration",
"enableLoginVerify": "Enable Verification on Login",
"enableLoginVerifyDescription": "When enabled, users must pass human verification during login",
"enablePasswordVerify": "Enable Verification on Password Reset",
"enablePasswordVerifyDescription": "When enabled, users must pass human verification during password reset",
"inputPlaceholder": "Please enter",
"saveSuccess": "Save Successful",
"title": "Terms of Service"
"saveFailed": "Save Failed"
},
"verifyCode": {
"title": "Verification Code Settings",
"description": "Configure email verification code sending rules and limits",
"expireTime": "Verification Code Validity",
"expireTimeDescription": "Validity period of verification codes (seconds)",
"interval": "Sending Interval",
"intervalDescription": "Minimum interval between two verification code sends (seconds)",
"dailyLimit": "Daily Sending Limit",
"dailyLimitDescription": "Maximum number of verification codes each user can send per day",
"seconds": "seconds",
"times": "time(s)",
"inputPlaceholder": "Please enter",
"saveSuccess": "Save Successful",
"saveFailed": "Save Failed"
}
}

View File

@ -1,10 +0,0 @@
{
"clientId": "Bot ID",
"clientIdDescription": "Telegram Bot ID, you can get it from @BotFather",
"clientSecret": "Bot Token",
"clientSecretDescription": "Telegram Bot Token, you can get it from @BotFather",
"enable": "Enable",
"enableDescription": "After enabling, mobile phone registration, login, binding, and unbinding functions will be enabled",
"saveFailed": "Save failed",
"saveSuccess": "Save successful"
}

View File

@ -1,16 +0,0 @@
{
"clientId": "ID de servicio",
"clientIdDescription": "ID de servicio de Apple, puedes obtenerlo en el Portal de Desarrolladores de Apple",
"clientSecret": "Clave Privada",
"clientSecretDescription": "El contenido de la clave privada (archivo .p8) utilizado para la autenticación con Apple",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, los usuarios pueden iniciar sesión con su ID de Apple",
"keyId": "ID de clave",
"keyIdDescription": "El ID de tu clave privada del Portal de Desarrolladores de Apple",
"redirectUri": "URL de redirección",
"redirectUriDescription": "Por favor, complete la dirección API de la URL redirigida después de pasar con éxito la autenticación de Apple. No use / al final.",
"saveFailed": "Error al guardar",
"saveSuccess": "Guardado exitoso",
"teamId": "ID del equipo",
"teamIdDescription": "ID de equipo de desarrolladores de Apple"
}

View File

@ -1,14 +1,149 @@
{
"invite": {
"commissionFirstTimeOnly": "Comisión Solo por la Primera Compra",
"commissionFirstTimeOnlyDescription": "Cuando está habilitado, la comisión se genera solo en el primer pago del invitador; puedes configurar usuarios individuales en la gestión de usuarios",
"enableForcedInvite": "Habilitar Invitación Forzada",
"enableForcedInviteDescription": "Cuando está habilitado, solo los usuarios invitados pueden registrarse",
"inputPlaceholder": "Introducir",
"inviteCommissionPercentage": "Porcentaje de Comisión por Invitación",
"inviteCommissionPercentageDescription": "Proporción de distribución de comisión global por defecto; puedes configurar proporciones individuales en la gestión de usuarios",
"inviteSettings": "Configuración de Invitaciones",
"saveSuccess": "Guardado Exitoso"
"apple": {
"clientId": "ID de servicio",
"clientIdDescription": "ID de servicio de Apple, puedes obtenerlo en el Portal de Desarrolladores de Apple",
"clientSecret": "Clave Privada",
"clientSecretDescription": "El contenido de la clave privada (archivo .p8) utilizado para la autenticación con Apple",
"description": "Autenticar usuarios con cuentas de Apple",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, los usuarios pueden iniciar sesión con su ID de Apple",
"keyId": "ID de clave",
"keyIdDescription": "El ID de tu clave privada del Portal de Desarrolladores de Apple",
"redirectUri": "URL de redirección",
"redirectUriDescription": "Por favor, complete la dirección API de la URL redirigida después de pasar con éxito la autenticación de Apple. No use / al final.",
"teamId": "ID del equipo",
"teamIdDescription": "ID de equipo de desarrolladores de Apple",
"title": "Inicio de sesión con Apple"
},
"common": {
"cancel": "Cancelar",
"save": "Guardar",
"saveFailed": "Error al guardar",
"saveSuccess": "Guardado con éxito"
},
"communicationMethods": "Communication Methods",
"device": {
"blockVirtualMachine": "Bloquear Máquina Virtual",
"blockVirtualMachineDescription": "Cuando está habilitado, se evitará que los dispositivos se ejecuten en máquinas virtuales o emuladores",
"communicationKey": "Clave de Comunicación",
"communicationKeyDescription": "La clave de comunicación se utiliza para la comunicación segura entre dispositivos y servidores",
"description": "Autenticar usuarios con identificadores de dispositivos",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, se admiten múltiples identificadores de dispositivos como IMEI/IDFA/IDFV/AndroidID/dirección MAC para el inicio de sesión y el registro",
"enableSecurity": "Habilitar Cifrado de Comunicación",
"enableSecurityDescription": "Cuando está habilitado, la comunicación entre dispositivos y servidores será cifrada",
"showAds": "Mostrar Anuncios",
"showAdsDescription": "Cuando está habilitado, se mostrarán anuncios en los dispositivos",
"title": "Autenticación de Dispositivos"
},
"deviceAuthMethods": "Device Authentication Methods",
"email": {
"basicSettings": "Configuraciones Básicas",
"description": "Autenticar usuarios con direcciones de correo electrónico",
"emailSuffixWhitelist": "Lista blanca de sufijos de correo electrónico",
"emailSuffixWhitelistDescription": "Cuando está habilitado, solo los correos electrónicos con sufijos en la lista pueden registrarse",
"emailVerification": "Verificación de correo electrónico",
"emailVerificationDescription": "Cuando esté habilitado, los usuarios deberán verificar su correo electrónico",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, se activarán las funciones de registro por correo electrónico, inicio de sesión, vinculación y desvinculación.",
"expirationEmailTemplate": "Plantilla de Aviso de Expiración",
"expirationTemplate": "Aviso de Expiración",
"inputPlaceholder": "Ingrese valor...",
"logs": "Registros",
"logsDescription": "Ver historial de correos electrónicos enviados y su estado",
"maintenanceEmailTemplate": "Plantilla de Aviso de Mantenimiento",
"maintenanceTemplate": "Aviso de Mantenimiento",
"sendFailure": "Error al enviar el correo electrónico de prueba, por favor verifica la configuración.",
"sendSuccess": "Correo de prueba enviado con éxito.",
"sendTestEmail": "Enviar correo electrónico de prueba",
"sendTestEmailDescription": "Envía un correo electrónico de prueba para verificar la configuración.",
"senderAddress": "Dirección del Remitente",
"senderAddressDescription": "La dirección de correo electrónico predeterminada utilizada para enviar correos electrónicos.",
"smtpAccount": "Cuenta SMTP",
"smtpAccountDescription": "La cuenta de correo electrónico utilizada para la autenticación.",
"smtpEncryptionMethod": "Método de Cifrado SMTP",
"smtpEncryptionMethodDescription": "Elija si desea habilitar el cifrado SSL/TLS.",
"smtpPassword": "Contraseña SMTP",
"smtpPasswordDescription": "La contraseña para la cuenta SMTP.",
"smtpServerAddress": "Dirección del servidor SMTP",
"smtpServerAddressDescription": "Especifique la dirección del servidor utilizada para enviar correos electrónicos.",
"smtpServerPort": "Puerto del Servidor SMTP",
"smtpServerPortDescription": "Especifique el puerto utilizado para conectarse al servidor SMTP.",
"smtpSettings": "Configuraciones SMTP",
"templateVariables": {
"code": {
"description": "Código de verificación de 6 dígitos",
"title": "Código de Verificación"
},
"expire": {
"description": "Tiempo de validez del código de verificación (minutos)",
"title": "Período de Validez"
},
"expireDate": {
"description": "Fecha de expiración del servicio, recuerda a los usuarios cuándo expira el servicio",
"title": "Fecha de Expiración"
},
"maintenanceDate": {
"description": "Fecha de mantenimiento del sistema, muestra la fecha de inicio del mantenimiento",
"title": "Fecha de Mantenimiento"
},
"maintenanceTime": {
"description": "Tiempo estimado de mantenimiento, muestra el período o duración del mantenimiento",
"title": "Duración del Mantenimiento"
},
"siteLogo": {
"description": "URL de la imagen del logo del sitio web",
"title": "Logo del Sitio"
},
"siteName": {
"description": "Nombre actual del sitio web",
"title": "Nombre del Sitio"
},
"title": "Variables de Plantilla de Correo Electrónico",
"type": {
"conditionalSyntax": "Soporta sintaxis condicional para cambiar contenido según el tipo",
"description": "Identificador de tipo de correo electrónico, 1 para código de verificación de registro, otros para código de verificación de restablecimiento de contraseña",
"title": "Tipo de Correo Electrónico"
}
},
"title": "Autenticación por Correo Electrónico",
"trafficExceedEmailTemplate": "Plantilla de Aviso de Límite de Tráfico",
"trafficTemplate": "Límite de Tráfico",
"verifyEmailTemplate": "Plantilla de Correo Electrónico de Verificación",
"verifyTemplate": "Correo Electrónico de Verificación",
"whitelistSuffixes": "Sufijos de la lista blanca",
"whitelistSuffixesDescription": "Utilizado para la verificación de correo electrónico durante el registro; uno por línea",
"whitelistSuffixesPlaceholder": "Introduce los sufijos de correo electrónico, uno por línea"
},
"facebook": {
"clientId": "ID de Cliente",
"clientIdDescription": "ID de la aplicación de Facebook desde la Consola de Desarrolladores de Facebook",
"clientSecret": "Secreto del Cliente",
"clientSecretDescription": "Secreto de la aplicación de Facebook desde la Consola de Desarrolladores de Facebook",
"description": "Autenticar usuarios con cuentas de Facebook",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, los usuarios pueden iniciar sesión con su cuenta de Facebook",
"title": "Inicio de sesión con Facebook"
},
"github": {
"clientId": "ID de Cliente de GitHub",
"clientIdDescription": "El ID de cliente de la configuración de tu aplicación OAuth de GitHub",
"clientSecret": "Secreto del Cliente de GitHub",
"clientSecretDescription": "El secreto del cliente de la configuración de tu aplicación OAuth de GitHub",
"description": "Autenticar usuarios con cuentas de GitHub",
"enable": "Habilitar la autenticación de GitHub",
"enableDescription": "Permitir a los usuarios iniciar sesión con sus cuentas de GitHub",
"title": "Inicio de sesión con GitHub"
},
"google": {
"clientId": "ID de Cliente",
"clientIdDescription": "ID de cliente de Google OAuth 2.0 desde la Consola de Google Cloud",
"clientSecret": "Secreto del Cliente",
"clientSecretDescription": "Secreto del cliente de Google OAuth 2.0 desde la Consola de Google Cloud",
"description": "Autenticar usuarios con cuentas de Google",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, los usuarios pueden iniciar sesión con su cuenta de Google",
"title": "Inicio de sesión con Google"
},
"log": {
"content": "Contenido",
@ -23,52 +158,49 @@
"to": "Destinatario",
"updatedAt": "Actualizado En"
},
"register": {
"day": "Día",
"hour": "Hora",
"ipRegistrationLimit": "Límite de Registro por IP",
"ipRegistrationLimitDescription": "Cuando está habilitado, las IP que cumplan con los requisitos de la regla estarán restringidas para registrarse; ten en cuenta que la determinación de IP puede causar problemas debido a CDNs o proxies de frontend",
"minute": "Minuto",
"month": "Mes",
"noLimit": "Sin Límite",
"penaltyTime": "Tiempo de Penalización (minutos)",
"penaltyTimeDescription": "Los usuarios deben esperar a que expire el tiempo de penalización antes de registrarse nuevamente",
"registerSettings": "Configuración de Registro",
"registrationLimitCount": "Conteo de Límite de Registro",
"registrationLimitCountDescription": "Habilitar penalización después de alcanzar el límite de registro",
"saveSuccess": "Guardado Exitoso",
"stopNewUserRegistration": "Detener Registro de Nuevos Usuarios",
"stopNewUserRegistrationDescription": "Cuando está habilitado, nadie puede registrarse",
"trialDuration": "Duración de la Prueba",
"trialRegistration": "Registro de Prueba",
"trialRegistrationDescription": "Habilitar registro de prueba; modifica el paquete de prueba y la duración primero",
"trialSubscribePlan": "Plan de Suscripción de Prueba",
"trialSubscribePlanDescription": "Selecciona el plan de suscripción de prueba",
"year": "Año"
"phone": {
"accessLabel": "Acceso",
"applyPlatform": "Aplicar Plataforma",
"description": "Autenticar usuarios con números de teléfono",
"enable": "Habilitar",
"enableTip": "Después de habilitar, se activarán las funciones de registro, inicio de sesión, vinculación y desvinculación del teléfono móvil",
"endpointLabel": "Punto final",
"logs": "Registros",
"logsDescription": "Ver historial de mensajes SMS enviados y su estado",
"phoneNumberLabel": "Número de Teléfono",
"placeholders": {
"template": "Su código de verificación es {code}, válido por 5 minutos"
},
"platform": "Plataforma de SMS",
"platformConfigTip": "Por favor, complete la configuración proporcionada de {key}",
"platformTip": "Por favor, seleccione la plataforma de SMS",
"secretLabel": "Secreto",
"sendFailed": "Envío fallido",
"sendSuccess": "Envío exitoso",
"settings": "Configuración",
"signNameLabel": "Nombre de firma",
"template": "Plantilla de SMS",
"templateCodeLabel": "Código de plantilla",
"templateTip": "Por favor, complete la plantilla de SMS, mantenga {code} en el medio, de lo contrario, la función de SMS no funcionará",
"testSms": "Enviar SMS de prueba",
"testSmsPhone": "Ingrese número de teléfono",
"testSmsTip": "Envía un SMS de prueba para verificar tu configuración",
"title": "Autenticación por Teléfono",
"updateSuccess": "Actualización Exitosa",
"whitelistAreaCode": "Código de área en lista blanca",
"whitelistAreaCodeTip": "Por favor, introduce los códigos de área en la lista blanca, por ejemplo, 1, 852, 886, 888",
"whitelistValidation": "Verificación de lista blanca",
"whitelistValidationTip": "Cuando está habilitado, solo los códigos de área en la lista blanca pueden enviar SMS"
},
"verify": {
"inputPlaceholder": "Introducir",
"loginVerificationCode": "Código de Verificación de Inicio de Sesión",
"loginVerificationCodeDescription": "Verificación humana durante el inicio de sesión",
"registrationVerificationCode": "Código de Verificación de Registro",
"registrationVerificationCodeDescription": "Verificación humana durante el registro",
"resetPasswordVerificationCode": "Código de Verificación para Restablecer Contraseña",
"resetPasswordVerificationCodeDescription": "Verificación humana durante el restablecimiento de contraseña",
"saveSuccess": "Guardado Exitoso",
"turnstileSecretDescription": "Clave secreta de Turnstile proporcionada por Cloudflare",
"turnstileSiteKeyDescription": "Clave del sitio de Turnstile proporcionada por Cloudflare",
"verifySettings": "Configuración de Verificación"
},
"verify-code": {
"dailyLimit": "Límite Diario",
"dailyLimitDescription": "Número máximo de códigos de verificación que se pueden enviar por día",
"expireTime": "Tiempo de Expiración",
"expireTimeDescription": "Tiempo de expiración del código de verificación (segundos)",
"interval": "Intervalo de Envío",
"intervalDescription": "Intervalo mínimo entre el envío de códigos de verificación (segundos)",
"saveSuccess": "Guardado Exitoso",
"second": "segundos",
"times": "veces",
"verifyCodeSettings": "Configuración del Código de Verificación"
"socialAuthMethods": "Social Authentication Methods",
"telegram": {
"clientId": "ID del Bot",
"clientIdDescription": "ID del Bot de Telegram, lo puedes obtener de @BotFather",
"clientSecret": "Token del Bot",
"clientSecretDescription": "Token del Bot de Telegram, lo puedes obtener de @BotFather",
"description": "Autenticar usuarios con cuentas de Telegram",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, se activarán las funciones de registro, inicio de sesión, vinculación y desvinculación de teléfonos móviles",
"title": "Inicio de sesión con Telegram"
}
}

View File

@ -1,14 +0,0 @@
{
"blockVirtualMachine": "Bloquear Máquina Virtual",
"blockVirtualMachineDescription": "Cuando está habilitado, se evitará que los dispositivos se ejecuten en máquinas virtuales o emuladores",
"communicationKey": "Clave de Comunicación",
"communicationKeyDescription": "La clave de comunicación se utiliza para la comunicación segura entre dispositivos y servidores",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, se admiten múltiples identificadores de dispositivos como IMEI/IDFA/IDFV/AndroidID/dirección MAC para el inicio de sesión y el registro",
"enableSecurity": "Habilitar Cifrado de Comunicación",
"enableSecurityDescription": "Cuando está habilitado, la comunicación entre dispositivos y servidores será cifrada",
"saveFailed": "Error al guardar",
"saveSuccess": "Guardado exitoso",
"showAds": "Mostrar Anuncios",
"showAdsDescription": "Cuando está habilitado, se mostrarán anuncios en los dispositivos"
}

View File

@ -1,49 +0,0 @@
{
"emailBasicConfigDescription": "Configura los ajustes del servidor SMTP y las opciones de verificación de correo electrónico",
"emailLogsDescription": "Ver el historial de correos electrónicos enviados y su estado",
"emailSuffixWhitelist": "Lista blanca de sufijos de correo electrónico",
"emailSuffixWhitelistDescription": "Cuando está habilitado, solo los correos electrónicos con sufijos en la lista pueden registrarse",
"emailVerification": "Verificación de correo electrónico",
"emailVerificationDescription": "Cuando esté habilitado, los usuarios deberán verificar su correo electrónico",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, se activarán las funciones de registro por correo electrónico, inicio de sesión, vinculación y desvinculación.",
"expiration_email_template": "Plantilla de Aviso de Vencimiento",
"expiration_email_templateDescription": "Los marcadores de posición {after}.variable{before} serán reemplazados con datos reales. Asegúrate de mantener estas variables.",
"failed": "Fallido",
"inputPlaceholder": "Ingrese valor...",
"logs": "Registros",
"maintenance_email_template": "Plantilla de Aviso de Mantenimiento",
"maintenance_email_templateDescription": "Los marcadores de posición {after}.variable{before} serán reemplazados con datos reales. Asegúrese de mantener estas variables.",
"recipient": "Destinatario",
"saveFailed": "Error al guardar la configuración",
"saveSuccess": "Configuración guardada con éxito.",
"sendFailure": "Error al enviar el correo electrónico de prueba, por favor verifica la configuración.",
"sendSuccess": "Correo de prueba enviado con éxito.",
"sendTestEmail": "Enviar correo electrónico de prueba",
"sendTestEmailDescription": "Envía un correo electrónico de prueba para verificar la configuración.",
"senderAddress": "Dirección del Remitente",
"senderAddressDescription": "La dirección de correo electrónico predeterminada utilizada para enviar correos electrónicos.",
"sent": "Enviado",
"sentAt": "Enviado A",
"settings": "Configuración",
"smtpAccount": "Cuenta SMTP",
"smtpAccountDescription": "La cuenta de correo electrónico utilizada para la autenticación.",
"smtpEncryptionMethod": "Método de Cifrado SMTP",
"smtpEncryptionMethodDescription": "Elija si desea habilitar el cifrado SSL/TLS.",
"smtpPassword": "Contraseña SMTP",
"smtpPasswordDescription": "La contraseña para la cuenta SMTP.",
"smtpServerAddress": "Dirección del servidor SMTP",
"smtpServerAddressDescription": "Especifique la dirección del servidor utilizada para enviar correos electrónicos.",
"smtpServerPort": "Puerto del Servidor SMTP",
"smtpServerPortDescription": "Especifique el puerto utilizado para conectarse al servidor SMTP.",
"status": "Estado",
"subject": "Asunto",
"template": "Plantilla",
"traffic_exceed_email_template": "Plantilla de Notificación de Exceso de Tráfico",
"traffic_exceed_email_templateDescription": "Los marcadores de posición {after}.variable{before} serán reemplazados con datos reales. Asegúrate de mantener estas variables.",
"verify_email_template": "Plantilla de Correo Electrónico de Verificación",
"verify_email_templateDescription": "Los marcadores de posición {after}.variable{before} serán reemplazados con datos reales. Asegúrese de mantener estas variables.",
"whitelistSuffixes": "Sufijos de la lista blanca",
"whitelistSuffixesDescription": "Utilizado para la verificación de correo electrónico durante el registro; uno por línea",
"whitelistSuffixesPlaceholder": "Introduce los sufijos de correo electrónico, uno por línea"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "ID de Cliente",
"clientIdDescription": "ID de la aplicación de Facebook desde la Consola de Desarrolladores de Facebook",
"clientSecret": "Secreto del Cliente",
"clientSecretDescription": "Secreto de la aplicación de Facebook desde la Consola de Desarrolladores de Facebook",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, los usuarios pueden iniciar sesión con su cuenta de Facebook",
"saveFailed": "Error al guardar",
"saveSuccess": "Guardado exitoso"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "ID de Cliente de GitHub",
"clientIdDescription": "El ID de cliente de la configuración de tu aplicación OAuth de GitHub",
"clientSecret": "Secreto del Cliente de GitHub",
"clientSecretDescription": "El secreto del cliente de la configuración de tu aplicación OAuth de GitHub",
"enable": "Habilitar la autenticación de GitHub",
"enableDescription": "Permitir a los usuarios iniciar sesión con sus cuentas de GitHub",
"saveFailed": "Error al guardar la configuración de GitHub",
"saveSuccess": "Configuración de GitHub guardada con éxito"
}

View File

@ -1,10 +0,0 @@
{
"clientId": "ID de Cliente",
"clientIdDescription": "ID de cliente de Google OAuth 2.0 desde la Consola de Google Cloud",
"clientSecret": "Secreto del Cliente",
"clientSecretDescription": "Secreto del cliente de Google OAuth 2.0 desde la Consola de Google Cloud",
"enable": "Habilitar",
"enableDescription": "Después de habilitar, los usuarios pueden iniciar sesión con su cuenta de Google",
"saveFailed": "Error al guardar",
"saveSuccess": "Guardado exitoso"
}

View File

@ -1,32 +0,0 @@
{
"accessLabel": "Acceso",
"applyPlatform": "Aplicar Plataforma",
"enable": "Habilitar",
"enableTip": "Después de habilitar, se activarán las funciones de registro, inicio de sesión, vinculación y desvinculación del teléfono móvil",
"endpointLabel": "Punto final",
"logs": "Registros",
"phoneNumberLabel": "Número de Teléfono",
"placeholders": {
"template": "Su código de verificación es {code}, válido por 5 minutos",
"templateCode": "Ingrese el código de la plantilla"
},
"platform": "Plataforma de SMS",
"platformConfigTip": "Por favor, complete la configuración proporcionada de {key}",
"platformTip": "Por favor, seleccione la plataforma de SMS",
"secretLabel": "Secreto",
"sendFailed": "Envío fallido",
"sendSuccess": "Envío exitoso",
"settings": "Configuración",
"signNameLabel": "Nombre de firma",
"template": "Plantilla de SMS",
"templateCodeLabel": "Código de plantilla",
"templateTip": "Por favor, complete la plantilla de SMS, mantenga {code} en el medio, de lo contrario, la función de SMS no funcionará",
"testSms": "Enviar SMS de prueba",
"testSmsPhone": "Ingrese número de teléfono",
"testSmsTip": "Envía un SMS de prueba para verificar tu configuración",
"updateSuccess": "Actualización Exitosa",
"whitelistAreaCode": "Código de área en lista blanca",
"whitelistAreaCodeTip": "Por favor, introduce los códigos de área en la lista blanca, por ejemplo, 1, 852, 886, 888",
"whitelistValidation": "Verificación de lista blanca",
"whitelistValidationTip": "Cuando está habilitado, solo los códigos de área en la lista blanca pueden enviar SMS"
}

View File

@ -8,9 +8,12 @@
"createRule": "Agregar Regla",
"createSuccess": "Regla creada con éxito",
"createdAt": "Creado En",
"default": "Predeterminado",
"defaultRule": "Regla Predeterminada",
"delete": "Eliminar",
"deleteSuccess": "Regla eliminada con éxito",
"deleteWarning": "¿Está seguro de que desea eliminar esta regla? Esta acción no se puede deshacer.",
"direct": "Directo",
"downloadTemplate": "Descargar Plantilla",
"edit": "Editar",
"editRule": "Editar Regla",
@ -29,11 +32,14 @@
"noValidRules": "No se encontraron reglas válidas",
"pleaseUploadFile": "Por favor, suba un archivo YAML",
"preview": "Vista Previa",
"reject": "Rechazar",
"rulesFormat": "Formato de regla: tipo de regla, contenido de coincidencia, [política], donde la política es opcional.\nSi no se especifica la política, se utilizará automáticamente el nombre del grupo de reglas actual. Ejemplos:",
"rulesLabel": "Contenido de la Regla",
"searchRule": "Buscar nombre de regla",
"selectTags": "Seleccionar etiquetas de nodo",
"selectType": "Seleccionar tipo de regla",
"tags": "Etiquetas de Nodo",
"tagsLabel": "Etiquetas de Nodo",
"type": "Tipo de Regla",
"updateSuccess": "Regla actualizada con éxito"
}

View File

@ -33,21 +33,22 @@
"description": "Descripción",
"edit": "Editar",
"editNodeGroup": "Editar grupo de nodos",
"form": {
"cancel": "Cancelar",
"confirm": "Confirmar",
"description": "Descripción",
"name": "Nombre"
},
"name": "Nombre",
"title": "Lista de grupos de nodos",
"updatedAt": "Fecha de actualización"
},
"groupForm": {
"cancel": "Cancelar",
"confirm": "Confirmar",
"description": "Descripción",
"name": "Nombre"
},
"node": {
"abnormal": "Anormal",
"actions": "Acciones",
"address": "Dirección",
"all": "Todo",
"basicInfo": "Información Básica",
"cancel": "Cancelar",
"confirm": "Confirmar",
"confirmDelete": "¿Está seguro de que desea eliminar?",
@ -59,96 +60,120 @@
"delete": "Eliminar",
"deleteSuccess": "Eliminación exitosa",
"deleteWarning": "Después de eliminar, los datos no se podrán recuperar. Proceda con precaución.",
"detail": "Detalle",
"disabled": "Deshabilitado",
"disk": "Disco",
"edit": "Editar",
"editNode": "Editar nodo",
"enable": "Habilitar",
"form": {
"allowInsecure": "Permitir inseguro",
"cancel": "Cancelar",
"city": "Ciudad",
"confirm": "Confirmar",
"country": "País",
"edit": "Editar",
"editSecurity": "Editar configuración de seguridad",
"enableTLS": "Habilitar TLS",
"encryptionMethod": "Método de encriptación",
"flow": "Algoritmo de control de flujo",
"groupId": "Grupo de Nodo",
"hopInterval": "Intervalo de salto",
"hopPorts": "Puertos de salto",
"hopPortsPlaceholder": "Varios puertos separados por comas",
"name": "Nombre",
"obfsPassword": "Contraseña de ofuscación",
"obfsPasswordPlaceholder": "Dejar en blanco para no ofuscar",
"path": "Ruta",
"pleaseSelect": "Por favor seleccione",
"port": "Puerto del servidor",
"protocol": "Protocolo",
"relayHost": "Dirección de retransmisión",
"relayMode": "Modo de retransmisión",
"relayModeOptions": {
"all": "Todos",
"none": "Ninguno",
"random": "Aleatorio"
},
"relayPort": "Puerto de retransmisión",
"relayPrefix": "Prefijo de retransmisión",
"remarks": "Observaciones",
"security": "Seguridad",
"securityConfig": "Configuración de seguridad",
"security_config": {
"fingerprint": "Huella digital",
"privateKey": "Clave privada",
"privateKeyPlaceholder": "Dejar en blanco para generar automáticamente",
"publicKey": "Clave pública",
"publicKeyPlaceholder": "Dejar en blanco para generar automáticamente",
"serverAddress": "Dirección del servidor",
"serverAddressPlaceholder": "Dirección objetivo de REALITY, por defecto usa SNI",
"serverName": "Nombre del servidor (SNI)",
"serverNamePlaceholder": "Obligatorio para REALITY, debe coincidir con el backend",
"serverPort": "Puerto del servidor",
"serverPortPlaceholder": "Puerto objetivo de REALITY, por defecto 443",
"shortId": "ID corto",
"shortIdPlaceholder": "Dejar en blanco para generar automáticamente",
"sni": "Indicación de nombre del servidor (SNI)"
},
"selectEncryptionMethod": "Seleccionar método de encriptación",
"selectNodeGroup": "Seleccionar grupo de nodos",
"selectProtocol": "Seleccionar protocolo",
"selectRelayMode": "Seleccionar modo de retransmisión",
"serverAddr": "Dirección del servidor",
"serverKey": "Clave del Servidor",
"serverName": "Nombre del servicio",
"speedLimit": "Límite de velocidad",
"speedLimitPlaceholder": "Sin límite",
"tags": "Etiquetas",
"tagsPlaceholder": "Usa Enter o coma (,) para ingresar múltiples etiquetas",
"trafficRatio": "Tasa de tráfico",
"transport": "Protocolo de transporte",
"transportConfig": "Configuración del protocolo de transporte",
"transportHost": "Dirección del servicio de transporte",
"transportPath": "Ruta de transporte",
"transportServerName": "Nombre del servicio de transporte"
},
"enabled": "Habilitado",
"expireTime": "Tiempo de Expiración",
"hide": "Ocultar",
"id": "ID",
"ipAddresses": "Direcciones IP",
"lastUpdated": "Última actualización",
"location": "Ubicación",
"memory": "Memoria",
"name": "Nombre",
"noData": "--",
"node": "Nodo",
"nodeDetail": "Detalle del Nodo",
"nodeGroup": "Grupo de nodos",
"nodeStatus": "Estado del Nodo",
"normal": "Normal",
"onlineCount": "Número de usuarios en línea",
"onlineUsers": "Usuarios en línea",
"protocol": "Protocolo",
"rate": "Tasa",
"relay": "Retransmisión",
"serverAddr": "Dirección del servidor",
"speedLimit": "Límite de velocidad",
"status": "Estado",
"subscribeId": "ID de Suscripción",
"subscribeName": "Nombre de Suscripción",
"subscription": "Suscripción",
"tags": "Etiquetas",
"trafficRatio": "Tasa de tráfico",
"trafficUsage": "Uso de Tráfico",
"type": "Tipo",
"updateSuccess": "Actualización exitosa",
"updatedAt": "Fecha de actualización"
"updatedAt": "Fecha de actualización",
"userAccount": "Cuenta de Usuario",
"userDetail": "Detalle del Usuario",
"userId": "ID de Usuario"
},
"nodeForm": {
"allowInsecure": "Permitir Inseguro",
"cancel": "Cancelar",
"city": "Ciudad",
"confirm": "Confirmar",
"congestionController": "Controlador de Congestión",
"country": "País",
"disableSni": "Deshabilitar SNI",
"edit": "Editar",
"editSecurity": "Editar Configuración de Seguridad",
"enableTLS": "Habilitar TLS",
"encryptionMethod": "Método de Cifrado",
"fingerprint": "Huella Digital",
"flow": "Algoritmo de Control de Flujo",
"groupId": "Grupo de Nodos",
"hopInterval": "Intervalo de Salto",
"hopPorts": "Puertos de Salto",
"hopPortsPlaceholder": "Separe múltiples puertos con comas",
"name": "Nombre",
"obfsPassword": "Contraseña de Ofuscación",
"obfsPasswordPlaceholder": "Deje en blanco para no ofuscar",
"path": "Ruta",
"pleaseSelect": "Por Favor Seleccione",
"port": "Puerto del Servidor",
"protocol": "Protocolo",
"reduceRtt": "Reducir RTT",
"relayHost": "Host de Relevo",
"relayMode": "Modo de Relevo",
"relayPort": "Puerto de Relevo",
"relayPrefix": "Prefijo de Relevo",
"remarks": "Observaciones",
"security": "Seguridad",
"securityConfig": "Configuración de Seguridad",
"selectEncryptionMethod": "Seleccionar Método de Cifrado",
"selectNodeGroup": "Seleccionar Grupo de Nodos",
"selectProtocol": "Seleccionar Protocolo",
"selectRelayMode": "Seleccionar Modo de Relevo",
"serverAddr": "Dirección del Servidor",
"serverKey": "Clave del Servidor",
"serverName": "Nombre del Servicio",
"speedLimit": "Límite de Velocidad",
"speedLimitPlaceholder": "Ilimitado",
"tags": "Etiquetas",
"tagsPlaceholder": "Use Enter o coma (,) para ingresar múltiples etiquetas",
"trafficRatio": "Tasa de Tráfico",
"transport": "Configuración del Protocolo de Transporte",
"transportConfig": "Configuración del Protocolo de Transporte",
"transportHost": "Dirección del Servidor de Transporte",
"transportPath": "Ruta de Transporte",
"transportServerName": "Nombre del Servidor de Transporte",
"udpRelayMode": "Modo de Relevo UDP"
},
"relayModeOptions": {
"all": "Todos",
"none": "Ninguno",
"random": "Aleatorio"
},
"securityConfig": {
"fingerprint": "Huella Digital",
"privateKey": "Clave Privada",
"privateKeyPlaceholder": "Deje en blanco para auto-generación",
"publicKey": "Clave Pública",
"publicKeyPlaceholder": "Deje en blanco para auto-generación",
"serverAddress": "Dirección del Servidor",
"serverAddressPlaceholder": "Dirección de destino REALITY, por defecto usando SNI",
"serverName": "Nombre del Servidor (SNI)",
"serverNamePlaceholder": "REALITY requerido, consistente con el backend",
"serverPort": "Puerto del Servidor",
"serverPortPlaceholder": "Puerto de destino REALITY, por defecto 443",
"shortId": "ID Corto",
"shortIdPlaceholder": "Deje en blanco para auto-generación",
"sni": "Indicación de Nombre del Servidor (SNI)"
},
"tabs": {
"node": "Nodo",

Some files were not shown because too many files have changed in this diff Show More