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

View File

@@ -1,18 +1,18 @@
import crypto, { BinaryToTextEncoding } from 'crypto';
import crypto, { BinaryToTextEncoding } from "crypto";
function md5(data: string, digest: BinaryToTextEncoding = 'hex') {
return crypto.createHash('md5').update(data).digest(digest);
function md5(data: string, digest: BinaryToTextEncoding = "hex") {
return crypto.createHash("md5").update(data).digest(digest);
}
function sha256(data: string, digest: BinaryToTextEncoding = 'hex') {
return crypto.createHash('sha256').update(data).digest(digest);
function sha256(data: string, digest: BinaryToTextEncoding = "hex") {
return crypto.createHash("sha256").update(data).digest(digest);
}
function hmacSha256(data: string, digest: BinaryToTextEncoding = 'base64') {
return crypto.createHmac('sha256', data).update(Buffer.alloc(0)).digest(digest);
function hmacSha256(data: string, digest: BinaryToTextEncoding = "base64") {
return crypto.createHmac("sha256", data).update(Buffer.alloc(0)).digest(digest);
}
function base64(data: string) {
return Buffer.from(data).toString('base64');
return Buffer.from(data).toString("base64");
}
export const hashUtils = {
md5,

View File

@@ -66,10 +66,15 @@ export const Constants = {
code: 404,
message: '页面/文件/资源不存在',
},
preview: {
code: 10001,
message: '对不起,预览环境不允许修改此数据',
},
siteOff:{
code: 10010,
message: '站点已关闭',
},
openKeyError: {
code: 20000,
message: 'ApiToken错误',

View File

@@ -7,3 +7,4 @@ export * from './vip-exception.js';
export * from './common-exception.js';
export * from './not-found-exception.js';
export * from './param-exception.js';
export * from './site-off-exception.js';

View File

@@ -0,0 +1,9 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
/**
*/
export class SiteOffException extends BaseException {
constructor(message) {
super('SiteOffException', Constants.res.siteOff.code, message ? message : Constants.res.siteOff.message);
}
}

View File

@@ -171,7 +171,7 @@ export class SysSuiteSetting extends BaseSettings {
static __key__ = 'sys.suite';
static __access__ = 'private';
enabled = false;
enabled:boolean = false;
registerGift?: {
productId: number;
@@ -180,3 +180,25 @@ export class SysSuiteSetting extends BaseSettings {
intro?: string;
}
export type SiteHidden = {
enabled: boolean;
openPath?: string;
//md5 hash 两次后保存
openPassword?: string;
autoHiddenTimes?: number;
hiddenOpenApi?: boolean
};
export class SysSafeSetting extends BaseSettings {
static __title__ = '站点安全设置';
static __key__ = 'sys.safe';
static __access__ = 'private';
// 站点隐藏
hidden:SiteHidden = {
enabled: false,
hiddenOpenApi:false,
autoHiddenTimes: 5,
};
}

View File

@@ -84,6 +84,7 @@
"qrcode": "^1.5.4",
"radix-vue": "^1.9.16",
"sortablejs": "^1.15.3",
"spark-md5": "^3.0.2",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"theme-colors": "^0.1.0",

View File

@@ -54,23 +54,23 @@ const steps = ref<Step[]>([
descriptions: ["本教程演示如何自动申请证书并部署到Nginx上", "仅需3步全自动申请部署证书"],
body: () => {
return <SimpleSteps></SimpleSteps>;
}
},
},
{
image: "/static/doc/images/1-add.png",
title: "创建证书流水线",
descriptions: ["点击添加证书流水线,填写证书申请信息"]
descriptions: ["点击添加证书流水线,填写证书申请信息"],
},
{
image: "/static/doc/images/3-add-success.png",
title: "流水线创建成功",
descriptions: ["点击手动触发即可申请证书"]
descriptions: ["点击手动触发即可申请证书"],
},
{
title: "接下来演示如何自动部署证书",
descriptions: ["如果您只需要申请证书,那么到这一步就可以了"]
}
]
descriptions: ["如果您只需要申请证书,那么到这一步就可以了"],
},
],
},
{
title: "添加部署证书任务",
@@ -79,29 +79,29 @@ const steps = ref<Step[]>([
{
image: "/static/doc/images/5-1-add-host.png",
title: "添加证书部署任务",
descriptions: ["这里演示自动部署证书到nginx", "本系统提供海量部署插件,满足您的各种部署需求"]
descriptions: ["这里演示自动部署证书到nginx", "本系统提供海量部署插件,满足您的各种部署需求"],
},
{
image: "/static/doc/images/5-2-add-host.png",
title: "填写任务参数",
descriptions: ["填写主机上证书文件的路径", "选择主机ssh登录授权"]
descriptions: ["填写主机上证书文件的路径", "选择主机ssh登录授权"],
},
{
image: "/static/doc/images/5-3-add-host.png",
title: "让新证书生效",
descriptions: ["执行重启脚本", "让证书生效"]
descriptions: ["执行重启脚本", "让证书生效"],
},
{
image: "/static/doc/images/5-4-add-host.png",
title: "部署任务添加成功",
descriptions: ["现在可以运行"]
descriptions: ["现在可以运行"],
},
{
image: "/static/doc/images/5-5-plugin-list.png",
title: "本系统提供茫茫多的部署插件",
descriptions: ["您可以根据自身需求将证书部署到各种应用和平台"]
}
]
descriptions: ["您可以根据自身需求将证书部署到各种应用和平台"],
},
],
},
{
title: "运行与测试",
@@ -110,44 +110,44 @@ const steps = ref<Step[]>([
{
image: "/static/doc/images/9-start.png",
title: "运行测试一下",
descriptions: ["点击手动触发按钮,即可测试运行"]
descriptions: ["点击手动触发按钮,即可测试运行"],
},
{
image: "/static/doc/images/10-1-log.png",
title: "查看日志",
descriptions: ["点击任务可以查看状态和日志"]
descriptions: ["点击任务可以查看状态和日志"],
},
{
image: "/static/doc/images/11-1-error.png",
title: "执行失败如何排查",
descriptions: ["查看错误日志"]
descriptions: ["查看错误日志"],
},
{
image: "/static/doc/images/11-2-error.png",
title: "执行失败如何排查",
descriptions: ["查看错误日志", "这里报的是nginx容器不存在修改命令改成正确的nginx容器名称即可"]
descriptions: ["查看错误日志", "这里报的是nginx容器不存在修改命令改成正确的nginx容器名称即可"],
},
{
image: "/static/doc/images/12-1-log-success.png",
title: "执行成功",
descriptions: ["修改正确后,重新点击手动触发,重新运行一次,执行成功"]
descriptions: ["修改正确后,重新点击手动触发,重新运行一次,执行成功"],
},
{
image: "/static/doc/images/12-2-skip-log.png",
title: "成功后自动跳过",
descriptions: ["可以看到成功过的将会自动跳过,不会重复执行,只有当参数变更或者证书更新了,才会重新运行"]
descriptions: ["可以看到成功过的将会自动跳过,不会重复执行,只有当参数变更或者证书更新了,才会重新运行"],
},
{
image: "/static/doc/images/13-1-result.png",
title: "查看证书部署成功",
descriptions: ["访问nginx上的网站可以看到证书已经部署成功"]
descriptions: ["访问nginx上的网站可以看到证书已经部署成功"],
},
{
image: "/static/doc/images/13-3-download.png",
title: "还可以下载证书,手动部署",
descriptions: ["如果还没有好用的部署插件,没办法自动部署,你还可以下载证书,手动部署"]
}
]
descriptions: ["如果还没有好用的部署插件,没办法自动部署,你还可以下载证书,手动部署"],
},
],
},
{
title: "设置定时执行和邮件通知",
@@ -156,22 +156,19 @@ const steps = ref<Step[]>([
{
image: "/static/doc/images/14-timer.png",
title: "设置定时执行",
descriptions: [
"流水线测试成功,接下来配置定时触发,以后每天定时执行就不用管了",
"推荐配置每天运行一次在到期前35天才会重新申请新证书并部署没到期前会自动跳过不会重复申请。"
]
descriptions: ["流水线测试成功,接下来配置定时触发,以后每天定时执行就不用管了", "推荐配置每天运行一次在到期前35天才会重新申请新证书并部署没到期前会自动跳过不会重复申请。"],
},
{
image: "/static/doc/images/15-1-email.png",
title: "设置邮件通知",
descriptions: ["建议选择监听'错误时'和'错误转成功'两种即可,在意外失败时可以尽快去排查问题,(基础版需要配置邮件服务器)"]
descriptions: ["建议选择监听'错误时'和'错误转成功'两种即可,在意外失败时可以尽快去排查问题,(基础版需要配置邮件服务器)"],
},
{
title: "教程结束",
descriptions: ["感谢观看,希望对你有所帮助"]
}
]
}
descriptions: ["感谢观看,希望对你有所帮助"],
},
],
},
]);
const current = ref(0);

View File

@@ -16,6 +16,6 @@ export const util = {
router: routerUtils,
tree: treeUtils,
hash: hashUtils,
amount: amountUtils
amount: amountUtils,
};
export const utils = util;

View File

@@ -5,5 +5,5 @@ export const amountUtils = {
toYuan(amount: number): number {
return parseFloat((amount / 100).toFixed(2));
}
},
};

View File

@@ -47,4 +47,13 @@ export default {
}
return desc.replace(/\[(.*)\]\((.*)\)/g, '<a href="$2" target="_blank">$1</a>');
},
randomString(length: number) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
},
};

View File

@@ -1,5 +1,5 @@
export const hashUtils = {
md5(data: string) {
throw new Error("Not implemented");
}
},
};

View File

@@ -7,5 +7,5 @@ export const site = {
title: function (titleText: string, baseTitle?: string) {
const processTitle = baseTitle || env.TITLE || "Certd";
window.document.title = `${processTitle}${titleText ? ` | ${titleText}` : ""}`;
}
},
};

View File

@@ -46,7 +46,7 @@ export class WebStorage {
const stringData = JSON.stringify({
value,
time: Date.now(),
expire: expire != null ? new Date().getTime() + expire * 1000 : null
expire: expire != null ? new Date().getTime() + expire * 1000 : null,
});
this.storage.setItem(this.getKey(key), stringData);
}

View File

@@ -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 },
});
}

View File

@@ -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,
},
},
},
},
};
}

View File

@@ -103,3 +103,4 @@ export async function GetSmsTypeDefine(type: string) {
},
});
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -14,36 +14,19 @@ import DefineOptions from "unplugin-vue-define-options/vite";
// 增加环境变量 _
process.env.VITE_APP_VERSION = require("./package.json").version;
process.env.VITE_APP_BUILD_TIME = require("dayjs")().format("YYYY-M-D HH:mm:ss");
import { theme } from "ant-design-vue";
import * as https from "node:https";
const { defaultAlgorithm, defaultSeed } = theme;
const mapToken = defaultAlgorithm(defaultSeed);
export default ({ command, mode }) => {
console.log("args", command, mode);
const env = loadEnv(mode, process.cwd());
let devServerFs: any = {};
let devAlias: any[] = [];
if (mode.startsWith("debug")) {
devAlias = [
{ find: /@fast-crud\/fast-crud\/dist/, replacement: path.resolve("../../fast-crud/src/") },
// { find: /@fast-crud\/fast-crud$/, replacement: path.resolve("../../fast-crud/src/") },
{ find: /@fast-crud\/fast-extends\/dist/, replacement: path.resolve("../../fast-extends/src/") },
// { find: /@fast-crud\/fast-extends$/, replacement: path.resolve("../../fast-extends/src/") },
// { find: /@fast-crud\/ui-antdv$/, replacement: path.resolve("../../ui/ui-antdv/src/") },
// { find: /@fast-crud\/ui-interface$/, replacement: path.resolve("../../ui/ui-interface/src/") }
{ find: /@fast-crud\/ui-antdv4\/dist/, replacement: path.resolve("../../ui/ui-antdv4/src/") }
];
devServerFs = {
// 这里配置dev启动时读取的项目根目录
allow: ["../../../"]
};
console.log("devAlias", devAlias);
}
const devServerFs: any = {};
const devAlias: any[] = [];
const base = "/";
// if (mode.startsWith("dev")) {
// base = "/dev";
// }
return {
base: "/",
base: base,
plugins: [
DefineOptions(),
vueJsx(),
@@ -52,12 +35,12 @@ export default ({ command, mode }) => {
inject: {
data: {
title: env.VITE_APP_TITLE,
projectPath: env.VITE_APP_PROJECT_PATH
}
}
projectPath: env.VITE_APP_PROJECT_PATH,
},
},
}),
// 压缩build后的代码
viteCompression()
viteCompression(),
//主题色替换
//...configThemePlugin(true),
// viteThemePlugin({
@@ -71,29 +54,29 @@ export default ({ command, mode }) => {
drop: command === "build" ? ["debugger"] : [],
pure: ["console.log", "debugger"],
jsxFactory: "h",
jsxFragment: "Fragment"
jsxFragment: "Fragment",
},
resolve: {
alias: [...devAlias, { find: "/@", replacement: path.resolve("./src") }, { find: "/#", replacement: path.resolve("./types") }],
dedupe: ["vue"]
dedupe: ["vue"],
},
optimizeDeps: {
include: ["ant-design-vue"]
include: ["ant-design-vue"],
},
build: {
rollupOptions: {
plugins: [visualizer()]
}
plugins: [visualizer()],
},
},
css: {
preprocessorOptions: {
less: {
// 修改默认主题颜色配置less变量
// modifyVars: generateModifyVars(),
javascriptEnabled: true
javascriptEnabled: true,
// modifyVars: mapToken
}
}
},
},
},
server: {
host: "0.0.0.0",
@@ -105,9 +88,9 @@ export default ({ command, mode }) => {
//配套后端 https://github.com/fast-crud/fs-server-js
target: "https://127.0.0.1:7002",
//忽略证书
agent: new https.Agent({ rejectUnauthorized: false })
}
}
}
agent: new https.Agent({ rejectUnauthorized: false }),
},
},
},
};
};

View File

@@ -19,6 +19,7 @@ import * as libServer from '@certd/lib-server';
import * as commercial from '@certd/commercial-core';
import * as upload from '@midwayjs/upload';
import { setLogger } from '@certd/acme-client';
import {HiddenMiddleware} from "./middleware/hidden.js";
process.on('uncaughtException', error => {
console.error('未捕获的异常:', error);
// 在这里可以添加日志记录、发送错误通知等操作
@@ -77,6 +78,8 @@ export class MainConfiguration {
this.app.useMiddleware([
//统一异常处理
GlobalExceptionMiddleware,
//站点隐藏
HiddenMiddleware,
//预览模式限制修改id<1000的数据
PreviewMiddleware,
//授权处理

View File

@@ -0,0 +1,66 @@
import {Body, Controller, Get, Inject, Post, Provide} from '@midwayjs/core';
import {Constants, NotFoundException, ParamException, SysInstallInfo, SysSettingsService} from '@certd/lib-server';
import {utils} from "@certd/basic";
import {hiddenStatus, SafeService} from "../../modules/sys/settings/safe-service.js";
import {IMidwayKoaContext} from "@midwayjs/koa";
const unhiddenHtml = `
<html lang="en">
<head>
<title>certd解除站点隐藏</title>
</head>
<body>
<div style="margin:50px;width:500px">
<h3>解除站点隐藏</h3>
<form method="post">
请输入解除密码: <input type="password" name="password" /> <button type="submit">确定</button>
</form>
</div>
</body>
</html>
`
@Provide()
@Controller('/api/unhidden')
export class HnhiddenController {
@Inject()
ctx: IMidwayKoaContext;
@Inject()
safeService: SafeService;
@Inject()
sysSettingsService: SysSettingsService;
@Post('/:randomPath', {summary: Constants.per.guest})
async randomPath(@Body("password") password: any) {
await this.checkUnhiddenPath()
const hiddenSetting = await this.safeService.getHiddenSetting()
if (utils.hash.md5(password) === hiddenSetting.openPassword) {
//解锁
hiddenStatus.isHidden = false;
const setting = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo)
const bindUrl = setting.bindUrl
//解锁成功,跳转回首页,redirect
this.ctx.response.redirect(bindUrl || "/");
return
} else {
//密码错误
throw new ParamException('解锁密码错误');
}
}
@Get('/:randomPath', {summary: Constants.per.guest})
async unhiddenGet() {
await this.checkUnhiddenPath()
this.ctx.response.body = unhiddenHtml
}
async checkUnhiddenPath() {
const hiddenSetting = await this.safeService.getHiddenSetting()
if (this.ctx.path != `/api/unhidden/${hiddenSetting.openPath}`) {
this.ctx.res.statusCode = 404
throw new NotFoundException("Page not found")
}
}
}

View File

@@ -0,0 +1,39 @@
import {ALL, Body, Controller, Inject, Post, Provide} from '@midwayjs/core';
import {BaseController, SysSafeSetting} from '@certd/lib-server';
import {cloneDeep} from 'lodash-es';
import {SafeService} from "../../../modules/sys/settings/safe-service.js";
/**
*/
@Provide()
@Controller('/api/sys/settings/safe')
export class SysSettingsController extends BaseController {
@Inject()
safeService: SafeService;
@Post("/get", { summary: "sys:settings:view" })
async safeGet() {
const res = await this.safeService.getSafeSetting()
const clone:SysSafeSetting = cloneDeep(res);
delete clone.hidden?.openPassword;
return this.ok(clone);
}
@Post("/save", { summary: "sys:settings:edit" })
async safeSave(@Body(ALL) body: any) {
await this.safeService.saveSafeSetting(body);
return this.ok({});
}
/**
* 立即隐藏
*/
@Post("/hidden", { summary: "sys:settings:edit" })
async hiddenImmediate() {
await this.safeService.hiddenImmediately();
return this.ok({});
}
}

View File

@@ -1,12 +1,19 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { CrudController, SysPrivateSettings, SysPublicSettings, SysSettingsEntity, SysSettingsService } from '@certd/lib-server';
import { merge } from 'lodash-es';
import { PipelineService } from '../../../modules/pipeline/service/pipeline-service.js';
import { UserSettingsService } from '../../../modules/mine/service/user-settings-service.js';
import { getEmailSettings } from '../../../modules/sys/settings/fix.js';
import { http, logger, simpleNanoId } from '@certd/basic';
import { CodeService } from '../../../modules/basic/service/code-service.js';
import { SmsServiceFactory } from '../../../modules/basic/sms/factory.js';
import {ALL, Body, Controller, Inject, Post, Provide, Query} from '@midwayjs/core';
import {
CrudController,
SysPrivateSettings,
SysPublicSettings,
SysSafeSetting,
SysSettingsEntity,
SysSettingsService
} from '@certd/lib-server';
import {cloneDeep, merge} from 'lodash-es';
import {PipelineService} from '../../../modules/pipeline/service/pipeline-service.js';
import {UserSettingsService} from '../../../modules/mine/service/user-settings-service.js';
import {getEmailSettings} from '../../../modules/sys/settings/fix.js';
import {http, logger, simpleNanoId, utils} from '@certd/basic';
import {CodeService} from '../../../modules/basic/service/code-service.js';
import {SmsServiceFactory} from '../../../modules/basic/sms/factory.js';
/**
@@ -159,4 +166,29 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
async getSmsTypeDefine(@Body('type') type: string) {
return this.ok(SmsServiceFactory.getDefine(type));
}
@Post("/safe/get", { summary: "sys:settings:view" })
async safeGet() {
const res = await this.service.getSetting<SysSafeSetting>(SysSafeSetting);
const clone:SysSafeSetting = cloneDeep(res);
delete clone.hidden?.openPassword;
return this.ok(clone);
}
@Post("/safe/save", { summary: "sys:settings:edit" })
async safeSave(@Body(ALL) body: any) {
if(body.hidden.openPassword){
body.hidden.openPassword = utils.hash.md5(body.hidden.openPassword);
}
const blankSetting = new SysSafeSetting()
const setting = await this.service.getSetting<SysSafeSetting>(SysSafeSetting);
const newSetting = merge(blankSetting,cloneDeep(setting), body);
if(newSetting.hidden?.enabled && !newSetting.hidden?.openPassword){
throw new Error("首次设置需要填写解锁密码")
}
await this.service.saveSetting(blankSetting);
return this.ok({});
}
}

View File

@@ -0,0 +1,54 @@
import {Inject, Provide} from '@midwayjs/core';
import {IMidwayKoaContext, IWebMiddleware, NextFunction} from '@midwayjs/koa';
import {hiddenStatus, SafeService} from "../modules/sys/settings/safe-service.js";
import {SiteOffException} from "@certd/lib-server";
/**
* 隐藏环境
*/
@Provide()
export class HiddenMiddleware implements IWebMiddleware {
@Inject()
hiddenService: SafeService;
resolve() {
return async (ctx: IMidwayKoaContext, next: NextFunction) => {
async function pass() {
hiddenStatus.updateRequestTime()
await next();
}
const hiddenSetting = await this.hiddenService.getHiddenSetting();
if (!hiddenSetting || !hiddenSetting?.enabled) {
//未开启站点隐藏,直接通过
return await pass()
}
const req = ctx.request;
if (hiddenSetting.hiddenOpenApi === false && req.url.startsWith(`/api/v1/`) ) {
//不隐藏开放接口
await next();
return
}
//判断当前是否是隐藏状态
if (!hiddenStatus.isHidden) {
return await pass()
}
//判断是否有解锁文件,如果有就返回true并删除文件
if (hiddenStatus.hasUnHiddenFile()) {
//临时修改为未隐藏
hiddenStatus.isHidden = false;
return await pass()
}
if (req.url === `/api/unhidden/${hiddenSetting.openPath}`) {
return await pass();
}
throw new SiteOffException('此站点已关闭');
}
}
}

View File

@@ -4,6 +4,7 @@ import { UserService } from '../sys/authority/service/user-service.js';
import { PlusService, SysInstallInfo, SysPrivateSettings, SysSettingsService } from '@certd/lib-server';
import { nanoid } from 'nanoid';
import crypto from 'crypto';
import {SafeService} from "../sys/settings/safe-service.js";
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -18,6 +19,8 @@ export class AutoAInitSite {
sysSettingsService: SysSettingsService;
@Inject()
plusService: PlusService;
@Inject()
safeService: SafeService;
@Init()
async init() {
@@ -57,6 +60,8 @@ export class AutoAInitSite {
logger.error('授权许可验证失败', e);
}
//加载站点隐藏配置
await this.safeService.reloadHiddenStatus(true)
logger.info('初始化站点完成');
}

View File

@@ -0,0 +1,110 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {SiteHidden, SysSafeSetting, SysSettingsService} from "@certd/lib-server";
import fs from "fs";
import {logger, utils} from "@certd/basic";
import {cloneDeep, merge} from "lodash-es";
export class HiddenStatus {
isHidden = false;
lastRequestTime = 0;
intervalId: any = null;
hasUnHiddenFile() {
if (fs.existsSync(`./data/.unhidden`)) {
fs.unlinkSync(`./data/.unhidden`)
return true
}
return false
}
updateRequestTime() {
this.lastRequestTime = Date.now();
}
startCheck(autoHiddenTimes = 5) {
this.stopCheck()
this.intervalId = setInterval(() => {
//默认5分钟后自动隐藏
if (!this.isHidden && Date.now() - this.lastRequestTime > 1000 * 60 * autoHiddenTimes) {
this.isHidden = true;
}
}, 1000 * 60)
}
stopCheck() {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
}
}
export const hiddenStatus = new HiddenStatus();
@Provide('safeService')
@Scope(ScopeEnum.Request, {allowDowngrade: true})
export class SafeService {
@Inject()
sysSettingsService: SysSettingsService;
async reloadHiddenStatus(immediate = false) {
const hidden = await this.getHiddenSetting()
if (hidden.enabled) {
logger.error("启动站点隐藏");
hiddenStatus.isHidden = false
if (immediate) {
hiddenStatus.isHidden = true;
}
const autoHiddenTimes = hidden.autoHiddenTimes || 5;
hiddenStatus.startCheck(autoHiddenTimes);
} else {
logger.error("关闭站点隐藏");
hiddenStatus.isHidden = false;
hiddenStatus.stopCheck()
}
}
async getHiddenSetting(): Promise<SiteHidden> {
const safeSetting = await this.getSafeSetting()
return safeSetting.hidden || {enabled: false}
}
async getSafeSetting() {
return await this.sysSettingsService.getSetting<SysSafeSetting>(SysSafeSetting)
}
async hiddenImmediately() {
return hiddenStatus.isHidden = true
}
async saveSafeSetting(body: SysSafeSetting) {
// 更新hidden配置
if (body.hidden.openPassword) {
body.hidden.openPassword = utils.hash.md5(body.hidden.openPassword);
}
const blankSetting = new SysSafeSetting()
const setting = await this.getSafeSetting()
const newSetting = merge(blankSetting, cloneDeep(setting), body);
if (newSetting.hidden?.enabled && !newSetting.hidden?.openPassword) {
throw new Error("首次设置需要填写解锁密码")
}
if(isNaN(newSetting.hidden.autoHiddenTimes) || newSetting.hidden.autoHiddenTimes < 1){
newSetting.hidden.autoHiddenTimes = 1
}
await this.sysSettingsService.saveSetting(newSetting);
await this.reloadHiddenStatus(false)
}
}

View File

@@ -128,11 +128,11 @@ export class DeployCertToTencentAll extends AbstractTaskPlugin {
});
let certId:string = null
if (typeof certId === 'string') {
certId = this.tencentCertId as string;
} else {
if (typeof certId === 'object') {
//上传
certId = await this.uploadToTencent(access,this.tencentCertId as CertInfo);
} else {
certId = this.tencentCertId as string;
}
const params = {