Commit graph

19 commits

Author SHA1 Message Date
MHSanaei
5c902ca298
feat(frontend): inbound Hysteria stream sub-form (auth + udpIdleTimeout + masquerade)
Restore the inbound side of Hysteria stream configuration that was
previously hidden — the legacy modal exposed these knobs but the
Pattern A rewrite gated them out.

schemas/protocols/stream/hysteria.ts:
- HysteriaMasqueradeSchema covers the inbound-only masquerade wire
  shape: type ('proxy'|'file'|'string'), dir, url, rewriteHost,
  insecure, content, headers, statusCode. The three masquerade types
  cover the spectrum: reverse-proxy upstream, serve static files, or
  return a fixed string body.
- HysteriaStreamSettingsSchema gains 3 inbound-side optional fields:
  protocol, udpIdleTimeout, masquerade. Outbound side is untouched
  (the legacy class accepted both wire shapes via the same struct).

InboundFormModal.tsx:
- New hysteria stream sub-form section in streamTab, gated by
  protocol === HYSTERIA. Fields: version (disabled, locked to 2),
  auth, udpIdleTimeout, masquerade Switch + nested type-Select with
  three conditional sub-blocks (proxy URL+rewriteHost+insecure,
  file dir, string statusCode+body+headers).
- onValuesChange cascade: switching TO hysteria seeds streamSettings
  with the hysteria branch (forcing network='hysteria' + TLS); switching
  AWAY from hysteria snaps back to TCP so the standard network
  selector has a valid starting point.

masquerade headers use the HeaderMapEditor v1 component.
2026-05-26 13:44:00 +02:00
MHSanaei
e01acae843
feat(frontend): XHTTP advanced fields on outbound modal
Replace the 'edit via JSON' deferred-features hint with the full XHTTP
sub-form matching the legacy modal's XhttpFields helper.

schemas/protocols/stream/xhttp.ts:
- New XHttpXmuxSchema: 6 connection-multiplexing knobs
  (maxConcurrency, maxConnections, cMaxReuseTimes, hMaxRequestTimes,
  hMaxReusableSecs, hKeepAlivePeriod).
- XHttpStreamSettingsSchema gains 5 outbound-only fields and one
  UI-only toggle: scMinPostsIntervalMs, uplinkChunkSize, noGRPCHeader,
  xmux, enableXmux.

outbound-form-adapter.ts:
- New stripUiOnlyStreamFields() drops xhttpSettings.enableXmux on the
  way to wire so the panel never embeds the UI toggle into the saved
  config. xray-core ignores unknown fields anyway, but the panel reads
  back its own emitted JSON, so a clean wire shape matters.

OutboundFormModal.tsx:
- Headers editor (HeaderMapEditor v1) for xhttpSettings.headers.
- Padding obfs Switch + 4 conditional fields (key/header/placement/
  method) when on.
- Uplink HTTP method Select with GET disabled outside packet-up.
- Session placement + session key (key shown when placement != path).
- Sequence placement + sequence key (same pattern).
- packet-up mode: scMinPostsIntervalMs, scMaxEachPostBytes, uplink
  data placement + key + chunk size (key/chunk-size shown when
  placement != body).
- stream-up / stream-one mode: noGRPCHeader Switch.
- XMUX Switch + 6 nested fields when on.
2026-05-26 13:19:08 +02:00
MHSanaei
19204f9e04
feat(frontend): Hysteria stream sub-form (schema branch + outbound UI)
Add the 7th branch to NetworkSettingsSchema for Hysteria transport.

schemas/protocols/stream/hysteria.ts:
- HysteriaStreamSettingsSchema covers the full wire shape: version=2,
  auth, congestion (''|'brutal'), up/down bandwidth strings, optional
  udphop sub-object for port-hopping, receive-window tuning fields,
  maxIdleTimeout, keepAlivePeriod, disablePathMTUDiscovery.

schemas/protocols/stream/index.ts:
- NetworkSchema gains 'hysteria'.
- NetworkSettingsSchema gains the 7th branch
  { network: 'hysteria', hysteriaSettings: HysteriaStreamSettingsSchema }.

OutboundFormModal.tsx:
- NETWORK_OPTIONS keeps the 6 standard transports for non-hysteria
  protocols; when protocol === 'hysteria', a 7th option is appended
  (matches the legacy [...NETWORKS, 'hysteria'] gate).
- newStreamSlice handles the 'hysteria' case with sensible defaults
  matching the legacy HysteriaStreamSettings constructor.
- New sub-form when network === 'hysteria': 8 common fields (auth,
  congestion, up, down, udphop Switch + 3 nested fields when on,
  maxIdleTimeout, keepAlivePeriod, disablePathMTUDiscovery).
- Receive-window tuning fields are still edit-via-JSON (rarely
  touched + would clutter the form).
2026-05-26 13:10:37 +02:00
MHSanaei
b554bb6b75
feat(frontend): outbound form schema + wire adapter foundation
Lay the groundwork for OutboundFormModal's Pattern A rewrite:

- schemas/forms/outbound-form.ts: discriminated-union form values across
  all 12 outbound protocols, with flat per-protocol settings shapes that
  match the legacy class fields (vmess vnext / trojan-ss-socks-http
  servers / wireguard csv address-reserved all flattened).

- lib/xray/outbound-form-adapter.ts: rawOutboundToFormValues converts
  wire-shape outbound JSON to typed form values; formValuesToWirePayload
  re-nests on submit. Replaces the Outbound.fromJson/toJson dependency
  the modal currently has on the legacy class hierarchy.

- test/outbound-form-adapter.test.ts: 15 round-trip cases covering each
  protocol's wire quirks (vmess vnext flatten, vless reverse-wrap,
  wireguard csv↔array, blackhole response wrap, DNS rule normalization,
  mux gating).
2026-05-26 11:58:36 +02:00
MHSanaei
d2f3a7baa7
feat(frontend): InboundFormValues schema for Pattern A rewrite
Foundation for the InboundFormModal rewrite. Mirrors the wire Inbound
shape (intersection of core fields + protocol settings DU + stream/security
DUs) plus the DB-side fields (up/down/total/trafficReset/nodeId/...) that
flow through DBInbound rather than the xray config slice.

InboundStreamFormSchema is exported separately so individual sub-form
sections can rule against just the stream portion when needed.

FallbackRowSchema is co-located here even though fallbacks save via a
distinct endpoint after the main POST — they belong to the same form
state from the user's perspective.

No modal changes in this commit. Foundation only; subsequent turns swap
the modal's `inboundRef`/`dbFormRef` mutable-class state for
Form.useForm<InboundFormValues>().
2026-05-26 01:21:30 +02:00
MHSanaei
f79e486f9f
refactor(frontend): swap InboundFormModal option dicts to schemas/primitives
Extends primitives/options.ts with the five inbound-only option dicts
(TLS_VERSION_OPTION, TLS_CIPHER_OPTION, USAGE_OPTION,
DOMAIN_STRATEGY_OPTION, TCP_CONGESTION_OPTION) and lifts InboundFormModal
off @/models/inbound for 10 of its 12 imports. Only the Inbound class
and SSMethods (inbound vs outbound versions diverge by 2 entries) still
come from @/models/.

Widens NODE_ELIGIBLE_PROTOCOLS Set element type to string since the new
primitives const exposes a narrow literal union that `.has(arbitraryString)`
would otherwise reject.
2026-05-26 01:14:05 +02:00
MHSanaei
2d74dbe7ad
refactor(frontend): lift outbound option dictionaries to schemas/primitives
Adds schemas/primitives/options.ts with UTLS_FINGERPRINT, ALPN_OPTION,
SNIFFING_OPTION, USERS_SECURITY, MODE_OPTION (all identical between
models/inbound.ts and models/outbound.ts) plus the outbound-only
WireguardDomainStrategy, Address_Port_Strategy, and DNSRuleActions.

OutboundFormModal now pulls 9 consts from primitives. Only `Outbound`
(the class) and `SSMethods` (whose inbound/outbound versions diverge by
2 legacy aliases — keep the picker open for the Pattern A rewrite) still
come from @/models/outbound.

Drops three stale `as string[]` casts on what are now readonly tuples.
2026-05-26 01:11:51 +02:00
MHSanaei
40ca58d42e
refactor(frontend): lift OutboundProtocols + OutboundDomainStrategies to schemas/primitives
Moves the two outbound-side consts out of models/outbound.ts and into
schemas/primitives/outbound-protocol.ts. Renames the export to
OutboundProtocols to disambiguate from the inbound Protocols const
(different key casing — PascalCase vs ALL CAPS — and partly different
member set, so they cannot share a single const).

OutboundsTab.tsx keeps its 15+ Protocols.X call sites by aliasing
the import. FinalMaskForm.tsx and BasicsTab.tsx swap directly.
Drops a stale `as string[]` cast in BasicsTab that no longer fits
the new readonly-tuple typing.

After this commit only the two big form modals
(InboundFormModal/OutboundFormModal) plus three intentional parity
tests still import from @/models/.
2026-05-26 01:07:02 +02:00
MHSanaei
4ce2503c1e
refactor(frontend): lift Protocols + TLS_FLOW_CONTROL consts to schemas/primitives
Step 4b. The Protocols and TLS_FLOW_CONTROL enums on models/inbound.ts
were dragging five page files into that 3,300-line module just to read
literal string constants. Lifting them to schemas/primitives lets those
pages drop the @/models/inbound import entirely.

  - schemas/primitives/protocol.ts now exports a Protocols const map
    alongside the existing ProtocolSchema. TUN stays in the const for
    parity (legacy panel deployments may have saved TUN inbounds) even
    though the Go validator no longer accepts it as a new write.
  - schemas/primitives/flow.ts now exports TLS_FLOW_CONTROL. The
    empty-string default isn't keyed because the legacy never had a
    NONE entry — call sites compare against the two real flow values.

Updated five consumers:
  - useInbounds.ts: TRACKED_PROTOCOLS now annotated readonly string[]
    so .includes(string) keeps narrowing through the array literal
  - QrCodeModal.tsx, InboundInfoModal.tsx: Protocols
  - ClientFormModal.tsx, ClientBulkAddModal.tsx: TLS_FLOW_CONTROL

Suite: 89 tests across 8 files; typecheck + lint clean.

models/inbound.ts is now imported by:
  - InboundFormModal.tsx (heavy use of Inbound class + getSettings)
  - test/inbound-link.test.ts + test/shadow.test.ts + test/headers.test.ts
    (intentional — these are parity tests against the legacy class)

OutboundFormModal still imports from models/outbound. Both form modals
are the multi-day Pattern A rewrites the plan scopes separately.
2026-05-26 00:51:52 +02:00
MHSanaei
d14eb6923f
feat(frontend): stream extras + full InboundSchema with DU intersection
Step 3d's last scaffolding piece before link generators. Three new
stream-extras schemas land alongside the network/security DUs:

  - finalmask: TcpMask[] + UdpMask[] + QuicParams. Mask `settings` stays
    record<string, unknown> for now — there are 13 UDP mask types and 3
    TCP mask types with distinct per-type setting shapes, and modeling
    them all as DUs would dwarf the rest of stream/ without buying
    anything the shadow harness doesn't already catch. Tightened in
    Step 6.
  - sockopt: 17 socket-tuning knobs (TCP keepalive, TFO, mark, tproxy,
    mptcp, dialer proxy, IPv6-only, congestion). `interfaceName` field
    matches the panel class naming; serializers rename to `interface` on
    the wire.
  - external-proxy: rows ship per inbound describing edge fronts (CDN
    mirrors). Used by link generators to fan out share URLs.

schemas/api/inbound.ts composes the top-level wire shape with
intersection-of-DUs:

  StreamSettingsSchema = NetworkSettingsSchema
    .and(SecuritySettingsSchema)
    .and(StreamExtrasSchema)

  InboundSchema = InboundCoreSchema.and(InboundSettingsSchema)

A fixture (vless-ws-tls.json) exercises the full shape — protocol DU,
network DU, security DU, and TLS cert file branch in one round trip.
The snapshot pins the canonical parsed form so the upcoming link
extractor consumes typed input with no class hierarchy underneath.

Suite: 65 tests across 7 files; typecheck + lint clean. Zod 4
intersection-of-DUs works.
2026-05-26 00:00:34 +02:00
MHSanaei
9721dae2b6
feat(frontend): stream and security Zod families with discriminated unions
Stand up the remaining Step 2 families. NetworkSettingsSchema is a
6-branch DU on `network` covering tcp/kcp/ws/grpc/httpupgrade/xhttp, with
asymmetric per-network wire keys (tcpSettings, wsSettings, ...) preserved
exactly so fixtures round-trip byte-identical. SecuritySettingsSchema is a
3-branch DU on `security` covering none/tls/reality. TLS certs use a
file-vs-inline union; uTLS fingerprints are shared between TLS and Reality
via a single primitive enum.

Hysteria-as-network, finalmask, and sockopt are not in the plan's Step 2
inventory and are deferred to Step 6 (Tighten) - they're orthogonal extras
on the stream root, not network-discriminated branches.

Resolves a Security identifier collision in protocols/index.ts by
re-exporting the type alias as SecurityKind (the `Security` name is taken
by the namespace re-export).
2026-05-25 23:13:29 +02:00
MHSanaei
8d45cd8c68
feat(frontend): protocol-leaf Zod schemas with discriminated unions
Stand up schemas/primitives (Port, Flow, Protocol, Sniffing) and per-protocol
leaf schemas for all 10 inbound and 13 outbound xray protocols. The leaves
omit any inner `protocol` literal — the discriminator lives at the parent
level so consumers narrow on `.protocol` without redundant projection. Wire
shape is preserved per protocol: vmess outbound stays in `vnext[]`, trojan
and shadowsocks outbound in `servers[]`, vless outbound flat, http/socks
outbound in `servers[].users[]`.

Cross-protocol atoms (port, flow, sniffing dest, protocol enum) live in
primitives. Protocol-specific enums (vmess security, ss method/network,
hysteria version, freedom domain strategy, dns rule action) stay with their
leaves. Tagged-wrapper `z.discriminatedUnion('protocol', [...])` composes
both InboundSettingsSchema and OutboundSettingsSchema; existing class-based
models in src/models/ are untouched and will be retired in Step 3 once the
golden-file safety net is in place.
2026-05-25 23:02:08 +02:00
MHSanaei
9cf35234a5
feat(frontend): schema-guard Inbound and Outbound form submits
The two largest forms in the panel send to the backend without ever
checking their own port range or required-ness. Schema-gate the
top-level fields so obviously bad payloads stop at the client.

InboundFormModal: InboundFormSchema (port 1-65535 int, non-empty
protocol, the rest of the keys present) runs as a safeParse just
before the HttpUtil.post in submit(). The 2000+ lines of protocol-
specific subform code stay untouched - that's a separate effort and
the existing per-protocol logic (e.g. canEnableStream, isFallbackHost)
already gates most of the structural correctness.

OutboundFormModal: OutboundTagSchema (trim + min 1) replaces the
hand-rolled `if (!ob.tag?.trim()) messageApi.error('Tag is required')`
check. The duplicateTag check stays inline because it needs the
existingTags prop.

Both schemas emit i18n keys for messages with a defaultValue fallback,
matching the pattern in BalancerFormModal and SettingsPage.
2026-05-25 18:10:24 +02:00
MHSanaei
4ecbb0e55f
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.
2026-05-25 17:55:21 +02:00
MHSanaei
a3012daa8f
feat(frontend): migrate five secondary form modals to Zod schemas
Apply the schema + safeParse-on-submit pattern (introduced for
ClientFormModal / ClientBulkAddModal) to five more forms:

- ClientBulkAdjustModal: ClientBulkAdjustFormSchema enforces 'at least
  one of addDays / addGB is non-zero' via .refine(), replacing the
  ad-hoc days+gb check.
- BalancerFormModal: BalancerFormSchema covers tag and selector
  required-ness; the duplicate-tag check stays inline since it needs
  the otherTags prop. Per-field validateStatus now reads from the
  parsed issues map.
- RuleFormModal: RuleFormSchema captures the form shape (no required
  fields - every property is optional by design). safeParse short-
  circuits if anything is structurally wrong.
- CustomGeoFormModal: CustomGeoFormSchema folds the regex alias rule
  and the http(s) URL validation (including URL parse) into the
  schema, replacing a 20-line validate() function.
- TwoFactorModal: TotpCodeSchema (z.string().regex(/^\d{6}$/)) drives
  both the disabled-state of the OK button and the safeParse gate
  before the TOTP comparison.

Schemas live alongside the matching API schemas:
- ClientBulkAdjustFormSchema in schemas/client.ts
- BalancerFormSchema / RuleFormSchema / CustomGeoFormSchema in schemas/xray.ts
- TotpCodeSchema in schemas/login.ts (next to LoginFormSchema)

No UX change for valid inputs.
2026-05-25 17:45:02 +02:00
MHSanaei
6bbc9f6769
feat(frontend): drive form validation from Zod schemas
NodeFormModal — full conversion to AntD Form.useForm with antdRule
on every required field. Inline field errors replace the single
'fillRequired' toast. testConnection now runs validateFields(['address','port'])
before sending.

ClientFormModal and ClientBulkAddModal — minimal conversion: keep the
existing useState-driven controlled-component pattern, but replace the
hand-rolled `if (!form.x)` checks with schema.safeParse(form). The
schema is the single source of truth for required-ness and types;
ClientCreateFormSchema layers on the create-only `inboundIds.min(1)` rule.

New schemas (in src/schemas/):
  NodeFormSchema (node.ts)
  ClientFormSchema / ClientCreateFormSchema (client.ts)
  ClientBulkAddFormSchema (client.ts)

Other 16+ form modals stay on the current pattern — the antdRule adapter
ships from the first Zod pass for opportunistic migration as forms are
touched.
2026-05-25 16:41:56 +02:00
MHSanaei
c16fb93899
fix(frontend): allow null slices in client/summary schemas
Go's encoding/json emits nil []T as null, not []. The initial
ClientPageResponseSchema and ClientHydrateSchema rejected null
inboundIds / summary.online / summary.depleted / etc., causing
[zod] warnings on every empty list.

Add nullableStringArray / nullableNumberArray helpers that accept
null and transform to [] so consuming code keeps seeing arrays.
Mark ClientRecord.traffic and .reverse nullable too (reverse is
explicitly null in MarshalJSON when storage is empty).
2026-05-25 16:30:48 +02:00
MHSanaei
d00ddc3f58
feat(frontend): extend Zod validation to remaining query/mutation hooks
Adds Zod schemas for client/inbound/xray/node-probe endpoints and wires
useNodeMutations, useClients, useInbounds, useXraySetting, useDatepicker
through parseMsg. Drops the duplicated per-file ApiMsg<T> interfaces and
the local ClientRecord / OutboundTrafficRow / XraySettingsValue / DefaultsPayload
declarations in favour of schema-inferred types re-exported from the
new src/schemas/ modules.

API boundary now validates: clients list/paged, clients onlines,
clients lastOnline, clients get/hydrate, inbounds slim, inbounds get,
inbounds options, defaultSettings, xray config, xray outbounds traffic,
xray testOutbound, xray getXrayResult, getDefaultJsonConfig, nodes probe,
nodes test. Mutation responses that consume obj (bulkAdjust, delDepleted,
nodes probe / test) get response validation; pass-through mutations stay
agnostic. NodeFormModal type-aligned to Msg<ProbeResult>.
2026-05-25 16:14:00 +02:00
MHSanaei
6846fac1cc
feat(frontend): add Zod runtime validation at API boundary
Introduces Zod 4 schemas for response validation on the three highest-traffic
endpoints (server/status, nodes/list, setting/all) and a Zod->AntD form rule
adapter, replacing the duplicated per-file ApiMsg<T> interfaces. Validation
runs safeParse with console.warn + raw-payload fallback so backend drift never
breaks the UI for users.

Login form switches to schema-driven rules as the proof-of-life for the
adapter. Class-based models stay untouched; remaining query/mutation hooks
and form modals will migrate in follow-ups.
2026-05-25 16:02:27 +02:00