PersonalAccessTokens.vue 9.6 KB

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