mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 01:19:34 +00:00
feat(frontend): block invalid settings saves with Zod pre-save check
Tighten AllSettingSchema with the actual valid ranges and patterns: - webPort / subPort / ldapPort: integer 1-65535 - pageSize: integer 1-1000 - sessionMaxAge: integer >= 1 - tgCpu: integer 0-100 (percentage) - subUpdates: integer 1-168 (hours) - expireDiff / trafficDiff / ldapDefault*: non-negative integers - webBasePath / subPath / subJsonPath / subClashPath: must start with / The existing useAllSettings save path runs AllSettingSchema.partial() through safeParse and logs drift without blocking. SettingsPage now adds a stronger gate before the mutation: run the full schema against the draft and, on failure, surface the first issue (field path + message) via the existing messageApi.error so the user actually sees what's wrong instead of silently sending bad data to the backend. Use cases caught: port out of range, negative quota, sub path missing leading slash, page size set to 0, tgCpu > 100.
This commit is contained in:
@@ -29,6 +29,7 @@ import { setMessageInstance } from '@/utils/messageBus';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useAllSettings } from '@/api/queries/useAllSettings';
|
||||
import { AllSettingSchema } from '@/schemas/setting';
|
||||
import AppSidebar from '@/components/AppSidebar';
|
||||
import GeneralTab from './GeneralTab';
|
||||
import SecurityTab from './SecurityTab';
|
||||
@@ -148,6 +149,18 @@ export default function SettingsPage() {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
const result = AllSettingSchema.safeParse(allSetting);
|
||||
if (!result.success) {
|
||||
const issue = result.error.issues[0];
|
||||
const fieldPath = issue?.path.join('.') ?? 'value';
|
||||
const msgKey = issue?.message ?? 'somethingWentWrong';
|
||||
messageApi.error(`${fieldPath}: ${t(msgKey, { defaultValue: msgKey })}`);
|
||||
return;
|
||||
}
|
||||
await saveAll();
|
||||
}
|
||||
|
||||
function restartPanel() {
|
||||
modal.confirm({
|
||||
title: t('pages.settings.restartPanel'),
|
||||
@@ -301,7 +314,7 @@ export default function SettingsPage() {
|
||||
<Row className="header-row">
|
||||
<Col xs={24} sm={10} className="header-actions">
|
||||
<Space>
|
||||
<Button type="primary" disabled={saveDisabled} onClick={saveAll}>
|
||||
<Button type="primary" disabled={saveDisabled} onClick={onSave}>
|
||||
{t('pages.settings.save')}
|
||||
</Button>
|
||||
<Button type="primary" danger disabled={!saveDisabled} onClick={restartPanel}>
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const port = z.number().int().min(1).max(65535);
|
||||
const nonNegativeInt = z.number().int().min(0);
|
||||
const absolutePath = z.string().regex(/^\//, 'pages.settings.validation.pathLeadingSlash');
|
||||
|
||||
export const AllSettingSchema = z.object({
|
||||
webListen: z.string().optional(),
|
||||
webDomain: z.string().optional(),
|
||||
webPort: z.number().optional(),
|
||||
webPort: port.optional(),
|
||||
webCertFile: z.string().optional(),
|
||||
webKeyFile: z.string().optional(),
|
||||
webBasePath: z.string().optional(),
|
||||
sessionMaxAge: z.number().optional(),
|
||||
webBasePath: absolutePath.optional(),
|
||||
sessionMaxAge: z.number().int().min(1).optional(),
|
||||
trustedProxyCIDRs: z.string().optional(),
|
||||
pageSize: z.number().optional(),
|
||||
expireDiff: z.number().optional(),
|
||||
trafficDiff: z.number().optional(),
|
||||
pageSize: z.number().int().min(1).max(1000).optional(),
|
||||
expireDiff: nonNegativeInt.optional(),
|
||||
trafficDiff: nonNegativeInt.optional(),
|
||||
remarkModel: z.string().optional(),
|
||||
datepicker: z.enum(['gregorian', 'jalalian']).optional(),
|
||||
tgBotEnable: z.boolean().optional(),
|
||||
@@ -22,7 +26,7 @@ export const AllSettingSchema = z.object({
|
||||
tgRunTime: z.string().optional(),
|
||||
tgBotBackup: z.boolean().optional(),
|
||||
tgBotLoginNotify: z.boolean().optional(),
|
||||
tgCpu: z.number().optional(),
|
||||
tgCpu: z.number().int().min(0).max(100).optional(),
|
||||
tgLang: z.string().optional(),
|
||||
twoFactorEnable: z.boolean().optional(),
|
||||
twoFactorToken: z.string().optional(),
|
||||
@@ -36,18 +40,18 @@ export const AllSettingSchema = z.object({
|
||||
subEnableRouting: z.boolean().optional(),
|
||||
subRoutingRules: z.string().optional(),
|
||||
subListen: z.string().optional(),
|
||||
subPort: z.number().optional(),
|
||||
subPath: z.string().optional(),
|
||||
subJsonPath: z.string().optional(),
|
||||
subPort: port.optional(),
|
||||
subPath: absolutePath.optional(),
|
||||
subJsonPath: absolutePath.optional(),
|
||||
subClashEnable: z.boolean().optional(),
|
||||
subClashPath: z.string().optional(),
|
||||
subClashPath: absolutePath.optional(),
|
||||
subDomain: z.string().optional(),
|
||||
externalTrafficInformEnable: z.boolean().optional(),
|
||||
externalTrafficInformURI: z.string().optional(),
|
||||
restartXrayOnClientDisable: z.boolean().optional(),
|
||||
subCertFile: z.string().optional(),
|
||||
subKeyFile: z.string().optional(),
|
||||
subUpdates: z.number().optional(),
|
||||
subUpdates: z.number().int().min(1).max(168).optional(),
|
||||
subEncrypt: z.boolean().optional(),
|
||||
subShowInfo: z.boolean().optional(),
|
||||
subEmailInRemark: z.boolean().optional(),
|
||||
@@ -61,7 +65,7 @@ export const AllSettingSchema = z.object({
|
||||
timeLocation: z.string().optional(),
|
||||
ldapEnable: z.boolean().optional(),
|
||||
ldapHost: z.string().optional(),
|
||||
ldapPort: z.number().optional(),
|
||||
ldapPort: port.optional(),
|
||||
ldapUseTLS: z.boolean().optional(),
|
||||
ldapBindDN: z.string().optional(),
|
||||
ldapPassword: z.string().optional(),
|
||||
@@ -76,9 +80,9 @@ export const AllSettingSchema = z.object({
|
||||
ldapInboundTags: z.string().optional(),
|
||||
ldapAutoCreate: z.boolean().optional(),
|
||||
ldapAutoDelete: z.boolean().optional(),
|
||||
ldapDefaultTotalGB: z.number().optional(),
|
||||
ldapDefaultExpiryDays: z.number().optional(),
|
||||
ldapDefaultLimitIP: z.number().optional(),
|
||||
ldapDefaultTotalGB: nonNegativeInt.optional(),
|
||||
ldapDefaultExpiryDays: nonNegativeInt.optional(),
|
||||
ldapDefaultLimitIP: nonNegativeInt.optional(),
|
||||
hasTgBotToken: z.boolean().optional(),
|
||||
hasTwoFactorToken: z.boolean().optional(),
|
||||
hasLdapPassword: z.boolean().optional(),
|
||||
|
||||
Reference in New Issue
Block a user