api.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import type {
  2. AddDomainPayload,
  3. AdminResourceInventory,
  4. AdminUser,
  5. ApiToken,
  6. Analytics,
  7. AuditLogEntry,
  8. DnsCredential,
  9. Domain,
  10. DomainPatchPayload,
  11. InboundMailbox,
  12. InboundMessage,
  13. RuntimeConfig,
  14. SendEvent,
  15. SmtpCredential,
  16. SmtpRelay,
  17. SmtpRelayPayload,
  18. SystemEmailSettings,
  19. User,
  20. UserMergeOptions,
  21. UserMergePreview,
  22. UserMergeResult,
  23. UserRole,
  24. UserStatus,
  25. Webhook,
  26. WebhookDelivery,
  27. WebhookDeliveryFilters,
  28. WebhookPatchPayload,
  29. WebhookPayload
  30. } from '../types';
  31. interface RequestOptions extends RequestInit {
  32. data?: unknown;
  33. }
  34. async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
  35. const headers = new Headers(options.headers);
  36. if (options.data !== undefined && !headers.has('Content-Type')) {
  37. headers.set('Content-Type', 'application/json');
  38. }
  39. const response = await send(path, {
  40. method: options.method || 'GET',
  41. headers,
  42. body: options.data === undefined ? options.body : JSON.stringify(options.data)
  43. });
  44. const text = response.text;
  45. const payload = text ? JSON.parse(text) : {};
  46. if (response.status < 200 || response.status >= 300) {
  47. throw new Error(payload.error || payload.message || requestFailedMessage(response.status));
  48. }
  49. return payload as T;
  50. }
  51. function send(
  52. path: string,
  53. options: { method: string; headers: Headers; body?: BodyInit | null }
  54. ): Promise<{ status: number; text: string }> {
  55. if (typeof globalThis.fetch === 'function') {
  56. return globalThis.fetch(path, {
  57. method: options.method,
  58. headers: options.headers,
  59. body: options.body
  60. }).then(async (response) => ({
  61. status: response.status,
  62. text: await response.text()
  63. }));
  64. }
  65. return new Promise((resolve, reject) => {
  66. const xhr = new XMLHttpRequest();
  67. xhr.open(options.method, path, true);
  68. options.headers.forEach((value, key) => xhr.setRequestHeader(key, value));
  69. xhr.onload = () => resolve({ status: xhr.status, text: xhr.responseText || '' });
  70. xhr.onerror = () => reject(new Error(networkFailedMessage()));
  71. xhr.send((options.body || null) as XMLHttpRequestBodyInit | null);
  72. });
  73. }
  74. function requestFailedMessage(status: number) {
  75. return currentLocale().startsWith('en') ? `Request failed: ${status}` : `请求失败:${status}`;
  76. }
  77. function networkFailedMessage() {
  78. return currentLocale().startsWith('en') ? 'Network request failed' : '网络请求失败';
  79. }
  80. function currentLocale() {
  81. return document.documentElement.lang || window.localStorage.getItem('mailhub.locale') || 'zh-CN';
  82. }
  83. export const api = {
  84. me: () => request<{ user: User }>('/api/me'),
  85. config: () => request<RuntimeConfig>('/api/config'),
  86. domains: () => request<{ domains: Domain[] }>('/api/domains'),
  87. events: () => request<{ events: SendEvent[] }>('/api/events'),
  88. event: (id: number) => request<{ event: SendEvent | null }>(`/api/events/${id}`),
  89. inboundMailboxes: () => request<{ mailboxes: InboundMailbox[] }>('/api/inbound-mailboxes'),
  90. createInboundMailbox: (data: { address: string; displayName?: string }) =>
  91. request<{ mailbox: InboundMailbox }>('/api/inbound-mailboxes', { method: 'POST', data }),
  92. inboundMessages: (mailboxId?: number | null) => {
  93. const query = mailboxId ? `?mailboxId=${mailboxId}` : '';
  94. return request<{ messages: InboundMessage[] }>(`/api/inbound-messages${query}`);
  95. },
  96. inboundMessage: (id: number) => request<{ message: InboundMessage | null }>(`/api/inbound-messages/${id}`),
  97. markInboundMessageRead: (id: number, read = true) =>
  98. request<{ message: InboundMessage | null }>(`/api/inbound-messages/${id}`, { method: 'PATCH', data: { read } }),
  99. analytics: (days = 7) => request<{ analytics: Analytics }>(`/api/analytics?days=${days}`),
  100. smtpCredential: () => request<{ credential: SmtpCredential | null }>('/api/smtp-credential'),
  101. saveSmtpCredential: (data: { username: string; password?: string }) =>
  102. request<{ credential: SmtpCredential }>('/api/smtp-credential', { method: 'PUT', data }),
  103. smtpCredentials: () => request<{ credentials: SmtpCredential[] }>('/api/smtp-credentials'),
  104. smtpCredentialDetail: (id: number) => request<{ credential: SmtpCredential }>(`/api/smtp-credentials/${id}`),
  105. saveSmtpLoginCredential: (data: { username: string; password?: string }, id?: number) =>
  106. request<{ credential: SmtpCredential }>(id ? `/api/smtp-credentials/${id}` : '/api/smtp-credentials', {
  107. method: id ? 'PATCH' : 'POST',
  108. data
  109. }),
  110. deleteSmtpCredential: (id: number) =>
  111. request<{ deleted: boolean }>(`/api/smtp-credentials/${id}`, { method: 'DELETE' }),
  112. smtpRelays: () => request<{ relays: SmtpRelay[] }>('/api/smtp-relays'),
  113. smtpRelay: (id: number) => request<{ relay: SmtpRelay }>(`/api/smtp-relays/${id}`),
  114. saveSmtpRelay: (data: SmtpRelayPayload, id?: number) =>
  115. request<{ relay: SmtpRelay }>(id ? `/api/smtp-relays/${id}` : '/api/smtp-relays', {
  116. method: id ? 'PATCH' : 'POST',
  117. data
  118. }),
  119. deleteSmtpRelay: (id: number) =>
  120. request<{ deleted: boolean }>(`/api/smtp-relays/${id}`, { method: 'DELETE' }),
  121. dnsCredentials: () => request<{ credentials: DnsCredential[] }>('/api/dns-credentials'),
  122. saveDnsCredential: (data: Record<string, unknown>, id?: number) =>
  123. request<{ credential: DnsCredential }>(id ? `/api/dns-credentials/${id}` : '/api/dns-credentials', {
  124. method: id ? 'PATCH' : 'POST',
  125. data
  126. }),
  127. testDnsCredential: (id: number) =>
  128. request<{ ok: boolean; detail?: string; provider?: string; error?: string }>(`/api/dns-credentials/${id}/test`, {
  129. method: 'POST'
  130. }),
  131. deleteDnsCredential: (id: number) =>
  132. request<{ deleted: boolean }>(`/api/dns-credentials/${id}`, { method: 'DELETE' }),
  133. apiTokens: () => request<{ tokens: ApiToken[] }>('/api/api-tokens'),
  134. createApiToken: (name: string) =>
  135. request<{ token: ApiToken }>('/api/api-tokens', { method: 'POST', data: { name } }),
  136. deleteApiToken: (id: number) => request<{ deleted: boolean }>(`/api/api-tokens/${id}`, { method: 'DELETE' }),
  137. createDomain: (data: AddDomainPayload) => request<{ domain: Domain }>('/api/domains', { method: 'POST', data }),
  138. patchDomain: (id: number, data: DomainPatchPayload) =>
  139. request<{ domain: Domain }>(`/api/domains/${id}`, { method: 'PATCH', data }),
  140. checkDomain: (id: number) => request<{ domain: Domain }>(`/api/domains/${id}/check`, { method: 'POST' }),
  141. applyDns: (id: number) =>
  142. request<{ domain: Domain; apply?: Domain['status']['apply'] }>(`/api/domains/${id}/apply-dns`, { method: 'POST' }),
  143. rotateDkim: (id: number, selector?: string) =>
  144. request<{ domain: Domain }>(`/api/domains/${id}/rotate-dkim`, { method: 'POST', data: { selector } }),
  145. sendTest: (
  146. id: number,
  147. data: {
  148. from?: string;
  149. to: string;
  150. subject?: string;
  151. text?: string;
  152. html?: string;
  153. tracking?: boolean | { opens?: boolean; clicks?: boolean };
  154. smtpRelayId?: number | string | null;
  155. }
  156. ) =>
  157. request<{ queued: boolean }>(`/api/domains/${id}/test-send`, { method: 'POST', data }),
  158. deleteDomain: (id: number) => request<{ deleted: boolean }>(`/api/domains/${id}`, { method: 'DELETE' }),
  159. adminSettings: () => request<{ settings: RuntimeConfig }>('/api/admin/settings'),
  160. saveAdminSettings: (data: Partial<RuntimeConfig>) =>
  161. request<{ settings: RuntimeConfig }>('/api/admin/settings', { method: 'PATCH', data }),
  162. adminUsers: () => request<{ users: AdminUser[] }>('/api/admin/users'),
  163. updateAdminUser: (id: number, data: { role?: UserRole; status?: UserStatus; password?: string }) =>
  164. request<{ user: AdminUser }>(`/api/admin/users/${id}`, { method: 'PATCH', data }),
  165. approveAdminUser: (id: number) =>
  166. request<{ user: AdminUser }>(`/api/admin/users/${id}/approve`, { method: 'POST' }),
  167. resendAdminVerification: (id: number) =>
  168. request<{
  169. verificationEmailSent?: boolean;
  170. message: string;
  171. result?: SystemMailActionResult;
  172. }>(`/api/admin/users/${id}/resend-verification`, { method: 'POST' }),
  173. sendAdminPasswordReset: (id: number) =>
  174. request<{ result: SystemMailActionResult }>(`/api/admin/users/${id}/password-reset`, { method: 'POST' }),
  175. setAdminTemporaryPassword: (id: number, password: string) =>
  176. request<{ user: AdminUser }>(`/api/admin/users/${id}/temporary-password`, {
  177. method: 'POST',
  178. data: { password }
  179. }),
  180. adminResources: () => request<{ inventory: AdminResourceInventory }>('/api/admin/resources'),
  181. transferAdminDomain: (
  182. id: number,
  183. data: { targetUserId: number; dnsCredentialMode?: 'domain_only' | 'with_dns_credential' | 'clear_dns_credential' }
  184. ) => request<{ domain: Domain }>(`/api/admin/resources/domains/${id}/transfer`, { method: 'POST', data }),
  185. transferAdminDnsCredential: (id: number, data: { targetUserId: number }) =>
  186. request<{ credential: DnsCredential }>(`/api/admin/resources/dns-credentials/${id}/transfer`, {
  187. method: 'POST',
  188. data
  189. }),
  190. transferAdminApiTokens: (data: { tokenIds: number[]; targetUserId: number }) =>
  191. request<{ tokens: ApiToken[] }>('/api/admin/resources/api-tokens/transfer', { method: 'POST', data }),
  192. previewUserMerge: (data: { sourceUserId: number; targetUserId: number }) =>
  193. request<{ preview: UserMergePreview }>('/api/admin/migrations/user-merge/preview', { method: 'POST', data }),
  194. executeUserMerge: (data: {
  195. sourceUserId: number;
  196. targetUserId: number;
  197. options: Partial<UserMergeOptions>;
  198. confirmation: string;
  199. }) => request<{ result: UserMergeResult }>('/api/admin/migrations/user-merge/execute', { method: 'POST', data }),
  200. adminSystemEmail: () => request<{ settings: SystemEmailSettings }>('/api/admin/system-email'),
  201. saveAdminSystemEmail: (data: Partial<SystemEmailSettings>) =>
  202. request<{ settings: SystemEmailSettings }>('/api/admin/system-email', { method: 'PATCH', data }),
  203. testAdminSystemEmail: (to?: string) =>
  204. request<{ result: SystemMailActionResult }>('/api/admin/system-email/test', {
  205. method: 'POST',
  206. data: to ? { to } : {}
  207. }),
  208. adminAuditLogs: (query = '') =>
  209. request<{ logs: AuditLogEntry[] }>(`/api/admin/audit-logs${query ? `?${query}` : ''}`),
  210. resendVerification: (email: string) =>
  211. request<{ message: string }>('/api/auth/resend-verification', { method: 'POST', data: { email } }),
  212. forgotPassword: (email: string) =>
  213. request<{ message: string }>('/api/auth/forgot-password', { method: 'POST', data: { email } }),
  214. resetPassword: (token: string, password: string) =>
  215. request<{ message: string }>('/api/auth/reset-password', { method: 'POST', data: { token, password } }),
  216. logout: () => request<{ ok: boolean }>('/api/logout', { method: 'POST' }),
  217. webhooks: (domainId?: number | null) => {
  218. const params = new URLSearchParams();
  219. if (domainId === null) params.set('domainId', 'null');
  220. else if (domainId !== undefined) params.set('domainId', String(domainId));
  221. const query = params.toString();
  222. return request<{ webhooks: Webhook[] }>(`/api/webhooks${query ? `?${query}` : ''}`);
  223. },
  224. createWebhook: (data: WebhookPayload) =>
  225. request<{ webhook: Webhook }>('/api/webhooks', { method: 'POST', data }),
  226. updateWebhook: (id: number, data: WebhookPatchPayload) =>
  227. request<{ webhook: Webhook }>(`/api/webhooks/${id}`, { method: 'PATCH', data }),
  228. deleteWebhook: (id: number) =>
  229. request<{ deleted: boolean }>(`/api/webhooks/${id}`, { method: 'DELETE' }),
  230. rotateWebhookSecret: (id: number) =>
  231. request<{ webhook: Webhook }>(`/api/webhooks/${id}/rotate-secret`, { method: 'POST' }),
  232. testWebhook: (id: number) =>
  233. request<{ delivery: WebhookDelivery }>(`/api/webhooks/${id}/test`, { method: 'POST' }),
  234. webhookDeliveries: (filters: WebhookDeliveryFilters = {}) => {
  235. const params = new URLSearchParams();
  236. if (filters.status) params.set('status', String(filters.status));
  237. if (filters.webhookId != null) params.set('webhookId', String(filters.webhookId));
  238. if (filters.eventType) params.set('eventType', String(filters.eventType));
  239. if (filters.limit != null) params.set('limit', String(filters.limit));
  240. const query = params.toString();
  241. return request<{ deliveries: WebhookDelivery[] }>(
  242. `/api/webhook-deliveries${query ? `?${query}` : ''}`
  243. );
  244. },
  245. replayWebhookDelivery: (id: number) =>
  246. request<{ delivery: WebhookDelivery }>(`/api/webhook-deliveries/${id}/replay`, { method: 'POST' })
  247. };
  248. interface SystemMailActionResult {
  249. ok: boolean;
  250. message: string;
  251. queueId?: string;
  252. }