inbound-form-adapter.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import type { InboundFormValues, TrafficReset } from '@/schemas/forms/inbound-form';
  2. import type { InboundSettings } from '@/schemas/protocols/inbound';
  3. import type { StreamSettings } from '@/schemas/api/inbound';
  4. import type { Sniffing } from '@/schemas/primitives';
  5. // Plain-data adapter between the panel's stored inbound row shape and
  6. // the typed InboundFormValues that Form.useForm<T> carries inside
  7. // InboundFormModal. No dependency on the legacy Inbound/DBInbound
  8. // classes — the modal hands the raw row in, takes typed values out, and
  9. // on submit calls formValuesToWirePayload() to get a payload ready to
  10. // POST to /panel/api/inbounds/add or /update/:id.
  11. export interface RawInboundRow {
  12. port?: number;
  13. listen?: string;
  14. protocol?: string;
  15. tag?: string;
  16. settings?: unknown;
  17. streamSettings?: unknown;
  18. sniffing?: unknown;
  19. up?: number;
  20. down?: number;
  21. total?: number;
  22. remark?: string;
  23. enable?: boolean;
  24. expiryTime?: number;
  25. trafficReset?: string;
  26. lastTrafficResetTime?: number;
  27. nodeId?: number | null;
  28. clientStats?: unknown;
  29. }
  30. // The wire payload — settings/streamSettings/sniffing arrive as JSON
  31. // strings, mirroring what the Go endpoints expect (xray-core wants the
  32. // nested config slices as strings to round-trip through its loader).
  33. export interface WireInboundPayload {
  34. up: number;
  35. down: number;
  36. total: number;
  37. remark: string;
  38. enable: boolean;
  39. expiryTime: number;
  40. trafficReset: TrafficReset;
  41. lastTrafficResetTime: number;
  42. listen: string;
  43. port: number;
  44. protocol: string;
  45. settings: string;
  46. streamSettings: string;
  47. sniffing: string;
  48. tag: string;
  49. clientStats?: unknown;
  50. nodeId?: number;
  51. }
  52. function coerceJsonObject(value: unknown): Record<string, unknown> {
  53. if (value == null) return {};
  54. if (typeof value === 'object' && !Array.isArray(value)) {
  55. return value as Record<string, unknown>;
  56. }
  57. if (typeof value !== 'string') return {};
  58. const trimmed = value.trim();
  59. if (trimmed === '') return {};
  60. try {
  61. const parsed = JSON.parse(trimmed);
  62. return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
  63. ? (parsed as Record<string, unknown>)
  64. : {};
  65. } catch {
  66. return {};
  67. }
  68. }
  69. const TRAFFIC_RESETS: TrafficReset[] = ['never', 'hourly', 'daily', 'weekly', 'monthly'];
  70. function coerceTrafficReset(v: unknown): TrafficReset {
  71. return typeof v === 'string' && (TRAFFIC_RESETS as string[]).includes(v)
  72. ? (v as TrafficReset)
  73. : 'never';
  74. }
  75. // Map a raw DB row (settings/streamSettings/sniffing as string OR object)
  76. // into the typed InboundFormValues. Does NOT validate against the schema —
  77. // callers that want a hard guarantee should follow up with
  78. // InboundFormSchema.safeParse(...).
  79. export function rawInboundToFormValues(row: RawInboundRow): InboundFormValues {
  80. const protocol = (row.protocol || 'vless') as InboundSettings['protocol'];
  81. const settings = coerceJsonObject(row.settings) as InboundSettings['settings'];
  82. const rawStream = coerceJsonObject(row.streamSettings);
  83. const streamSettings = Object.keys(rawStream).length > 0
  84. ? (rawStream as StreamSettings)
  85. : undefined;
  86. const sniffing = coerceJsonObject(row.sniffing) as unknown as Sniffing;
  87. return {
  88. remark: row.remark ?? '',
  89. enable: row.enable ?? true,
  90. port: row.port ?? 0,
  91. listen: row.listen ?? '',
  92. tag: row.tag ?? '',
  93. expiryTime: row.expiryTime ?? 0,
  94. sniffing,
  95. streamSettings,
  96. up: row.up ?? 0,
  97. down: row.down ?? 0,
  98. total: row.total ?? 0,
  99. trafficReset: coerceTrafficReset(row.trafficReset),
  100. lastTrafficResetTime: row.lastTrafficResetTime ?? 0,
  101. nodeId: row.nodeId ?? null,
  102. protocol,
  103. settings,
  104. } as InboundFormValues;
  105. }
  106. export function formValuesToWirePayload(values: InboundFormValues): WireInboundPayload {
  107. const payload: WireInboundPayload = {
  108. up: values.up,
  109. down: values.down,
  110. total: values.total,
  111. remark: values.remark,
  112. enable: values.enable,
  113. expiryTime: values.expiryTime,
  114. trafficReset: values.trafficReset,
  115. lastTrafficResetTime: values.lastTrafficResetTime,
  116. listen: values.listen,
  117. port: values.port,
  118. protocol: values.protocol,
  119. settings: JSON.stringify(values.settings ?? {}),
  120. streamSettings: values.streamSettings ? JSON.stringify(values.streamSettings) : '',
  121. sniffing: JSON.stringify(values.sniffing ?? {}),
  122. tag: values.tag,
  123. };
  124. if (values.nodeId != null) payload.nodeId = values.nodeId;
  125. return payload;
  126. }