api.ts 11 KB

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