Passkey.vue 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. <script setup lang="ts">
  2. import type { Passkey } from '@/api/passkey'
  3. import passkey from '@/api/passkey'
  4. import ReactiveFromNow from '@/components/ReactiveFromNow/ReactiveFromNow.vue'
  5. import { formatDateTime } from '@/lib/helper'
  6. import { useUserStore } from '@/pinia'
  7. import AddPasskey from '@/views/preference/components/AddPasskey.vue'
  8. import { DeleteOutlined, EditOutlined, KeyOutlined } from '@ant-design/icons-vue'
  9. import { message } from 'ant-design-vue'
  10. import dayjs from 'dayjs'
  11. import relativeTime from 'dayjs/plugin/relativeTime'
  12. dayjs.extend(relativeTime)
  13. const user = useUserStore()
  14. const getListLoading = ref(true)
  15. const data = ref([]) as Ref<Passkey[]>
  16. const passkeyName = ref('')
  17. function getList() {
  18. getListLoading.value = true
  19. passkey.get_list().then(r => {
  20. data.value = r
  21. }).finally(() => {
  22. getListLoading.value = false
  23. })
  24. }
  25. onMounted(() => {
  26. getList()
  27. })
  28. const modifyIdx = ref(-1)
  29. function update(id: number, record: Passkey) {
  30. passkey.update(id, record).then(() => {
  31. getList()
  32. modifyIdx.value = -1
  33. message.success($gettext('Update successfully'))
  34. })
  35. }
  36. function remove(item: Passkey) {
  37. passkey.remove(item.id).then(() => {
  38. getList()
  39. message.success($gettext('Remove successfully'))
  40. // if current passkey is removed, clear it from user store
  41. if (user.passkeyLoginAvailable && user.passkeyRawId === item.raw_id)
  42. user.passkeyRawId = ''
  43. })
  44. }
  45. </script>
  46. <template>
  47. <div>
  48. <div>
  49. <h3>
  50. {{ $gettext('Passkey') }}
  51. </h3>
  52. <p>
  53. {{ $gettext('Passkeys are webauthn credentials that validate your identity using touch, '
  54. + 'facial recognition, a device password, or a PIN. '
  55. + 'They can be used as a password replacement or as a 2FA method.') }}
  56. </p>
  57. </div>
  58. <AList
  59. class="mt-4"
  60. bordered
  61. :data-source="data"
  62. >
  63. <template #header>
  64. <div class="flex items-center justify-between">
  65. <div class="font-bold">
  66. {{ $gettext('Your passkeys') }}
  67. </div>
  68. <AddPasskey @created="() => getList()" />
  69. </div>
  70. </template>
  71. <template #renderItem="{ item, index }">
  72. <AListItem>
  73. <AListItemMeta>
  74. <template #title>
  75. <div class="flex gap-2">
  76. <KeyOutlined />
  77. <div v-if="index !== modifyIdx">
  78. {{ item.name }}
  79. </div>
  80. <div v-else>
  81. <AInput v-model:value="passkeyName" />
  82. </div>
  83. </div>
  84. </template>
  85. <template #description>
  86. {{ $gettext('Created at') }}: {{ formatDateTime(item.created_at) }} · {{
  87. $gettext('Last used at') }}: <ReactiveFromNow :time="item.last_used_at" />
  88. </template>
  89. </AListItemMeta>
  90. <template #extra>
  91. <div v-if="modifyIdx !== index">
  92. <AButton
  93. type="link"
  94. size="small"
  95. @click="() => {
  96. modifyIdx = index
  97. passkeyName = item.name
  98. }"
  99. >
  100. <EditOutlined />
  101. </AButton>
  102. <APopconfirm
  103. :title="$gettext('Are you sure to delete this passkey immediately?')"
  104. @confirm="() => remove(item)"
  105. >
  106. <AButton
  107. type="link"
  108. danger
  109. size="small"
  110. >
  111. <DeleteOutlined />
  112. </AButton>
  113. </APopconfirm>
  114. </div>
  115. <div v-else>
  116. <AButton
  117. size="small"
  118. @click="() => update(item.id, { ...item, name: passkeyName })"
  119. >
  120. {{ $gettext('Save') }}
  121. </AButton>
  122. <AButton
  123. type="link"
  124. size="small"
  125. @click="() => {
  126. modifyIdx = -1
  127. passkeyName = item.name
  128. }"
  129. >
  130. {{ $gettext('Cancel') }}
  131. </AButton>
  132. </div>
  133. </template>
  134. </AListItem>
  135. </template>
  136. </AList>
  137. </div>
  138. </template>
  139. <style scoped lang="less">
  140. </style>