mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-07-02 00:59:23 +00:00
Add js-base64 library for Base64 encoding/decoding and enhance password management features. Update axios.js to use Base64 for encoding site key and namespace password. Implement password hint functionality in NamespaceAccess and NamespaceSettingsCard components, including dialogs for setting and verifying password hints. Refactor kvServerProvider to support password hint updates during password management operations.
This commit is contained in:
parent
c42c878ac8
commit
31cff8a867
7711
package-lock.json
generated
Normal file
7711
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,7 @@
|
|||||||
"@microsoft/clarity": "^1.0.0",
|
"@microsoft/clarity": "^1.0.0",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"idb": "^8.0.2",
|
"idb": "^8.0.2",
|
||||||
|
"js-base64": "^3.7.7",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"pinyin-pro": "^3.26.0",
|
"pinyin-pro": "^3.26.0",
|
||||||
"ratelimit-header-parser": "^0.1.0",
|
"ratelimit-header-parser": "^0.1.0",
|
||||||
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -20,6 +20,9 @@ importers:
|
|||||||
idb:
|
idb:
|
||||||
specifier: ^8.0.2
|
specifier: ^8.0.2
|
||||||
version: 8.0.2
|
version: 8.0.2
|
||||||
|
js-base64:
|
||||||
|
specifier: ^3.7.7
|
||||||
|
version: 3.7.7
|
||||||
js-yaml:
|
js-yaml:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@ -2305,6 +2308,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
js-base64@3.7.7:
|
||||||
|
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@ -5887,6 +5893,8 @@ snapshots:
|
|||||||
|
|
||||||
jiti@2.4.2: {}
|
jiti@2.4.2: {}
|
||||||
|
|
||||||
|
js-base64@3.7.7: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-tokens@9.0.1: {}
|
js-tokens@9.0.1: {}
|
||||||
|
@ -2,6 +2,8 @@ import axios from "axios";
|
|||||||
import { getSetting } from "@/utils/settings";
|
import { getSetting } from "@/utils/settings";
|
||||||
import { parseRateLimit } from "ratelimit-header-parser";
|
import { parseRateLimit } from "ratelimit-header-parser";
|
||||||
import RateLimitModal from "@/components/RateLimitModal.vue";
|
import RateLimitModal from "@/components/RateLimitModal.vue";
|
||||||
|
import { Base64 } from "js-base64";
|
||||||
|
|
||||||
// 基本配置
|
// 基本配置
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
// 可以在这里添加基础配置,例如超时时间等
|
// 可以在这里添加基础配置,例如超时时间等
|
||||||
@ -14,13 +16,14 @@ axiosInstance.interceptors.request.use(
|
|||||||
// 确保每次请求时都获取最新的 siteKey
|
// 确保每次请求时都获取最新的 siteKey
|
||||||
const siteKey = getSetting("server.siteKey");
|
const siteKey = getSetting("server.siteKey");
|
||||||
if (siteKey) {
|
if (siteKey) {
|
||||||
requestConfig.headers["x-site-key"] = siteKey;
|
requestConfig.headers["x-site-key"] = Base64.encode(siteKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动添加命名空间密码
|
// 自动添加命名空间密码
|
||||||
const namespacePassword = getSetting("namespace.password");
|
const namespacePassword = getSetting("namespace.password");
|
||||||
if (namespacePassword) {
|
if (namespacePassword) {
|
||||||
requestConfig.headers["x-namespace-password"] = namespacePassword;
|
requestConfig.headers["x-namespace-password"] =
|
||||||
|
Base64.encode(namespacePassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestConfig;
|
return requestConfig;
|
||||||
|
@ -26,9 +26,12 @@
|
|||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title class="text-h6">输入访问密码</v-card-title>
|
<v-card-title class="text-h6">输入访问密码</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
|
<div v-if="passwordHint" class="text-body-2 mb-4">
|
||||||
|
<v-icon icon="mdi-lightbulb-outline" color="warning" class="mr-1" />
|
||||||
|
提示:{{ passwordHint }}
|
||||||
|
</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="password"
|
v-model="password"
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
label="密码"
|
label="密码"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
:error="!!error"
|
:error="!!error"
|
||||||
@ -67,6 +70,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getSetting, setSetting } from "@/utils/settings";
|
import { getSetting, setSetting } from "@/utils/settings";
|
||||||
import axios from "@/axios/axios";
|
import axios from "@/axios/axios";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NamespaceAccess",
|
name: "NamespaceAccess",
|
||||||
data() {
|
data() {
|
||||||
@ -78,6 +82,7 @@ export default {
|
|||||||
showPassword: false,
|
showPassword: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
accessType: "PUBLIC", // 默认为公开访问
|
accessType: "PUBLIC", // 默认为公开访问
|
||||||
|
passwordHint: null, // 密码提示
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@ -97,6 +102,8 @@ export default {
|
|||||||
["PRIVATE", "PROTECTED", "PUBLIC"].includes(response.data.accessType)
|
["PRIVATE", "PROTECTED", "PUBLIC"].includes(response.data.accessType)
|
||||||
) {
|
) {
|
||||||
this.accessType = response.data.accessType;
|
this.accessType = response.data.accessType;
|
||||||
|
// 保存密码提示
|
||||||
|
this.passwordHint = response.data.passwordHint || null;
|
||||||
} else {
|
} else {
|
||||||
//this.$router.push("/settings");
|
//this.$router.push("/settings");
|
||||||
return;
|
return;
|
||||||
@ -132,11 +139,11 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const uuid = getSetting("device.uuid");
|
const uuid = getSetting("device.uuid");
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${getSetting("server.domain")}/${uuid}/_check`,
|
`${getSetting("server.domain")}/${uuid}/_checkpassword`,
|
||||||
{ password }
|
{ password }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.data || response.data.success === false) {
|
if (response.status != 200) {
|
||||||
throw new Error(response.data?.error?.message || "密码错误");
|
throw new Error(response.data?.error?.message || "密码错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,3 +192,11 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.namespace-access {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -102,19 +102,32 @@
|
|||||||
</template>
|
</template>
|
||||||
<v-card-title>访问密码</v-card-title>
|
<v-card-title>访问密码</v-card-title>
|
||||||
<v-card-subtitle class="mt-2">
|
<v-card-subtitle class="mt-2">
|
||||||
设置访问密码以保护数据安全,留空表示无需密码
|
设置访问密码以保护数据安全,可以将老师、电教的名字、学号等作为密码
|
||||||
</v-card-subtitle>
|
</v-card-subtitle>
|
||||||
</v-card-item>
|
</v-card-item>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-form ref="passwordForm" @submit.prevent="savePassword">
|
<v-form ref="passwordForm" @submit.prevent="savePassword">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
|
v-if="namespaceInfo.hasPassword"
|
||||||
|
v-model="passwordForm.oldPassword"
|
||||||
|
label="当前密码"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
class="mb-4"
|
||||||
|
:loading="passwordLoading"
|
||||||
|
:rules="[(v) => !!v || '请输入当前密码']"
|
||||||
|
>
|
||||||
|
<template #prepend-inner>
|
||||||
|
<v-icon icon="mdi-lock" />
|
||||||
|
</template>
|
||||||
|
</v-text-field><v-text-field
|
||||||
v-model="passwordForm.newPassword"
|
v-model="passwordForm.newPassword"
|
||||||
label="新密码"
|
label="新密码"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
hide-details="auto"
|
hide-details="auto"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
type="password"
|
|
||||||
:loading="passwordLoading"
|
:loading="passwordLoading"
|
||||||
>
|
>
|
||||||
<template #prepend-inner>
|
<template #prepend-inner>
|
||||||
@ -122,14 +135,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="passwordForm.confirmPassword"
|
v-model="passwordForm.confirmPassword"
|
||||||
label="确认新密码"
|
label="确认新密码"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
hide-details="auto"
|
hide-details="auto"
|
||||||
class="mb-6"
|
class="mb-4"
|
||||||
type="password"
|
|
||||||
:loading="passwordLoading"
|
:loading="passwordLoading"
|
||||||
:rules="[
|
:rules="[
|
||||||
(v) =>
|
(v) =>
|
||||||
@ -144,19 +158,33 @@
|
|||||||
</v-text-field>
|
</v-text-field>
|
||||||
|
|
||||||
<div class="d-flex justify-space-between align-center">
|
<div class="d-flex justify-space-between align-center">
|
||||||
<v-btn
|
<div>
|
||||||
v-if="namespaceInfo.hasPassword"
|
<v-btn
|
||||||
color="error"
|
v-if="namespaceInfo.hasPassword"
|
||||||
variant="tonal"
|
color="error"
|
||||||
:loading="passwordLoading"
|
variant="tonal"
|
||||||
@click="showDeleteConfirm = true"
|
:loading="passwordLoading"
|
||||||
>
|
@click="confirmDeletePassword"
|
||||||
删除密码
|
class="mr-2"
|
||||||
<template #prepend>
|
>
|
||||||
<v-icon icon="mdi-lock-remove" />
|
删除密码
|
||||||
</template>
|
<template #prepend>
|
||||||
</v-btn>
|
<v-icon icon="mdi-lock-remove" />
|
||||||
<v-spacer v-else />
|
</template>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
v-if="namespaceInfo.hasPassword"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
:loading="hintLoading"
|
||||||
|
@click="openHintDialog"
|
||||||
|
>
|
||||||
|
设置密码提示
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-lightbulb-outline" />
|
||||||
|
</template>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -171,13 +199,63 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-form>
|
</v-form>
|
||||||
<setting-item
|
<!--<setting-item
|
||||||
setting-key="namespace.password"
|
setting-key="namespace.password"
|
||||||
title="访问密码"
|
title="访问密码"
|
||||||
></setting-item>
|
></setting-item>-->
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 密码提示设置对话框 -->
|
||||||
|
<v-dialog v-model="showHintDialog" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-item>
|
||||||
|
<v-card-title>设置密码提示</v-card-title>
|
||||||
|
<v-card-subtitle class="mt-2">
|
||||||
|
设置一个提示帮助记忆密码
|
||||||
|
</v-card-subtitle>
|
||||||
|
</v-card-item>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="passwordHintForm.hint"
|
||||||
|
label="密码提示"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
class="mb-4"
|
||||||
|
:loading="hintLoading"
|
||||||
|
placeholder="例如:我的生日"
|
||||||
|
>
|
||||||
|
<template #prepend-inner>
|
||||||
|
<v-icon icon="mdi-lightbulb-outline" />
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
当前提示:{{ namespaceInfo.passwordHint || "未设置" }}
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="grey-darken-1"
|
||||||
|
variant="text"
|
||||||
|
@click="showHintDialog = false"
|
||||||
|
:disabled="hintLoading"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="text"
|
||||||
|
:loading="hintLoading"
|
||||||
|
@click="savePasswordHint"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
<!-- 删除密码确认对话框 -->
|
<!-- 删除密码确认对话框 -->
|
||||||
<v-dialog v-model="showDeleteConfirm" max-width="400">
|
<v-dialog v-model="showDeleteConfirm" max-width="400">
|
||||||
<v-card>
|
<v-card>
|
||||||
@ -208,6 +286,56 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 密码验证对话框 -->
|
||||||
|
<v-dialog v-model="showVerifyDialog" max-width="400" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-item>
|
||||||
|
<v-card-title>验证密码</v-card-title>
|
||||||
|
<v-card-subtitle class="mt-2">
|
||||||
|
请输入当前密码以继续操作
|
||||||
|
</v-card-subtitle>
|
||||||
|
</v-card-item>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="verifyForm.password"
|
||||||
|
label="当前密码"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
class="mb-4"
|
||||||
|
:loading="verifyLoading"
|
||||||
|
:error="!!verifyForm.error"
|
||||||
|
:error-messages="verifyForm.error"
|
||||||
|
@keyup.enter="verifyPassword"
|
||||||
|
>
|
||||||
|
<template #prepend-inner>
|
||||||
|
<v-icon icon="mdi-lock" />
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="grey-darken-1"
|
||||||
|
variant="text"
|
||||||
|
@click="cancelVerify"
|
||||||
|
:disabled="verifyLoading"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="text"
|
||||||
|
:loading="verifyLoading"
|
||||||
|
:disabled="!verifyForm.password"
|
||||||
|
@click="verifyPassword"
|
||||||
|
>
|
||||||
|
确认
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
<v-snackbar
|
<v-snackbar
|
||||||
v-model="showSnackbar"
|
v-model="showSnackbar"
|
||||||
:timeout="3000"
|
:timeout="3000"
|
||||||
@ -226,18 +354,38 @@
|
|||||||
import SettingsCard from "@/components/SettingsCard.vue";
|
import SettingsCard from "@/components/SettingsCard.vue";
|
||||||
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
import { kvServerProvider } from "@/utils/providers/kvServerProvider";
|
||||||
import { getSetting } from "@/utils/settings";
|
import { getSetting } from "@/utils/settings";
|
||||||
import SettingItem from "@/components/settings/SettingItem.vue";
|
import axios from "@/axios/axios";
|
||||||
|
|
||||||
|
// Helper function to get request headers
|
||||||
|
const getHeaders = () => {
|
||||||
|
const headers = { Accept: "application/json" };
|
||||||
|
const siteKey = getSetting("server.siteKey");
|
||||||
|
const password = getSetting("namespace.password");
|
||||||
|
|
||||||
|
if (siteKey) {
|
||||||
|
headers["x-site-key"] = siteKey;
|
||||||
|
}
|
||||||
|
if (password) {
|
||||||
|
headers["x-namespace-password"] = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "NamespaceSettingsCard",
|
name: "NamespaceSettingsCard",
|
||||||
components: { SettingsCard, SettingItem },
|
components: { SettingsCard },
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
passwordLoading: false,
|
passwordLoading: false,
|
||||||
|
hintLoading: false,
|
||||||
|
verifyLoading: false,
|
||||||
showSnackbar: false,
|
showSnackbar: false,
|
||||||
showDeleteConfirm: false,
|
showDeleteConfirm: false,
|
||||||
|
showHintDialog: false,
|
||||||
|
showVerifyDialog: false,
|
||||||
snackbarText: "",
|
snackbarText: "",
|
||||||
snackbarColor: "success",
|
snackbarColor: "success",
|
||||||
namespaceInfo: {
|
namespaceInfo: {
|
||||||
@ -245,6 +393,7 @@ export default {
|
|||||||
name: "",
|
name: "",
|
||||||
accessType: "PUBLIC",
|
accessType: "PUBLIC",
|
||||||
hasPassword: false,
|
hasPassword: false,
|
||||||
|
passwordHint: null,
|
||||||
},
|
},
|
||||||
namespaceForm: {
|
namespaceForm: {
|
||||||
name: "",
|
name: "",
|
||||||
@ -252,8 +401,18 @@ export default {
|
|||||||
},
|
},
|
||||||
passwordForm: {
|
passwordForm: {
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
|
oldPassword: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
},
|
},
|
||||||
|
passwordHintForm: {
|
||||||
|
hint: "",
|
||||||
|
},
|
||||||
|
verifyForm: {
|
||||||
|
password: "",
|
||||||
|
error: "",
|
||||||
|
action: null, // 'delete' | 'hint'
|
||||||
|
onSuccess: null,
|
||||||
|
},
|
||||||
originalForm: {
|
originalForm: {
|
||||||
name: "",
|
name: "",
|
||||||
accessType: "PUBLIC",
|
accessType: "PUBLIC",
|
||||||
@ -292,14 +451,17 @@ export default {
|
|||||||
if (!this.passwordForm.newPassword) {
|
if (!this.passwordForm.newPassword) {
|
||||||
return true; // 允许清空密码
|
return true; // 允许清空密码
|
||||||
}
|
}
|
||||||
return (
|
const isConfirmMatch = this.passwordForm.newPassword === this.passwordForm.confirmPassword;
|
||||||
this.passwordForm.newPassword === this.passwordForm.confirmPassword
|
if (this.namespaceInfo.hasPassword) {
|
||||||
);
|
return isConfirmMatch && !!this.passwordForm.oldPassword;
|
||||||
|
}
|
||||||
|
return isConfirmMatch;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await this.loadNamespaceInfo();
|
await this.loadNamespaceInfo();
|
||||||
|
await this.loadPasswordHint();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -311,6 +473,7 @@ export default {
|
|||||||
this.namespaceInfo = response.data;
|
this.namespaceInfo = response.data;
|
||||||
this.namespaceForm.name = response.data.name;
|
this.namespaceForm.name = response.data.name;
|
||||||
this.namespaceForm.accessType = response.data.accessType;
|
this.namespaceForm.accessType = response.data.accessType;
|
||||||
|
this.passwordForm.passwordHint = response.data.passwordHint || "";
|
||||||
// 保存原始值用于比较
|
// 保存原始值用于比较
|
||||||
this.originalForm = { ...this.namespaceForm };
|
this.originalForm = { ...this.namespaceForm };
|
||||||
}
|
}
|
||||||
@ -361,56 +524,164 @@ export default {
|
|||||||
|
|
||||||
this.passwordLoading = true;
|
this.passwordLoading = true;
|
||||||
try {
|
try {
|
||||||
const oldPassword = getSetting("namespace.password");
|
|
||||||
const response = await kvServerProvider.updatePassword(
|
const response = await kvServerProvider.updatePassword(
|
||||||
this.passwordForm.newPassword || null, // 如果为空字符串则发送null
|
this.passwordForm.newPassword || null,
|
||||||
oldPassword
|
this.passwordForm.oldPassword || null
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.namespaceInfo.hasPassword = !!this.passwordForm.newPassword;
|
this.namespaceInfo.hasPassword = !!this.passwordForm.newPassword;
|
||||||
this.passwordForm = {
|
this.passwordForm = {
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
|
oldPassword: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
};
|
};
|
||||||
this.showSuccess("密码已更新");
|
this.showSuccess("密码已更新");
|
||||||
this.$router.push("/");
|
this.$router.push("/");
|
||||||
} else {
|
} else {
|
||||||
console.log(response);
|
throw new Error(response.error?.message || "保存失败");
|
||||||
throw new Error(response.error.message || "保存失败 #1");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("保存密码失败:", error);
|
console.error("保存密码失败:", error);
|
||||||
this.showError(error.message || "保存密码失败");
|
this.showError(error.response?.data?.message || "保存密码失败");
|
||||||
} finally {
|
} finally {
|
||||||
this.passwordLoading = false;
|
this.passwordLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async confirmDeletePassword() {
|
||||||
|
this.verifyForm = {
|
||||||
|
password: "",
|
||||||
|
error: "",
|
||||||
|
action: "delete",
|
||||||
|
onSuccess: () => {
|
||||||
|
this.showDeleteConfirm = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.showVerifyDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
async deletePassword() {
|
async deletePassword() {
|
||||||
this.passwordLoading = true;
|
this.passwordLoading = true;
|
||||||
try {
|
try {
|
||||||
const response = await kvServerProvider.deletePassword();
|
const response = await kvServerProvider.deletePassword();
|
||||||
|
|
||||||
if (response.status == 200) {
|
if (response.status === 200) {
|
||||||
this.namespaceInfo.hasPassword = false;
|
this.namespaceInfo.hasPassword = false;
|
||||||
|
this.namespaceInfo.passwordHint = null;
|
||||||
this.passwordForm = {
|
this.passwordForm = {
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
|
oldPassword: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
};
|
};
|
||||||
this.showDeleteConfirm = false;
|
this.showDeleteConfirm = false;
|
||||||
this.showSuccess("密码已删除");
|
this.showSuccess("密码已删除");
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.error.message || "删除失败");
|
throw new Error(response.error?.message || "删除失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("删除密码失败:", error);
|
console.error("删除密码失败:", error);
|
||||||
this.showError(error.message || "删除密码失败");
|
this.showError(error.response?.data?.message || "删除密码失败");
|
||||||
} finally {
|
} finally {
|
||||||
this.passwordLoading = false;
|
this.passwordLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async loadPasswordHint() {
|
||||||
|
try {
|
||||||
|
const serverUrl = getSetting("server.domain");
|
||||||
|
const machineId = getSetting("device.uuid");
|
||||||
|
const response = await axios.get(
|
||||||
|
`${serverUrl}/${machineId}/_hint`,
|
||||||
|
{ headers: getHeaders() }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data && response.data.passwordHint !== undefined) {
|
||||||
|
this.namespaceInfo.passwordHint = response.data.passwordHint;
|
||||||
|
this.passwordHintForm.hint = response.data.passwordHint || "";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载密码提示失败:", error);
|
||||||
|
this.showError("加载密码提示失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async savePasswordHint() {
|
||||||
|
this.hintLoading = true;
|
||||||
|
try {
|
||||||
|
const serverUrl = getSetting("server.domain");
|
||||||
|
const machineId = getSetting("device.uuid");
|
||||||
|
const response = await axios.put(
|
||||||
|
`${serverUrl}/${machineId}/_hint`,
|
||||||
|
{ hint: this.passwordHintForm.hint || null },
|
||||||
|
{ headers: getHeaders() }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
this.namespaceInfo.passwordHint = response.data.passwordHint;
|
||||||
|
this.showSuccess("密码提示已更新");
|
||||||
|
this.showHintDialog = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("保存密码提示失败:", error);
|
||||||
|
this.showError(error.response?.data?.message || "保存密码提示失败");
|
||||||
|
} finally {
|
||||||
|
this.hintLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async openHintDialog() {
|
||||||
|
this.verifyForm = {
|
||||||
|
password: "",
|
||||||
|
error: "",
|
||||||
|
action: "hint",
|
||||||
|
onSuccess: () => {
|
||||||
|
this.showHintDialog = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.showVerifyDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelVerify() {
|
||||||
|
this.showVerifyDialog = false;
|
||||||
|
this.verifyForm = {
|
||||||
|
password: "",
|
||||||
|
error: "",
|
||||||
|
action: null,
|
||||||
|
onSuccess: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async verifyPassword() {
|
||||||
|
if (!this.verifyForm.password) return;
|
||||||
|
|
||||||
|
this.verifyLoading = true;
|
||||||
|
this.verifyForm.error = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${getSetting("server.domain")}/${getSetting("device.uuid")}/_checkpassword`,
|
||||||
|
{ password: this.verifyForm.password },
|
||||||
|
{ headers: getHeaders() }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status == 200) {
|
||||||
|
// 验证成功,执行对应操作
|
||||||
|
this.showVerifyDialog = false;
|
||||||
|
if (this.verifyForm.onSuccess) {
|
||||||
|
this.verifyForm.onSuccess();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.verifyForm.error = "密码错误";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("密码验证失败:", error);
|
||||||
|
this.verifyForm.error = "密码验证失败";
|
||||||
|
} finally {
|
||||||
|
this.verifyLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
showSuccess(message) {
|
showSuccess(message) {
|
||||||
this.snackbarColor = "success";
|
this.snackbarColor = "success";
|
||||||
this.snackbarText = message;
|
this.snackbarText = message;
|
||||||
|
@ -649,6 +649,7 @@ import "../styles/transitions.scss"; // 添加新的样式导入
|
|||||||
import "../styles/global.scss";
|
import "../styles/global.scss";
|
||||||
import { pinyin } from "pinyin-pro";
|
import { pinyin } from "pinyin-pro";
|
||||||
import { debounce, throttle } from "@/utils/debounce";
|
import { debounce, throttle } from "@/utils/debounce";
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Classworks 作业板",
|
name: "Classworks 作业板",
|
||||||
@ -1815,9 +1816,12 @@ export default {
|
|||||||
if (!configParam) return false;
|
if (!configParam) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 解码base64并解析JSON
|
// 解码base64为二进制字符串
|
||||||
// 使用更安全的base64解码方式,确保支持UTF-8字符(包括中文)
|
const binaryString = atob(configParam);
|
||||||
const decodedString = this.safeBase64Decode(configParam);
|
// 将二进制字符串转换为Uint8Array
|
||||||
|
const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0));
|
||||||
|
// 将Uint8Array解码为UTF-8字符串
|
||||||
|
const decodedString = new TextDecoder().decode(bytes);
|
||||||
const decodedConfig = JSON.parse(decodedString);
|
const decodedConfig = JSON.parse(decodedString);
|
||||||
console.log("从URL读取配置:", decodedConfig);
|
console.log("从URL读取配置:", decodedConfig);
|
||||||
|
|
||||||
@ -2067,30 +2071,7 @@ export default {
|
|||||||
// 安全的Base64解码函数,支持UTF-8字符(包括中文)
|
// 安全的Base64解码函数,支持UTF-8字符(包括中文)
|
||||||
safeBase64Decode(base64String) {
|
safeBase64Decode(base64String) {
|
||||||
try {
|
try {
|
||||||
// 替换URL安全base64中的特殊字符
|
return Base64.decode(base64String);
|
||||||
const normalizedString = base64String
|
|
||||||
.replace(/-/g, "+")
|
|
||||||
.replace(/_/g, "/");
|
|
||||||
|
|
||||||
// 添加填充字符
|
|
||||||
const paddedString = normalizedString.padEnd(
|
|
||||||
normalizedString.length +
|
|
||||||
((4 - (normalizedString.length % 4 || 4)) % 4),
|
|
||||||
"="
|
|
||||||
);
|
|
||||||
|
|
||||||
// 解码base64为二进制字符串
|
|
||||||
const binaryString = atob(paddedString);
|
|
||||||
|
|
||||||
// 将二进制字符串转换为Uint8Array
|
|
||||||
const bytes = new Uint8Array(binaryString.length);
|
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将Uint8Array解码为UTF-8字符串
|
|
||||||
const decoder = new TextDecoder("utf-8");
|
|
||||||
return decoder.decode(bytes);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Base64解码错误:", e);
|
console.error("Base64解码错误:", e);
|
||||||
throw new Error("无法解码配置数据");
|
throw new Error("无法解码配置数据");
|
||||||
|
@ -70,7 +70,7 @@ export const kvServerProvider = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async updatePassword(newPassword, oldPassword) {
|
async updatePassword(newPassword, oldPassword, passwordHint = null) {
|
||||||
try {
|
try {
|
||||||
const serverUrl = getSetting("server.domain");
|
const serverUrl = getSetting("server.domain");
|
||||||
const machineId = getSetting("device.uuid");
|
const machineId = getSetting("device.uuid");
|
||||||
@ -78,19 +78,21 @@ export const kvServerProvider = {
|
|||||||
const res = await axios.post(
|
const res = await axios.post(
|
||||||
`${serverUrl}/${machineId}/_password`,
|
`${serverUrl}/${machineId}/_password`,
|
||||||
{
|
{
|
||||||
newPassword,
|
password: newPassword,
|
||||||
oldPassword,
|
oldPassword,
|
||||||
|
passwordHint,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: getHeaders(),
|
headers: getHeaders(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.status == 200) {
|
if (res.status === 200) {
|
||||||
setSetting("namespace.password", newPassword);
|
// 更新本地存储的密码
|
||||||
|
setSetting("namespace.password", newPassword || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatResponse(res);
|
return res;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return formatError(
|
return formatError(
|
||||||
error.response?.data?.message || "更新密码失败",
|
error.response?.data?.message || "更新密码失败",
|
||||||
@ -107,12 +109,8 @@ export const kvServerProvider = {
|
|||||||
const res = await axios.delete(`${serverUrl}/${machineId}/_password`, {
|
const res = await axios.delete(`${serverUrl}/${machineId}/_password`, {
|
||||||
headers: getHeaders(),
|
headers: getHeaders(),
|
||||||
});
|
});
|
||||||
|
setSetting("namespace.password", "");
|
||||||
if (res.status === 200) {
|
return res;
|
||||||
setSetting("namespace.password", null); // 清除本地存储的密码
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatResponse(res);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return formatError(
|
return formatError(
|
||||||
error.response?.data?.message || "删除密码失败",
|
error.response?.data?.message || "删除密码失败",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user