client.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import { z } from 'zod';
  2. const nullableStringArray = z
  3. .array(z.string())
  4. .nullable()
  5. .transform((v) => v ?? []);
  6. const nullableNumberArray = z
  7. .array(z.number())
  8. .nullable()
  9. .transform((v) => v ?? []);
  10. export const ClientTrafficSchema = z.object({
  11. up: z.number().optional(),
  12. down: z.number().optional(),
  13. total: z.number().optional(),
  14. expiryTime: z.number().optional(),
  15. enable: z.boolean().optional(),
  16. lastOnline: z.number().optional(),
  17. lastSubFetch: z.number().optional(),
  18. resetMax: z.number().optional(),
  19. resetCount: z.number().optional(),
  20. });
  21. export const ClientRecordSchema = z
  22. .object({
  23. id: z.number().optional(),
  24. email: z.string(),
  25. subId: z.string().optional(),
  26. uuid: z.string().optional(),
  27. password: z.string().optional(),
  28. auth: z.string().optional(),
  29. flow: z.string().optional(),
  30. security: z.string().optional(),
  31. totalGB: z.number().optional(),
  32. expiryTime: z.number().optional(),
  33. limitIp: z.number().optional(),
  34. limitHwid: z.number().optional(),
  35. tgId: z.union([z.number(), z.string()]).optional(),
  36. group: z.string().optional(),
  37. comment: z.string().optional(),
  38. enable: z.boolean().optional(),
  39. reset: z.number().optional(),
  40. resetDay: z.number().optional(),
  41. resetWeekday: z.number().optional(),
  42. resetMax: z.number().optional(),
  43. trafficReset: z.string().optional(),
  44. trafficResetDay: z.number().optional(),
  45. inboundIds: nullableNumberArray.optional(),
  46. traffic: ClientTrafficSchema.nullable().optional(),
  47. reverse: z.object({ tag: z.string().optional() }).loose().nullable().optional(),
  48. privateKey: z.string().optional(),
  49. publicKey: z.string().optional(),
  50. allowedIPs: z.string().optional(),
  51. preSharedKey: z.string().optional(),
  52. keepAlive: z.number().optional(),
  53. forwardedPorts: z.string().optional(),
  54. secret: z.string().optional(),
  55. adTag: z.string().optional(),
  56. createdAt: z.number().optional(),
  57. updatedAt: z.number().optional(),
  58. })
  59. .loose();
  60. // AmneziaWG's server block, used by the clients page to render a
  61. // downloadable per-client .conf without a second round trip. Unlike
  62. // WireGuard's flattened wgPublicKey/wgMtu/wgDns below, this stays a nested
  63. // object — AmneziaWG has many more fields (the obfuscation parameter set) and
  64. // buildAmneziaWGClientConfig (pages/clients/amneziawgConfig.ts) already
  65. // expects this exact nested shape. Mirrors the backend's
  66. // InboundOption.AwgServer (internal/web/service/inbound.go).
  67. export const AwgServerOptionSchema = z
  68. .object({
  69. publicKey: z.string().optional(),
  70. mtu: z.number().optional(),
  71. primaryDns: z.string().optional(),
  72. secondaryDns: z.string().optional(),
  73. jc: z.number().optional(),
  74. jmin: z.number().optional(),
  75. jmax: z.number().optional(),
  76. s1: z.number().optional(),
  77. s2: z.number().optional(),
  78. s3: z.number().optional(),
  79. s4: z.number().optional(),
  80. h1: z.string().optional(),
  81. h2: z.string().optional(),
  82. h3: z.string().optional(),
  83. h4: z.string().optional(),
  84. i1: z.string().optional(),
  85. i2: z.string().optional(),
  86. i3: z.string().optional(),
  87. i4: z.string().optional(),
  88. i5: z.string().optional(),
  89. headerProtectionKey: z.string().optional(),
  90. contentPaddingAddition: z.string().optional(),
  91. rekeyAfterTime: z.string().optional(),
  92. rekeyTimeout: z.string().optional(),
  93. rejectAfterTime: z.string().optional(),
  94. keepaliveTimeout: z.string().optional(),
  95. maxHandshakeAttempts: z.string().optional(),
  96. randomTrailers: z.boolean().optional(),
  97. disableCookies: z.boolean().optional(),
  98. })
  99. .loose();
  100. export const TuicServerOptionSchema = z
  101. .object({
  102. certificate: z.string().optional(),
  103. congestion_control: z.string().optional(),
  104. alpn: z.array(z.string()).optional(),
  105. udp_relay_mode: z.string().optional(),
  106. zero_rtt_handshake: z.boolean().optional(),
  107. log_level: z.string().optional(),
  108. max_idle_time: z.number().optional(),
  109. authentication_timeout: z.number().optional(),
  110. max_udp_relay_packet_size: z.number().optional(),
  111. sni: z.string().optional(),
  112. })
  113. .loose();
  114. export type TuicServerOption = z.infer<typeof TuicServerOptionSchema>;
  115. export const InboundOptionSchema = z
  116. .object({
  117. id: z.number(),
  118. remark: z.string().optional(),
  119. tag: z.string().optional(),
  120. protocol: z.string().optional(),
  121. port: z.number().optional(),
  122. network: z.string().optional(),
  123. security: z.string().optional(),
  124. tlsFlowCapable: z.boolean().optional(),
  125. ssMethod: z.string().optional(),
  126. wgPublicKey: z.string().optional(),
  127. wgMtu: z.number().optional(),
  128. wgDns: z.string().optional(),
  129. awgServer: AwgServerOptionSchema.nullable().optional(),
  130. tuicServer: TuicServerOptionSchema.nullable().optional(),
  131. mtprotoDomain: z.string().optional(),
  132. // Hosting node id; absent/null for this panel's own inbounds (#4997).
  133. nodeId: z.number().nullable().optional(),
  134. // Share-host resolution inputs, mirroring the backend resolveInboundAddress so
  135. // the clients page picks the same WireGuard endpoint host as the subscription:
  136. // the hosting node address, the inbound listen, and its share-address strategy.
  137. nodeAddress: z.string().optional(),
  138. listen: z.string().optional(),
  139. shareAddr: z.string().optional(),
  140. shareAddrStrategy: z.string().optional(),
  141. })
  142. .loose();
  143. export const InboundOptionsSchema = z.array(InboundOptionSchema);
  144. // The *Count fields are exact; the email arrays stop at the server's cap and
  145. // only feed the hover popovers, so never derive a counter from their length.
  146. export const ClientsSummarySchema = z.object({
  147. total: z.number(),
  148. active: z.number(),
  149. onlineCount: z.number().optional().default(0),
  150. depletedCount: z.number().optional().default(0),
  151. expiringCount: z.number().optional().default(0),
  152. deactiveCount: z.number().optional().default(0),
  153. online: nullableStringArray,
  154. depleted: nullableStringArray,
  155. expiring: nullableStringArray,
  156. deactive: nullableStringArray,
  157. });
  158. const nullableClientArray = z
  159. .array(ClientRecordSchema)
  160. .nullable()
  161. .transform((v) => v ?? []);
  162. export const ClientPageResponseSchema = z.object({
  163. items: nullableClientArray,
  164. total: z.number(),
  165. filtered: z.number(),
  166. page: z.number(),
  167. pageSize: z.number(),
  168. summary: ClientsSummarySchema.nullable().optional(),
  169. groups: nullableStringArray.optional(),
  170. });
  171. // A per-client external link surfaced in the client's subscription:
  172. // kind=link is a single share link, kind=subscription is a remote sub URL.
  173. export const ExternalLinkSchema = z
  174. .object({
  175. id: z.number().int().optional().default(0),
  176. kind: z.enum(['link', 'subscription']).default('link'),
  177. value: z.string(),
  178. remark: z.string().optional().default(''),
  179. enable: z.preprocess((v) => (v == null ? true : v), z.boolean()).default(true),
  180. expiryTime: z.number().int().optional().default(0),
  181. namePrefix: z.string().optional().default(''),
  182. lastFetchAt: z.number().int().optional().default(0),
  183. lastFetchError: z.string().optional().default(''),
  184. })
  185. .loose();
  186. export const ExternalLinkListSchema = z
  187. .array(ExternalLinkSchema)
  188. .nullable()
  189. .transform((v) => v ?? []);
  190. // tunnelAllowedIPs carries the real, per-inbound AllowedIPs value (keyed by
  191. // inbound id) for every WireGuard/AmneziaWG inbound this client is attached
  192. // to. ClientRecord's own allowedIPs is a single string and cannot represent
  193. // two different addresses when one identity holds both a WireGuard and an
  194. // AmneziaWG attachment at once -- this is what lets the edit form show each
  195. // protocol's real, distinct address instead of one ambiguous shared field.
  196. export const ClientHydrateSchema = z.object({
  197. client: ClientRecordSchema,
  198. inboundIds: nullableNumberArray,
  199. externalLinks: ExternalLinkListSchema.optional(),
  200. tunnelAllowedIPs: z.record(z.number().int(), z.string()).optional(),
  201. });
  202. export const BulkAdjustResultSchema = z.object({
  203. adjusted: z.number(),
  204. skipped: z.array(z.object({ email: z.string(), reason: z.string() })).optional(),
  205. });
  206. export const BulkDeleteResultSchema = z.object({
  207. deleted: z.number(),
  208. skipped: z.array(z.object({ email: z.string(), reason: z.string() })).optional(),
  209. });
  210. export const BulkSetEnableResultSchema = z.object({
  211. changed: z.number(),
  212. skipped: z.array(z.object({ email: z.string(), reason: z.string() })).optional(),
  213. });
  214. export const BulkCreateResultSchema = z.object({
  215. created: z.number(),
  216. skipped: z.array(z.object({ email: z.string(), reason: z.string() })).optional(),
  217. });
  218. export const DelDepletedResultSchema = z.object({
  219. deleted: z.number().optional(),
  220. });
  221. export const BulkAttachResultSchema = z.object({
  222. attached: z
  223. .array(z.string())
  224. .nullable()
  225. .transform((v) => v ?? []),
  226. skipped: z
  227. .array(z.string())
  228. .nullable()
  229. .transform((v) => v ?? []),
  230. errors: z
  231. .array(z.string())
  232. .nullable()
  233. .transform((v) => v ?? []),
  234. });
  235. export const BulkDetachResultSchema = z.object({
  236. detached: z
  237. .array(z.string())
  238. .nullable()
  239. .transform((v) => v ?? []),
  240. skipped: z
  241. .array(z.string())
  242. .nullable()
  243. .transform((v) => v ?? []),
  244. errors: z
  245. .array(z.string())
  246. .nullable()
  247. .transform((v) => v ?? []),
  248. });
  249. export const OnlinesSchema = nullableStringArray;
  250. export const OnlineByNodeSchema = z
  251. .record(z.string(), nullableStringArray)
  252. .nullable()
  253. .transform((v) => v ?? {});
  254. export const ActiveInboundsByNodeSchema = z
  255. .record(z.string(), nullableStringArray)
  256. .nullable()
  257. .transform((v) => v ?? {});
  258. export const GroupSummarySchema = z.object({
  259. name: z.string(),
  260. clientCount: z.number(),
  261. trafficUsed: z
  262. .number()
  263. .nullable()
  264. .transform((v) => v ?? 0),
  265. up: z
  266. .number()
  267. .nullable()
  268. .transform((v) => v ?? 0),
  269. down: z
  270. .number()
  271. .nullable()
  272. .transform((v) => v ?? 0),
  273. });
  274. export const GroupSummaryListSchema = z
  275. .array(GroupSummarySchema)
  276. .nullable()
  277. .transform((v) => v ?? []);
  278. export function hasForbiddenClientChars(value: string): boolean {
  279. if (value.includes('/') || value.includes('\\') || value.includes(' ')) return true;
  280. for (let i = 0; i < value.length; i++) {
  281. const code = value.charCodeAt(i);
  282. if (code < 0x20 || code === 0x7f) return true;
  283. }
  284. return false;
  285. }
  286. export const ClientFormSchema = z.object({
  287. email: z
  288. .string()
  289. .trim()
  290. .min(1, 'pages.clients.email')
  291. .refine((v) => !hasForbiddenClientChars(v), 'pages.clients.emailInvalidChars'),
  292. subId: z.string().refine((v) => !hasForbiddenClientChars(v), 'pages.clients.subIdInvalidChars'),
  293. uuid: z.string(),
  294. password: z.string(),
  295. auth: z.string(),
  296. flow: z.string(),
  297. security: z.string(),
  298. reverseTag: z.string(),
  299. totalGB: z.number().min(0),
  300. delayedStart: z.boolean(),
  301. delayedDays: z.number().int().min(0),
  302. reset: z.number().int().min(0),
  303. resetDay: z.number().int().min(0).max(31),
  304. resetWeekday: z.number().int().min(0).max(7),
  305. resetMax: z.number().int().min(0),
  306. trafficReset: z.enum(['never', 'hourly', 'daily', 'weekly', 'monthly']),
  307. trafficResetDay: z.number().int().min(1).max(31),
  308. limitIp: z.number().int().min(0),
  309. limitHwid: z.number().int().min(0),
  310. tgId: z.number().int().min(0),
  311. group: z.string(),
  312. comment: z.string(),
  313. enable: z.boolean(),
  314. inboundIds: z.array(z.number()),
  315. });
  316. export const ClientCreateFormSchema = ClientFormSchema.extend({
  317. inboundIds: z.array(z.number()).min(1, 'pages.clients.selectInbound'),
  318. });
  319. export const ClientBulkAdjustFormSchema = z
  320. .object({
  321. addDays: z.number().int(),
  322. addGB: z.number(),
  323. flow: z.string().optional().default(''),
  324. limitHwid: z.number().int().min(0).nullable().optional(),
  325. adTag: z.string().optional().default(''),
  326. })
  327. .refine(
  328. (v) =>
  329. v.addDays !== 0 ||
  330. v.addGB !== 0 ||
  331. v.flow !== '' ||
  332. (v.limitHwid !== undefined && v.limitHwid !== null) ||
  333. (v.adTag !== undefined && v.adTag.trim() !== ''),
  334. {
  335. error: 'pages.clients.bulkAdjustNothing',
  336. },
  337. )
  338. .refine(
  339. (v) => {
  340. const tag = v.adTag?.trim();
  341. if (!tag || tag === 'none') return true;
  342. return /^[0-9a-fA-F]{32}$/.test(tag);
  343. },
  344. {
  345. error: 'pages.inbounds.form.mtgAdTagInvalid',
  346. path: ['adTag'],
  347. },
  348. );
  349. export const ClientBulkAddFormSchema = z.object({
  350. emailMethod: z.number().int().min(0).max(4),
  351. firstNum: z.number().int().min(1),
  352. lastNum: z.number().int().min(1),
  353. emailPrefix: z.string(),
  354. emailPostfix: z.string(),
  355. quantity: z.number().int().min(1).max(1000),
  356. subId: z.string(),
  357. group: z.string(),
  358. comment: z.string(),
  359. flow: z.string(),
  360. limitIp: z.number().int().min(0),
  361. limitHwid: z.number().int().min(0),
  362. totalGB: z.number().min(0),
  363. expiryTime: z.number(),
  364. reset: z.number().int().min(0),
  365. resetDay: z.number().int().min(0).max(31),
  366. resetWeekday: z.number().int().min(0).max(7),
  367. resetMax: z.number().int().min(0),
  368. trafficReset: z.enum(['never', 'hourly', 'daily', 'weekly', 'monthly']).optional(),
  369. trafficResetDay: z.number().int().min(1).max(31).optional(),
  370. inboundIds: z.array(z.number()).min(1, 'pages.clients.selectInbound'),
  371. });
  372. export type ClientRecord = z.infer<typeof ClientRecordSchema>;
  373. export type ClientTraffic = z.infer<typeof ClientTrafficSchema>;
  374. export type InboundOption = z.infer<typeof InboundOptionSchema>;
  375. export type ExternalLink = z.infer<typeof ExternalLinkSchema>;
  376. export type ClientsSummary = z.infer<typeof ClientsSummarySchema>;
  377. export type ClientPageResponse = z.infer<typeof ClientPageResponseSchema>;
  378. export type ClientHydrate = z.infer<typeof ClientHydrateSchema>;
  379. export type BulkAdjustResult = z.infer<typeof BulkAdjustResultSchema>;
  380. export type BulkDeleteResult = z.infer<typeof BulkDeleteResultSchema>;
  381. export type BulkSetEnableResult = z.infer<typeof BulkSetEnableResultSchema>;
  382. export type BulkCreateResult = z.infer<typeof BulkCreateResultSchema>;
  383. export type BulkAttachResult = z.infer<typeof BulkAttachResultSchema>;
  384. export type BulkDetachResult = z.infer<typeof BulkDetachResultSchema>;
  385. export type ClientBulkAddFormValues = z.infer<typeof ClientBulkAddFormSchema>;
  386. export type ClientBulkAdjustFormValues = z.infer<typeof ClientBulkAdjustFormSchema>;
  387. export type ClientFormValues = z.infer<typeof ClientFormSchema>;
  388. export type GroupSummary = z.infer<typeof GroupSummarySchema>;