pref: 安全特性支持,站点隐藏功能

This commit is contained in:
xiaojunnuo
2025-04-14 17:40:23 +08:00
parent 251b0c58de
commit d76d56fcce
37 changed files with 1028 additions and 349 deletions
@@ -4,7 +4,7 @@ export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
data: query,
});
}
@@ -12,7 +12,7 @@ export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj
data: obj,
});
}
@@ -20,7 +20,7 @@ export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj
data: obj,
});
}
@@ -28,7 +28,7 @@ export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
params: { id },
});
}
@@ -36,6 +36,14 @@ export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
params: { id },
});
}
export async function Unlock(id: any) {
return await request({
url: apiPrefix + "/unlockBlock",
method: "post",
data: { id },
});
}
@@ -1,6 +1,7 @@
import * as api from "./api";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/user";
import { Modal, notification } from "ant-design-vue";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@@ -26,16 +27,36 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
pageRequest,
addRequest,
editRequest,
delRequest
delRequest,
},
rowHandle: {
fixed: "right"
fixed: "right",
buttons: {
unlock: {
title: "解除登录锁定",
text: null,
type: "link",
icon: "ion:lock-open-outline",
click: async ({ row }) => {
Modal.confirm({
title: "提示",
content: "确定要解除该用户的登录锁定吗?",
onOk: async () => {
await api.Unlock(row.id);
notification.success({
message: "解除成功",
});
},
});
},
},
},
},
table: {
scroll: {
//使用固定列时需要设置此值,并且大于等于列宽度之和的值
x: 1400
}
x: 1400,
},
},
columns: {
id: {
@@ -44,8 +65,8 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
form: { show: false }, // 表单配置
column: {
width: 100,
sorter: true
}
sorter: true,
},
},
createTime: {
title: "创建时间",
@@ -53,8 +74,8 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
form: { show: false }, // 表单配置
column: {
width: 180,
sorter: true
}
sorter: true,
},
},
// updateTime: {
// title: "修改时间",
@@ -72,64 +93,64 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
form: {
rules: [
{ required: true, message: "请输入用户名" },
{ max: 50, message: "最大50个字符" }
]
{ max: 50, message: "最大50个字符" },
],
},
editForm: { component: { disabled: false } },
column: {
sorter: true,
width: 200
}
width: 200,
},
},
password: {
title: "密码",
type: "text",
key: "password",
column: {
show: false
show: false,
},
form: {
rules: [{ max: 50, message: "最大50个字符" }],
component: {
showPassword: true
showPassword: true,
},
helper: "填写则修改密码"
}
helper: "填写则修改密码",
},
},
nickName: {
title: "昵称",
type: "text",
search: { show: true }, // 开启查询
form: {
rules: [{ max: 50, message: "最大50个字符" }]
rules: [{ max: 50, message: "最大50个字符" }],
},
column: {
sorter: true
}
sorter: true,
},
},
email: {
title: "邮箱",
type: "text",
search: { show: true }, // 开启查询
form: {
rules: [{ max: 50, message: "最大50个字符" }]
rules: [{ max: 50, message: "最大50个字符" }],
},
column: {
sorter: true,
width: 160
}
width: 160,
},
},
mobile: {
title: "手机号",
type: "text",
search: { show: true }, // 开启查询
form: {
rules: [{ max: 50, message: "最大50个字符" }]
rules: [{ max: 50, message: "最大50个字符" }],
},
column: {
sorter: true,
width: 130
}
width: 130,
},
},
avatar: {
title: "头像",
@@ -140,12 +161,12 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
//设置高度,修复操作列错位的问题
style: {
height: "30px",
width: "auto"
width: "auto",
},
buildUrl(key: string) {
return `/api/basic/file/download?&key=` + key;
}
}
},
},
},
form: {
component: {
@@ -154,7 +175,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
cropper: {
aspectRatio: 1,
autoCropArea: 1,
viewMode: 0
viewMode: 0,
},
onReady: null,
uploader: {
@@ -162,17 +183,17 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
action: "/basic/file/upload",
name: "file",
headers: {
Authorization: "Bearer " + userStore.getToken
Authorization: "Bearer " + userStore.getToken,
},
successHandle(res: any) {
return res;
}
},
},
buildUrl(key: string) {
return `/api/basic/file/download?&key=` + key;
}
}
}
},
},
},
},
status: {
title: "状态",
@@ -180,24 +201,24 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
dict: dict({
data: [
{ label: "启用", value: 1, color: "green" },
{ label: "禁用", value: 0, color: "red" }
]
{ label: "禁用", value: 0, color: "red" },
],
}),
column: {
align: "center",
sorter: true,
width: 100
}
width: 100,
},
},
remark: {
title: "备注",
type: "text",
column: {
sorter: true
sorter: true,
},
form: {
rules: [{ max: 100, message: "最大100个字符" }]
}
rules: [{ max: 100, message: "最大100个字符" }],
},
},
roles: {
title: "角色",
@@ -205,17 +226,17 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
dict: dict({
url: "/sys/authority/role/list",
value: "id",
label: "name"
label: "name",
}), // 数据字典
form: {
component: { mode: "multiple" }
component: { mode: "multiple" },
},
column: {
width: 250,
sortable: true
}
}
}
}
sortable: true,
},
},
},
},
};
}
@@ -103,3 +103,4 @@ export async function GetSmsTypeDefine(type: string) {
},
});
}
@@ -5,8 +5,8 @@
<!-- </template>-->
<div class="sys-settings-body md:p-5">
<a-tabs :active-key="activeKey" type="card" class="sys-settings-tabs" @update:active-key="onChange">
<a-tab-pane key="" tab="基本设置">
<SettingBase v-if="activeKey === ''" />
<a-tab-pane key="base" tab="基本设置">
<SettingBase v-if="activeKey === 'base'" />
</a-tab-pane>
<a-tab-pane key="register" tab="注册设置">
<SettingRegister v-if="activeKey === 'register'" />
@@ -14,6 +14,9 @@
<a-tab-pane v-if="settingsStore.isComm" key="payment" tab="支付设置">
<SettingPayment v-if="activeKey === 'payment'" />
</a-tab-pane>
<a-tab-pane key="save" tab="安全设置">
<SettingSafe v-if="activeKey === 'save'" />
</a-tab-pane>
</a-tabs>
</div>
</fs-page>
@@ -23,6 +26,7 @@
import SettingBase from "/@/views/sys/settings/tabs/base.vue";
import SettingRegister from "/@/views/sys/settings/tabs/register.vue";
import SettingPayment from "/@/views/sys/settings/tabs/payment.vue";
import SettingSafe from "/@/views/sys/settings/tabs/safe.vue";
import { useRoute, useRouter } from "vue-router";
import { ref } from "vue";
import { useSettingStore } from "/@/store/settings";
@@ -30,11 +34,11 @@ defineOptions({
name: "SysSettings",
});
const settingsStore = useSettingStore();
const activeKey = ref("");
const activeKey = ref("base");
const route = useRoute();
const router = useRouter();
if (route.query.tab) {
activeKey.value = (route.query.tab as string) || "";
activeKey.value = (route.query.tab as string) || "base";
}
function onChange(value: string) {
@@ -52,7 +56,7 @@ function onChange(value: string) {
<style lang="less">
.page-sys-settings {
.sys-settings-form {
width: 500px;
width: 600px;
max-width: 100%;
padding: 20px;
}
@@ -0,0 +1,151 @@
<template>
<div class="sys-settings-form sys-settings-safe">
<a-form ref="formRef" :model="formState" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off">
<div>站点隐藏</div>
<a-form-item label="启用站点隐藏" :name="['hidden', 'enabled']" :required="true">
<a-switch v-model:checked="formState.hidden.enabled" />
<div class="helper">可以在平时关闭站点的可访问性需要时再打开增强站点安全性</div>
</a-form-item>
<a-form-item v-if="formState.hidden.enabled" label="随机地址" :name="['hidden', 'openPath']" :required="true">
<a-input-search v-model:value="formState.hidden.openPath" :allow-clear="true" @search="changeOpenPath">
<template #enterButton>
<fs-icon icon="ion:refresh"></fs-icon>
</template>
</a-input-search>
<div class="helper">站点被隐藏后需要访问此URL解锁才能正常访问</div>
</a-form-item>
<a-form-item v-if="formState.hidden.enabled" label="完整解除隐藏地址" :name="['hidden', 'openPath']" :required="true">
<div class="flex"><fs-copyable v-model="openUrl" class="flex-inline"></fs-copyable></div>
<div class="helper red">请保存好此地址</div>
</a-form-item>
<a-form-item v-if="formState.hidden.enabled" label="解除密码" :name="['hidden', 'openPassword']" :required="false">
<a-input-password v-model:value="formState.hidden.openPassword" :allow-clear="true" />
<div class="helper">解除隐藏时需要输入密码第一次需要设置密码填写则重置密码</div>
</a-form-item>
<a-form-item v-if="formState.hidden.enabled" label="自动隐藏时间" :name="['hidden', 'autoHiddenTimes']" :required="true">
<a-input-number v-model:value="formState.hidden.autoHiddenTimes" :allow-clear="true" />
<div class="helper">多少分钟内无请求自动隐藏</div>
</a-form-item>
<a-form-item v-if="formState.hidden.enabled" label="隐藏开放接口" :name="['hidden', 'hiddenOpenApi']" :required="true">
<a-switch v-model:checked="formState.hidden.hiddenOpenApi" />
<div class="helper">是否隐藏开放接口是否放开/api/v1开头的接口</div>
</a-form-item>
<a-form-item v-if="formState.hidden.enabled" label="立即隐藏站点">
<loading-button class="ml-1" type="primary" html-type="button" :click="doHiddenImmediate">立即隐藏</loading-button>
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 16 }">
<loading-button type="primary" html-type="button" :click="onClick">保存</loading-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="tsx">
import { computed, reactive, ref } from "vue";
import { merge } from "lodash-es";
import { Modal, notification } from "ant-design-vue";
import { request } from "/@/api/service";
import { util, utils } from "/@/utils";
defineOptions({
name: "SettingSafe",
});
const api = {
async SettingGet() {
return await request({
url: "/sys/settings/safe/get",
method: "post",
});
},
async SettingSave(data: any) {
return await request({
url: "/sys/settings/safe/save",
method: "post",
data,
});
},
async HiddenImmediate() {
return await request({
url: "/sys/settings/safe/hidden",
method: "post",
});
},
};
const defaultState = {
hidden: {
enabled: false,
autoHiddenTimes: 5,
hiddenOpenApi: false,
},
};
const formRef = ref<any>(defaultState);
type SiteHidden = {
enabled: boolean;
openPath?: string;
autoHiddenTimes?: number;
openPassword?: string;
hiddenOpenApi?: boolean;
};
const formState = reactive<
Partial<{
hidden: SiteHidden;
}>
>({
hidden: { enabled: false },
});
function changeOpenPath() {
formState.hidden.openPath = util.randomString(16);
}
async function loadSettings() {
const data: any = await api.SettingGet();
merge(formState, defaultState, formState, data);
if (!formState.hidden.openPath) {
changeOpenPath();
}
}
loadSettings();
const openUrl = computed(() => {
const url = new URL(window.location.href);
url.pathname = `/api/unhidden/${formState.hidden?.openPath || ""}`;
//@ts-ignore
url.query = undefined;
url.hash = "";
return url.href;
});
const onClick = async () => {
const form = await formRef.value.validateFields();
//密码md5
// if (form.hidden?.openPassword) {
// form.hidden.openPassword = util.hash.md5(form.hidden.openPassword);
// }
await api.SettingSave(form);
await loadSettings();
notification.success({
message: "保存成功",
});
};
async function doHiddenImmediate() {
Modal.confirm({
title: "确定要立即隐藏站点吗?",
content: "隐藏后,将无法访问站点,请谨慎操作",
async onOk() {
await api.HiddenImmediate();
notification.success({
message: "站点已隐藏",
});
},
});
}
</script>
<style lang="less">
.sys-settings-base {
}
</style>