PersonalAccessTokens.vue 9.7 KB

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