PersonalAccessTokens.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. <template>
  2. <div>
  3. <h3 class="font-bold text-xl">Information</h3>
  4. <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
  5. <p class="my-6">
  6. Your API access tokens can be used with the
  7. <a
  8. href="https://github.com/anonaddy/browser-extension"
  9. target="_blank"
  10. rel="nofollow noopener noreferrer"
  11. class="text-indigo-700"
  12. >open-source</a
  13. >
  14. browser extension on
  15. <a
  16. href="https://addons.mozilla.org/en-GB/firefox/addon/anonaddy/"
  17. target="_blank"
  18. rel="nofollow noopener noreferrer"
  19. class="text-indigo-700"
  20. >Firefox</a
  21. >
  22. or
  23. <a
  24. href="https://chrome.google.com/webstore/detail/anonaddy/iadbdpnoknmbdeolbapdackdcogdmjpe"
  25. target="_blank"
  26. rel="nofollow noopener noreferrer"
  27. class="text-indigo-700"
  28. >Chrome / Brave</a
  29. >
  30. to create new aliases. They can also be used with the mobile apps. Simply paste a token you've
  31. created into the browser extension or mobile apps to get started. Your API access tokens are
  32. secret and should be treated like your password. For more information please see the
  33. <a
  34. href="https://app.anonaddy.com/docs"
  35. target="_blank"
  36. rel="nofollow noopener noreferrer"
  37. class="text-indigo-700"
  38. >API documentation</a
  39. >.
  40. </p>
  41. <button
  42. @click="openCreateTokenModal"
  43. class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  44. >
  45. Create New Token
  46. </button>
  47. <div class="mt-6">
  48. <h3 class="font-bold text-xl">Personal Access Tokens</h3>
  49. <div class="my-4 w-24 border-b-2 border-grey-200"></div>
  50. <p class="my-6">
  51. Tokens you have created that can be used to access the API. To revoke an access token simply
  52. click the delete button next to it.
  53. </p>
  54. <div>
  55. <p class="mb-0" v-if="tokens.length === 0">
  56. You have not created any personal access tokens.
  57. </p>
  58. <div class="table w-full text-sm md:text-base" v-if="tokens.length > 0">
  59. <div class="table-row">
  60. <div class="table-cell p-1 md:p-4 font-semibold">Name</div>
  61. <div class="table-cell p-1 md:p-4 font-semibold">Created</div>
  62. <div class="table-cell p-1 md:p-4 font-semibold">Last Used</div>
  63. <div class="table-cell p-1 md:p-4 font-semibold">Expires At</div>
  64. <div class="table-cell p-1 md:p-4"></div>
  65. </div>
  66. <div
  67. v-for="token in tokens"
  68. :key="token.id"
  69. class="table-row even:bg-grey-50 odd:bg-white"
  70. >
  71. <div class="table-cell p-1 md:p-4">{{ token.name }}</div>
  72. <div class="table-cell p-1 md:p-4">{{ $filters.timeAgo(token.created_at) }}</div>
  73. <div v-if="token.last_used_at" class="table-cell p-1 md:p-4">
  74. {{ $filters.timeAgo(token.last_used_at) }}
  75. </div>
  76. <div v-else class="table-cell p-1 md:p-4">Not used yet</div>
  77. <div v-if="token.expires_at" class="table-cell p-1 md:p-4">
  78. {{ $filters.formatDate(token.expires_at) }}
  79. </div>
  80. <div v-else class="table-cell p-1 md:p-4">Does not expire</div>
  81. <div class="table-cell p-1 md:p-4 text-right">
  82. <a
  83. class="text-red-500 font-bold cursor-pointer focus:outline-none"
  84. @click="showRevokeModal(token)"
  85. >
  86. Delete
  87. </a>
  88. </div>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. <Modal :open="createTokenModalOpen" @close="closeCreateTokenModal">
  94. <template v-if="!accessToken" v-slot:title> Create New Token </template>
  95. <template v-else v-slot:title> Personal Access Token </template>
  96. <template v-slot:content>
  97. <div v-show="!accessToken">
  98. <p class="mt-4 text-grey-700">
  99. What's this token going to be used for? Give it a short name so that you remember later.
  100. You can also select an expiry date for the token if you wish.
  101. </p>
  102. <div class="mt-6">
  103. <div v-if="isObject(form.errors)" class="mb-3 text-red-500">
  104. <ul>
  105. <li v-for="error in form.errors" :key="error[0]">
  106. {{ error[0] }}
  107. </li>
  108. </ul>
  109. </div>
  110. <label for="create-token-name" class="block text-grey-700 text-sm my-2"> Name: </label>
  111. <input
  112. v-model="form.name"
  113. type="text"
  114. id="create-token-name"
  115. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-4"
  116. :class="form.errors.name ? 'border-red-500' : ''"
  117. placeholder="e.g. Firefox extension"
  118. autofocus
  119. />
  120. <label for="create-token-name" class="block text-grey-700 text-sm my-2">
  121. Expiration:
  122. </label>
  123. <div class="block relative mb-6">
  124. <select
  125. v-model="form.expiration"
  126. class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:ring"
  127. :class="form.errors.expiration ? 'border border-red-500' : ''"
  128. >
  129. <option value="day">1 day</option>
  130. <option value="week">1 week</option>
  131. <option value="month">1 month</option>
  132. <option value="year">1 year</option>
  133. <option :value="null">No expiration</option>
  134. </select>
  135. <div
  136. class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
  137. >
  138. <svg
  139. class="fill-current h-4 w-4"
  140. xmlns="http://www.w3.org/2000/svg"
  141. viewBox="0 0 20 20"
  142. >
  143. <path
  144. d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
  145. />
  146. </svg>
  147. </div>
  148. </div>
  149. <button
  150. @click="store"
  151. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  152. :class="loading ? 'cursor-not-allowed' : ''"
  153. :disabled="loading"
  154. >
  155. Create Token
  156. <loader v-if="loading" />
  157. </button>
  158. <button
  159. @click="closeCreateTokenModal"
  160. 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"
  161. >
  162. Close
  163. </button>
  164. </div>
  165. </div>
  166. <div v-show="accessToken">
  167. <p class="my-4 text-grey-700">
  168. This is your new personal access token. This is the only time the token will ever be
  169. displayed, so please make a note of it in a safe place (e.g. password manager)!
  170. </p>
  171. <textarea
  172. v-model="accessToken"
  173. @click="selectTokenTextArea"
  174. id="token-text-area"
  175. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 text-md break-all"
  176. rows="1"
  177. readonly
  178. >
  179. </textarea>
  180. <div class="text-center">
  181. <img :src="qrCode" class="inline-block" alt="QR Code" />
  182. <p class="text-left text-sm text-grey-700">
  183. You can scan this QR code to automatically login to the AnonAddy for Android mobile
  184. app.
  185. </p>
  186. </div>
  187. <div class="mt-6">
  188. <button
  189. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  190. v-clipboard="() => accessToken"
  191. v-clipboard:success="clipboardSuccess"
  192. v-clipboard:error="clipboardError"
  193. >
  194. Copy To Clipboard
  195. </button>
  196. <button
  197. @click="closeCreateTokenModal"
  198. 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"
  199. >
  200. Close
  201. </button>
  202. </div>
  203. </div>
  204. </template>
  205. </Modal>
  206. <Modal :open="revokeTokenModalOpen" @close="closeRevokeTokenModal">
  207. <template v-slot:title> ARevoke API Access Token </template>
  208. <template v-slot:content>
  209. <p class="my-4 text-grey-700">
  210. Any browser extension, application or script using this API access token will no longer be
  211. able to access the API. This action cannot be undone.
  212. </p>
  213. <div class="mt-6">
  214. <button
  215. @click="revoke"
  216. class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus:outline-none"
  217. :class="revokeTokenLoading ? 'cursor-not-allowed' : ''"
  218. :disabled="revokeTokenLoading"
  219. >
  220. Revoke Token
  221. <loader v-if="revokeTokenLoading" />
  222. </button>
  223. <button
  224. @click="closeRevokeTokenModal"
  225. 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"
  226. >
  227. Close
  228. </button>
  229. </div>
  230. </template>
  231. </Modal>
  232. </div>
  233. </template>
  234. <script>
  235. import Modal from './../Modal.vue'
  236. export default {
  237. components: {
  238. Modal,
  239. },
  240. data() {
  241. return {
  242. accessToken: null,
  243. qrCode: null,
  244. createTokenModalOpen: false,
  245. revokeTokenModalOpen: false,
  246. tokens: [],
  247. tokenToRevoke: null,
  248. form: {
  249. name: '',
  250. expiration: null,
  251. errors: {},
  252. },
  253. loading: false,
  254. revokeTokenLoading: false,
  255. }
  256. },
  257. mounted() {
  258. this.getTokens()
  259. },
  260. watch: {
  261. 'form.expiration'() {
  262. delete this.form.errors.expiration
  263. },
  264. },
  265. methods: {
  266. getTokens() {
  267. axios.get('/settings/personal-access-tokens').then(response => {
  268. this.tokens = response.data.data
  269. })
  270. },
  271. store() {
  272. this.loading = true
  273. this.accessToken = null
  274. this.qrCode = null
  275. this.form.errors = {}
  276. axios
  277. .post('/settings/personal-access-tokens', this.form)
  278. .then(response => {
  279. this.loading = false
  280. this.form.name = ''
  281. this.form.expiration = null
  282. this.form.errors = {}
  283. this.tokens.push(response.data.token)
  284. this.accessToken = response.data.accessToken
  285. this.qrCode = response.data.qrCode
  286. })
  287. .catch(error => {
  288. this.loading = false
  289. if (this.isObject(error.response.data)) {
  290. this.form.errors = error.response.data.errors
  291. } else {
  292. this.error()
  293. }
  294. })
  295. },
  296. showRevokeModal(token) {
  297. this.tokenToRevoke = token
  298. this.revokeTokenModalOpen = true
  299. },
  300. revoke() {
  301. this.revokeTokenLoading = true
  302. axios
  303. .delete(`/settings/personal-access-tokens/${this.tokenToRevoke.id}`)
  304. .then(response => {
  305. this.revokeTokenLoading = false
  306. this.revokeTokenModalOpen = false
  307. this.tokenToRevoke = null
  308. this.getTokens()
  309. })
  310. .catch(error => {
  311. this.revokeTokenLoading = false
  312. this.revokeTokenModalOpen = false
  313. this.error()
  314. })
  315. },
  316. openCreateTokenModal() {
  317. this.accessToken = null
  318. this.qrCode = null
  319. this.createTokenModalOpen = true
  320. },
  321. closeCreateTokenModal() {
  322. this.createTokenModalOpen = false
  323. },
  324. closeRevokeTokenModal() {
  325. this.revokeTokenModalOpen = false
  326. },
  327. selectTokenTextArea() {
  328. let textArea = document.getElementById('token-text-area')
  329. textArea.focus()
  330. textArea.select()
  331. },
  332. isObject(val) {
  333. return _.isObject(val) && !_.isEmpty(val)
  334. },
  335. clipboardSuccess() {
  336. this.success('Copied to clipboard')
  337. },
  338. clipboardError() {
  339. this.error('Could not copy to clipboard')
  340. },
  341. success(text = '') {
  342. this.$notify({
  343. title: 'Success',
  344. text: text,
  345. type: 'success',
  346. })
  347. },
  348. error(text = 'An error has occurred, please try again later') {
  349. this.$notify({
  350. title: 'Error',
  351. text: text,
  352. type: 'error',
  353. })
  354. },
  355. },
  356. }
  357. </script>