mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +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:
parent
a3012daa8f
commit
4ecbb0e55f
2 changed files with 34 additions and 17 deletions
|
|
@ -29,6 +29,7 @@ import { setMessageInstance } from '@/utils/messageBus';
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||||
import { useAllSettings } from '@/api/queries/useAllSettings';
|
import { useAllSettings } from '@/api/queries/useAllSettings';
|
||||||
|
import { AllSettingSchema } from '@/schemas/setting';
|
||||||
import AppSidebar from '@/components/AppSidebar';
|
import AppSidebar from '@/components/AppSidebar';
|
||||||
import GeneralTab from './GeneralTab';
|
import GeneralTab from './GeneralTab';
|
||||||
import SecurityTab from './SecurityTab';
|
import SecurityTab from './SecurityTab';
|
||||||
|
|
@ -148,6 +149,18 @@ export default function SettingsPage() {
|
||||||
return url.toString();
|
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() {
|
function restartPanel() {
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: t('pages.settings.restartPanel'),
|
title: t('pages.settings.restartPanel'),
|
||||||
|
|
@ -301,7 +314,7 @@ export default function SettingsPage() {
|
||||||
<Row className="header-row">
|
<Row className="header-row">
|
||||||
<Col xs={24} sm={10} className="header-actions">
|
<Col xs={24} sm={10} className="header-actions">
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" disabled={saveDisabled} onClick={saveAll}>
|
<Button type="primary" disabled={saveDisabled} onClick={onSave}>
|
||||||
{t('pages.settings.save')}
|
{t('pages.settings.save')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="primary" danger disabled={!saveDisabled} onClick={restartPanel}>
|
<Button type="primary" danger disabled={!saveDisabled} onClick={restartPanel}>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
import { z } from 'zod';
|
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({
|
export const AllSettingSchema = z.object({
|
||||||
webListen: z.string().optional(),
|
webListen: z.string().optional(),
|
||||||
webDomain: z.string().optional(),
|
webDomain: z.string().optional(),
|
||||||
webPort: z.number().optional(),
|
webPort: port.optional(),
|
||||||
webCertFile: z.string().optional(),
|
webCertFile: z.string().optional(),
|
||||||
webKeyFile: z.string().optional(),
|
webKeyFile: z.string().optional(),
|
||||||
webBasePath: z.string().optional(),
|
webBasePath: absolutePath.optional(),
|
||||||
sessionMaxAge: z.number().optional(),
|
sessionMaxAge: z.number().int().min(1).optional(),
|
||||||
trustedProxyCIDRs: z.string().optional(),
|
trustedProxyCIDRs: z.string().optional(),
|
||||||
pageSize: z.number().optional(),
|
pageSize: z.number().int().min(1).max(1000).optional(),
|
||||||
expireDiff: z.number().optional(),
|
expireDiff: nonNegativeInt.optional(),
|
||||||
trafficDiff: z.number().optional(),
|
trafficDiff: nonNegativeInt.optional(),
|
||||||
remarkModel: z.string().optional(),
|
remarkModel: z.string().optional(),
|
||||||
datepicker: z.enum(['gregorian', 'jalalian']).optional(),
|
datepicker: z.enum(['gregorian', 'jalalian']).optional(),
|
||||||
tgBotEnable: z.boolean().optional(),
|
tgBotEnable: z.boolean().optional(),
|
||||||
|
|
@ -22,7 +26,7 @@ export const AllSettingSchema = z.object({
|
||||||
tgRunTime: z.string().optional(),
|
tgRunTime: z.string().optional(),
|
||||||
tgBotBackup: z.boolean().optional(),
|
tgBotBackup: z.boolean().optional(),
|
||||||
tgBotLoginNotify: z.boolean().optional(),
|
tgBotLoginNotify: z.boolean().optional(),
|
||||||
tgCpu: z.number().optional(),
|
tgCpu: z.number().int().min(0).max(100).optional(),
|
||||||
tgLang: z.string().optional(),
|
tgLang: z.string().optional(),
|
||||||
twoFactorEnable: z.boolean().optional(),
|
twoFactorEnable: z.boolean().optional(),
|
||||||
twoFactorToken: z.string().optional(),
|
twoFactorToken: z.string().optional(),
|
||||||
|
|
@ -36,18 +40,18 @@ export const AllSettingSchema = z.object({
|
||||||
subEnableRouting: z.boolean().optional(),
|
subEnableRouting: z.boolean().optional(),
|
||||||
subRoutingRules: z.string().optional(),
|
subRoutingRules: z.string().optional(),
|
||||||
subListen: z.string().optional(),
|
subListen: z.string().optional(),
|
||||||
subPort: z.number().optional(),
|
subPort: port.optional(),
|
||||||
subPath: z.string().optional(),
|
subPath: absolutePath.optional(),
|
||||||
subJsonPath: z.string().optional(),
|
subJsonPath: absolutePath.optional(),
|
||||||
subClashEnable: z.boolean().optional(),
|
subClashEnable: z.boolean().optional(),
|
||||||
subClashPath: z.string().optional(),
|
subClashPath: absolutePath.optional(),
|
||||||
subDomain: z.string().optional(),
|
subDomain: z.string().optional(),
|
||||||
externalTrafficInformEnable: z.boolean().optional(),
|
externalTrafficInformEnable: z.boolean().optional(),
|
||||||
externalTrafficInformURI: z.string().optional(),
|
externalTrafficInformURI: z.string().optional(),
|
||||||
restartXrayOnClientDisable: z.boolean().optional(),
|
restartXrayOnClientDisable: z.boolean().optional(),
|
||||||
subCertFile: z.string().optional(),
|
subCertFile: z.string().optional(),
|
||||||
subKeyFile: z.string().optional(),
|
subKeyFile: z.string().optional(),
|
||||||
subUpdates: z.number().optional(),
|
subUpdates: z.number().int().min(1).max(168).optional(),
|
||||||
subEncrypt: z.boolean().optional(),
|
subEncrypt: z.boolean().optional(),
|
||||||
subShowInfo: z.boolean().optional(),
|
subShowInfo: z.boolean().optional(),
|
||||||
subEmailInRemark: z.boolean().optional(),
|
subEmailInRemark: z.boolean().optional(),
|
||||||
|
|
@ -61,7 +65,7 @@ export const AllSettingSchema = z.object({
|
||||||
timeLocation: z.string().optional(),
|
timeLocation: z.string().optional(),
|
||||||
ldapEnable: z.boolean().optional(),
|
ldapEnable: z.boolean().optional(),
|
||||||
ldapHost: z.string().optional(),
|
ldapHost: z.string().optional(),
|
||||||
ldapPort: z.number().optional(),
|
ldapPort: port.optional(),
|
||||||
ldapUseTLS: z.boolean().optional(),
|
ldapUseTLS: z.boolean().optional(),
|
||||||
ldapBindDN: z.string().optional(),
|
ldapBindDN: z.string().optional(),
|
||||||
ldapPassword: z.string().optional(),
|
ldapPassword: z.string().optional(),
|
||||||
|
|
@ -76,9 +80,9 @@ export const AllSettingSchema = z.object({
|
||||||
ldapInboundTags: z.string().optional(),
|
ldapInboundTags: z.string().optional(),
|
||||||
ldapAutoCreate: z.boolean().optional(),
|
ldapAutoCreate: z.boolean().optional(),
|
||||||
ldapAutoDelete: z.boolean().optional(),
|
ldapAutoDelete: z.boolean().optional(),
|
||||||
ldapDefaultTotalGB: z.number().optional(),
|
ldapDefaultTotalGB: nonNegativeInt.optional(),
|
||||||
ldapDefaultExpiryDays: z.number().optional(),
|
ldapDefaultExpiryDays: nonNegativeInt.optional(),
|
||||||
ldapDefaultLimitIP: z.number().optional(),
|
ldapDefaultLimitIP: nonNegativeInt.optional(),
|
||||||
hasTgBotToken: z.boolean().optional(),
|
hasTgBotToken: z.boolean().optional(),
|
||||||
hasTwoFactorToken: z.boolean().optional(),
|
hasTwoFactorToken: z.boolean().optional(),
|
||||||
hasLdapPassword: z.boolean().optional(),
|
hasLdapPassword: z.boolean().optional(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue