Refactor API client to remove password handling in token revocation and authorization methods; update UI components to eliminate password input fields and related logic; streamline device management and password management functionalities.

This commit is contained in:
Sunwuyuan 2025-11-16 14:46:17 +08:00
parent 95dedb6e29
commit d9d62e8f8c
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
10 changed files with 488 additions and 1276 deletions

View File

@ -24,7 +24,7 @@ const props = defineProps({
},
description: {
type: String,
default: '请输入设备 UUID 和密码'
default: '请输入设备 UUID'
},
closable: {
type: Boolean,
@ -35,9 +35,9 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue', 'success'])
const isLoading = ref(false)
const showPassword = ref(false)
const deviceUuid = ref('')
const devicePassword = ref('')
// UUID
watch(() => props.modelValue, (isOpen) => {
@ -62,21 +62,17 @@ const handleAuth = async () => {
toast.error('请输入设备 UUID')
return
}
if (!devicePassword.value) {
toast.error('请输入设备密码')
return
}
isLoading.value = true
try {
// UUID
const deviceInfo = await apiClient.verifyDevicePassword(deviceUuid.value, devicePassword.value)
// UUID
const deviceInfo = await apiClient.getDeviceInfo(deviceUuid.value)
// deviceStore
deviceStore.setDeviceUuid(deviceUuid.value)
toast.success('认证成功')
emit('success', deviceUuid.value, devicePassword.value, deviceInfo)
emit('success', deviceUuid.value, deviceInfo)
} catch (error) {
toast.error('认证失败:' + error.message)
} finally {
@ -84,10 +80,7 @@ const handleAuth = async () => {
}
}
//
const togglePasswordVisibility = () => {
showPassword.value = !showPassword.value
}
</script>
<template>
@ -110,30 +103,7 @@ const togglePasswordVisibility = () => {
/>
</div>
<!-- 密码输入 -->
<div class="space-y-2">
<Label for="device-password">设备密码 *</Label>
<div class="relative">
<Input
id="device-password"
:type="showPassword ? 'text' : 'password'"
v-model="devicePassword"
placeholder="输入设备密码"
@keyup.enter="handleAuth"
/>
<Button
type="button"
variant="ghost"
size="icon"
class="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
@click="togglePasswordVisibility"
tabindex="-1"
>
<Eye v-if="!showPassword" class="h-4 w-4 text-muted-foreground" />
<EyeOff v-else class="h-4 w-4 text-muted-foreground" />
</Button>
</div>
</div>
</div>
<DialogFooter>

View File

@ -0,0 +1,396 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useAccountStore } from '@/stores/account'
import { deviceStore } from '@/lib/deviceStore'
import { apiClient } from '@/lib/api'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import DropdownMenu from '@/components/ui/dropdown-menu/DropdownMenu.vue'
import DropdownItem from '@/components/ui/dropdown-menu/DropdownItem.vue'
import LoginDialog from '@/components/LoginDialog.vue'
import DeviceRegisterDialog from '@/components/DeviceRegisterDialog.vue'
import {
ChevronDown,
Plus,
Monitor,
Search,
Clock,
User,
Check,
Settings,
Layers
} from 'lucide-vue-next'
import { toast } from 'vue-sonner'
const props = defineProps({
deviceInfo: {
type: Object,
default: null
},
deviceUuid: {
type: String,
default: ''
}
})
const emit = defineEmits(['device-changed'])
const accountStore = useAccountStore()
//
const showDropdown = ref(false)
const showLoginDialog = ref(false)
const showRegisterDialog = ref(false)
const showManualInputDialog = ref(false)
const searchQuery = ref('')
const manualUuid = ref('')
const isLoading = ref(false)
//
const accountDevices = ref([])
const historyDevices = ref([])
//
const currentDevice = computed(() => {
if (props.deviceInfo) {
return {
name: props.deviceInfo.name || props.deviceInfo.deviceName || '未命名设备',
namespace: props.deviceInfo.namespace,
uuid: props.deviceUuid,
isOwned: !!props.deviceInfo.account
}
}
return {
name: '未选择设备',
namespace: props.deviceUuid,
uuid: props.deviceUuid,
isOwned: false
}
})
//
const filteredAccountDevices = computed(() => {
if (!searchQuery.value) return accountDevices.value
const query = searchQuery.value.toLowerCase()
return accountDevices.value.filter(device =>
(device.name || '').toLowerCase().includes(query) ||
device.uuid.toLowerCase().includes(query) ||
(device.namespace || '').toLowerCase().includes(query)
)
})
//
const filteredHistoryDevices = computed(() => {
if (!searchQuery.value) return historyDevices.value
const query = searchQuery.value.toLowerCase()
return historyDevices.value.filter(device =>
(device.name || '').toLowerCase().includes(query) ||
device.uuid.toLowerCase().includes(query)
)
})
//
const loadAccountDevices = async () => {
if (!accountStore.isAuthenticated) return
isLoading.value = true
try {
const response = await apiClient.getAccountDevices()
accountDevices.value = response.data || []
} catch (error) {
console.error('Failed to load account devices:', error)
toast.error('加载设备列表失败')
} finally {
isLoading.value = false
}
}
//
const loadHistoryDevices = () => {
historyDevices.value = deviceStore.getDeviceHistory()
}
//
const switchToDevice = async (device) => {
try {
deviceStore.setDeviceUuid(device.uuid)
deviceStore.addDeviceToHistory({
uuid: device.uuid,
name: device.name || device.deviceName
})
showDropdown.value = false
searchQuery.value = ''
emit('device-changed', device.uuid)
toast.success(`已切换到: ${device.name || device.uuid}`)
} catch (error) {
toast.error('切换设备失败')
}
}
// UUID
const handleManualInput = () => {
const uuid = manualUuid.value.trim()
if (!uuid) {
toast.error('请输入设备UUID')
return
}
if (!/^[0-9a-fA-F-]{8,}$/.test(uuid)) {
toast.error('UUID格式不正确')
return
}
switchToDevice({ uuid, name: '' })
showManualInputDialog.value = false
manualUuid.value = ''
}
//
const handleLoginSuccess = async (token) => {
showLoginDialog.value = false
await accountStore.login(token)
await loadAccountDevices()
toast.success('登录成功')
}
//
const handleDeviceRegistered = () => {
showRegisterDialog.value = false
loadAccountDevices()
loadHistoryDevices()
emit('device-changed')
}
//
watch(showDropdown, (isOpen) => {
if (isOpen) {
loadHistoryDevices()
if (accountStore.isAuthenticated) {
loadAccountDevices()
}
} else {
searchQuery.value = ''
}
})
onMounted(() => {
loadHistoryDevices()
})
</script>
<template>
<div class="relative">
<!-- 设备切换器触发按钮 -->
<DropdownMenu v-model:open="showDropdown">
<template #trigger="{ toggle, open }">
<Button
variant="ghost"
class="h-8 px-3 max-w-[300px] justify-start font-normal hover:bg-accent/50 border border-border"
@click="toggle"
>
<div class="flex items-center gap-2 min-w-0 flex-1">
<Monitor class="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div class="flex flex-col items-start min-w-0 flex-1">
<div class="truncate text-sm font-medium max-w-[180px]">
{{ currentDevice.name }}
</div>
</div>
<div class="flex items-center gap-1 ml-auto">
<Badge v-if="currentDevice.isOwned" variant="secondary" class="h-4 px-1 text-[10px]">
已绑定
</Badge>
<ChevronDown
class="h-3 w-3 text-muted-foreground flex-shrink-0 transition-transform duration-200"
:class="{ 'rotate-180': open }"
/>
</div>
</div>
</Button>
</template>
<!-- 下拉菜单内容 -->
<div class="w-auto p-0">
<!-- 搜索框 -->
<div class="p-3 border-b">
<div class="relative">
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
v-model="searchQuery"
placeholder="搜索设备..."
class="pl-9 h-8"
/>
</div>
</div>
<!-- 当前设备信息 -->
<div class="p-3 border-b bg-muted/20">
<div class="text-xs font-medium text-muted-foreground mb-1">当前设备</div>
<div class="flex items-center gap-2">
<Monitor class="h-4 w-4 text-primary" />
<div class="flex-1 min-w-0">
<div class="font-medium text-sm truncate">{{ currentDevice.name }}</div>
<code class="text-xs text-muted-foreground truncate block">
{{ currentDevice.namespace }}
</code>
</div>
<Check class="h-4 w-4 text-green-500" />
</div>
</div>
<div class="max-h-80 overflow-y-auto">
<!-- 账户设备 -->
<div v-if="accountStore.isAuthenticated">
<div class="px-3 py-2 text-xs font-medium text-muted-foreground flex items-center gap-2">
<User class="h-3 w-3" />
账户设备
</div>
<div v-if="isLoading" class="px-3 py-4 text-center text-sm text-muted-foreground">
加载中...
</div>
<div v-else-if="filteredAccountDevices.length === 0" class="px-3 py-2">
<div class="text-sm text-muted-foreground text-center py-2">
{{ searchQuery ? '未找到匹配的设备' : '暂无绑定设备' }}
</div>
</div>
<div v-else>
<DropdownItem
v-for="device in filteredAccountDevices"
:key="device.uuid"
@click="switchToDevice(device)"
class="cursor-pointer"
>
<div class="flex items-center gap-2 w-full">
<Monitor class="h-4 w-4 text-muted-foreground" />
<div class="flex-1 min-w-0">
<div class="font-medium text-sm truncate">
{{ device.name || '未命名设备' }}
</div>
<div class="text-xs text-muted-foreground truncate">
{{ device.namespace}}
</div>
</div>
</div>
</DropdownItem>
</div>
<Separator class="my-1" />
</div>
<!-- 历史设备 -->
<div v-if="filteredHistoryDevices.length > 0">
<div class="px-3 py-2 text-xs font-medium text-muted-foreground flex items-center gap-2">
<Clock class="h-3 w-3" />
最近使用
</div>
<DropdownItem
v-for="device in filteredHistoryDevices.slice(0, 5)"
:key="device.uuid"
@click="switchToDevice(device)"
class="cursor-pointer"
>
<div class="flex items-center gap-2 w-full">
<Monitor class="h-4 w-4 text-muted-foreground" />
<div class="flex-1 min-w-0">
<div class="font-medium text-sm truncate">
{{ device.name || '未命名设备' }}
</div>
<div class="text-xs text-muted-foreground truncate">
{{ device.namespace }}
</div>
</div>
</div>
</DropdownItem>
<Separator class="my-1" />
</div>
</div>
<!-- 操作按钮 -->
<div class="p-2 border-t bg-muted/20 space-y-1">
<DropdownItem
v-if="!accountStore.isAuthenticated"
@click="showLoginDialog = true"
class="cursor-pointer text-primary"
>
<User class="h-4 w-4" />
登录账户
</DropdownItem>
<DropdownItem
@click="showManualInputDialog = true"
class="cursor-pointer"
>
<Settings class="h-4 w-4" />
手动输入UUID
</DropdownItem>
<DropdownItem
@click="showRegisterDialog = true"
class="cursor-pointer text-primary"
>
<Plus class="h-4 w-4" />
注册新设备
</DropdownItem> <DropdownItem
@click="showRegisterDialog = true"
class="cursor-pointer text-primary"
>
<Plus class="h-4 w-4" />
高级选项
</DropdownItem>
</div>
</div>
</DropdownMenu>
<!-- 手动输入UUID对话框 -->
<Dialog v-model:open="showManualInputDialog">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>手动输入设备UUID</DialogTitle>
<DialogDescription>
输入已存在的设备UUID来快速切换
</DialogDescription>
</DialogHeader>
<div class="space-y-4 py-4">
<Input
v-model="manualUuid"
placeholder="输入设备UUID"
@keyup.enter="handleManualInput"
/>
</div>
<div class="flex justify-end gap-2">
<Button variant="outline" @click="showManualInputDialog = false">
取消
</Button>
<Button @click="handleManualInput" :disabled="!manualUuid.trim()">
确定
</Button>
</div>
</DialogContent>
</Dialog>
<!-- 登录对话框 -->
<LoginDialog
v-model="showLoginDialog"
:on-success="handleLoginSuccess"
/>
<!-- 设备注册对话框 -->
<DeviceRegisterDialog
v-model="showRegisterDialog"
@confirm="handleDeviceRegistered"
/>
</div>
</template>

View File

@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Edit } from 'lucide-vue-next'
import { toast } from 'vue-sonner'
import PasswordInput from './PasswordInput.vue'
const props = defineProps({
modelValue: {
@ -23,10 +23,7 @@ const props = defineProps({
type: String,
default: ''
},
hasPassword: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'success'])
@ -34,7 +31,6 @@ const emit = defineEmits(['update:modelValue', 'success'])
const accountStore = useAccountStore()
const deviceName = ref('')
const password = ref('')
const isSubmitting = ref(false)
const isOpen = computed({
@ -42,15 +38,12 @@ const isOpen = computed({
set: (val) => {
if (val) {
deviceName.value = props.currentName || ''
password.value = ''
}
emit('update:modelValue', val)
}
})
const needsPassword = computed(() => {
return props.hasPassword && !accountStore.isAuthenticated
})
const updateDeviceName = async () => {
if (!deviceName.value.trim()) {
@ -63,7 +56,6 @@ const updateDeviceName = async () => {
await apiClient.setDeviceName(
props.deviceUuid,
deviceName.value.trim(),
needsPassword.value ? password.value : null,
accountStore.isAuthenticated ? accountStore.token : null
)
@ -104,23 +96,7 @@ const updateDeviceName = async () => {
/>
</div>
<div v-if="needsPassword">
<PasswordInput
v-model="password"
label="设备密码"
placeholder="输入设备密码"
:device-uuid="deviceUuid"
:show-hint="true"
:show-strength="false"
required
/>
</div>
<div v-if="accountStore.isAuthenticated && hasPassword" class="p-3 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
<p class="text-sm text-blue-700 dark:text-blue-300">
您已登录绑定的账户无需输入密码
</p>
</div>
</div>
<DialogFooter>

View File

@ -50,15 +50,6 @@ const features = [
iconBg: 'bg-orange-500/10',
iconColor: 'text-orange-600 dark:text-orange-400',
},
{
title: '高级设置',
description: '密码管理、安全设置和其他高级功能',
icon: Settings,
path: '/password-manager',
color: 'from-gray-500 to-slate-500',
iconBg: 'bg-gray-500/10',
iconColor: 'text-gray-600 dark:text-gray-400',
},
]
const navigateTo = (path) => {

View File

@ -1,274 +0,0 @@
<script setup>
import { ref, computed } from 'vue'
import { useAccountStore } from '@/stores/account'
import { apiClient } from '@/lib/api'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import PasswordInput from './PasswordInput.vue'
import { toast } from 'vue-sonner'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
deviceUuid: {
type: String,
required: true
},
deviceName: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue', 'success'])
const accountStore = useAccountStore()
const password = ref('')
const isSubmitting = ref(false)
const showDeleteConfirm = ref(false)
const showHintDialog = ref(false)
const passwordHint = ref('')
const isSettingHint = ref(false)
const isOpen = computed({
get: () => props.modelValue,
set: (val) => {
if (!val) {
password.value = ''
}
emit('update:modelValue', val)
}
})
const resetPassword = async () => {
if (!password.value.trim()) {
toast.error('请输入新密码')
return
}
isSubmitting.value = true
try {
// 使
if (accountStore.isAuthenticated) {
await apiClient.resetDevicePasswordAsOwner(
props.deviceUuid,
password.value,
null, // passwordHint
accountStore.token
)
} else {
// 使
await apiClient.setDevicePassword(
props.deviceUuid,
{ password: password.value }
)
}
toast.success('密码重置成功')
isOpen.value = false
emit('success')
} catch (error) {
toast.error('重置密码失败:' + error.message)
} finally {
isSubmitting.value = false
}
}
const confirmDeletePassword = () => {
//
isOpen.value = false
//
setTimeout(() => {
showDeleteConfirm.value = true
}, 100)
}
const deletePassword = async () => {
isSubmitting.value = true
try {
await apiClient.deleteDevicePassword(props.deviceUuid, null, accountStore.token)
toast.success('密码已删除')
showDeleteConfirm.value = false
emit('success')
} catch (error) {
toast.error('删除密码失败:' + error.message)
} finally {
isSubmitting.value = false
}
}
const openHintDialog = () => {
//
isOpen.value = false
//
setTimeout(() => {
showHintDialog.value = true
}, 100)
}
const setPasswordHint = async () => {
isSettingHint.value = true
try {
await apiClient.setDevicePasswordHint(
props.deviceUuid,
passwordHint.value,
null,
accountStore.token
)
toast.success('密码提示已设置')
showHintDialog.value = false
passwordHint.value = ''
emit('success')
} catch (error) {
toast.error('设置密码提示失败:' + error.message)
} finally {
isSettingHint.value = false
}
}
const handleDeleteCancel = () => {
showDeleteConfirm.value = false
//
setTimeout(() => {
isOpen.value = true
}, 100)
}
const handleHintCancel = () => {
showHintDialog.value = false
passwordHint.value = ''
//
setTimeout(() => {
isOpen.value = true
}, 100)
}
</script>
<template>
<Dialog v-model:open="isOpen">
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>重置设备密码</DialogTitle>
<DialogDescription>
为设备 {{ deviceName || deviceUuid }} 设置新密码
</DialogDescription>
</DialogHeader>
<div class="space-y-4 py-4">
<div class="p-3 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
<p class="text-sm text-blue-700 dark:text-blue-300">
您已登录绑定的账户可以直接重置密码而无需输入当前密码
</p>
</div>
<div>
<PasswordInput
v-model="password"
label="新密码"
placeholder="输入新密码"
:show-hint="false"
:show-strength="true"
:min-length="8"
required
/>
</div>
</div>
<DialogFooter class="flex-col gap-2 sm:flex-row sm:justify-between">
<div class="flex gap-2">
<Button
variant="destructive"
@click="confirmDeletePassword"
:disabled="isSubmitting"
class="flex-1 sm:flex-none"
>
删除密码
</Button>
<Button
variant="outline"
@click="openHintDialog"
:disabled="isSubmitting"
class="flex-1 sm:flex-none"
>
设置提示
</Button>
</div>
<div class="flex gap-2">
<Button variant="outline" @click="isOpen = false" :disabled="isSubmitting">
取消
</Button>
<Button @click="resetPassword" :disabled="isSubmitting || !password.trim()">
{{ isSubmitting ? '重置中...' : '确认重置' }}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 删除密码确认对话框 -->
<AlertDialog v-model:open="showDeleteConfirm">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除密码</AlertDialogTitle>
<AlertDialogDescription>
确定要删除设备 "{{ deviceName || deviceUuid }}" 的密码吗删除后任何人都可以访问该设备
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="handleDeleteCancel">取消</AlertDialogCancel>
<AlertDialogAction @click="deletePassword" :disabled="isSubmitting">
{{ isSubmitting ? '删除中...' : '确认删除' }}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 设置密码提示对话框 -->
<Dialog v-model:open="showHintDialog">
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>设置密码提示</DialogTitle>
<DialogDescription>
为设备 {{ deviceName || deviceUuid }} 设置密码提示
</DialogDescription>
</DialogHeader>
<div class="space-y-4 py-4">
<div>
<Label for="hint">密码提示</Label>
<Input
id="hint"
v-model="passwordHint"
placeholder="输入密码提示(可选)"
:disabled="isSettingHint"
class="mt-1.5"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="handleHintCancel" :disabled="isSettingHint">
取消
</Button>
<Button @click="setPasswordHint" :disabled="isSettingHint">
{{ isSettingHint ? '设置中...' : '确认设置' }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View File

@ -84,7 +84,7 @@ class ApiClient {
}
async revokeToken(targetToken, authOptions = {}) {
const { deviceUuid, password, usePathParam = true, bearerToken } = authOptions;
const { deviceUuid, usePathParam = true, bearerToken } = authOptions;
if (usePathParam) {
// 使用路径参数方式 (推荐)
@ -94,9 +94,6 @@ class ApiClient {
headers['Authorization'] = `Bearer ${bearerToken}`;
} else if (deviceUuid) {
headers['x-device-uuid'] = deviceUuid;
if (password) {
headers['x-device-password'] = password;
}
}
return this.fetch(`/apps/tokens/${targetToken}`, {
@ -112,9 +109,6 @@ class ApiClient {
headers['Authorization'] = `Bearer ${bearerToken}`;
} else if (deviceUuid) {
headers['x-device-uuid'] = deviceUuid;
if (password) {
headers['x-device-password'] = password;
}
}
return this.fetch(`/apps/tokens?${params}`, {
@ -126,19 +120,18 @@ class ApiClient {
// 应用安装接口 (对应后端的 /apps/devices/:uuid/install/:appId)
async authorizeApp(appId, deviceUuid, options = {}) {
const { password, note, token } = options;
const { note, token } = options;
const headers = {
'x-device-uuid': deviceUuid,
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// 使用新的安装接口
return this.fetch(`/apps/devices/${deviceUuid}/install/${appId}?password=${password}`, {
return this.fetch(`/apps/devices/${deviceUuid}/install/${appId}`, {
method: 'POST',
headers,
body: JSON.stringify({ note: note || '应用授权' }),
@ -146,14 +139,10 @@ class ApiClient {
}
// 设备级别的应用卸载,使用新的 uninstall 接口
async revokeDeviceToken(deviceUuid, installId, password = null, token = null) {
async revokeDeviceToken(deviceUuid, installId, token = null) {
const params = new URLSearchParams({ uuid: deviceUuid });
const headers = {};
if (password) {
params.set('password', password);
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
@ -164,76 +153,9 @@ class ApiClient {
});
}
// 设备密码管理 API
async setDevicePassword(deviceUuid, data, token = null) {
const { newPassword, currentPassword, passwordHint } = data;
// 检查设备是否已设置密码
const deviceInfo = await this.getDeviceInfo(deviceUuid);
const hasPassword = deviceInfo.hasPassword;
if (hasPassword) {
// 使用PUT修改密码
const params = new URLSearchParams();
params.set('uuid', deviceUuid);
params.set('newPassword', newPassword);
if (currentPassword) {
params.set('currentPassword', currentPassword);
}
if (passwordHint !== undefined) {
params.set('passwordHint', passwordHint);
}
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return this.fetch(`/devices/${deviceUuid}/password?${params}`, {
method: 'PUT',
headers,
});
} else {
// 使用POST初次设置密码
const params = new URLSearchParams();
params.set('newPassword', newPassword);
if (passwordHint !== undefined) {
params.set('passwordHint', passwordHint);
}
return this.fetch(`/devices/${deviceUuid}/password?${params}`, {
method: 'POST',
});
}
}
async deleteDevicePassword(deviceUuid, password, token = null) {
const params = new URLSearchParams({ uuid: deviceUuid });
const headers = {};
// 如果提供了账户token使用JWT认证
if (token) {
headers['Authorization'] = `Bearer ${token}`;
} else if (password) {
params.set('password', password);
}
return this.fetch(`/devices/${deviceUuid}/password?${params}`, {
method: 'DELETE',
headers,
});
}
async setDevicePasswordHint(deviceUuid, hint, password = null, token = null) {
return this.authenticatedFetch(`/devices/${deviceUuid}/password-hint`, {
method: 'PUT',
body: JSON.stringify({ hint, password }),
}, token)
}
async getDevicePasswordHint(deviceUuid) {
return this.fetch(`/devices/${deviceUuid}/password-hint`)
}
// 设备授权相关 API
async bindDeviceCode(deviceCode, token) {
@ -292,32 +214,7 @@ class ApiClient {
return this.fetch(`/apps/devices/${deviceUuid}/apps`)
}
// 密码提示管理 API
async getPasswordHint(deviceUuid) {
try {
const response = await this.fetch(`/devices/${deviceUuid}`)
return { hint: response.device?.passwordHint || '' }
} catch (error) {
// 如果接口不存在,返回空提示
return { hint: '' }
}
}
async setPasswordHint(deviceUuid, hint, password) {
try {
return await this.fetch(`/devices/${deviceUuid}/password-hint?password=${encodeURIComponent(password)}`, {
method: 'PUT',
headers: {
'x-device-uuid': deviceUuid,
},
body: JSON.stringify({ passwordHint: hint }),
})
} catch (error) {
// 如果接口不存在,忽略错误
console.log('Password hint API not available')
return { success: false }
}
}
// 账户相关 APIAuthorization 由 axios 拦截器统一注入)
async getOAuthProviders() {
@ -391,16 +288,13 @@ class ApiClient {
}
// 设备名称管理 API
async setDeviceName(deviceUuid, name, password = null, token = null) {
async setDeviceName(deviceUuid, name, token = null) {
const headers = {
'x-device-uuid': deviceUuid,
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
if (password) {
headers['x-device-password'] = password;
}
return this.fetch(`/devices/${deviceUuid}/name`, {
method: 'PUT',
@ -409,42 +303,7 @@ class ApiClient {
});
}
// 修改设备密码 API
async updateDevicePassword(deviceUuid, currentPassword, newPassword, passwordHint = null, token = null) {
const headers = {
'x-device-uuid': deviceUuid,
};
// 如果提供了账户token使用JWT认证账户拥有者无需当前密码
if (token) {
headers['Authorization'] = `Bearer ${token}`;
} else if (currentPassword) {
headers['x-device-password'] = currentPassword;
}
const body = { newPassword, passwordHint };
// 只有在非账户拥有者时才需要发送当前密码
if (!token && currentPassword) {
body.currentPassword = currentPassword;
}
return this.fetch(`/devices/${deviceUuid}/password`, {
method: 'PUT',
headers,
body: JSON.stringify(body),
});
}
// 验证设备密码 API
async verifyDevicePassword(deviceUuid, password) {
return this.fetch(`/devices/${deviceUuid}`, {
method: 'GET',
headers: {
'x-device-uuid': deviceUuid,
'x-device-password': password,
},
});
}
// 设备注册 API
async registerDevice(uuid, deviceName, token = null) {
@ -454,17 +313,7 @@ class ApiClient {
}, token)
}
// 账户拥有者重置设备密码 API
async resetDevicePasswordAsOwner(deviceUuid, newPassword, passwordHint = null, token) {
return this.fetch(`/devices/${deviceUuid}/password`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'x-device-uuid': deviceUuid,
},
body: JSON.stringify({ newPassword, passwordHint }),
});
}
// 兼容性方法 - 保持旧的API调用方式
async getTokens(deviceUuid, options = {}) {
@ -476,11 +325,10 @@ class ApiClient {
return this.revokeToken(targetToken, { deviceUuid, usePathParam: true });
}
// 便捷方法使用设备UUID和密码删除token
async revokeTokenByDevice(targetToken, deviceUuid, password = null) {
// 便捷方法使用设备UUID删除token
async revokeTokenByDevice(targetToken, deviceUuid) {
return this.revokeToken(targetToken, {
deviceUuid,
password,
usePathParam: true
});
}

View File

@ -19,7 +19,6 @@ import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { CheckCircle2, XCircle, Loader2, Shield, Key, AlertCircle, User, Plus, Check } from 'lucide-vue-next'
import AppCard from '@/components/AppCard.vue'
import PasswordInput from '@/components/PasswordInput.vue'
import LoginDialog from '@/components/LoginDialog.vue'
import DeviceRegisterDialog from '@/components/DeviceRegisterDialog.vue'
import { toast } from 'vue-sonner'
@ -46,12 +45,8 @@ const customDeviceUuid = ref('')
const showRegisterDialog = ref(false)
const deviceRequired = ref(false)
//
const hasPassword = computed(() => deviceInfo.value?.hasPassword || false)
//
const inputDeviceCode = ref('')
const authPassword = ref('')
const authNote = ref('')
//
@ -153,10 +148,6 @@ const authorizeWithDeviceCode = async () => {
note: authNote.value || '设备代码授权',
}
if (hasPassword.value && authPassword.value) {
authData.password = authPassword.value
}
const authResult = await apiClient.authorizeApp(appId.value, deviceUuid.value, authData)
const token = authResult.token
@ -182,10 +173,6 @@ const authorizeWithCallback = async () => {
note: authNote.value || '回调授权',
}
if (hasPassword.value && authPassword.value) {
authData.password = authPassword.value
}
const authResult = await apiClient.authorizeApp(appId.value, deviceUuid.value, authData)
const token = authResult.token
@ -221,7 +208,7 @@ const goHome = () => {
const retry = () => {
step.value = 'input'
errorMessage.value = ''
authPassword.value = ''
}
//
@ -314,10 +301,7 @@ onMounted(async () => {
<code class="text-xs font-mono bg-muted px-3 py-2 rounded flex-1 truncate">
{{ deviceUuid }}
</code>
<Badge v-if="hasPassword" variant="secondary" class="shrink-0">
<Shield class="h-3 w-3 mr-1" />
已保护
</Badge>
</div>
<!-- 设备绑定状态 -->
@ -379,18 +363,7 @@ onMounted(async () => {
</div>
<!-- 密码输入使用统一组件 -->
<div v-if="hasPassword">
<PasswordInput
v-model="authPassword"
label="设备密码"
placeholder="输入设备密码以确认授权"
:device-uuid="deviceUuid"
:show-hint="true"
:show-strength="false"
required
:error="step === 'error' && errorMessage.includes('密码') ? '密码错误' : ''"
/>
</div>
<!-- 授权按钮 -->
<div class="space-y-3 pt-2">
@ -398,7 +371,7 @@ onMounted(async () => {
@click="handleSubmit"
class="w-full"
size="lg"
:disabled="(isDeviceCodeMode && !currentDeviceCode) || (hasPassword && !authPassword)"
:disabled="(isDeviceCodeMode && !currentDeviceCode)"
>
<Key class="mr-2 h-4 w-4" />
确认授权

View File

@ -27,7 +27,6 @@ import {
} from 'lucide-vue-next'
import { toast } from 'vue-sonner'
import EditDeviceNameDialog from '@/components/EditDeviceNameDialog.vue'
import ResetDevicePasswordDialog from '@/components/ResetDevicePasswordDialog.vue'
const router = useRouter()
const accountStore = useAccountStore()
@ -35,7 +34,6 @@ const accountStore = useAccountStore()
const devices = ref([])
const isLoading = ref(false)
const showEditNameDialog = ref(false)
const showResetPasswordDialog = ref(false)
const showDeleteDialog = ref(false)
const currentDevice = ref(null)
@ -84,22 +82,11 @@ const editDeviceName = (device) => {
showEditNameDialog.value = true
}
//
const resetPassword = (device) => {
currentDevice.value = device
showResetPasswordDialog.value = true
}
//
const handleDeviceNameUpdated = async () => {
await loadDevices()
}
//
const handlePasswordReset = async () => {
toast.success('密码重置成功')
}
//
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString('zh-CN')
@ -209,15 +196,7 @@ onMounted(() => {
<Edit class="h-3 w-3 mr-1" />
重命名
</Button>
<Button
variant="outline"
size="sm"
@click="resetPassword(device)"
class="flex-1"
>
<Lock class="h-3 w-3 mr-1" />
重置密码
</Button>
</div>
<Button
@ -245,14 +224,7 @@ onMounted(() => {
@success="handleDeviceNameUpdated"
/>
<!-- 重置密码弹框 -->
<ResetDevicePasswordDialog
v-if="currentDevice"
v-model="showResetPasswordDialog"
:device-uuid="currentDevice.uuid"
:device-name="currentDevice.name || ''"
@success="handlePasswordReset"
/>
<!-- 解绑确认对话框 -->
<AlertDialog v-model:open="showDeleteDialog">

View File

@ -10,14 +10,14 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Plus, Trash2, Key, Shield, RefreshCw, Copy, CheckCircle2, Settings, Package, Clock, AlertCircle, Lock, Info, User, LogOut, Layers, ChevronDown, TestTube2 } from 'lucide-vue-next'
import { Plus, Trash2, Key, Shield, RefreshCw, Copy, CheckCircle2, Settings, Package, Clock, AlertCircle, Lock, Info, User, LogOut, Layers, ChevronDown, TestTube2, Edit } from 'lucide-vue-next'
import DropdownMenu from '@/components/ui/dropdown-menu/DropdownMenu.vue'
import DropdownItem from '@/components/ui/dropdown-menu/DropdownItem.vue'
import AppCard from '@/components/AppCard.vue'
import TokenList from '@/components/TokenList.vue'
import PasswordInput from '@/components/PasswordInput.vue'
import LoginDialog from '@/components/LoginDialog.vue'
import DeviceRegisterDialog from '@/components/DeviceRegisterDialog.vue'
import DeviceSwitcher from '@/components/DeviceSwitcher.vue'
import EditDeviceNameDialog from '@/components/EditDeviceNameDialog.vue'
import EditNamespaceDialog from '@/components/EditNamespaceDialog.vue'
import FeatureNavigation from '@/components/FeatureNavigation.vue'
@ -36,7 +36,7 @@ const appInfoCache = ref({}) // 缓存应用信息
const showAuthorizeDialog = ref(false)
const showRevokeDialog = ref(false)
const showRegisterDialog = ref(false)
const showPasswordDialog = ref(false)
const showLoginDialog = ref(false)
const showEditNameDialog = ref(false)
const showEditNamespaceDialog = ref(false)
@ -47,19 +47,14 @@ const showTokenDialog = ref(false)
// Form data
const appIdToAuthorize = ref('')
const authPassword = ref('')
const authNote = ref('')
const devicePassword = ref('')
const newPassword = ref('')
const currentPassword = ref('')
const passwordHint = ref('')
const revokePassword = ref('') //
// 使OAuth
const { handleOAuthCallback } = useOAuthCallback()
// 使
const hasPassword = computed(() => deviceInfo.value?.hasPassword || false)
// namespace UUID
const namespaceEqualsUuid = computed(() => {
@ -90,28 +85,13 @@ const loadDeviceInfo = async () => {
try {
const info = await apiClient.getDeviceInfo(deviceUuid.value)
deviceInfo.value = info
//
if (info.passwordHint) {
passwordHint.value = info.passwordHint
}
} catch (error) {
console.log('Failed to load device info:', error)
// deviceInfonullhasPasswordfalse
deviceInfo.value = null
}
}
//
const loadPasswordHint = async () => {
try {
const data = await apiClient.getPasswordHint(deviceUuid.value)
if (data.hint) {
passwordHint.value = data.hint
}
} catch (error) {
console.log('Failed to load password hint')
}
}
const loadTokens = async () => {
if (!deviceUuid.value) return
@ -157,10 +137,6 @@ const authorizeApp = async () => {
note: authNote.value || '授权访问',
}
if (hasPassword.value && authPassword.value) {
options.password = authPassword.value
}
// tokenaxios Authorization
//
@ -172,7 +148,6 @@ const authorizeApp = async () => {
showAuthorizeDialog.value = false
appIdToAuthorize.value = ''
authPassword.value = ''
authNote.value = ''
await loadTokens()
@ -190,23 +165,15 @@ const confirmRevoke = (token) => {
const revokeToken = async () => {
if (!selectedToken.value) return
//
if (!accountStore.isAuthenticated && hasPassword.value && !revokePassword.value) {
alert('请输入设备密码')
return
}
try {
// 使ID
await apiClient.revokeDeviceToken(
deviceUuid.value,
selectedToken.value.id,
accountStore.isAuthenticated ? null : revokePassword.value,
accountStore.isAuthenticated ? accountStore.token : null
)
showRevokeDialog.value = false
selectedToken.value = null
revokePassword.value = ''
await loadTokens()
toast.success('撤销成功')
} catch (error) {
@ -226,47 +193,27 @@ const copyToClipboard = async (text, id) => {
}
}
const updateUuid = () => {
showRegisterDialog.value = false
deviceUuid.value = deviceStore.getDeviceUuid()
const updateUuid = (newUuid = null) => {
if (newUuid) {
deviceUuid.value = newUuid
} else {
deviceUuid.value = deviceStore.getDeviceUuid()
}
//
if (deviceUuid.value) {
deviceStore.addDeviceToHistory({ uuid: deviceUuid.value, name: deviceInfo.value?.name || deviceInfo.value?.deviceName })
deviceStore.addDeviceToHistory({
uuid: deviceUuid.value,
name: deviceInfo.value?.name || deviceInfo.value?.deviceName
})
}
loadDeviceInfo()
loadDeviceAccount()
loadTokens()
}
const setPassword = async () => {
if (!newPassword.value) return
try {
const data = {
newPassword: newPassword.value,
}
if (hasPassword.value && !accountStore.isAuthenticated) {
data.currentPassword = currentPassword.value
}
await apiClient.setDevicePassword(
deviceUuid.value,
data,
accountStore.isAuthenticated ? accountStore.token : null
)
// hasPassword
await loadDeviceInfo()
showPasswordDialog.value = false
newPassword.value = ''
currentPassword.value = ''
toast.success('密码设置成功')
} catch (error) {
toast.error('设置密码失败:' + error.message)
}
}
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString('zh-CN')
@ -369,6 +316,12 @@ const handleDeviceNameUpdated = async (newName) => {
await loadDeviceInfo()
}
//
const handleDeviceRegistered = () => {
showRegisterDialog.value = false
updateUuid()
}
// namespace
const handleNamespaceUpdated = async (newNamespace) => {
if (deviceInfo.value) {
@ -393,10 +346,7 @@ onMounted(async () => {
//
await loadDeviceAccount()
//
if (hasPassword.value && !passwordHint.value) {
await loadPasswordHint()
}
// tokens
await loadTokens()
@ -410,7 +360,7 @@ onMounted(async () => {
<div class="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-10">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="flex items-center gap-4">
<!--<div class="rounded-lg bg-gradient-to-br from-primary to-primary/80 p-2.5 shadow-lg">
<Shield class="h-6 w-6 text-primary-foreground" />
</div>-->
@ -420,17 +370,16 @@ onMounted(async () => {
</h1>
<p class="text-sm text-muted-foreground">文档形键值数据库</p>
</div>
<!-- 分隔线 -->
<div class="h-8 w-px bg-border"></div>
<!-- Vercel风格设备切换器 -->
<DeviceSwitcher
:device-info="deviceInfo"
:device-uuid="deviceUuid"
@device-changed="updateUuid"
/>
</div>
<div class="flex items-center gap-2">
<!-- 切换设备按钮 -->
<Button
variant="outline"
size="sm"
@click="showRegisterDialog = true"
title="切换设备"
>
切换设备
</Button>
<!-- 账户状态 -->
<template v-if="accountStore.isAuthenticated">
<DropdownMenu v-model:open="showUserMenu" class="z-50">
@ -475,10 +424,7 @@ onMounted(async () => {
<Layers class="h-4 w-4" />
设备管理
</DropdownItem>
<DropdownItem @click="$router.push('/password-manager')">
<Settings class="h-4 w-4" />
高级设置
</DropdownItem>
<DropdownItem @click="$router.push('/auto-auth-management')">
<Shield class="h-4 w-4" />
自动授权配置
@ -534,6 +480,7 @@ onMounted(async () => {
您的命名空间当前使用设备 UUID建议修改为更有意义的名称如班级名房间号等方便自动授权时识别
</p>
<Button
v-if="accountStore.isAuthenticated"
variant="outline"
size="sm"
@click="showEditNamespaceDialog = true"
@ -560,20 +507,21 @@ onMounted(async () => {
<CardTitle class="text-lg">
{{ deviceInfo?.name || '设备' }}
</CardTitle>
<Button
v-if="accountStore.isAuthenticated"
variant="ghost"
size="sm"
@click="showEditNameDialog = true"
class="h-6 w-6 p-0"
>
<Edit class="h-3 w-3" />
</Button>
</div>
<CardDescription>设备命名空间标识符</CardDescription>
</div>
</div>
<div class="flex items-center gap-2 flex-wrap">
<Badge
variant="outline"
class="px-3 py-1"
:class="hasPassword ? 'border-green-500 text-green-700 dark:text-green-400' : 'border-yellow-500 text-yellow-700 dark:text-yellow-400'"
>
<Lock v-if="hasPassword" class="h-3 w-3 mr-1.5" />
<AlertCircle v-else class="h-3 w-3 mr-1.5" />
{{ hasPassword ? '已设密码' : '未设密码' }}
</Badge>
<!-- 设备账户绑定状态 -->
<Badge v-if="deviceInfo?.account" variant="secondary" class="px-3 py-1">
@ -589,14 +537,7 @@ onMounted(async () => {
<User class="h-4 w-4 mr-2" />
绑定到账户
</Button>
<Button
@click="$router.push('/password-manager')"
variant="outline"
size="sm"
>
<Settings class="h-4 w-4 mr-1" />
高级设置
</Button>
<Button
@click="$router.push('/auto-auth-management')"
variant="outline"
@ -617,6 +558,7 @@ onMounted(async () => {
<div class="flex items-center justify-between mb-2">
<Label class="text-sm font-medium">命名空间</Label>
<Button
v-if="accountStore.isAuthenticated"
variant="ghost"
size="sm"
@click="showEditNamespaceDialog = true"
@ -654,24 +596,10 @@ onMounted(async () => {
<div class="text-2xl font-bold text-primary">{{ tokens.length }}</div>
<div class="text-xs text-muted-foreground">令牌数</div>
</div>
<div class="p-3 rounded-lg bg-muted/50 text-center">
<div class="text-2xl font-bold text-primary">
{{ hasPassword ? '安全' : '未设置' }}
</div>
<div class="text-xs text-muted-foreground">安全状态</div>
</div>
</div>
<!-- Password Hint (if exists) -->
<div v-if="hasPassword && passwordHint" class="p-4 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
<div class="flex items-start gap-2">
<Info class="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5" />
<div class="flex-1">
<p class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">密码提示</p>
<p class="text-sm text-blue-700 dark:text-blue-300">{{ passwordHint }}</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
@ -764,20 +692,9 @@ onMounted(async () => {
placeholder="为此授权添加备注"
/>
</div>
<div v-if="hasPassword">
<PasswordInput
v-model="authPassword"
label="设备密码"
placeholder="输入设备密码"
:device-uuid="deviceUuid"
:show-hint="true"
:show-strength="false"
:required="!accountStore.isAuthenticated"
/><br/>
<p v-if="accountStore.isAuthenticated" class="text-xs text-muted-foreground mt-2">
已登录绑定账户无需输入密码
</p>
</div>
<p v-if="accountStore.isAuthenticated" class="text-xs text-muted-foreground mt-2">
已登录绑定账户无需输入密码
</p>
</div>
<DialogFooter>
<Button variant="outline" @click="showAuthorizeDialog = false">
@ -811,32 +728,7 @@ onMounted(async () => {
</div>
</div>
<!-- 如果没有登录账户且设备有密码显示密码输入框 -->
<div v-if="!accountStore.isAuthenticated && hasPassword" class="space-y-2">
<Label for="revoke-password">设备密码</Label>
<PasswordInput
id="revoke-password"
v-model="revokePassword"
placeholder="请输入设备密码"
required
/>
</div>
<!-- 显示当前认证状态 -->
<div class="text-sm text-muted-foreground">
<div v-if="accountStore.isAuthenticated" class="flex items-center gap-2 text-green-600">
<CheckCircle2 class="h-4 w-4" />
已登录账户无需输入密码
</div>
<div v-else-if="!hasPassword" class="flex items-center gap-2 text-blue-600">
<Info class="h-4 w-4" />
设备未设置密码
</div>
<div v-else class="flex items-center gap-2 text-orange-600">
<Lock class="h-4 w-4" />
需要验证设备密码
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showRevokeDialog = false">
@ -897,57 +789,7 @@ onMounted(async () => {
</Dialog>
<Dialog v-model:open="showPasswordDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ hasPassword ? '修改密码' : '设置密码' }}</DialogTitle>
<DialogDescription>
{{ hasPassword ? '输入当前密码和新密码' : '为设备设置密码以增强安全性' }}
</DialogDescription>
</DialogHeader>
<div class="space-y-4 py-4">
<div v-if="hasPassword && !accountStore.isAuthenticated">
<PasswordInput
v-model="currentPassword"
label="当前密码"
placeholder="输入当前密码"
:device-uuid="deviceUuid"
:show-hint="true"
:show-strength="false"
required
/>
</div>
<div v-if="accountStore.isAuthenticated && hasPassword" class="p-4 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
<div class="flex items-start gap-2">
<Info class="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5" />
<div class="flex-1">
<p class="text-sm font-medium text-blue-900 dark:text-blue-100">账户已登录</p>
<p class="text-sm text-blue-700 dark:text-blue-300">您已登录绑定的账户无需输入当前密码</p>
</div>
</div>
</div>
<div>
<PasswordInput
v-model="newPassword"
label="新密码"
placeholder="输入新密码"
:show-hint="false"
:show-strength="true"
:min-length="8"
required
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showPasswordDialog = false">
取消
</Button>
<Button @click="setPassword">
确认
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<!-- 登录弹框 -->
@ -962,7 +804,7 @@ onMounted(async () => {
/> <!-- -->
<DeviceRegisterDialog
v-model="showRegisterDialog"
@confirm="updateUuid"
@confirm="handleDeviceRegistered"
:required="deviceRequired"
/>
@ -971,7 +813,6 @@ onMounted(async () => {
v-model="showEditNameDialog"
:device-uuid="deviceUuid"
:current-name="deviceInfo?.deviceName || ''"
:has-password="hasPassword"
@success="handleDeviceNameUpdated"
/>

View File

@ -1,275 +1,41 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { apiClient } from '@/lib/api'
import { deviceStore } from '@/lib/deviceStore'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Badge } from '@/components/ui/badge'
import PasswordInput from '@/components/PasswordInput.vue'
import EditDeviceNameDialog from '@/components/EditDeviceNameDialog.vue'
import {
Shield,
Key,
Trash2,
Edit,
Info,
AlertTriangle,
CheckCircle2,
ArrowLeft,
Lock,
Unlock,
Eye,
EyeOff,
HelpCircle,
RefreshCw,
Smartphone,
Copy
AlertTriangle,
Settings,
Shield,
Key
} from 'lucide-vue-next'
import DeviceRegisterDialog from '@/components/DeviceRegisterDialog.vue'
import EditDeviceNameDialog from '@/components/EditDeviceNameDialog.vue'
const router = useRouter()
const deviceUuid = ref('')
const deviceInfo = ref(null)
//
const hasPassword = computed(() => deviceInfo.value?.hasPassword || false)
const passwordHint = ref('')
// Dialog states
const showChangePasswordDialog = ref(false)
const showDeletePasswordDialog = ref(false)
const showHintDialog = ref(false)
const showResetDeviceDialog = ref(false)
const showRegisterDialog = ref(false)
const showEditNameDialog = ref(false)
const deviceRequired = ref(false)
// Form data
const currentPassword = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const deleteConfirmPassword = ref('')
const newHint = ref('')
const hintPassword = ref('')
// UI states
const isLoading = ref(false)
const successMessage = ref('')
const errorMessage = ref('')
const copied = ref(null)
//
const copyToClipboard = async (text, type) => {
try {
await navigator.clipboard.writeText(text)
copied.value = type
setTimeout(() => {
copied.value = null
}, 2000)
} catch (error) {
console.error('Failed to copy:', error)
}
}
//
const loadDeviceInfo = async () => {
try {
const info = await apiClient.getDeviceInfo(deviceUuid.value)
deviceInfo.value = info
if (info.passwordHint) {
passwordHint.value = info.passwordHint
}
} catch (error) {
console.log('Failed to load device info:', error)
deviceInfo.value = null
}
}
//
const loadPasswordHint = async () => {
try {
const data = await apiClient.getPasswordHint(deviceUuid.value)
if (data.hint) {
passwordHint.value = data.hint
}
} catch (error) {
console.log('Failed to load password hint')
}
}
//
const changePassword = async () => {
if (!newPassword.value || !currentPassword.value) {
errorMessage.value = '请填写所有必填字段'
return
}
if (newPassword.value !== confirmPassword.value) {
errorMessage.value = '新密码与确认密码不一致'
return
}
if (newPassword.value === currentPassword.value) {
errorMessage.value = '新密码不能与当前密码相同'
return
}
isLoading.value = true
errorMessage.value = ''
try {
await apiClient.setDevicePassword(deviceUuid.value, {
currentPassword: currentPassword.value,
newPassword: newPassword.value
})
successMessage.value = '密码修改成功!'
showChangePasswordDialog.value = false
currentPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
setTimeout(() => {
successMessage.value = ''
}, 3000)
} catch (error) {
errorMessage.value = error.message || '密码修改失败'
} finally {
isLoading.value = false
}
}
//
const deletePassword = async () => {
if (!deleteConfirmPassword.value) {
errorMessage.value = '请输入当前密码'
return
}
isLoading.value = true
errorMessage.value = ''
try {
await apiClient.deleteDevicePassword(deviceUuid.value, deleteConfirmPassword.value)
//
await loadDeviceInfo()
successMessage.value = '密码已删除!'
showDeletePasswordDialog.value = false
deleteConfirmPassword.value = ''
passwordHint.value = ''
setTimeout(() => {
successMessage.value = ''
}, 3000)
} catch (error) {
errorMessage.value = error.message || '密码删除失败'
} finally {
isLoading.value = false
}
}
//
const setPasswordHint = async () => {
if (!hintPassword.value) {
errorMessage.value = '请输入当前密码'
return
}
isLoading.value = true
errorMessage.value = ''
try {
const result = await apiClient.setPasswordHint(deviceUuid.value, newHint.value, hintPassword.value)
passwordHint.value = newHint.value
successMessage.value = '密码提示已更新!'
showHintDialog.value = false
newHint.value = ''
hintPassword.value = ''
setTimeout(() => {
successMessage.value = ''
}, 3000)
} catch (error) {
errorMessage.value = error.message || '设置密码提示失败'
} finally {
isLoading.value = false
}
}
//
const setNewPassword = async () => {
if (!newPassword.value) {
errorMessage.value = '请输入新密码'
return
}
if (newPassword.value !== confirmPassword.value) {
errorMessage.value = '密码与确认密码不一致'
return
}
isLoading.value = true
errorMessage.value = ''
try {
await apiClient.setDevicePassword(deviceUuid.value, {
newPassword: newPassword.value
})
//
await loadDeviceInfo()
successMessage.value = '密码设置成功!'
newPassword.value = ''
confirmPassword.value = ''
setTimeout(() => {
successMessage.value = ''
}, 3000)
} catch (error) {
errorMessage.value = error.message || '密码设置失败'
} finally {
isLoading.value = false
}
}
//
const goBack = () => {
router.push('/')
}
//
const handleDeviceReset = () => {
deviceUuid.value = deviceStore.getDeviceUuid()
loadDeviceInfo()
successMessage.value = '设备已重置!'
setTimeout(() => {
successMessage.value = ''
}, 3000)
}
// UUID
const updateUuid = () => {
showRegisterDialog.value = false
deviceUuid.value = deviceStore.getDeviceUuid()
loadDeviceInfo()
}
//
const handleDeviceNameUpdated = async () => {
await loadDeviceInfo()
successMessage.value = '设备名称已更新!'
setTimeout(() => {
successMessage.value = ''
}, 3000)
//
const handleDeviceNameUpdated = () => {
//
}
onMounted(async () => {
@ -277,20 +43,13 @@ onMounted(async () => {
const existingUuid = deviceStore.getDeviceUuid()
if (!existingUuid) {
deviceRequired.value = true
// UUID
showRegisterDialog.value = true
} else {
deviceUuid.value = existingUuid
//
await loadDeviceInfo()
//
if (hasPassword.value && !passwordHint.value) {
await loadPasswordHint()
}
}
})
</script>
<template>
@ -406,83 +165,7 @@ onMounted(async () => {
</CardContent>
</Card>
<!-- Password Status Card -->
<Card class="mb-6 border-2">
<CardHeader>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-primary/10 p-2">
<Shield class="h-6 w-6 text-primary" />
</div>
<div>
<CardTitle>密码状态</CardTitle>
<CardDescription>当前设备的密码保护状态</CardDescription>
</div>
</div>
<Badge :variant="hasPassword ? 'default' : 'secondary'" class="text-sm">
<Lock v-if="hasPassword" class="h-3 w-3 mr-1" />
<Unlock v-else class="h-3 w-3 mr-1" />
{{ hasPassword ? '已设置密码' : '未设置密码' }}
</Badge>
</div>
</CardHeader>
<CardContent>
<div class="space-y-4">
<!-- Password Hint -->
<div v-if="hasPassword && passwordHint" class="p-4 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
<div class="flex items-start gap-2">
<HelpCircle class="h-5 w-5 text-blue-600 dark:text-blue-400 mt-0.5" />
<div class="flex-1">
<p class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">密码提示</p>
<p class="text-sm text-blue-700 dark:text-blue-300">{{ passwordHint }}</p>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-wrap gap-3 pt-2">
<Button
v-if="!hasPassword"
@click="showChangePasswordDialog = true"
class="flex-1 sm:flex-none"
>
<Key class="h-4 w-4 mr-2" />
设置密码
</Button>
<Button
v-if="hasPassword"
@click="showChangePasswordDialog = true"
variant="outline"
class="flex-1 sm:flex-none"
>
<Edit class="h-4 w-4 mr-2" />
修改密码
</Button>
<Button
v-if="hasPassword"
@click="showHintDialog = true"
variant="outline"
class="flex-1 sm:flex-none"
>
<Info class="h-4 w-4 mr-2" />
{{ passwordHint ? '修改提示' : '设置提示' }}
</Button>
<Button
v-if="hasPassword"
@click="showDeletePasswordDialog = true"
variant="destructive"
class="flex-1 sm:flex-none"
>
<Trash2 class="h-4 w-4 mr-2" />
删除密码
</Button>
</div>
</div>
</CardContent>
</Card>
<!-- 设备管理部分 -->
<h2 class="text-xl font-semibold mt-12 mb-4">设备管理</h2>
@ -515,174 +198,11 @@ onMounted(async () => {
</div>
<!-- Change/Set Password Dialog -->
<Dialog v-model:open="showChangePasswordDialog">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>{{ hasPassword ? '修改密码' : '设置密码' }}</DialogTitle>
<DialogDescription>
{{ hasPassword ? '请输入当前密码和新密码' : '为您的设备设置一个安全的密码' }}
</DialogDescription>
</DialogHeader>
<div class="space-y-4 py-4">
<!-- Current Password (only when changing) -->
<div v-if="hasPassword">
<PasswordInput
v-model="currentPassword"
label="当前密码"
placeholder="输入当前密码"
:device-uuid="deviceUuid"
:show-hint="true"
:show-strength="false"
required
/>
</div>
<!-- New Password -->
<div>
<PasswordInput
v-model="newPassword"
label="新密码"
placeholder="输入新密码"
:show-hint="false"
:show-strength="true"
:min-length="8"
required
/>
</div>
<!-- Confirm Password -->
<div>
<PasswordInput
v-model="confirmPassword"
label="确认新密码"
placeholder="再次输入新密码"
:show-hint="false"
:show-strength="false"
:confirm-password="newPassword"
required
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showChangePasswordDialog = false" :disabled="isLoading">
取消
</Button>
<Button
@click="hasPassword ? changePassword() : setNewPassword()"
:disabled="isLoading || !newPassword || newPassword !== confirmPassword || (hasPassword && !currentPassword)"
>
{{ isLoading ? '处理中...' : (hasPassword ? '修改密码' : '设置密码') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Delete Password Dialog -->
<Dialog v-model:open="showDeletePasswordDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>删除密码</DialogTitle>
<DialogDescription>
删除密码后您的设备将不再受密码保护此操作无法撤销
</DialogDescription>
</DialogHeader>
<div class="space-y-4 py-4">
<div class="p-4 rounded-lg bg-yellow-50 dark:bg-yellow-950/20 border border-yellow-200 dark:border-yellow-900">
<div class="flex items-start gap-2">
<AlertTriangle class="h-5 w-5 text-yellow-600 dark:text-yellow-400 mt-0.5" />
<div class="text-sm text-yellow-800 dark:text-yellow-200">
<p class="font-medium mb-1">警告</p>
<p>删除密码后任何拥有您设备 UUID 的人都可以管理您的授权应用</p>
</div>
</div>
</div>
<div>
<PasswordInput
v-model="deleteConfirmPassword"
label="输入当前密码以确认"
placeholder="输入当前密码"
:device-uuid="deviceUuid"
:show-hint="true"
:show-strength="false"
required
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showDeletePasswordDialog = false" :disabled="isLoading">
取消
</Button>
<Button
variant="destructive"
@click="deletePassword"
:disabled="isLoading || !deleteConfirmPassword"
>
{{ isLoading ? '删除中...' : '确认删除' }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Set/Update Hint Dialog -->
<Dialog v-model:open="showHintDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ passwordHint ? '修改密码提示' : '设置密码提示' }}</DialogTitle>
<DialogDescription>
密码提示可以帮助您在忘记密码时回忆起密码
</DialogDescription>
</DialogHeader>
<div class="space-y-4 py-4">
<div v-if="passwordHint" class="p-3 rounded-lg bg-muted">
<Label class="text-xs text-muted-foreground">当前提示</Label>
<p class="text-sm mt-1">{{ passwordHint }}</p>
</div>
<div class="space-y-2">
<Label for="new-hint">新的密码提示</Label>
<Input
id="new-hint"
v-model="newHint"
placeholder="例如:我的宠物名字加生日"
/>
<p class="text-xs text-muted-foreground">
提示不应包含密码本身而是能帮助您回忆密码的信息
</p>
</div>
<div>
<PasswordInput
v-model="hintPassword"
label="当前密码"
placeholder="输入当前密码以确认"
:device-uuid="deviceUuid"
:show-hint="true"
:show-strength="false"
required
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showHintDialog = false" :disabled="isLoading">
取消
</Button>
<Button
@click="setPasswordHint"
:disabled="isLoading || !hintPassword"
>
{{ isLoading ? '保存中...' : '保存提示' }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 设备重置弹框 -->
<DeviceRegisterDialog
@ -703,7 +223,6 @@ onMounted(async () => {
v-model="showEditNameDialog"
:device-uuid="deviceUuid"
:current-name="deviceInfo?.deviceName || ''"
:has-password="hasPassword"
@success="handleDeviceNameUpdated"
/>
</div>