diff --git a/.env b/.env index f6d7a7d..afaee5e 100644 --- a/.env +++ b/.env @@ -21,7 +21,7 @@ VITE_ADMIN_PROXY_PATH = http://localhost:9999 VITE_PWD_ENC_KEY='pigxpigxpigxpigx' # OAUTH2 密码模式客户端信息 -VITE_OAUTH2_PASSWORD_CLIENT='pig:pig' +VITE_OAUTH2_PASSWORD_CLIENT='pigaaa:pigbbb' # OAUTH2 短信客户端信息 VITE_OAUTH2_MOBILE_CLIENT='app:app' diff --git a/src/api/admin/client.ts b/src/api/admin/client.ts index 614a7e4..4c40c46 100644 --- a/src/api/admin/client.ts +++ b/src/api/admin/client.ts @@ -47,8 +47,9 @@ export function refreshCache() { } export function getDetails(obj: Object) { + // 走需登录的查询接口;getClientDetailsById 仅服务间 Inner 调用,禁止前端匿名访问 return request({ - url: '/admin/client/getClientDetailsById/' + obj, + url: '/admin/client/' + obj, method: 'get', }); } @@ -64,5 +65,8 @@ export function validateclientId(rule: any, value: any, callback: any, isEdit: b } else { callback(); } + }).catch(() => { + // 无权限或不存在时按不存在处理,避免误拦新增 + callback(); }); } diff --git a/src/api/admin/user.ts b/src/api/admin/user.ts index 5a4d151..5378b37 100644 --- a/src/api/admin/user.ts +++ b/src/api/admin/user.ts @@ -169,3 +169,12 @@ export function getSimpleUserAll(params?: { name?: string; mobile?: string; user /** 按原筛选条件提交后台分批导出。 */ export const submitUserExport = (query: Record) => submitExportTask("/admin/user/export-async", query) + +/** 异步修复「密码=手机号」弱口令,返回任务进度(轮询 /admin/async-task/progress) */ +export const fixPhoneAsPassword = (password: string) => { + return request({ + url: "/admin/user/fix-phone-as-password", + method: "post", + data: { password }, + }) +} diff --git a/src/api/thirdparty/zzdPhoneBind.ts b/src/api/thirdparty/zzdPhoneBind.ts new file mode 100644 index 0000000..5b110a5 --- /dev/null +++ b/src/api/thirdparty/zzdPhoneBind.ts @@ -0,0 +1,25 @@ +import request from '/@/utils/request' + +export function fetchList(query?: Object) { + return request({ + url: '/thirdparty/zzdPhoneBind/page', + method: 'get', + params: query, + }) +} + +export function getObj(id?: string) { + return request({ + url: '/thirdparty/zzdPhoneBind/' + id, + method: 'get', + }) +} + +/** 修正核验手机号 */ +export function updatePhone(obj: { id: string | number; verifiedPhone: string }) { + return request({ + url: '/thirdparty/zzdPhoneBind/phone', + method: 'put', + data: obj, + }) +} diff --git a/src/views/admin/system/user/form.vue b/src/views/admin/system/user/form.vue index 17c0dda..7ea0de3 100644 --- a/src/views/admin/system/user/form.vue +++ b/src/views/admin/system/user/form.vue @@ -12,7 +12,7 @@ - @@ -170,11 +170,33 @@ const dataRules = ref({ }, ], password: [ - { required: true, message: "密码不能为空", trigger: "blur" }, { - min: 6, - max: 20, - message: "用户密码长度必须介于 5 和 20 之间", + validator: (_rule: any, value: any, callback: any) => { + // 编辑时占位 ****** 不校验;新增或修改了明文密码则校验 + if (dataForm.userId && (!value || String(value).includes("******"))) { + return callback() + } + if (!value) { + return callback(new Error("密码不能为空")) + } + const pwd = String(value) + if (pwd.length < 8) { + return callback(new Error("密码长度不能少于8位")) + } + if (pwd.length > 20) { + return callback(new Error("密码长度不能超过20位")) + } + if (!/[A-Za-z]/.test(pwd) || !/[0-9]/.test(pwd)) { + return callback(new Error("密码必须同时包含字母和数字")) + } + if (dataForm.phone && pwd === String(dataForm.phone).trim()) { + return callback(new Error("密码不能与手机号相同")) + } + if (dataForm.username && pwd.toLowerCase() === String(dataForm.username).trim().toLowerCase()) { + return callback(new Error("密码不能与用户名相同")) + } + callback() + }, trigger: "blur", }, ], diff --git a/src/views/admin/system/user/index.vue b/src/views/admin/system/user/index.vue index 217a7ba..9c2f819 100644 --- a/src/views/admin/system/user/index.vue +++ b/src/views/admin/system/user/index.vue @@ -75,6 +75,17 @@ > 审批 + + 修复弱口令 +
@@ -224,8 +235,34 @@ + + + + + + + + + + + + + + +

{{ fixWeakPwdResultMessage }}

+ + +
+
@@ -233,8 +270,9 @@ import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue" import { useBatchExport } from "/@/hooks/batchExport" import { submitUserExport as submitBatchExport } from "/@/api/admin/user" +import type { AsyncTaskPollingConfig, AsyncTaskProgress } from "/@/api/asyncTask" -import { delObj, pageList, putObj } from "/@/api/admin/user" +import { delObj, pageList, putObj, fixPhoneAsPassword } from "/@/api/admin/user" import { putGlobalFace } from "/@/api/admin/userFace" import { BasicTableProps, useTable } from "/@/hooks/table" import { useMessage, useMessageBox } from "/@/hooks/message" @@ -243,6 +281,8 @@ import { auth } from "/@/utils/authFunction" import { getDicts } from "/@/api/admin/dict" import ProcessDrawer from "/@/components/processDrawer/index.vue" import { useDict } from "/@/hooks/dict" +import { downloadExportFile } from "/@/utils/exportDownload" +import other from "/@/utils/other" const UserForm = defineAsyncComponent(() => import("./form.vue")) const FaceManage = defineAsyncComponent(() => import("./face-manage.vue")) @@ -403,4 +443,110 @@ const changePassword = async (row: any) => { useMessage().success(t("common.optSuccessText")) getDataList() } + +const fixWeakPwdVisible = ref(false) +const fixWeakPwdLoading = ref(false) +const fixWeakPwdForm = reactive({ password: "" }) +const fixWeakPwdResultVisible = ref(false) +const fixWeakPwdResultMessage = ref("") +const fixWeakPwdResultText = ref("") +const fixWeakPwdResultFileUrl = ref("") +const fixWeakPwdResultFileName = ref("弱口令修复名单.txt") +const fixWeakPwdProgressRef = ref>() +const fixWeakPwdTaskConfig: AsyncTaskPollingConfig = { + progressUrl: "/admin/async-task/progress", + redisKey: "admin:fix:phone-password", + waitingMessage: "正在后台分页扫描用户密码,人多时可能较久,请勿关闭页面", + downloadButtonText: "下载名单", + pollTimeoutMs: 6 * 60 * 60 * 1000, +} + +const openFixWeakPwdDialog = () => { + fixWeakPwdForm.password = "" + fixWeakPwdVisible.value = true +} + +const resolveFileAbsoluteUrl = (fileUrl: string) => { + if (!fileUrl) return "" + if (/^https?:\/\//i.test(fileUrl)) return fileUrl + return `${String(import.meta.env.VITE_API_URL || "").replace(/\/$/, "")}${other.adaptationUrl(fileUrl)}` +} + +const submitFixWeakPwd = async () => { + const pwd = (fixWeakPwdForm.password || "").trim() + if (!pwd || pwd.length < 8) { + useMessage().error("新密码长度不能少于8位") + return + } + if (!/[A-Za-z]/.test(pwd) || !/[0-9]/.test(pwd)) { + useMessage().error("新密码必须同时包含字母和数字") + return + } + try { + await useMessageBox().confirm("确认将所有「密码=手机号」账号重置为该统一新密码?此操作不可撤销,将在后台异步执行。") + } catch { + return + } + fixWeakPwdLoading.value = true + fixWeakPwdProgressRef.value?.startSubmitting("修复弱口令") + try { + const { data } = await fixPhoneAsPassword(pwd) + if (!data?.taskId) throw new Error("任务响应缺少任务 ID") + fixWeakPwdVisible.value = false + fixWeakPwdProgressRef.value?.start(data) + } catch (err: any) { + const message = err.msg || err.message || "修复任务提交失败" + fixWeakPwdProgressRef.value?.submissionFailed(message) + useMessage().error(message) + } finally { + fixWeakPwdLoading.value = false + } +} + +const handleFixWeakPwdSuccess = async (progress: AsyncTaskProgress) => { + fixWeakPwdResultMessage.value = progress.message || "修复完成" + fixWeakPwdResultFileUrl.value = progress.resultFileUrl || "" + fixWeakPwdResultFileName.value = progress.resultFileName || "弱口令修复名单.txt" + fixWeakPwdResultText.value = "" + if (progress.resultFileUrl) { + try { + downloadExportFile(progress.resultFileUrl, fixWeakPwdResultFileName.value) + } catch { + /* 下载失败仍展示结果弹窗 */ + } + try { + const abs = resolveFileAbsoluteUrl(progress.resultFileUrl) + const resp = await fetch(abs) + if (resp.ok) { + fixWeakPwdResultText.value = await resp.text() + } + } catch { + /* 跨域或鉴权失败时靠下载的 txt */ + } + } + fixWeakPwdResultVisible.value = true + useMessage().success(progress.message || "修复完成") +} + +const handleFixWeakPwdFailure = (progress: AsyncTaskProgress) => { + useMessage().error(progress.message || "修复弱口令失败") +} + +const downloadFixWeakPwdResult = () => { + if (!fixWeakPwdResultFileUrl.value) return + try { + downloadExportFile(fixWeakPwdResultFileUrl.value, fixWeakPwdResultFileName.value) + } catch (error: any) { + useMessage().error(error?.message || "下载名单失败") + } +} + +const copyFixWeakPwdResult = async () => { + try { + await navigator.clipboard.writeText(fixWeakPwdResultText.value) + useMessage().success("名单已复制") + } catch { + useMessage().error("复制失败,请手动全选复制或下载名单") + } +} diff --git a/src/views/thirdparty/zzdPhoneBind/form.vue b/src/views/thirdparty/zzdPhoneBind/form.vue new file mode 100644 index 0000000..fa40e0d --- /dev/null +++ b/src/views/thirdparty/zzdPhoneBind/form.vue @@ -0,0 +1,84 @@ + + + diff --git a/src/views/thirdparty/zzdPhoneBind/index.vue b/src/views/thirdparty/zzdPhoneBind/index.vue new file mode 100644 index 0000000..f0c5296 --- /dev/null +++ b/src/views/thirdparty/zzdPhoneBind/index.vue @@ -0,0 +1,86 @@ + + +