diff --git a/frontend/src/schemas/forms/inbound-form.ts b/frontend/src/schemas/forms/inbound-form.ts new file mode 100644 index 00000000..960db1eb --- /dev/null +++ b/frontend/src/schemas/forms/inbound-form.ts @@ -0,0 +1,83 @@ +import { z } from 'zod'; + +import { PortSchema, SniffingSchema } from '@/schemas/primitives'; +import { InboundSettingsSchema } from '@/schemas/protocols/inbound'; +import { SecuritySettingsSchema } from '@/schemas/protocols/security'; +import { NetworkSettingsSchema, StreamExtrasSchema } from '@/schemas/protocols/stream'; + +// InboundFormValues = the values shape Form.useForm() carries in +// InboundFormModal. Mirrors the wire shape (so submission can hand +// values straight to Schema.parse + POST) plus the DB-side fields that +// the panel's /panel/api/inbounds/add endpoint expects alongside. +// +// Differences from schemas/api/inbound.ts InboundSchema: +// - settings/streamSettings/sniffing are nested OBJECTS here, not the +// JSON strings the endpoint accepts. The form holds typed data; the +// submit handler stringifies right before POSTing. +// - Adds DB fields not in InboundSchema: up, down, total, trafficReset, +// lastTrafficResetTime, nodeId. These flow through the DBInbound row, +// not the xray-config slice. + +export const InboundStreamFormSchema = NetworkSettingsSchema + .and(SecuritySettingsSchema) + .and(StreamExtrasSchema); +export type InboundStreamFormValues = z.infer; + +export const TrafficResetSchema = z.enum(['never', 'hourly', 'daily', 'weekly', 'monthly']); +export type TrafficReset = z.infer; + +// Db-side fields layered on top of the xray slice. These mirror the +// DBInbound model — they live in the SQL row, not in xray's config. +export const InboundDbFieldsSchema = z.object({ + up: z.number().int().min(0).default(0), + down: z.number().int().min(0).default(0), + total: z.number().int().min(0).default(0), + trafficReset: TrafficResetSchema.default('never'), + lastTrafficResetTime: z.number().int().default(0), + nodeId: z.number().int().nullable().optional(), +}); +export type InboundDbFields = z.infer; + +// Base fields that apply to every inbound regardless of protocol or +// transport. The protocol-specific `settings` and the transport-specific +// `streamSettings` are layered on via intersection below. +export const InboundFormBaseSchema = z.object({ + remark: z.string().default(''), + enable: z.boolean().default(true), + port: PortSchema, + listen: z.string().default(''), + tag: z.string().default(''), + expiryTime: z.number().int().default(0), + clientStats: z.string().optional(), + sniffing: SniffingSchema.default({ + enabled: false, + destOverride: ['http', 'tls', 'quic', 'fakedns'], + metadataOnly: false, + routeOnly: false, + ipsExcluded: [], + domainsExcluded: [], + }), + streamSettings: InboundStreamFormSchema.optional(), +}); +export type InboundFormBase = z.infer; + +// Full form values = base + db fields + protocol-discriminated settings. +// Consumers narrow on `.protocol` to access the matching settings branch. +export const InboundFormSchema = InboundFormBaseSchema + .and(InboundDbFieldsSchema) + .and(InboundSettingsSchema); +export type InboundFormValues = z.infer; + +// Fallback rows ride alongside the inbound submission for VLESS/Trojan +// hosts. They're saved via a separate endpoint after the main inbound +// POST returns, so the schema lives here but is not part of the wire +// inbound payload. +export const FallbackRowSchema = z.object({ + rowKey: z.string(), + childId: z.number().int().nullable(), + name: z.string().default(''), + alpn: z.string().default(''), + path: z.string().default(''), + xver: z.number().int().min(0).max(2).default(0), +}); +export type FallbackRow = z.infer;