Domains.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  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. <div class="bg-white rounded shadow overflow-x-auto">
  35. <table v-if="initialDomains.length" class="w-full whitespace-no-wrap">
  36. <tr class="text-left font-semibold text-grey-500 text-sm tracking-wider">
  37. <th class="p-4">
  38. <div class="flex items-center">
  39. Created
  40. <div class="inline-flex flex-col">
  41. <icon
  42. name="chevron-up"
  43. @click.native="sort('created_at', 'asc')"
  44. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  45. :class="{ 'text-grey-800': isCurrentSort('created_at', 'asc') }"
  46. />
  47. <icon
  48. name="chevron-down"
  49. @click.native="sort('created_at', 'desc')"
  50. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  51. :class="{
  52. 'text-grey-800': isCurrentSort('created_at', 'desc'),
  53. }"
  54. />
  55. </div>
  56. </div>
  57. </th>
  58. <th class="p-4">
  59. <div class="flex items-center">
  60. Domain
  61. <div class="inline-flex flex-col">
  62. <icon
  63. name="chevron-up"
  64. @click.native="sort('domain', 'asc')"
  65. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  66. :class="{ 'text-grey-800': isCurrentSort('domain', 'asc') }"
  67. />
  68. <icon
  69. name="chevron-down"
  70. @click.native="sort('domain', 'desc')"
  71. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  72. :class="{ 'text-grey-800': isCurrentSort('domain', 'desc') }"
  73. />
  74. </div>
  75. </div>
  76. </th>
  77. <th class="p-4">
  78. <div class="flex items-center">
  79. Description
  80. </div>
  81. </th>
  82. <th class="p-4 items-center">
  83. <div class="flex items-center">
  84. Alias Count
  85. <div class="inline-flex flex-col">
  86. <icon
  87. name="chevron-up"
  88. @click.native="sort('aliases', 'asc')"
  89. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  90. :class="{ 'text-grey-800': isCurrentSort('aliases', 'asc') }"
  91. />
  92. <icon
  93. name="chevron-down"
  94. @click.native="sort('aliases', 'desc')"
  95. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  96. :class="{ 'text-grey-800': isCurrentSort('aliases', 'desc') }"
  97. />
  98. </div>
  99. </div>
  100. </th>
  101. <th class="p-4 items-center" colspan="2">
  102. <div class="flex items-center">
  103. Active
  104. <div class="inline-flex flex-col">
  105. <icon
  106. name="chevron-up"
  107. @click.native="sort('active', 'asc')"
  108. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  109. :class="{ 'text-grey-800': isCurrentSort('active', 'asc') }"
  110. />
  111. <icon
  112. name="chevron-down"
  113. @click.native="sort('active', 'desc')"
  114. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  115. :class="{ 'text-grey-800': isCurrentSort('active', 'desc') }"
  116. />
  117. </div>
  118. </div>
  119. </th>
  120. </tr>
  121. <tr
  122. v-for="domain in queriedDomains"
  123. :key="domain.id"
  124. class="hover:bg-grey-50 focus-within:bg-grey-50 h-20"
  125. >
  126. <td class="border-grey-200 border-t">
  127. <div class="p-4 flex items-center">
  128. <span
  129. class="tooltip outline-none text-sm"
  130. :data-tippy-content="domain.created_at | formatDate"
  131. >{{ domain.created_at | timeAgo }}</span
  132. >
  133. </div>
  134. </td>
  135. <td class="border-grey-200 border-t">
  136. <div class="p-4 flex items-center focus:text-indigo-500">
  137. <span
  138. class="tooltip cursor-pointer outline-none"
  139. data-tippy-content="Click to copy"
  140. v-clipboard="() => domain.domain"
  141. v-clipboard:success="clipboardSuccess"
  142. v-clipboard:error="clipboardError"
  143. >{{ domain.domain | truncate(30) }}</span
  144. >
  145. </div>
  146. </td>
  147. <td class="border-grey-200 border-t w-64">
  148. <div class="p-4 text-sm">
  149. <div
  150. v-if="domainIdToEdit === domain.id"
  151. class="w-full flex items-center justify-between"
  152. >
  153. <input
  154. @keyup.enter="editDomain(domain)"
  155. @keyup.esc="domainIdToEdit = domainDescriptionToEdit = ''"
  156. v-model="domainDescriptionToEdit"
  157. type="text"
  158. class="appearance-none bg-grey-100 border text-grey-700 focus:outline-none rounded px-2 py-1"
  159. :class="
  160. domainDescriptionToEdit.length > 100 ? 'border-red-500' : 'border-transparent'
  161. "
  162. placeholder="Add description"
  163. tabindex="0"
  164. autofocus
  165. />
  166. <icon
  167. name="close"
  168. class="inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  169. @click.native="domainIdToEdit = domainDescriptionToEdit = ''"
  170. />
  171. <icon
  172. name="save"
  173. class="inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  174. @click.native="editDomain(domain)"
  175. />
  176. </div>
  177. <div v-else-if="domain.description" class="flex items-center justify-between w-full">
  178. <span class="tooltip outline-none" :data-tippy-content="domain.description">{{
  179. domain.description | truncate(25)
  180. }}</span>
  181. <icon
  182. name="edit"
  183. class="inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  184. @click.native="
  185. ;(domainIdToEdit = domain.id), (domainDescriptionToEdit = domain.description)
  186. "
  187. />
  188. </div>
  189. <div v-else class="w-full flex justify-center">
  190. <icon
  191. name="plus"
  192. class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  193. @click.native="domainIdToEdit = domain.id"
  194. />
  195. </div>
  196. </div>
  197. </td>
  198. <td class="border-grey-200 border-t">
  199. <div class="p-4 flex items-center">
  200. {{ domain.aliases.length }}
  201. </div>
  202. </td>
  203. <td class="border-grey-200 border-t">
  204. <div class="p-4 flex items-center">
  205. <Toggle
  206. v-model="domain.active"
  207. @on="activateDomain(domain)"
  208. @off="deactivateDomain(domain)"
  209. />
  210. </div>
  211. </td>
  212. <td class="border-grey-200 border-t w-px">
  213. <div
  214. class="px-4 flex items-center cursor-pointer outline-none focus:text-indigo-500"
  215. @click="openDeleteModal(domain.id)"
  216. tabindex="-1"
  217. >
  218. <icon name="trash" class="block w-6 h-6 text-grey-200 fill-current" />
  219. </div>
  220. </td>
  221. </tr>
  222. <tr v-if="queriedDomains.length === 0">
  223. <td
  224. class="border-grey-200 border-t p-4 text-center h-24 text-lg text-grey-700"
  225. colspan="5"
  226. >
  227. No domains found for that search!
  228. </td>
  229. </tr>
  230. </table>
  231. <div v-else class="p-8 text-center text-lg text-grey-700">
  232. <h1 class="mb-6 text-xl text-indigo-800 font-semibold">
  233. This is where you can set up and view custom domains
  234. </h1>
  235. <div class="mx-auto mb-6 w-24 border-b-2 border-grey-200"></div>
  236. <p class="mb-4">
  237. To get started all you have to do is add an MX record to your domain and then add the
  238. domain here by clicking the button above.
  239. </p>
  240. <p class="mb-4">
  241. The new record needs to have the following values:
  242. </p>
  243. <p class="mb-4">
  244. Host: <b>@</b><br />
  245. Value: <b>mail.anonaddy.me</b><br />
  246. Priority: <b>10</b><br />
  247. TTL: <b>3600</b>
  248. </p>
  249. <p>
  250. Once the DNS changes propagate you will be able to recieve emails at your own domain.
  251. </p>
  252. </div>
  253. </div>
  254. <Modal :open="addDomainModalOpen" @close="addDomainModalOpen = false">
  255. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  256. <h2
  257. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  258. >
  259. Add new domain
  260. </h2>
  261. <p class="mt-4 text-grey-700">
  262. Make sure you add the following MX record to your domain.<br /><br />
  263. Host: <b>@</b><br />
  264. Value: <b>mail.anonaddy.me</b><br />
  265. Priority: <b>10</b><br />
  266. TTL: <b>3600</b><br /><br />
  267. Just include the domain/subdomain e.g. example.com without any http protocol.
  268. </p>
  269. <div class="mt-6">
  270. <p v-show="errors.newDomain" class="mb-3 text-red-500">
  271. {{ errors.newDomain }}
  272. </p>
  273. <input
  274. v-model="newDomain"
  275. type="text"
  276. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-6"
  277. :class="errors.newDomain ? 'border-red-500' : ''"
  278. placeholder="example.com"
  279. autofocus
  280. />
  281. <button
  282. @click="validateNewDomain"
  283. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  284. :class="addDomainLoading ? 'cursor-not-allowed' : ''"
  285. :disabled="addDomainLoading"
  286. >
  287. Add Domain
  288. </button>
  289. <button
  290. @click="addDomainModalOpen = false"
  291. 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"
  292. >
  293. Cancel
  294. </button>
  295. </div>
  296. </div>
  297. </Modal>
  298. <Modal :open="deleteDomainModalOpen" @close="closeDeleteModal">
  299. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  300. <h2
  301. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  302. >
  303. Delete domain
  304. </h2>
  305. <p class="mt-4 text-grey-700">
  306. Are you sure you want to delete this domain? You will no longer be able to receive any
  307. emails at this domain.
  308. </p>
  309. <div class="mt-6">
  310. <button
  311. type="button"
  312. @click="deleteDomain(domainIdToDelete)"
  313. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
  314. :class="deleteDomainLoading ? 'cursor-not-allowed' : ''"
  315. :disabled="deleteDomainLoading"
  316. >
  317. Delete domain
  318. </button>
  319. <button
  320. @click="closeDeleteModal"
  321. 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"
  322. >
  323. Cancel
  324. </button>
  325. </div>
  326. </div>
  327. </Modal>
  328. </div>
  329. </template>
  330. <script>
  331. import Modal from './../components/Modal.vue'
  332. import Toggle from './../components/Toggle.vue'
  333. import tippy from 'tippy.js'
  334. export default {
  335. props: {
  336. initialDomains: {
  337. type: Array,
  338. required: true,
  339. },
  340. },
  341. components: {
  342. Modal,
  343. Toggle,
  344. },
  345. mounted() {
  346. this.addTooltips()
  347. },
  348. data() {
  349. return {
  350. domains: this.initialDomains,
  351. newDomain: '',
  352. search: '',
  353. addDomainLoading: false,
  354. addDomainModalOpen: false,
  355. domainIdToDelete: null,
  356. domainIdToEdit: '',
  357. domainDescriptionToEdit: '',
  358. deleteDomainLoading: false,
  359. deleteDomainModalOpen: false,
  360. currentSort: 'created_at',
  361. currentSortDir: 'desc',
  362. errors: {},
  363. }
  364. },
  365. watch: {
  366. queriedDomains: _.debounce(function() {
  367. this.addTooltips()
  368. }, 50),
  369. domainIdToEdit: _.debounce(function() {
  370. this.addTooltips()
  371. }, 50),
  372. },
  373. computed: {
  374. queriedDomains() {
  375. return _.filter(this.domains, domain => domain.domain.includes(this.search))
  376. },
  377. },
  378. methods: {
  379. addTooltips() {
  380. tippy('.tooltip', {
  381. arrow: true,
  382. arrowType: 'round',
  383. })
  384. },
  385. isCurrentSort(col, dir) {
  386. return this.currentSort === col && this.currentSortDir === dir
  387. },
  388. validateNewDomain(e) {
  389. this.errors = {}
  390. if (!this.newDomain) {
  391. this.errors.newDomain = 'Domain name required'
  392. } else if (!this.validDomain(this.newDomain)) {
  393. this.errors.newDomain = 'Please enter a valid domain name without http:// or https://'
  394. }
  395. if (!this.errors.newDomain) {
  396. this.addNewDomain()
  397. }
  398. e.preventDefault()
  399. },
  400. addNewDomain() {
  401. this.addDomainLoading = true
  402. axios
  403. .post(
  404. '/domains',
  405. JSON.stringify({
  406. domain: this.newDomain,
  407. }),
  408. {
  409. headers: { 'Content-Type': 'application/json' },
  410. }
  411. )
  412. .then(({ data }) => {
  413. this.addDomainLoading = false
  414. this.domains.push(data.data)
  415. this.reSort()
  416. this.newDomain = ''
  417. this.addDomainModalOpen = false
  418. this.success('Custom domain added')
  419. })
  420. .catch(error => {
  421. this.addDomainLoading = false
  422. if (error.response.status == 422) {
  423. this.error(error.response.data.errors.domain[0])
  424. } else {
  425. this.error()
  426. }
  427. })
  428. },
  429. openDeleteModal(id) {
  430. this.deleteDomainModalOpen = true
  431. this.domainIdToDelete = id
  432. },
  433. closeDeleteModal() {
  434. this.deleteDomainModalOpen = false
  435. this.domainIdToDelete = null
  436. },
  437. editDomain(domain) {
  438. if (this.domainDescriptionToEdit.length > 100) {
  439. return this.error('Description cannot be more than 100 characters')
  440. }
  441. axios
  442. .patch(
  443. `/domains/${domain.id}`,
  444. JSON.stringify({
  445. description: this.domainDescriptionToEdit,
  446. }),
  447. {
  448. headers: { 'Content-Type': 'application/json' },
  449. }
  450. )
  451. .then(response => {
  452. domain.description = this.domainDescriptionToEdit
  453. this.domainIdToEdit = ''
  454. this.domainDescriptionToEdit = ''
  455. this.success('Domain description updated')
  456. })
  457. .catch(error => {
  458. this.domainIdToEdit = ''
  459. this.domainDescriptionToEdit = ''
  460. this.error()
  461. })
  462. },
  463. activateDomain(domain) {
  464. axios
  465. .post(
  466. `/active-domains`,
  467. JSON.stringify({
  468. id: domain.id,
  469. }),
  470. {
  471. headers: { 'Content-Type': 'application/json' },
  472. }
  473. )
  474. .then(response => {
  475. //
  476. })
  477. .catch(error => {
  478. this.error()
  479. })
  480. },
  481. deactivateDomain(domain) {
  482. axios
  483. .delete(`/active-domains/${domain.id}`)
  484. .then(response => {
  485. //
  486. })
  487. .catch(error => {
  488. this.error()
  489. })
  490. },
  491. deleteDomain(id) {
  492. this.deleteDomainLoading = true
  493. axios
  494. .delete(`/domains/${id}`)
  495. .then(response => {
  496. this.domains = _.filter(this.domains, domain => domain.id !== id)
  497. this.deleteDomainModalOpen = false
  498. this.deleteDomainLoading = false
  499. })
  500. .catch(error => {
  501. this.error()
  502. this.deleteDomainLoading = false
  503. this.deleteDomainModalOpen = false
  504. })
  505. },
  506. sort(col, dir) {
  507. if (this.currentSort === col && this.currentSortDir === dir) {
  508. this.currentSort = 'created_at'
  509. this.currentSortDir = 'desc'
  510. } else {
  511. this.currentSort = col
  512. this.currentSortDir = dir
  513. }
  514. this.reSort()
  515. },
  516. reSort() {
  517. this.domains = _.orderBy(this.domains, [this.currentSort], [this.currentSortDir])
  518. },
  519. validDomain(domain) {
  520. let re = /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/
  521. return re.test(domain)
  522. },
  523. clipboardSuccess() {
  524. this.success('Copied to clipboard')
  525. },
  526. clipboardError() {
  527. this.error('Could not copy to clipboard')
  528. },
  529. success(text = '') {
  530. this.$notify({
  531. title: 'Success',
  532. text: text,
  533. type: 'success',
  534. })
  535. },
  536. error(text = 'An error has occurred, please try again later') {
  537. this.$notify({
  538. title: 'Error',
  539. text: text,
  540. type: 'error',
  541. })
  542. },
  543. },
  544. }
  545. </script>