1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-12-07 21:13:11 +00:00

feat: 重构链接生成器,合并预配置认证与设置分享功能,优化界面与安全性

This commit is contained in:
SunWuyuan 2025-11-02 15:48:39 +08:00
parent 7f166ffddc
commit 1999b09e59
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
4 changed files with 669 additions and 157 deletions

159
UNIFIED_LINK_GENERATOR.md Normal file
View File

@ -0,0 +1,159 @@
# 统一链接生成器功能测试
## 功能概述
新的统一链接生成器将预配置认证信息和设置分享功能合并到一个链接中,用户可以:
1. 输入命名空间和认证码(预配置认证)
2. 选择需要分享的设置项
3. 生成包含两种信息的统一链接
## 核心特性
### ✅ **统一链接生成**
- 同时包含预配置认证参数和设置配置
- 一个链接完成设备认证和设置应用
- 自动编码和参数组合
### ✅ **智能界面设计**
- 分层式界面:预配置认证 + 设置分享 + 链接生成
- 实时预览和状态显示
- 安全提醒和敏感信息保护
### ✅ **安全优化**
- 数据源设置和已变更设置默认排除 `server.kvToken`
- 敏感设置项标记和值遮盖
- 多重安全提醒
## 链接格式
### 基础预配置链接
```
https://domain.com/?namespace=classroom-001&authCode=pass123&autoExecute=true
```
### 包含设置的统一链接
```
https://domain.com/?namespace=classroom-001&authCode=pass123&autoExecute=true&config=eyJ...
```
其中:
- `namespace`: 设备命名空间
- `authCode`: 认证码(可选)
- `autoExecute`: 是否自动执行认证
- `config`: Base64编码的设置JSON对象
## 使用流程
1. **输入预配置信息**
- 命名空间(必填)
- 认证码(可选)
- 是否自动执行认证
2. **选择设置项**
- 快速选择:数据源设置、已变更设置、全选
- 手动选择:通过详细列表选择特定设置
- 安全保护:敏感设置默认不选中
3. **生成统一链接**
- 点击"生成统一链接"按钮
- 链接实时生成和预览
- 一键复制和测试
## 技术实现
### 参数组合逻辑
```javascript
// 1. 添加预配置参数
params.append("namespace", namespace);
params.append("authCode", authCode);
params.append("autoExecute", "true");
// 2. 添加设置配置
if (selectedSettings.length > 0) {
const configObj = {};
selectedSettings.forEach(key => {
configObj[key] = allSettings[key];
});
const base64Config = btoa(JSON.stringify(configObj));
params.append("config", base64Config);
}
```
### 自动更新机制
- 监听预配置表单变化
- 监听设置选择变化
- 实时生成统一链接
## 使用场景
### 1. **设备批量部署 + 环境配置**
```
https://classworks.example.com/?namespace=classroom-01&authCode=device01&autoExecute=true&config=eyJzZXJ2ZXIuZG9tYWluIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20ifQ==
```
- 自动认证为指定设备
- 自动配置服务器地址等设置
### 2. **演示环境快速部署**
```
https://classworks.example.com/?namespace=demo&autoExecute=true&config=eyJkaXNwbGF5LnRoZW1lIjoiZGFyayIsImVkaXQubW9kZSI6InJlYWRvbmx5In0=
```
- 自动认证为演示账号
- 自动应用演示环境设置
### 3. **培训环境标准化**
```
https://classworks.example.com/?namespace=training&authCode=train123&config=eyJkaXNwbGF5LnNob3dIZWxwIjp0cnVlLCJlZGl0LmVuYWJsZUd1aWRlIjp0cnVlfQ==
```
- 预配置培训账号
- 启用帮助和引导功能
## 安全考虑
### ✅ **默认安全**
- Token等敏感信息默认不包含
- 快速选择按钮智能排除敏感设置
- 明确的安全警告和提醒
### ✅ **用户控制**
- 用户仍可手动选择包含敏感设置
- 敏感设置有明确标记
- 提供充分的风险提示
### ✅ **传输安全**
- 建议HTTPS传输
- URL参数会被自动清理
- 设置信息经过Base64编码
## 兼容性
- ✅ 向后兼容现有的预配置功能
- ✅ 向后兼容现有的设置分享功能
- ✅ 新增统一链接格式
- ✅ 保持所有现有API接口
## 测试建议
1. **基础功能测试**
- 仅预配置信息的链接生成
- 预配置 + 设置的统一链接生成
- 链接复制和测试功能
2. **安全功能测试**
- 验证敏感设置默认不选中
- 验证敏感设置标记显示
- 验证安全提醒展示
3. **兼容性测试**
- 测试生成的链接是否正常工作
- 测试预配置认证是否正常
- 测试设置应用是否正常
## 优势总结
1. **用户体验**: 一个链接完成所有配置,无需多步操作
2. **部署效率**: 批量设备部署更加便捷
3. **管理简化**: 减少链接管理复杂度
4. **安全平衡**: 在便捷性和安全性之间找到平衡
5. **功能完整**: 涵盖认证和配置的完整解决方案

View File

@ -1,142 +1,356 @@
<template> <template>
<v-card border class="settings-link-generator mb-4"> <div>
<v-card-title class="text-h6"> <!-- 统一链接生成器卡片 -->
<v-icon start icon="mdi-link-variant" class="mr-2" /> <v-card border class="unified-link-generator">
设置分享 <v-card-title class="text-h6">
</v-card-title> <v-icon start icon="mdi-link-variant" class="mr-2" />
统一链接生成器
</v-card-title>
<v-card-text> <v-card-text>
<!-- 快速选择按钮 --> <div class="text-body-2 text-medium-emphasis mb-4">
<div class="d-flex mb-3 gap-2 flex-wrap"> 生成包含预配置认证信息和设置的统一链接可以同时预配置设备认证和应用设置
<v-btn </div>
size="small"
variant="tonal"
color="primary"
prepend-icon="mdi-select-all"
@click="selectAll"
>
全选
</v-btn>
<v-btn
size="small"
variant="tonal"
color="primary"
prepend-icon="mdi-server-network"
@click="selectDataSourceSettings"
>
数据源设置
</v-btn>
<v-btn
size="small"
variant="tonal"
color="primary"
prepend-icon="mdi-compare"
@click="selectChangedSettings"
>
已变更设置
</v-btn>
<v-btn <!-- 预配置认证信息部分 -->
size="small" <v-card variant="tonal" class="mb-4">
variant="tonal" <v-card-title class="text-subtitle-1">
color="error" <v-icon start>mdi-account-key</v-icon>
prepend-icon="mdi-select-remove" 预配置认证信息
@click="resetSelection" </v-card-title>
>
取消选择
</v-btn>
</div>
<!-- 选择摘要和链接 --> <v-card-text>
<div class="d-flex align-center mt-3 mb-3 flex-wrap gap-2"> <v-row>
<v-chip color="primary" class="mr-2"> <v-col cols="12" md="6">
已选 {{ selectedItems.length }} 项设置
</v-chip>
<template v-if="selectedItems.length > 0">
<v-chip
v-for="item in selectedItems"
:key="item"
size="small"
class="mr-1"
variant="text"
>
{{ getSettingDescription(item) }}
</v-chip>
</template>
</div>
<v-text-field
v-model="generatedLink"
label="生成的链接"
readonly
variant="outlined"
class="mb-2"
:append-inner-icon="linkCopied ? 'mdi-check' : 'mdi-content-copy'"
@click:append-inner="copyLink"
/>
<!-- 设置列表折叠面板 -->
<v-expansion-panels variant="accordion">
<v-expansion-panel>
<v-expansion-panel-title> 显示设置列表详情 </v-expansion-panel-title>
<v-expansion-panel-text>
<v-data-table
:items-per-page="settingItems.length"
:headers="headers"
:items="filteredItems"
item-value="key"
v-model="selectedItems"
show-select
density="compact"
class="rounded setting-table"
@update:selected="handleSelectionChange"
:sort-by="[{ key: 'isChanged', order: 'desc' }]"
>
<template v-slot:top>
<v-text-field <v-text-field
v-model="search" v-model="preconfigForm.namespace"
label="搜索设置" label="命名空间"
prepend-inner-icon="mdi-magnify" variant="outlined"
single-line prepend-inner-icon="mdi-identifier"
hide-details placeholder="例如: classroom-001"
class="mb-4" hint="设备的命名空间标识符"
></v-text-field> persistent-hint
</template> />
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="preconfigForm.authCode"
label="认证码"
variant="outlined"
prepend-inner-icon="mdi-lock-outline"
placeholder="设备认证码(可选)"
hint="留空则需要用户手动输入"
persistent-hint
/>
</v-col>
</v-row>
<template #[`item.description`]="{ item }"> <v-row class="mt-2">
<div class="d-flex align-center"> <v-col cols="12">
<v-icon size="small" :icon="item.icon" class="mr-2"></v-icon> <v-checkbox
{{ item.description }} v-model="preconfigForm.autoExecute"
</div> label="自动执行认证"
</template> hint="启用后会自动尝试认证,即使没有认证码也会尝试"
persistent-hint
<template #[`item.value`]="{ item }">
<span v-if="typeof item.value === 'boolean'">
{{ item.value ? "是" : "否" }}
</span>
<span v-else>{{ item.value }}</span>
</template>
<template #[`item.key`]="{ item }">
<span class="text-caption text-grey">{{ item.key }}</span>
</template>
<template #[`item.isChanged`]="{ item }">
<v-chip
size="x-small"
:color="item.isChanged ? 'warning' : 'success'"
:text="item.isChanged ? '已修改' : '默认'"
density="compact" density="compact"
/> />
</v-col>
</v-row>
<!-- 预配置信息预览 -->
<v-alert
v-if="preconfigForm.namespace"
type="info"
variant="tonal"
class="mt-3"
>
<div class="text-subtitle-2 mb-2">预配置信息</div>
<v-chip size="small" class="mr-2 mb-1">
<v-icon start size="small">mdi-identifier</v-icon>
命名空间: {{ preconfigForm.namespace }}
</v-chip>
<v-chip
v-if="preconfigForm.authCode"
size="small"
class="mr-2 mb-1"
color="warning"
>
<v-icon start size="small">mdi-lock</v-icon>
认证码: {{ preconfigForm.authCode.length > 8 ? preconfigForm.authCode.substring(0, 8) + "..." : preconfigForm.authCode }}
</v-chip>
<v-chip v-else size="small" class="mr-2 mb-1" color="grey">
<v-icon start size="small">mdi-lock-open</v-icon>
无认证码
</v-chip>
<v-chip
size="small"
class="mr-2 mb-1"
:color="preconfigForm.autoExecute ? 'success' : 'orange'"
>
<v-icon start size="small">{{
preconfigForm.autoExecute ? "mdi-play-circle" : "mdi-hand-back-left"
}}</v-icon>
{{ preconfigForm.autoExecute ? "自动认证" : "手动认证" }}
</v-chip>
</v-alert>
</v-card-text>
</v-card>
<!-- 设置分享部分 -->
<v-card variant="tonal" class="mb-4">
<v-card-title class="text-subtitle-1">
<v-icon start>mdi-cog-transfer</v-icon>
设置分享可选
</v-card-title>
<v-card-text>
<div class="text-body-2 text-medium-emphasis mb-3">
选择需要包含在链接中的设置项如果不选择任何设置将只生成预配置认证链接
</div>
<!-- 设置快速选择按钮 -->
<div class="d-flex mb-3 gap-2 flex-wrap">
<v-btn
size="small"
variant="tonal"
color="primary"
prepend-icon="mdi-server-network"
@click="selectDataSourceSettings"
>
数据源设置
</v-btn>
<v-btn
size="small"
variant="tonal"
color="primary"
prepend-icon="mdi-compare"
@click="selectChangedSettings"
>
已变更设置
</v-btn>
<v-btn
size="small"
variant="tonal"
color="success"
prepend-icon="mdi-select-all"
@click="selectAll"
>
全选
</v-btn>
<v-btn
size="small"
variant="tonal"
color="error"
prepend-icon="mdi-select-remove"
@click="resetSelection"
>
清除选择
</v-btn>
</div>
<!-- 选择摘要 -->
<div class="d-flex align-center mb-3 flex-wrap gap-2">
<v-chip color="primary" class="mr-2">
已选 {{ selectedItems.length }} 项设置
</v-chip>
<template v-if="selectedItems.length > 0">
<v-chip
v-for="item in selectedItems.slice(0, 3)"
:key="item"
size="small"
class="mr-1"
variant="text"
>
{{ getSettingDescription(item) }}
</v-chip>
<v-chip
v-if="selectedItems.length > 3"
size="small"
variant="text"
color="grey"
>
+{{ selectedItems.length - 3 }} 更多
</v-chip>
</template> </template>
</v-data-table> </div>
</v-expansion-panel-text>
</v-expansion-panel> <!-- 设置列表折叠面板 -->
</v-expansion-panels> <v-expansion-panels variant="accordion">
</v-card-text> <v-expansion-panel>
</v-card> <v-expansion-panel-title>
<template #default="{ expanded }">
<div class="d-flex align-center">
<v-icon class="mr-2">{{ expanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
显示设置列表详情
</div>
</template>
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-text-field
v-model="search"
label="搜索设置"
prepend-inner-icon="mdi-magnify"
single-line
hide-details
class="mb-4"
clearable
/>
<v-data-table
:items-per-page="settingItems.length"
:headers="headers"
:items="filteredItems"
item-value="key"
v-model="selectedItems"
show-select
density="compact"
class="rounded setting-table"
@update:selected="handleSelectionChange"
:sort-by="[{ key: 'isChanged', order: 'desc' }]"
>
<template #[`item.description`]="{ item }">
<div class="d-flex align-center">
<v-icon
size="small"
:icon="item.icon"
class="mr-2"
/>
{{ item.description }}
<v-chip
v-if="item.key === 'server.kvToken'"
size="x-small"
color="error"
class="ml-2"
>
敏感
</v-chip>
</div>
</template>
<template #[`item.value`]="{ item }">
<span v-if="typeof item.value === 'boolean'">
{{ item.value ? "是" : "否" }}
</span>
<span v-else-if="item.key === 'server.kvToken' && item.value">
{{ item.value.substring(0, 8) }}...
</span>
<span v-else>{{ item.value }}</span>
</template>
<template #[`item.key`]="{ item }">
<span class="text-caption text-grey">{{ item.key }}</span>
</template>
<template #[`item.isChanged`]="{ item }">
<v-chip
size="x-small"
:color="item.isChanged ? 'warning' : 'success'"
:text="item.isChanged ? '已修改' : '默认'"
density="compact"
/>
</template>
</v-data-table>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-card>
<!-- 链接生成和操作部分 -->
<v-card variant="outlined" class="mb-4">
<v-card-title class="text-subtitle-1">
<v-icon start>mdi-link</v-icon>
生成的统一链接
</v-card-title>
<v-card-text>
<!-- 操作按钮 -->
<div class="d-flex mb-3 gap-2 flex-wrap">
<v-btn
variant="flat"
color="primary"
prepend-icon="mdi-auto-fix"
@click="generateUnifiedLink"
:disabled="!preconfigForm.namespace.trim()"
>
生成统一链接
</v-btn>
<v-btn
variant="tonal"
color="success"
prepend-icon="mdi-test-tube"
@click="openTestLink"
:disabled="!unifiedLink"
>
测试链接
</v-btn>
<v-btn
variant="tonal"
color="error"
prepend-icon="mdi-delete"
@click="clearAll"
>
清空所有
</v-btn>
</div>
<!-- 生成的链接 -->
<v-text-field
v-model="unifiedLink"
label="统一链接"
readonly
variant="outlined"
class="mb-3"
:append-inner-icon="linkCopied ? 'mdi-check' : 'mdi-content-copy'"
@click:append-inner="copyUnifiedLink"
:placeholder="preconfigForm.namespace ? '点击「生成统一链接」按钮' : '请先输入命名空间'"
/>
<!-- 链接内容预览 -->
<v-alert
v-if="unifiedLink"
type="success"
variant="tonal"
class="mb-3"
>
<div class="text-subtitle-2 mb-2">链接包含内容</div>
<div class="d-flex flex-wrap gap-1">
<v-chip size="small" color="primary">
<v-icon start size="small">mdi-account-key</v-icon>
预配置认证
</v-chip>
<v-chip
v-if="selectedItems.length > 0"
size="small"
color="secondary"
>
<v-icon start size="small">mdi-cog</v-icon>
{{ selectedItems.length }} 项设置
</v-chip>
<v-chip v-else size="small" color="grey">
<v-icon start size="small">mdi-cog-off</v-icon>
无额外设置
</v-chip>
</div>
</v-alert>
</v-card-text>
</v-card>
<!-- 安全提醒 -->
<v-alert type="warning" variant="tonal">
<div class="text-subtitle-2 mb-2"> 安全提醒</div>
<ul class="text-body-2 pl-4">
<li>认证码和设置信息会在URL中传输请谨慎分发</li>
<li>建议仅在受信任的网络环境中使用</li>
<li>生产环境建议使用HTTPS协议</li>
<li>数据源设置和已变更设置默认不包含敏感Token信息</li>
</ul>
</v-alert>
</v-card-text>
</v-card>
</div>
</template> </template>
<script> <script>
@ -158,15 +372,22 @@ export default {
data() { data() {
return { return {
// //
selectedItems: [], selectedItems: [],
//
generatedLink: "", generatedLink: "",
//
linkCopied: false, linkCopied: false,
search: "", search: "",
//
preconfigForm: {
namespace: "",
authCode: "",
autoExecute: false,
},
//
unifiedLink: "",
headers: [ headers: [
{ title: "", key: "data-table-select" }, { title: "", key: "data-table-select" },
{ title: "设置项", key: "description", sortable: true }, { title: "设置项", key: "description", sortable: true },
@ -391,11 +612,14 @@ export default {
}, },
/** /**
* 选择数据源相关设置 * 选择数据源相关设置默认排除 server.kvToken
*/ */
selectDataSourceSettings() { selectDataSourceSettings() {
const dataSourceKeys = this.settingItems const dataSourceKeys = this.settingItems
.filter((item) => item.key.startsWith("server.")) .filter((item) =>
item.key.startsWith("server.") &&
item.key !== "server.kvToken" // Token
)
.map((item) => item.key); .map((item) => item.key);
this.selectedItems = dataSourceKeys; this.selectedItems = dataSourceKeys;
@ -403,11 +627,14 @@ export default {
}, },
/** /**
* 选择已修改的设置 * 选择已修改的设置默认排除 server.kvToken
*/ */
selectChangedSettings() { selectChangedSettings() {
const changedKeys = this.settingItems const changedKeys = this.settingItems
.filter((item) => item.isChanged) .filter((item) =>
item.isChanged &&
item.key !== "server.kvToken" // Token
)
.map((item) => item.key); .map((item) => item.key);
this.selectedItems = changedKeys; this.selectedItems = changedKeys;
@ -443,16 +670,154 @@ export default {
const setting = this.settingItems.find((item) => item.key === key); const setting = this.settingItems.find((item) => item.key === key);
return setting ? setting.description : key; return setting ? setting.description : key;
}, },
// ===== =====
/**
* 生成包含预配置信息和设置的统一链接
*/
generateUnifiedLink() {
if (!this.preconfigForm.namespace.trim()) {
return;
}
try {
const baseUrl = `${window.location.protocol}//${window.location.host}/`;
const params = new URLSearchParams();
//
params.append("namespace", this.preconfigForm.namespace.trim());
if (this.preconfigForm.authCode.trim()) {
params.append("authCode", this.preconfigForm.authCode.trim());
}
if (this.preconfigForm.autoExecute) {
params.append("autoExecute", "true");
}
//
if (this.selectedItems.length > 0) {
const allSettings = exportSettingsAsKeyValue();
const configObj = {};
for (const key of this.selectedItems) {
configObj[key] = allSettings[key];
}
// JSONbase64
const jsonString = JSON.stringify(configObj);
const utf8Encoder = new TextEncoder();
const utf8Bytes = utf8Encoder.encode(jsonString);
const base64String = btoa(
Array.from(utf8Bytes)
.map((byte) => String.fromCharCode(byte))
.join("")
);
params.append("config", base64String);
}
// URL
this.unifiedLink = `${baseUrl}?${params.toString()}`;
this.linkCopied = false;
console.log("生成统一链接:", this.unifiedLink);
console.log("包含预配置:", !!this.preconfigForm.namespace);
console.log("包含设置数量:", this.selectedItems.length);
} catch (error) {
console.error("生成统一链接失败:", error);
this.unifiedLink = "链接生成失败,请重试";
}
},
/**
* 复制统一链接到剪贴板
*/
async copyUnifiedLink() {
if (!this.unifiedLink) {
this.generateUnifiedLink();
}
if (!this.unifiedLink || this.unifiedLink.includes("失败")) {
return;
}
try {
await navigator.clipboard.writeText(this.unifiedLink);
this.linkCopied = true;
// 3
setTimeout(() => {
this.linkCopied = false;
}, 3000);
} catch (error) {
console.error("复制统一链接失败:", error);
}
},
/**
* 在新窗口中测试统一链接
*/
openTestLink() {
if (this.unifiedLink && !this.unifiedLink.includes("失败")) {
window.open(this.unifiedLink, "_blank");
}
},
/**
* 清空所有数据
*/
clearAll() {
this.preconfigForm = {
namespace: "",
authCode: "",
autoExecute: false,
};
this.selectedItems = [];
this.unifiedLink = "";
this.generatedLink = "";
this.linkCopied = false;
},
}, },
watch: { watch: {
// //
selectedItems: { selectedItems: {
handler() { handler() {
this.autoGenerateLink(); if (this.preconfigForm.namespace.trim()) {
this.generateUnifiedLink();
}
}, },
deep: true, deep: true,
}, },
//
"preconfigForm.namespace": {
handler() {
if (this.preconfigForm.namespace.trim()) {
this.generateUnifiedLink();
} else {
this.unifiedLink = "";
}
},
},
"preconfigForm.authCode": {
handler() {
if (this.preconfigForm.namespace.trim()) {
this.generateUnifiedLink();
}
},
},
"preconfigForm.autoExecute": {
handler() {
if (this.preconfigForm.namespace.trim()) {
this.generateUnifiedLink();
}
},
},
}, },
}; };
</script> </script>

View File

@ -376,7 +376,7 @@ export default {
value: "student", value: "student",
}, },
{ {
title: "分享设置", title: "预配链接",
icon: "mdi-share", icon: "mdi-share",
value: "share", value: "share",
}, },

View File

@ -232,23 +232,11 @@ export default {
// 迁移失败不影响URL生成继续执行 // 迁移失败不影响URL生成继续执行
} }
} }
// 获取认证token
const authtoken = getSetting("server.kvToken");
// 构建云端访问URL // 构建云端访问URL
let url = `${serverUrl}/${machineId}/${key}`; let url = `${serverUrl}/kv/${key}?token=${authtoken}`;
// 根据网站验证情况添加token参数
const namespaceInfo = await kvServerProvider.loadNamespaceInfo();
if (namespaceInfo && namespaceInfo.success !== false) {
const { accessType } = namespaceInfo;
// 如果是私有访问添加token参数
if (accessType === 'private' && siteKey) {
const urlObj = new URL(url);
urlObj.searchParams.set('token', siteKey);
url = urlObj.toString();
}
// 公开或受保护访问不需要token参数
}
return { return {
success: true, success: true,