Domains.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. <template>
  2. <div>
  3. <div class="mb-6 flex flex-col md:flex-row justify-between md:items-center">
  4. <div class="relative">
  5. <input
  6. v-model="search"
  7. @keyup.esc="search = ''"
  8. tabindex="0"
  9. type="text"
  10. class="w-full md:w-64 appearance-none shadow bg-white text-grey-700 focus:outline-none rounded py-3 pl-3 pr-8"
  11. placeholder="Search Domains"
  12. />
  13. <icon
  14. v-if="search"
  15. @click.native="search = ''"
  16. name="close-circle"
  17. class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
  18. />
  19. <icon
  20. v-else
  21. name="search"
  22. class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current pointer-events-none mr-2 flex items-center"
  23. />
  24. </div>
  25. <div class="mt-4 md:mt-0">
  26. <button
  27. @click="addDomainModalOpen = true"
  28. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none ml-auto"
  29. >
  30. Add Custom Domain
  31. </button>
  32. </div>
  33. </div>
  34. <vue-good-table
  35. v-if="initialDomains.length"
  36. @on-search="debounceToolips"
  37. :columns="columns"
  38. :rows="rows"
  39. :search-options="{
  40. enabled: true,
  41. skipDiacritics: true,
  42. externalQuery: search,
  43. }"
  44. :sort-options="{
  45. enabled: true,
  46. initialSortBy: { field: 'created_at', type: 'desc' },
  47. }"
  48. styleClass="vgt-table"
  49. >
  50. <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
  51. No domains found for that search!
  52. </div>
  53. <template slot="table-row" slot-scope="props">
  54. <span
  55. v-if="props.column.field == 'created_at'"
  56. class="tooltip outline-none text-sm"
  57. :data-tippy-content="rows[props.row.originalIndex].created_at | formatDate"
  58. >{{ props.row.created_at | timeAgo }}
  59. </span>
  60. <span v-else-if="props.column.field == 'domain'">
  61. <span
  62. class="tooltip cursor-pointer outline-none"
  63. data-tippy-content="Click to copy"
  64. v-clipboard="() => rows[props.row.originalIndex].domain"
  65. v-clipboard:success="clipboardSuccess"
  66. v-clipboard:error="clipboardError"
  67. >{{ props.row.domain | truncate(30) }}</span
  68. >
  69. </span>
  70. <span v-else-if="props.column.field == 'description'">
  71. <div v-if="domainIdToEdit === props.row.id" class="flex items-center">
  72. <input
  73. @keyup.enter="editDomain(rows[props.row.originalIndex])"
  74. @keyup.esc="domainIdToEdit = domainDescriptionToEdit = ''"
  75. v-model="domainDescriptionToEdit"
  76. type="text"
  77. class="flex-grow appearance-none bg-grey-100 border text-grey-700 focus:outline-none rounded px-2 py-1"
  78. :class="
  79. domainDescriptionToEdit.length > 200 ? 'border-red-500' : 'border-transparent'
  80. "
  81. placeholder="Add description"
  82. tabindex="0"
  83. autofocus
  84. />
  85. <icon
  86. name="close"
  87. class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
  88. @click.native="domainIdToEdit = domainDescriptionToEdit = ''"
  89. />
  90. <icon
  91. name="save"
  92. class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
  93. @click.native="editDomain(rows[props.row.originalIndex])"
  94. />
  95. </div>
  96. <div v-else-if="props.row.description" class="flex items-centers">
  97. <span
  98. class="tooltip outline-none"
  99. :data-tippy-content="rows[props.row.originalIndex].description"
  100. >{{ props.row.description | truncate(60) }}</span
  101. >
  102. <icon
  103. name="edit"
  104. class="inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer ml-2"
  105. @click.native="
  106. ;(domainIdToEdit = props.row.id), (domainDescriptionToEdit = props.row.description)
  107. "
  108. />
  109. </div>
  110. <div v-else class="flex justify-center">
  111. <icon
  112. name="plus"
  113. class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
  114. @click.native=";(domainIdToEdit = props.row.id), (domainDescriptionToEdit = '')"
  115. />
  116. </div>
  117. </span>
  118. <span v-else-if="props.column.field === 'default_recipient'">
  119. <div v-if="props.row.default_recipient">
  120. {{ props.row.default_recipient.email | truncate(30) }}
  121. <icon
  122. name="edit"
  123. class="ml-2 inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer"
  124. @click.native="openDomainDefaultRecipientModal(props.row)"
  125. />
  126. </div>
  127. <div class="flex justify-center" v-else>
  128. <icon
  129. name="plus"
  130. class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
  131. @click.native="openDomainDefaultRecipientModal(props.row)"
  132. />
  133. </div>
  134. </span>
  135. <span v-else-if="props.column.field === 'aliases_count'">
  136. {{ props.row.aliases_count }}
  137. </span>
  138. <span v-else-if="props.column.field === 'active'" class="flex items-center">
  139. <Toggle
  140. v-model="rows[props.row.originalIndex].active"
  141. @on="activateDomain(props.row.id)"
  142. @off="deactivateDomain(props.row.id)"
  143. />
  144. </span>
  145. <span v-else-if="props.column.field === 'catch_all'" class="flex items-center">
  146. <Toggle
  147. v-model="rows[props.row.originalIndex].catch_all"
  148. @on="enableCatchAll(props.row.id)"
  149. @off="disableCatchAll(props.row.id)"
  150. />
  151. </span>
  152. <span v-else-if="props.column.field === 'domain_sending_verified_at'">
  153. <div v-if="props.row.domain_sending_verified_at || props.row.domain_mx_validated_at">
  154. <svg
  155. v-if="props.row.domain_sending_verified_at && props.row.domain_mx_validated_at"
  156. class="h-5 w-5 inline-block"
  157. xmlns="http://www.w3.org/2000/svg"
  158. viewBox="0 0 20 20"
  159. >
  160. <g fill="none" fill-rule="evenodd">
  161. <circle class="text-green-200 fill-current" cx="10" cy="10" r="10"></circle>
  162. <polyline
  163. class="text-green-900 stroke-current"
  164. stroke-linecap="round"
  165. stroke-linejoin="round"
  166. stroke-width="2"
  167. points="6 10 8.667 12.667 14 7.333"
  168. ></polyline>
  169. </g>
  170. </svg>
  171. <svg
  172. v-else-if="!props.row.domain_mx_validated_at"
  173. xmlns="http://www.w3.org/2000/svg"
  174. viewBox="0 0 20 20"
  175. class="h-5 w-5 inline-block tooltip"
  176. data-tippy-content="MX records invalid"
  177. >
  178. <g fill="none" fill-rule="evenodd">
  179. <circle cx="10" cy="10" r="10" fill="#FF9B9B"></circle>
  180. <polyline
  181. stroke="#AB091E"
  182. stroke-linecap="round"
  183. stroke-linejoin="round"
  184. stroke-width="2"
  185. points="14 6 6 14"
  186. ></polyline>
  187. <polyline
  188. stroke="#AB091E"
  189. stroke-linecap="round"
  190. stroke-linejoin="round"
  191. stroke-width="2"
  192. points="6 6 14 14"
  193. ></polyline>
  194. </g>
  195. </svg>
  196. <svg
  197. v-else
  198. xmlns="http://www.w3.org/2000/svg"
  199. viewBox="0 0 20 20"
  200. class="h-5 w-5 inline-block tooltip"
  201. data-tippy-content="DNS records for sending invalid"
  202. >
  203. <g fill="none" fill-rule="evenodd">
  204. <circle cx="10" cy="10" r="10" fill="#FF9B9B"></circle>
  205. <polyline
  206. stroke="#AB091E"
  207. stroke-linecap="round"
  208. stroke-linejoin="round"
  209. stroke-width="2"
  210. points="14 6 6 14"
  211. ></polyline>
  212. <polyline
  213. stroke="#AB091E"
  214. stroke-linecap="round"
  215. stroke-linejoin="round"
  216. stroke-width="2"
  217. points="6 6 14 14"
  218. ></polyline>
  219. </g>
  220. </svg>
  221. <button
  222. @click="openCheckRecordsModal(rows[props.row.originalIndex])"
  223. class="focus:outline-none text-sm ml-2"
  224. >
  225. Recheck
  226. </button>
  227. </div>
  228. <button
  229. v-else
  230. @click="openCheckRecordsModal(rows[props.row.originalIndex])"
  231. class="focus:outline-none text-sm"
  232. >
  233. Check Records
  234. </button>
  235. </span>
  236. <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
  237. <icon
  238. name="trash"
  239. class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
  240. @click.native="openDeleteModal(props.row.id)"
  241. />
  242. </span>
  243. </template>
  244. </vue-good-table>
  245. <div v-else class="bg-white rounded shadow overflow-x-auto">
  246. <div class="p-8 text-center text-lg text-grey-700">
  247. <h1 class="mb-6 text-xl text-indigo-800 font-semibold">
  248. This is where you can set up and view custom domains
  249. </h1>
  250. <div class="mx-auto mb-6 w-24 border-b-2 border-grey-200"></div>
  251. <p class="mb-4">
  252. To get started all you have to do is add a TXT record to your domain to verify ownership
  253. and then add the domain here by clicking the button above.
  254. </p>
  255. <p class="mb-4">The TXT record needs to have the following values:</p>
  256. <p class="mb-4">
  257. Type: <b>TXT</b><br />
  258. Host: <b>@</b><br />
  259. Value: <b>aa-verify={{ aaVerify }}</b
  260. ><br />
  261. </p>
  262. <p>
  263. Once the DNS changes propagate and you have verified ownership of the domain you will need
  264. to add a few more records to be able to receive emails at your own domain.
  265. </p>
  266. </div>
  267. </div>
  268. <Modal :open="addDomainModalOpen" @close="closeCheckRecordsModal">
  269. <div v-if="!domainToCheck" class="max-w-2xl w-full bg-white rounded-lg shadow-2xl p-6">
  270. <h2
  271. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  272. >
  273. Add new domain
  274. </h2>
  275. <p class="mt-4 mb-2 text-grey-700">
  276. To verify ownership of the domain, please add the following TXT record and then click Add
  277. Domain below.
  278. </p>
  279. <div class="table w-full">
  280. <div class="table-row">
  281. <div class="table-cell py-2 font-semibold">Type</div>
  282. <div class="table-cell p-2 font-semibold">Host</div>
  283. <div class="table-cell py-2 font-semibold">Value/Points to</div>
  284. </div>
  285. <div class="table-row">
  286. <div class="table-cell py-2">TXT</div>
  287. <div class="table-cell p-2">@</div>
  288. <div class="table-cell py-2 break-all">aa-verify={{ aaVerify }}</div>
  289. </div>
  290. </div>
  291. <div class="mt-6">
  292. <p v-show="errors.newDomain" class="mb-3 text-red-500 text-sm">
  293. {{ errors.newDomain }}
  294. </p>
  295. <input
  296. v-model="newDomain"
  297. type="text"
  298. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-6"
  299. :class="errors.newDomain ? 'border-red-500' : ''"
  300. placeholder="example.com"
  301. autofocus
  302. />
  303. <button
  304. @click="validateNewDomain"
  305. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  306. :class="addDomainLoading ? 'cursor-not-allowed' : ''"
  307. :disabled="addDomainLoading"
  308. >
  309. Add Domain
  310. <loader v-if="addDomainLoading" />
  311. </button>
  312. <button
  313. @click="addDomainModalOpen = false"
  314. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  315. >
  316. Cancel
  317. </button>
  318. </div>
  319. </div>
  320. <div v-else class="max-w-2xl w-full bg-white rounded-lg shadow-2xl p-6">
  321. <h2
  322. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  323. >
  324. Check DNS records
  325. </h2>
  326. <p class="mt-4 mb-2 text-grey-700">
  327. Please set the following DNS records for your custom domain. If you have more than one MX
  328. record then the MX record below should have the lowest priority (e.g. 10).
  329. </p>
  330. <div class="table w-full">
  331. <div class="table-row">
  332. <div class="table-cell py-2 font-semibold">Type</div>
  333. <div class="table-cell py-2 px-4 font-semibold">Host</div>
  334. <div class="table-cell py-2 font-semibold">Value/Points to</div>
  335. </div>
  336. <div class="table-row">
  337. <div class="table-cell py-2">MX</div>
  338. <div class="table-cell py-2 px-4">@</div>
  339. <div class="table-cell py-2 break-words">{{ hostname }}</div>
  340. </div>
  341. <div class="table-row">
  342. <div class="table-cell py-2">TXT</div>
  343. <div class="table-cell py-2 px-4">@</div>
  344. <div class="table-cell py-2 break-words">v=spf1 mx -all</div>
  345. </div>
  346. <div class="table-row">
  347. <div class="table-cell py-2">CNAME</div>
  348. <div class="table-cell py-2 px-4">default._domainkey</div>
  349. <div class="table-cell py-2 break-words">default._domainkey.{{ domainName }}.</div>
  350. </div>
  351. <div class="table-row">
  352. <div class="table-cell py-2">TXT</div>
  353. <div class="table-cell py-2 px-4">_dmarc</div>
  354. <div class="table-cell py-2 break-words">v=DMARC1; p=quarantine; adkim=s</div>
  355. </div>
  356. </div>
  357. <div class="mt-6">
  358. <button
  359. @click="checkRecords(domainToCheck)"
  360. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  361. :class="checkRecordsLoading ? 'cursor-not-allowed' : ''"
  362. :disabled="checkRecordsLoading"
  363. >
  364. Check Records
  365. <loader v-if="checkRecordsLoading" />
  366. </button>
  367. <button
  368. @click="closeCheckRecordsModal"
  369. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  370. >
  371. Cancel
  372. </button>
  373. </div>
  374. </div>
  375. </Modal>
  376. <Modal :open="domainDefaultRecipientModalOpen" @close="closeDomainDefaultRecipientModal">
  377. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  378. <h2
  379. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  380. >
  381. Update Default Recipient
  382. </h2>
  383. <p class="my-4 text-grey-700">
  384. Select the default recipient for this domain. This overrides the default recipient in your
  385. account settings. Leave it empty if you would like to use the default recipient in your
  386. account settings.
  387. </p>
  388. <multiselect
  389. v-model="defaultRecipient"
  390. :options="recipientOptions"
  391. :multiple="false"
  392. :close-on-select="true"
  393. :clear-on-select="false"
  394. :searchable="false"
  395. :allow-empty="true"
  396. placeholder="Select recipient"
  397. label="email"
  398. track-by="email"
  399. :preselect-first="false"
  400. :show-labels="false"
  401. >
  402. </multiselect>
  403. <div class="mt-6">
  404. <button
  405. type="button"
  406. @click="editDefaultRecipient()"
  407. class="px-4 py-3 text-cyan-900 font-semibold bg-cyan-400 hover:bg-cyan-300 border border-transparent rounded focus:outline-none"
  408. :class="editDefaultRecipientLoading ? 'cursor-not-allowed' : ''"
  409. :disabled="editDefaultRecipientLoading"
  410. >
  411. Update Default Recipient
  412. <loader v-if="editDefaultRecipientLoading" />
  413. </button>
  414. <button
  415. @click="closeDomainDefaultRecipientModal()"
  416. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  417. >
  418. Cancel
  419. </button>
  420. </div>
  421. </div>
  422. </Modal>
  423. <Modal :open="deleteDomainModalOpen" @close="closeDeleteModal">
  424. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  425. <h2
  426. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  427. >
  428. Delete domain
  429. </h2>
  430. <p class="mt-4 text-grey-700">
  431. Are you sure you want to delete this domain? This will also delete all aliases associated
  432. with this domain. You will no longer be able to receive any emails at this domain.
  433. </p>
  434. <div class="mt-6">
  435. <button
  436. type="button"
  437. @click="deleteDomain(domainIdToDelete)"
  438. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
  439. :class="deleteDomainLoading ? 'cursor-not-allowed' : ''"
  440. :disabled="deleteDomainLoading"
  441. >
  442. Delete domain
  443. <loader v-if="deleteDomainLoading" />
  444. </button>
  445. <button
  446. @click="closeDeleteModal"
  447. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  448. >
  449. Cancel
  450. </button>
  451. </div>
  452. </div>
  453. </Modal>
  454. </div>
  455. </template>
  456. <script>
  457. import Modal from './../components/Modal.vue'
  458. import Toggle from './../components/Toggle.vue'
  459. import { roundArrow } from 'tippy.js'
  460. import 'tippy.js/dist/svg-arrow.css'
  461. import 'tippy.js/dist/tippy.css'
  462. import tippy from 'tippy.js'
  463. import Multiselect from 'vue-multiselect'
  464. export default {
  465. props: {
  466. initialDomains: {
  467. type: Array,
  468. required: true,
  469. },
  470. domainName: {
  471. type: String,
  472. required: true,
  473. },
  474. hostname: {
  475. type: String,
  476. required: true,
  477. },
  478. recipientOptions: {
  479. type: Array,
  480. required: true,
  481. },
  482. aaVerify: {
  483. type: String,
  484. required: true,
  485. },
  486. },
  487. components: {
  488. Modal,
  489. Toggle,
  490. Multiselect,
  491. },
  492. data() {
  493. return {
  494. newDomain: '',
  495. search: '',
  496. addDomainLoading: false,
  497. addDomainModalOpen: false,
  498. domainIdToDelete: null,
  499. domainIdToEdit: null,
  500. domainDescriptionToEdit: '',
  501. domainToCheck: null,
  502. deleteDomainLoading: false,
  503. deleteDomainModalOpen: false,
  504. checkRecordsLoading: false,
  505. domainDefaultRecipientModalOpen: false,
  506. defaultRecipientDomainToEdit: {},
  507. defaultRecipient: {},
  508. editDefaultRecipientLoading: false,
  509. errors: {},
  510. columns: [
  511. {
  512. label: 'Created',
  513. field: 'created_at',
  514. globalSearchDisabled: true,
  515. },
  516. {
  517. label: 'Domain',
  518. field: 'domain',
  519. },
  520. {
  521. label: 'Description',
  522. field: 'description',
  523. },
  524. {
  525. label: 'Default Recipient',
  526. field: 'default_recipient',
  527. sortable: false,
  528. globalSearchDisabled: true,
  529. },
  530. {
  531. label: 'Alias Count',
  532. field: 'aliases_count',
  533. type: 'number',
  534. globalSearchDisabled: true,
  535. },
  536. {
  537. label: 'Active',
  538. field: 'active',
  539. type: 'boolean',
  540. globalSearchDisabled: true,
  541. },
  542. {
  543. label: 'Catch-All',
  544. field: 'catch_all',
  545. type: 'boolean',
  546. globalSearchDisabled: true,
  547. },
  548. {
  549. label: 'Verified Records',
  550. field: 'domain_sending_verified_at',
  551. globalSearchDisabled: true,
  552. },
  553. {
  554. label: '',
  555. field: 'actions',
  556. sortable: false,
  557. globalSearchDisabled: true,
  558. },
  559. ],
  560. rows: this.initialDomains,
  561. tippyInstance: null,
  562. }
  563. },
  564. watch: {
  565. domainIdToEdit: _.debounce(function () {
  566. this.addTooltips()
  567. }, 50),
  568. },
  569. methods: {
  570. addTooltips() {
  571. if (this.tippyInstance) {
  572. _.each(this.tippyInstance, instance => instance.destroy())
  573. }
  574. this.tippyInstance = tippy('.tooltip', {
  575. arrow: roundArrow,
  576. allowHTML: true,
  577. })
  578. },
  579. debounceToolips: _.debounce(function () {
  580. this.addTooltips()
  581. }, 50),
  582. validateNewDomain(e) {
  583. this.errors = {}
  584. if (!this.newDomain) {
  585. this.errors.newDomain = 'Domain name required'
  586. } else if (!this.validDomain(this.newDomain)) {
  587. this.errors.newDomain = 'Please enter a valid domain name'
  588. }
  589. if (!this.errors.newDomain) {
  590. this.addNewDomain()
  591. }
  592. e.preventDefault()
  593. },
  594. addNewDomain() {
  595. this.addDomainLoading = true
  596. axios
  597. .post(
  598. '/api/v1/domains',
  599. JSON.stringify({
  600. domain: this.newDomain,
  601. }),
  602. {
  603. headers: { 'Content-Type': 'application/json' },
  604. }
  605. )
  606. .then(({ data }) => {
  607. this.addDomainLoading = false
  608. this.rows.push(data.data)
  609. this.newDomain = ''
  610. this.domainToCheck = data.data
  611. this.success('Custom domain added')
  612. })
  613. .catch(error => {
  614. this.addDomainLoading = false
  615. if (error.response.status === 422) {
  616. this.error(error.response.data.errors.domain[0])
  617. } else if (error.response.status === 429) {
  618. this.error('You are making too many requests')
  619. } else if (error.response.status === 404) {
  620. this.warn(
  621. 'Verification TXT record not found, this could be due to DNS caching, please try again shortly.'
  622. )
  623. } else {
  624. this.error()
  625. }
  626. })
  627. },
  628. checkRecords(domain) {
  629. this.checkRecordsLoading = true
  630. axios
  631. .get(`/domains/${domain.id}/check-sending`)
  632. .then(({ data }) => {
  633. this.checkRecordsLoading = false
  634. if (data.success === true) {
  635. this.closeCheckRecordsModal()
  636. this.success(data.message)
  637. domain.domain_sending_verified_at = data.data.domain_sending_verified_at
  638. domain.domain_mx_validated_at = data.data.domain_mx_validated_at
  639. } else {
  640. this.warn(data.message)
  641. }
  642. })
  643. .catch(error => {
  644. this.checkRecordsLoading = false
  645. if (error.response.status === 429) {
  646. this.error('Please wait a little while before checking the records again')
  647. } else {
  648. this.error()
  649. }
  650. })
  651. },
  652. openDeleteModal(id) {
  653. this.deleteDomainModalOpen = true
  654. this.domainIdToDelete = id
  655. },
  656. closeDeleteModal() {
  657. this.deleteDomainModalOpen = false
  658. this.domainIdToDelete = null
  659. },
  660. openDomainDefaultRecipientModal(domain) {
  661. this.domainDefaultRecipientModalOpen = true
  662. this.defaultRecipientDomainToEdit = domain
  663. this.defaultRecipient = domain.default_recipient
  664. },
  665. closeDomainDefaultRecipientModal() {
  666. this.domainDefaultRecipientModalOpen = false
  667. this.defaultRecipientDomainToEdit = {}
  668. this.defaultRecipient = {}
  669. },
  670. openCheckRecordsModal(domain) {
  671. this.domainToCheck = domain
  672. this.addDomainModalOpen = true
  673. },
  674. closeCheckRecordsModal() {
  675. this.domainToCheck = null
  676. this.addDomainModalOpen = false
  677. },
  678. editDomain(domain) {
  679. if (this.domainDescriptionToEdit.length > 200) {
  680. return this.error('Description cannot be more than 200 characters')
  681. }
  682. axios
  683. .patch(
  684. `/api/v1/domains/${domain.id}`,
  685. JSON.stringify({
  686. description: this.domainDescriptionToEdit,
  687. }),
  688. {
  689. headers: { 'Content-Type': 'application/json' },
  690. }
  691. )
  692. .then(response => {
  693. domain.description = this.domainDescriptionToEdit
  694. this.domainIdToEdit = null
  695. this.domainDescriptionToEdit = ''
  696. this.success('Domain description updated')
  697. })
  698. .catch(error => {
  699. this.domainIdToEdit = null
  700. this.domainDescriptionToEdit = ''
  701. this.error()
  702. })
  703. },
  704. editDefaultRecipient() {
  705. this.editDefaultRecipientLoading = true
  706. axios
  707. .patch(
  708. `/api/v1/domains/${this.defaultRecipientDomainToEdit.id}/default-recipient`,
  709. JSON.stringify({
  710. default_recipient: this.defaultRecipient ? this.defaultRecipient.id : '',
  711. }),
  712. {
  713. headers: { 'Content-Type': 'application/json' },
  714. }
  715. )
  716. .then(response => {
  717. let domain = _.find(this.rows, ['id', this.defaultRecipientDomainToEdit.id])
  718. domain.default_recipient = this.defaultRecipient
  719. this.domainDefaultRecipientModalOpen = false
  720. this.editDefaultRecipientLoading = false
  721. this.defaultRecipient = {}
  722. this.success("Domain's default recipient updated")
  723. })
  724. .catch(error => {
  725. this.domainDefaultRecipientModalOpen = false
  726. this.editDefaultRecipientLoading = false
  727. this.defaultRecipient = {}
  728. this.error()
  729. })
  730. },
  731. activateDomain(id) {
  732. axios
  733. .post(
  734. `/api/v1/active-domains`,
  735. JSON.stringify({
  736. id: id,
  737. }),
  738. {
  739. headers: { 'Content-Type': 'application/json' },
  740. }
  741. )
  742. .then(response => {
  743. //
  744. })
  745. .catch(error => {
  746. this.error()
  747. })
  748. },
  749. deactivateDomain(id) {
  750. axios
  751. .delete(`/api/v1/active-domains/${id}`)
  752. .then(response => {
  753. //
  754. })
  755. .catch(error => {
  756. this.error()
  757. })
  758. },
  759. enableCatchAll(id) {
  760. axios
  761. .post(
  762. `/api/v1/catch-all-domains`,
  763. JSON.stringify({
  764. id: id,
  765. }),
  766. {
  767. headers: { 'Content-Type': 'application/json' },
  768. }
  769. )
  770. .then(response => {
  771. //
  772. })
  773. .catch(error => {
  774. if (error.response !== undefined) {
  775. this.error(error.response.data)
  776. } else {
  777. this.error()
  778. }
  779. })
  780. },
  781. disableCatchAll(id) {
  782. axios
  783. .delete(`/api/v1/catch-all-domains/${id}`)
  784. .then(response => {
  785. //
  786. })
  787. .catch(error => {
  788. if (error.response !== undefined) {
  789. this.error(error.response.data)
  790. } else {
  791. this.error()
  792. }
  793. })
  794. },
  795. deleteDomain(id) {
  796. this.deleteDomainLoading = true
  797. axios
  798. .delete(`/api/v1/domains/${id}`)
  799. .then(response => {
  800. this.rows = _.reject(this.rows, domain => domain.id === id)
  801. this.deleteDomainModalOpen = false
  802. this.deleteDomainLoading = false
  803. })
  804. .catch(error => {
  805. this.error()
  806. this.deleteDomainLoading = false
  807. this.deleteDomainModalOpen = false
  808. })
  809. },
  810. validDomain(domain) {
  811. let re = /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/
  812. return re.test(domain)
  813. },
  814. clipboardSuccess() {
  815. this.success('Copied to clipboard')
  816. },
  817. clipboardError() {
  818. this.error('Could not copy to clipboard')
  819. },
  820. warn(text = '') {
  821. this.$notify({
  822. title: 'Information',
  823. text: text,
  824. type: 'warn',
  825. })
  826. },
  827. success(text = '') {
  828. this.$notify({
  829. title: 'Success',
  830. text: text,
  831. type: 'success',
  832. })
  833. },
  834. error(text = 'An error has occurred, please try again later') {
  835. this.$notify({
  836. title: 'Error',
  837. text: text,
  838. type: 'error',
  839. })
  840. },
  841. },
  842. }
  843. </script>