PersonalAccessTokens.vue 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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 generate new aliases. Simply paste the token generated below into the browser extension to
  31. get started. Your API access tokens are secret and should be treated like your password. For
  32. more information please see the <a href="/docs" class="text-indigo-700">API documentation</a>.
  33. </p>
  34. <button
  35. @click="openCreateTokenModal"
  36. class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  37. >
  38. Generate New Token
  39. </button>
  40. <div class="mt-6">
  41. <h3 class="font-bold text-xl">Personal Access Tokens</h3>
  42. <div class="my-4 w-24 border-b-2 border-grey-200"></div>
  43. <p class="my-6">
  44. Tokens you have generated that can be used to access the API. To revoke an access token
  45. simply click the delete button next to it.
  46. </p>
  47. <div>
  48. <p class="mb-0" v-if="tokens.length === 0">
  49. You have not created any personal access tokens.
  50. </p>
  51. <div class="table w-full text-sm md:text-base" v-if="tokens.length > 0">
  52. <div class="table-row">
  53. <div class="table-cell p-1 md:p-4 font-semibold">Name</div>
  54. <div class="table-cell p-1 md:p-4 font-semibold">Created</div>
  55. <div class="table-cell p-1 md:p-4 font-semibold">Expires</div>
  56. <div class="table-cell p-1 md:p-4"></div>
  57. </div>
  58. <div
  59. v-for="token in tokens"
  60. :key="token.id"
  61. class="table-row even:bg-grey-50 odd:bg-white"
  62. >
  63. <div class="table-cell p-1 md:p-4">{{ token.name }}</div>
  64. <div class="table-cell p-1 md:p-4">{{ token.created_at | timeAgo }}</div>
  65. <div class="table-cell p-1 md:p-4">{{ token.expires_at | timeAgo }}</div>
  66. <div class="table-cell p-1 md:p-4 text-right">
  67. <a
  68. class="text-red-500 font-bold cursor-pointer focus:outline-none"
  69. @click="showRevokeModal(token)"
  70. >
  71. Delete
  72. </a>
  73. </div>
  74. </div>
  75. </div>
  76. </div>
  77. </div>
  78. <Modal :open="createTokenModalOpen" @close="closeCreateTokenModal">
  79. <div v-if="!accessToken" class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  80. <h2
  81. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  82. >
  83. Create New Token
  84. </h2>
  85. <p class="mt-4 text-grey-700">
  86. What's this token going to be used for? Give it a short name so that you remember later.
  87. </p>
  88. <div class="mt-6">
  89. <div v-if="form.errors.length > 0" class="mb-3 text-red-500">
  90. <ul>
  91. <li v-for="error in form.errors" :key="error">
  92. {{ error }}
  93. </li>
  94. </ul>
  95. </div>
  96. <label for="create-token-name" class="block text-grey-700 text-sm my-2"> Name: </label>
  97. <input
  98. v-model="form.name"
  99. type="text"
  100. id="create-token-name"
  101. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-6"
  102. :class="form.errors.length > 0 ? 'border-red-500' : ''"
  103. placeholder="e.g. Firefox extension"
  104. autofocus
  105. />
  106. <button
  107. @click="store"
  108. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  109. :class="loading ? 'cursor-not-allowed' : ''"
  110. :disabled="loading"
  111. >
  112. Generate Token
  113. <loader v-if="loading" />
  114. </button>
  115. <button
  116. @click="closeCreateTokenModal"
  117. 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"
  118. >
  119. Close
  120. </button>
  121. </div>
  122. </div>
  123. <div v-else class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  124. <h2
  125. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  126. >
  127. Personal Access Token
  128. </h2>
  129. <p class="my-4 text-grey-700">
  130. This is your new personal access token. This is the only time the token will ever be
  131. displayed, so please make a note of it in a safe place (e.g. password manager)!
  132. </p>
  133. <textarea
  134. v-model="accessToken"
  135. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 text-sm"
  136. rows="10"
  137. readonly
  138. >
  139. </textarea>
  140. <div class="mt-6">
  141. <button
  142. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  143. v-clipboard="() => accessToken"
  144. v-clipboard:success="clipboardSuccess"
  145. v-clipboard:error="clipboardError"
  146. >
  147. Copy To Clipboard
  148. </button>
  149. <button
  150. @click="closeCreateTokenModal"
  151. 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"
  152. >
  153. Close
  154. </button>
  155. </div>
  156. </div>
  157. </Modal>
  158. <Modal :open="revokeTokenModalOpen" @close="closeRevokeTokenModal">
  159. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  160. <h2
  161. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  162. >
  163. Revoke API Access Token
  164. </h2>
  165. <p class="my-4 text-grey-700">
  166. Any browser extension, application or script using this API access token will no longer be
  167. able to access the API. This action cannot be undone.
  168. </p>
  169. <div class="mt-6">
  170. <button
  171. @click="revoke"
  172. class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus:outline-none"
  173. :class="revokeTokenLoading ? 'cursor-not-allowed' : ''"
  174. :disabled="revokeTokenLoading"
  175. >
  176. Revoke Token
  177. <loader v-if="revokeTokenLoading" />
  178. </button>
  179. <button
  180. @click="closeRevokeTokenModal"
  181. 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"
  182. >
  183. Close
  184. </button>
  185. </div>
  186. </div>
  187. </Modal>
  188. </div>
  189. </template>
  190. <script>
  191. import Modal from './../Modal.vue'
  192. export default {
  193. components: {
  194. Modal,
  195. },
  196. data() {
  197. return {
  198. accessToken: null,
  199. createTokenModalOpen: false,
  200. revokeTokenModalOpen: false,
  201. tokens: [],
  202. tokenToRevoke: null,
  203. form: {
  204. name: '',
  205. errors: [],
  206. },
  207. loading: false,
  208. revokeTokenLoading: false,
  209. }
  210. },
  211. mounted() {
  212. this.getTokens()
  213. },
  214. methods: {
  215. getTokens() {
  216. axios.get('/oauth/personal-access-tokens').then(response => {
  217. this.tokens = response.data
  218. })
  219. },
  220. store() {
  221. this.loading = true
  222. this.accessToken = null
  223. this.form.errors = []
  224. axios
  225. .post('/oauth/personal-access-tokens', this.form)
  226. .then(response => {
  227. this.loading = false
  228. this.form.name = ''
  229. this.form.errors = []
  230. this.tokens.push(response.data.token)
  231. this.accessToken = response.data.accessToken
  232. })
  233. .catch(error => {
  234. this.loading = false
  235. if (typeof error.response.data === 'object') {
  236. this.form.errors = _.flatten(_.toArray(error.response.data.errors))
  237. } else {
  238. this.error()
  239. }
  240. })
  241. },
  242. showRevokeModal(token) {
  243. this.tokenToRevoke = token
  244. this.revokeTokenModalOpen = true
  245. },
  246. revoke() {
  247. this.revokeTokenLoading = true
  248. axios.delete(`/oauth/personal-access-tokens/${this.tokenToRevoke.id}`).then(response => {
  249. this.revokeTokenLoading = false
  250. this.revokeTokenModalOpen = false
  251. this.tokenToRevoke = null
  252. this.getTokens()
  253. })
  254. },
  255. openCreateTokenModal() {
  256. this.accessToken = null
  257. this.createTokenModalOpen = true
  258. },
  259. closeCreateTokenModal() {
  260. this.createTokenModalOpen = false
  261. },
  262. closeRevokeTokenModal() {
  263. this.revokeTokenModalOpen = false
  264. },
  265. clipboardSuccess() {
  266. this.success('Copied to clipboard')
  267. },
  268. clipboardError() {
  269. this.error('Could not copy to clipboard')
  270. },
  271. success(text = '') {
  272. this.$notify({
  273. title: 'Success',
  274. text: text,
  275. type: 'success',
  276. })
  277. },
  278. error(text = 'An error has occurred, please try again later') {
  279. this.$notify({
  280. title: 'Error',
  281. text: text,
  282. type: 'error',
  283. })
  284. },
  285. },
  286. }
  287. </script>