client.ts 13 KB

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