PersonalAccessTokens.vue 11 KB

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