344 lines
9.4 KiB
JavaScript
344 lines
9.4 KiB
JavaScript
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||
|
||
import { ElMessageBox, ElNotification } from 'element-plus'
|
||
|
||
import { http } from '@/utils/axios'
|
||
|
||
export function usePage() {
|
||
const loading = ref(false)
|
||
const clientList = ref([])
|
||
const selectedClientId = ref(null)
|
||
const currentDomain = ref('')
|
||
|
||
// 解密后的各客户端授权码明文 { [clientId]: 'xxxx-yyyy-...' }
|
||
const revealedCodes = reactive({})
|
||
const codesRevealed = ref(false)
|
||
const revealCountdown = ref(0)
|
||
let countdownTimer = null
|
||
|
||
// 新增客户端弹窗
|
||
const createDialogVisible = ref(false)
|
||
const newClientName = ref('')
|
||
const createLoading = ref(false)
|
||
|
||
// 密码验证查看弹窗
|
||
const verifyDialogVisible = ref(false)
|
||
const verifyPassword = ref('')
|
||
const verifyLoading = ref(false)
|
||
|
||
const currentClient = computed(() => {
|
||
if (!selectedClientId.value || !Array.isArray(clientList.value)) return null
|
||
return clientList.value.find(c => c.id === selectedClientId.value) || null
|
||
})
|
||
|
||
const notify = (message, type = 'error') => {
|
||
ElNotification({
|
||
title: type === 'success' ? '操作成功' : (type === 'info' ? '提示' : '操作失败'),
|
||
message,
|
||
type,
|
||
})
|
||
}
|
||
|
||
const clearCountdownTimer = () => {
|
||
if (countdownTimer) {
|
||
clearInterval(countdownTimer)
|
||
countdownTimer = null
|
||
}
|
||
}
|
||
|
||
const hideCodesAutomatically = (silent = false) => {
|
||
clearCountdownTimer()
|
||
revealCountdown.value = 0
|
||
Object.keys(revealedCodes).forEach(k => delete revealedCodes[k])
|
||
codesRevealed.value = false
|
||
if (!silent) {
|
||
notify('授权码明文已达到 60 秒安全保护期,已自动隐藏', 'info')
|
||
}
|
||
}
|
||
|
||
const startRevealCountdown = (seconds = 60) => {
|
||
clearCountdownTimer()
|
||
revealCountdown.value = seconds
|
||
codesRevealed.value = true
|
||
|
||
countdownTimer = setInterval(() => {
|
||
revealCountdown.value -= 1
|
||
if (revealCountdown.value <= 0) {
|
||
hideCodesAutomatically(false)
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
const formatTime = (timeStr) => {
|
||
if (!timeStr) return '--'
|
||
try {
|
||
const d = new Date(timeStr)
|
||
if (isNaN(d.getTime())) return timeStr
|
||
return d.toLocaleDateString('zh-CN', {
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})
|
||
} catch {
|
||
return timeStr
|
||
}
|
||
}
|
||
|
||
const loadBaseInfo = async () => {
|
||
try {
|
||
const infoRes = await http.get('/api/v1/user/info')
|
||
if (infoRes?.data?.email && infoRes.data.email.includes('@')) {
|
||
currentDomain.value = infoRes.data.email.split('@')[1]
|
||
}
|
||
} catch {
|
||
// 忽略
|
||
}
|
||
}
|
||
|
||
const loadClientCredentials = async () => {
|
||
loading.value = true
|
||
try {
|
||
const response = await http.get('/api/v1/security/credentials/clients')
|
||
if (response.errorNo !== 0) {
|
||
throw new Error(response.errorMsg || '读取客户端授权码列表失败')
|
||
}
|
||
const list = Array.isArray(response.data) ? response.data : []
|
||
clientList.value = list
|
||
if (list.length > 0) {
|
||
if (!selectedClientId.value || !list.some(c => c.id === selectedClientId.value)) {
|
||
selectedClientId.value = list[0].id
|
||
}
|
||
} else {
|
||
selectedClientId.value = null
|
||
}
|
||
} catch (error) {
|
||
notify(error?.message || '读取客户端授权码列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const handleClientSelect = (val) => {
|
||
selectedClientId.value = val
|
||
}
|
||
|
||
const openCreateDialog = () => {
|
||
newClientName.value = ''
|
||
createDialogVisible.value = true
|
||
}
|
||
|
||
const closeCreateDialog = () => {
|
||
createDialogVisible.value = false
|
||
newClientName.value = ''
|
||
}
|
||
|
||
const submitCreateClient = async () => {
|
||
const name = newClientName.value.trim()
|
||
if (!name) {
|
||
notify('请输入客户端名称')
|
||
return
|
||
}
|
||
createLoading.value = true
|
||
try {
|
||
const response = await http.post('/api/v1/security/credentials/clients/create', {
|
||
client_name: name,
|
||
})
|
||
if (response.errorNo !== 0) {
|
||
throw new Error(response.errorMsg || '创建客户端专属授权码失败')
|
||
}
|
||
const code = response.data?.code || ''
|
||
const item = response.data?.client
|
||
closeCreateDialog()
|
||
await loadClientCredentials()
|
||
|
||
if (item && item.id) {
|
||
selectedClientId.value = item.id
|
||
revealedCodes[item.id] = code
|
||
startRevealCountdown(60)
|
||
}
|
||
notify(`客户端「${name}」专属授权码已创建并就绪(60秒内有效显示)`, 'success')
|
||
} catch (error) {
|
||
notify(error?.message || '创建客户端授权码失败')
|
||
} finally {
|
||
createLoading.value = false
|
||
}
|
||
}
|
||
|
||
const regenerateClientCode = async (client) => {
|
||
if (!client) return
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定重新生成客户端「${client.client_name}」的专属授权码吗?旧授权码将立即失效。`,
|
||
'安全提示',
|
||
{
|
||
confirmButtonText: '确定重新生成',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
|
||
loading.value = true
|
||
try {
|
||
const response = await http.post('/api/v1/security/credentials/clients/regenerate', {
|
||
client_id: client.id,
|
||
})
|
||
if (response.errorNo !== 0) {
|
||
throw new Error(response.errorMsg || '重新生成客户端授权码失败')
|
||
}
|
||
const newCode = response.data?.code || ''
|
||
revealedCodes[client.id] = newCode
|
||
startRevealCountdown(60)
|
||
notify(`客户端「${client.client_name}」专属授权码已重新生成(60秒内有效显示)`, 'success')
|
||
await loadClientCredentials()
|
||
} catch (error) {
|
||
notify(error?.message || '重新生成失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const revokeClientCode = async (client) => {
|
||
if (!client) return
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定撤销并删除客户端「${client.client_name}」吗?撤销后该设备将无法通过授权码登录收发信。`,
|
||
'安全提示',
|
||
{
|
||
confirmButtonText: '确定撤销删除',
|
||
cancelButtonText: '取消',
|
||
type: 'danger',
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
|
||
loading.value = true
|
||
try {
|
||
const response = await http.post('/api/v1/security/credentials/clients/revoke', {
|
||
client_id: client.id,
|
||
})
|
||
if (response.errorNo !== 0) {
|
||
throw new Error(response.errorMsg || '撤销客户端失败')
|
||
}
|
||
delete revealedCodes[client.id]
|
||
notify(`客户端「${client.client_name}」已成功撤销`, 'success')
|
||
await loadClientCredentials()
|
||
} catch (error) {
|
||
notify(error?.message || '撤销失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const copyText = async (text, label = '内容') => {
|
||
if (!text) return
|
||
try {
|
||
if (navigator?.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(text)
|
||
} else {
|
||
const input = document.createElement('input')
|
||
input.value = text
|
||
document.body.appendChild(input)
|
||
input.select()
|
||
document.execCommand('copy')
|
||
document.body.removeChild(input)
|
||
}
|
||
notify(`${label}已复制到剪贴板`, 'success')
|
||
} catch {
|
||
notify('复制失败,请手动选择复制')
|
||
}
|
||
}
|
||
|
||
const copyCode = async (codeText) => {
|
||
if (!codeText) {
|
||
notify('未找到可复制的授权码')
|
||
return
|
||
}
|
||
await copyText(codeText, '专属授权码')
|
||
}
|
||
|
||
const openVerifyDialog = () => {
|
||
verifyPassword.value = ''
|
||
verifyDialogVisible.value = true
|
||
}
|
||
|
||
const closeVerifyDialog = () => {
|
||
verifyDialogVisible.value = false
|
||
verifyPassword.value = ''
|
||
}
|
||
|
||
const submitVerifyAndReveal = async () => {
|
||
const password = verifyPassword.value.trim()
|
||
if (!password) {
|
||
notify('请输入当前账号登录密码')
|
||
return
|
||
}
|
||
verifyLoading.value = true
|
||
try {
|
||
const response = await http.post('/api/v1/security/credentials/clients/reveal', {
|
||
password,
|
||
})
|
||
if (response.errorNo !== 0) {
|
||
throw new Error(response.errorMsg || '密码验证失败,无法查看授权码')
|
||
}
|
||
const map = response.data || {}
|
||
Object.keys(revealedCodes).forEach(k => delete revealedCodes[k])
|
||
Object.assign(revealedCodes, map)
|
||
closeVerifyDialog()
|
||
const count = Object.keys(map).length
|
||
if (count > 0) {
|
||
startRevealCountdown(60)
|
||
notify('安全验证通过,授权码将在 60 秒内明文显示', 'success')
|
||
} else {
|
||
notify('安全验证通过,当前未发现加密密文,可点击重新生成', 'warning')
|
||
}
|
||
} catch (error) {
|
||
notify(error?.message || '验证失败,请检查密码是否正确')
|
||
} finally {
|
||
verifyLoading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadBaseInfo()
|
||
loadClientCredentials()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
clearCountdownTimer()
|
||
})
|
||
|
||
return {
|
||
loading,
|
||
clientList,
|
||
selectedClientId,
|
||
currentClient,
|
||
currentDomain,
|
||
revealedCodes,
|
||
codesRevealed,
|
||
revealCountdown,
|
||
createDialogVisible,
|
||
newClientName,
|
||
createLoading,
|
||
verifyDialogVisible,
|
||
verifyPassword,
|
||
verifyLoading,
|
||
handleClientSelect,
|
||
openCreateDialog,
|
||
closeCreateDialog,
|
||
submitCreateClient,
|
||
regenerateClientCode,
|
||
revokeClientCode,
|
||
copyText,
|
||
copyCode,
|
||
formatTime,
|
||
openVerifyDialog,
|
||
closeVerifyDialog,
|
||
submitVerifyAndReveal,
|
||
}
|
||
} |