Skip to content

Commit 3f3f38d

Browse files
authored
Merge pull request #34 from phenix3443/codex/fix-server-protocol-required-validation
2 parents 720dcc2 + 68fccfb commit 3f3f38d

File tree

4 files changed

+144
-17
lines changed

4 files changed

+144
-17
lines changed

apps/admin/src/sections/servers/form-schema/constants.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,7 @@ export const SECURITY = {
7272
} as const;
7373

7474
export const FLOWS = {
75-
vless: [
76-
"none",
77-
"xtls-rprx-vision",
78-
] as const,
75+
vless: ["none", "xtls-rprx-vision"] as const,
7976
} as const;
8077

8178
export const TUIC_UDP_RELAY_MODES = ["native", "quic"] as const;

apps/admin/src/sections/servers/form-schema/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type FieldConfig = {
44
name: string;
55
type: "input" | "select" | "switch" | "number" | "textarea";
66
label: string;
7+
required?: boolean;
78
placeholder?: string;
89
options?: readonly string[];
910
defaultValue?: any;

apps/admin/src/sections/servers/form-schema/useProtocolFields.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function useProtocolFields() {
4242
name: "port",
4343
type: "number",
4444
label: t("port", "Port"),
45+
required: true,
4546
min: 1,
4647
max: 65_535,
4748
placeholder: "1-65535",
@@ -149,6 +150,7 @@ export function useProtocolFields() {
149150
name: "port",
150151
type: "number",
151152
label: t("port", "Port"),
153+
required: true,
152154
min: 1,
153155
max: 65_535,
154156
placeholder: "1-65535",
@@ -259,6 +261,7 @@ export function useProtocolFields() {
259261
name: "port",
260262
type: "number",
261263
label: t("port", "Port"),
264+
required: true,
262265
min: 1,
263266
max: 65_535,
264267
placeholder: "1-65535",
@@ -551,6 +554,7 @@ export function useProtocolFields() {
551554
name: "port",
552555
type: "number",
553556
label: t("port", "Port"),
557+
required: true,
554558
min: 1,
555559
max: 65_535,
556560
placeholder: "1-65535",
@@ -661,6 +665,7 @@ export function useProtocolFields() {
661665
name: "port",
662666
type: "number",
663667
label: t("port", "Port"),
668+
required: true,
664669
min: 1,
665670
max: 65_535,
666671
placeholder: "1-65535",
@@ -788,6 +793,7 @@ export function useProtocolFields() {
788793
name: "port",
789794
type: "number",
790795
label: t("port", "Port"),
796+
required: true,
791797
min: 1,
792798
max: 65_535,
793799
placeholder: "1-65535",
@@ -881,6 +887,7 @@ export function useProtocolFields() {
881887
name: "port",
882888
type: "number",
883889
label: t("port", "Port"),
890+
required: true,
884891
min: 1,
885892
max: 65_535,
886893
placeholder: "1-65535",
@@ -901,6 +908,7 @@ export function useProtocolFields() {
901908
name: "port",
902909
type: "number",
903910
label: t("port", "Port"),
911+
required: true,
904912
min: 1,
905913
max: 65_535,
906914
placeholder: "1-65535",
@@ -978,6 +986,7 @@ export function useProtocolFields() {
978986
name: "port",
979987
type: "number",
980988
label: t("port", "Port"),
989+
required: true,
981990
min: 1,
982991
max: 65_535,
983992
placeholder: "1-65535",
@@ -1055,6 +1064,7 @@ export function useProtocolFields() {
10551064
name: "port",
10561065
type: "number",
10571066
label: t("port", "Port"),
1067+
required: true,
10581068
min: 1,
10591069
max: 65_535,
10601070
placeholder: "1-65535",
@@ -1091,6 +1101,7 @@ export function useProtocolFields() {
10911101
name: "port",
10921102
type: "number",
10931103
label: t("port", "Port"),
1104+
required: true,
10941105
min: 1,
10951106
max: 65_535,
10961107
placeholder: "1-65535",
@@ -1206,10 +1217,7 @@ export function useProtocolFields() {
12061217
name: "reality_public_key",
12071218
type: "input",
12081219
label: t("security_public_key", "Reality Public Key"),
1209-
placeholder: t(
1210-
"security_public_key_placeholder",
1211-
"Enter public key"
1212-
),
1220+
placeholder: t("security_public_key_placeholder", "Enter public key"),
12131221
group: "reality",
12141222
condition: (p) => p.security === "reality",
12151223
},

apps/admin/src/sections/servers/server-form.tsx

Lines changed: 130 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,33 @@ import {
5454
getLabel,
5555
getProtocolDefaultConfig,
5656
protocols as PROTOCOLS,
57+
type ProtocolType,
5758
useProtocolFields,
5859
} from "./form-schema";
5960

61+
function getFieldLabel(field: FieldConfig) {
62+
return (
63+
<>
64+
{field.label}
65+
{field.required ? (
66+
<span aria-hidden="true" className="ml-1 text-destructive">
67+
*
68+
</span>
69+
) : null}
70+
</>
71+
);
72+
}
73+
74+
function getVisibleRequiredFields(
75+
fields: FieldConfig[],
76+
protocolData: Record<string, any>
77+
) {
78+
return fields.filter(
79+
(field) =>
80+
field.required && (!field.condition || field.condition(protocolData, {}))
81+
);
82+
}
83+
6084
function DynamicField({
6185
field,
6286
control,
@@ -88,7 +112,7 @@ function DynamicField({
88112
{...commonProps}
89113
render={({ field: fieldProps }) => (
90114
<FormItem>
91-
<FormLabel>{field.label}</FormLabel>
115+
<FormLabel>{getFieldLabel(field)}</FormLabel>
92116
<FormControl>
93117
<EnhancedInput
94118
{...fieldProps}
@@ -178,7 +202,7 @@ function DynamicField({
178202
{...commonProps}
179203
render={({ field: fieldProps }) => (
180204
<FormItem>
181-
<FormLabel>{field.label}</FormLabel>
205+
<FormLabel>{getFieldLabel(field)}</FormLabel>
182206
<FormControl>
183207
<EnhancedInput
184208
{...fieldProps}
@@ -207,7 +231,7 @@ function DynamicField({
207231
{...commonProps}
208232
render={({ field: fieldProps }) => (
209233
<FormItem>
210-
<FormLabel>{field.label}</FormLabel>
234+
<FormLabel>{getFieldLabel(field)}</FormLabel>
211235
<FormControl>
212236
<Select
213237
onValueChange={(v) => fieldProps.onChange(v)}
@@ -239,7 +263,7 @@ function DynamicField({
239263
{...commonProps}
240264
render={({ field: fieldProps }) => (
241265
<FormItem>
242-
<FormLabel>{field.label}</FormLabel>
266+
<FormLabel>{getFieldLabel(field)}</FormLabel>
243267
<FormControl>
244268
<div className="pt-2">
245269
<Switch
@@ -260,7 +284,7 @@ function DynamicField({
260284
{...commonProps}
261285
render={({ field: fieldProps }) => (
262286
<FormItem className="col-span-2">
263-
<FormLabel>{field.label}</FormLabel>
287+
<FormLabel>{getFieldLabel(field)}</FormLabel>
264288
<FormControl>
265289
<textarea
266290
{...fieldProps}
@@ -399,12 +423,100 @@ export default function ServerForm(props: {
399423
// eslint-disable-next-line react-hooks/exhaustive-deps
400424
}, [initialValues]);
401425

426+
function validateEnabledProtocols(values: Record<string, any>) {
427+
let firstInvalidProtocol: ProtocolType | undefined;
428+
429+
for (const [index, protocol] of (values?.protocols || []).entries()) {
430+
if (!protocol?.enable) continue;
431+
432+
const protocolType = protocol.type as ProtocolType;
433+
const fields = PROTOCOL_FIELDS[protocolType] || [];
434+
const requiredFields = getVisibleRequiredFields(fields, protocol);
435+
436+
for (const field of requiredFields) {
437+
const value = protocol[field.name];
438+
const fieldPath = `protocols.${index}.${field.name}` as any;
439+
440+
if (field.type === "number") {
441+
const numericValue =
442+
typeof value === "number" ? value : Number(value ?? Number.NaN);
443+
const hasValue = Number.isFinite(numericValue);
444+
const inMinRange =
445+
field.min === undefined || numericValue >= field.min;
446+
const inMaxRange =
447+
field.max === undefined || numericValue <= field.max;
448+
449+
if (!(hasValue && inMinRange && inMaxRange)) {
450+
form.setError(fieldPath, {
451+
type: "manual",
452+
message: t(
453+
"validation.requiredNumberField",
454+
"{{field}} is required and must be between {{min}} and {{max}}",
455+
{
456+
field: field.label,
457+
min: field.min ?? 0,
458+
max: field.max ?? 65_535,
459+
}
460+
),
461+
});
462+
firstInvalidProtocol ??= protocolType;
463+
}
464+
continue;
465+
}
466+
467+
if (field.type === "select") {
468+
const hasValue = typeof value === "string" && value.trim().length > 0;
469+
const isAllowed =
470+
!field.options || (hasValue && field.options.includes(value));
471+
472+
if (!(hasValue && isAllowed)) {
473+
form.setError(fieldPath, {
474+
type: "manual",
475+
message: t(
476+
"validation.requiredSelectField",
477+
"{{field}} is required",
478+
{ field: field.label }
479+
),
480+
});
481+
firstInvalidProtocol ??= protocolType;
482+
}
483+
continue;
484+
}
485+
486+
const hasValue =
487+
typeof value === "string"
488+
? value.trim().length > 0
489+
: value !== null && value !== undefined;
490+
491+
if (!hasValue) {
492+
form.setError(fieldPath, {
493+
type: "manual",
494+
message: t("validation.requiredField", "{{field}} is required", {
495+
field: field.label,
496+
}),
497+
});
498+
firstInvalidProtocol ??= protocolType;
499+
}
500+
}
501+
}
502+
503+
if (firstInvalidProtocol) {
504+
setAccordionValue(firstInvalidProtocol);
505+
return false;
506+
}
507+
508+
return true;
509+
}
510+
402511
async function handleSubmit(values: Record<string, any>) {
512+
form.clearErrors();
513+
514+
if (!validateEnabledProtocols(values)) {
515+
return;
516+
}
517+
403518
const filteredProtocols = (values?.protocols || []).filter(
404-
(protocol: any) => {
405-
const port = Number(protocol?.port);
406-
return protocol && Number.isFinite(port) && port > 0 && port <= 65_535;
407-
}
519+
(protocol: any) => protocol?.enable
408520
);
409521

410522
const result = {
@@ -605,6 +717,15 @@ export default function ServerForm(props: {
605717
)}
606718
onCheckedChange={(checked) => {
607719
form.setValue(`protocols.${i}.enable`, checked);
720+
if (checked) {
721+
setAccordionValue(type);
722+
return;
723+
}
724+
725+
if (accordionValue === type) {
726+
setAccordionValue(undefined);
727+
}
728+
form.clearErrors(`protocols.${i}` as any);
608729
}}
609730
onClick={(e) => e.stopPropagation()}
610731
/>

0 commit comments

Comments
 (0)