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

pull/373/head
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,158 +1,166 @@
import { defineConfig } from "vitepress";
import {defineConfig} from "vitepress";
// Import lightbox plugin
import lightbox from "vitepress-plugin-lightbox";
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Certd",
titleTemplate: "开源SSL证书管理工具证书自动化申请部署让你的网站证书永不过期",
description: "Certd帮助文档,Certd是一款开源免费的全自动SSL证书管理工具证书自动化申请部署流水线自动证书申请、更新、续期通配符证书泛域名证书申请证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔。",
markdown: {
config: (md) => {
// Use lightbox plugin
md.use(lightbox, {});
}
},
sitemap: {
hostname: 'https://certd.docmirror.cn'
},
head: [
// [
// 'meta',
// {
// name: 'viewport',
// content:
// 'width=device-width,initial-scale=1,minimfast-cum-scale=1.0,maximum-scale=1.0,user-scalable=no',
// },
// ],
["meta", {
name: "keywords",
content: "证书自动申请、证书自动更新、证书自动续期、证书自动续签、证书管理工具、Certd、SSL证书自动部署、证书自动化https证书pfx证书der证书TLS证书nginx证书自动续签自动部署,SSL平台证书管理平台证书流水线"
}],
// ["meta", { name: "google-site-verification",content: "V5XLTSnXoT15uQotwpxJoQolUo2d5UbSL-TacsyOsC0"}],
//<meta name="baidu-site-verification" content="codeva-MiWN8Y07Ua" />
// ["meta", {name: "baidu-site-verification",content: "codeva-MiWN8Y07Ua"}],
["link", { rel: "icon", href: "/static/logo/logo.svg" }]
],
themeConfig: {
logo: "/static/logo/logo.svg",
search: {
provider: "local",
options: {
detailedView: true,
translations: {
button: {
buttonText: "搜索文档",
buttonAriaLabel: "搜索文档"
},
modal: {
noResultsText: "无法找到相关结果",
resetButtonTitle: "清除查询条件",
footer: {
selectText: "选择",
closeText: "关闭",
navigateText: "切换"
title: "Certd",
titleTemplate: "开源SSL证书管理工具证书自动化申请部署让你的网站证书永不过期",
description: "Certd帮助文档,Certd是一款开源免费的全自动SSL证书管理工具证书自动化申请部署流水线自动证书申请、更新、续期通配符证书泛域名证书申请证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔。",
markdown: {
config: (md) => {
// Use lightbox plugin
md.use(lightbox, {});
}
},
sitemap: {
hostname: 'https://certd.docmirror.cn'
},
head: [
// [
// 'meta',
// {
// name: 'viewport',
// content:
// 'width=device-width,initial-scale=1,minimfast-cum-scale=1.0,maximum-scale=1.0,user-scalable=no',
// },
// ],
["meta", {
name: "keywords",
content: "证书自动申请、证书自动更新、证书自动续期、证书自动续签、证书管理工具、Certd、SSL证书自动部署、证书自动化https证书pfx证书der证书TLS证书nginx证书自动续签自动部署,SSL平台证书管理平台证书流水线"
}],
// ["meta", { name: "google-site-verification",content: "V5XLTSnXoT15uQotwpxJoQolUo2d5UbSL-TacsyOsC0"}],
//<meta name="baidu-site-verification" content="codeva-MiWN8Y07Ua" />
// ["meta", {name: "baidu-site-verification",content: "codeva-MiWN8Y07Ua"}],
["link", {rel: "icon", href: "/static/logo/logo.svg"}]
],
themeConfig: {
logo: "/static/logo/logo.svg",
search: {
provider: "local",
options: {
detailedView: true,
translations: {
button: {
buttonText: "搜索文档",
buttonAriaLabel: "搜索文档"
},
modal: {
noResultsText: "无法找到相关结果",
resetButtonTitle: "清除查询条件",
footer: {
selectText: "选择",
closeText: "关闭",
navigateText: "切换"
}
}
}
}
}
}
}
},
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "首页", link: "/" },
{ text: "指南", link: "/guide/" },
{ text: "插件", link: "/deploy/" },
{ text: "商业版", link: "/comm/" },
{ text: "Demo体验", link: "https://certd.handfree.work" }
],
sidebar: {
"/guide/": [
{
text: "入门",
items: [
{ text: "简介", link: "/guide/" },
{ text: "快速开始", link: "/guide/start.md" },
{
text: "私有化部署",
items: [
{ text: "docker部署", link: "/guide/install/docker/" },
{ text: "宝塔面板部署", link: "/guide/install/baota/" },
{ text: "1Panel部署", link: "/guide/install/1panel/" },
{ text: "群晖部署", link: "/guide/use/synology/" },
{ text: "源码部署", link: "/guide/install/source/" }
]
},
{ text: "演示教程", link: "/guide/tutorial.md" },
{ text: "版本升级", link: "/guide/install/upgrade.md" }
]
},
{
text: "特性",
items: [
{ text: "CNAME代理校验", link: "/guide/feature/cname/index.md" },
{ text: "插件列表", link: "/guide/plugins.md" },
{ text: "多数据库支持", link: "/guide/install/database.md" },
{ text: "开放接口", link: "/guide/open/index.md" }
]
},
{
text: "常见问题",
items: [
{ text: "群晖证书部署", link: "/guide/use/synology/" },
{ text: "腾讯云密钥获取", link: "/guide/use/tencent/" },
{ text: "连接windows主机", link: "/guide/use/host/windows.md" },
{ text: "Google EAB获取", link: "/guide/use/google/" },
{ text: "阿里云相关", link: "/guide/use/aliyun/" },
{ text: "忘记密码", link: "/guide/use/forgotpasswd/" },
{ text: "数据备份", link: "/guide/use/backup/" },
{ text: "Certd本身的证书更新", link: "/guide/use/https/index.md" },
{ text: "js脚本插件使用", link: "/guide/use/custom-script/index.md" },
{ text: "邮箱配置", link: "/guide/use/email/index.md" },
{ text: "IPv6支持", link: "/guide/use/setting/ipv6.md" },
]
},
{
text: "其他",
items: [
{ text: "贡献代码", link: "/guide/development/index.md" },
{ text: "更新日志", link: "/guide/changelogs/CHANGELOG.md" },
{ text: "镜像说明", link: "/guide/image.md" },
{ text: "联系我们", link: "/guide/contact/" },
{ text: "捐赠", link: "/guide/donate/" },
{ text: "开源协议", link: "/guide/license/" },
{ text: "我的其他开源项目", link: "/guide/link/" },
// https://vitepress.dev/reference/default-theme-config
nav: [
{text: "首页", link: "/"},
{text: "指南", link: "/guide/"},
{text: "Demo体验", link: "https://certd.handfree.work"}
],
sidebar: {
"/guide/": [
{
text: "入门",
items: [
{text: "简介", link: "/guide/"},
{text: "快速开始", link: "/guide/start.md"},
{
text: "私有化部署",
items: [
{text: "docker部署", link: "/guide/install/docker/"},
{text: "宝塔面板部署", link: "/guide/install/baota/"},
{text: "1Panel部署", link: "/guide/install/1panel/"},
{text: "群晖部署", link: "/guide/use/synology/"},
{text: "源码部署", link: "/guide/install/source/"}
]
},
{text: "演示教程", link: "/guide/tutorial.md"},
{text: "版本升级", link: "/guide/install/upgrade.md"}
]
},
{
text: "特性",
items: [
{text: "CNAME代理校验", link: "/guide/feature/cname/index.md"},
{text: "插件列表", link: "/guide/plugins.md"},
{text: "多数据库支持", link: "/guide/install/database.md"},
{text: "开放接口", link: "/guide/open/index.md"},
{
text: "站点安全", items: [
{text: "安全特性", link: "/guide/feature/safe"},
{text: "站点隐藏", link: "/guide/feature/safe/hidden"},
{text: "安全生产建议", link: "/guide/feature/safe/suggest"},
]
},
]
}
],
"/deploy/":[
{
text: "部署证书插件",
items: [
{ text: "插件说明", link: "/deploy/index.md" },
{ text: "部署到ESXi", link: "/deploy/ESXi/index.md" },
]
}
],
"/comm/": [
{
text: "商业版",
items: [
{ text: "支付宝配置", link: "/comm/payments/alipay.md" },
{ text: "微信支付配置", link: "/comm/payments/wxpay.md" },
{ text: "彩虹易支付配置", link: "/comm/payments/yizhifu.md" },
]
}
]
,
},
]
},
{
text: "常见问题",
items: [
{text: "群晖证书部署", link: "/guide/use/synology/"},
{text: "腾讯云密钥获取", link: "/guide/use/tencent/"},
{text: "连接windows主机", link: "/guide/use/host/windows.md"},
{text: "Google EAB获取", link: "/guide/use/google/"},
{text: "阿里云相关", link: "/guide/use/aliyun/"},
{text: "忘记密码", link: "/guide/use/forgotpasswd/"},
{text: "数据备份", link: "/guide/use/backup/"},
{text: "Certd本身的证书更新", link: "/guide/use/https/index.md"},
{text: "js脚本插件使用", link: "/guide/use/custom-script/index.md"},
{text: "邮箱配置", link: "/guide/use/email/index.md"},
{text: "IPv6支持", link: "/guide/use/setting/ipv6.md"},
{text: "其他插件使用", link: "/deploy/"},
{text: "商业版说明", link: "/comm/"},
]
},
{
text: "其他",
items: [
{text: "贡献代码", link: "/guide/development/index.md"},
{text: "更新日志", link: "/guide/changelogs/CHANGELOG.md"},
{text: "镜像说明", link: "/guide/image.md"},
{text: "联系我们", link: "/guide/contact/"},
{text: "捐赠", link: "/guide/donate/"},
{text: "开源协议", link: "/guide/license/"},
{text: "我的其他开源项目", link: "/guide/link/"},
socialLinks: [
{ icon: "github", link: "https://github.com/certd/certd" }
],
footer: {
message: "Certd帮助文档 | <a href='https://beian.miit.gov.cn/' target='_blank'>粤ICP备14088435号</a> ",
copyright: "Copyright © 2021-present <a href='https://handfree.work/' target='_blank'>handfree.work</a> "
]
}
],
"/deploy/": [
{
text: "部署证书插件",
items: [
{text: "插件说明", link: "/deploy/index.md"},
{text: "部署到ESXi", link: "/deploy/ESXi/index.md"},
]
}
],
"/comm/": [
{
text: "商业版",
items: [
{text: "支付宝配置", link: "/comm/payments/alipay.md"},
{text: "微信支付配置", link: "/comm/payments/wxpay.md"},
{text: "彩虹易支付配置", link: "/comm/payments/yizhifu.md"},
]
}
]
,
},
socialLinks: [
{icon: "github", link: "https://github.com/certd/certd"}
],
footer: {
message: "Certd帮助文档 | <a href='https://beian.miit.gov.cn/' target='_blank'>粤ICP备14088435号</a> ",
copyright: "Copyright © 2021-present <a href='https://handfree.work/' target='_blank'>handfree.work</a> "
}
}
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,25 @@
# 站点隐藏
* 一般来说Certd设置好之后很少需要访问。
* 所以我们`平时`可以把`站点访问关闭`,需要的时候再打开,减少站点被攻击的风险
## 1、开启站点隐藏
`系统管理->系统设置->安全设置->站点隐藏 `
![](./images/hidden1.png)
:::warning
注意保存好`解除地址`和`解除密码`
:::
## 2、临时关闭站点隐藏
访问上面的`解除地址`,输入`解除密码``临时解除`站点隐藏
![](./images/hidden2.png)
## 3、忘记解除地址和解除密码怎么办
登录服务器,在数据库平级的目录下创建`.unhidden`文件即可`临时解除`站点隐藏

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,34 @@
# 站点安全特性
Certd 存储了证书以及授权等敏感数据,所以需要严格保障安全。
我们非常重视您的数据安全,提供了以下安全特性
## 1、 授权数据加密存储【默认开启】
* 所有的授权敏感字段会加密后存储
* 每个用户独立维护授权数据,连管理员都无权查看
![星号部分为加密数据](./images/access.png)
星号部分为加密数据
## 2、 密码防爆破【默认开启】
* 登录失败次数过多账号将被锁定最高24小时(重启服务可解除锁定)
* 用户登录密码加密hash后存储无法计算出密码明文
![](./images/login.png)
## 3、站点隐藏【建议开启】
* 一般来说Certd设置好之后后续很少需要访问修改。
* 所以我们平时可以把站点访问关闭,需要的时候再打开,减少站点被攻击的风险
* 请前往 `系统管理->系统设置->安全设置->开启站点隐藏`
* [站点隐藏设置说明](./hidden/)
![](./images/hidden.png)
## 4、登录二次验证
待实现
## 5、数据库自动备份【建议开启】
* [自动备份设置说明](../../use/backup/)
## 更多安全生产建议
[安全生产建议](./suggest.md)

View File

@ -0,0 +1,10 @@
# 安全生产建议
尽管`Cert`本身实现了很多安全特性,但`外部环境的安全`仍需要您来确保。
请`务必`遵循如下建议做好安全防护
* 请`务必`使用`HTTPS协议`访问本应用,避免被中间人攻击
* 请`务必`使用`web应用防火墙`防护本应用防止XSS、SQL注入等攻击
* 请`务必`做好`服务器本身`的安全防护,防止数据库泄露
* 请`务必`做好[`数据备份`](../../use/backup/),避免数据丢失
* 建议开启[`站点隐藏`](./hidden/)功能

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 = {

View File

@ -46,7 +46,7 @@ importers:
packages/core/acme-client:
dependencies:
'@certd/basic':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../basic
'@peculiar/x509':
specifier: ^1.11.0
@ -204,11 +204,11 @@ importers:
packages/core/pipeline:
dependencies:
'@certd/basic':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../basic
'@certd/plus-core':
specifier: ^1.33.0
version: link:../../pro/plus-core
specifier: ^1.33.2
version: 1.33.2
dayjs:
specifier: ^1.11.7
version: 1.11.13
@ -412,7 +412,7 @@ importers:
packages/libs/lib-k8s:
dependencies:
'@certd/basic':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/basic
'@kubernetes/client-node':
specifier: 0.21.0
@ -452,17 +452,17 @@ importers:
packages/libs/lib-server:
dependencies:
'@certd/acme-client':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/acme-client
'@certd/basic':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/basic
'@certd/pipeline':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/pipeline
'@certd/plus-core':
specifier: ^1.33.0
version: link:../../pro/plus-core
specifier: ^1.33.2
version: 1.33.2
'@midwayjs/cache':
specifier: ~3.14.0
version: 3.14.0
@ -604,16 +604,16 @@ importers:
packages/plugins/plugin-cert:
dependencies:
'@certd/acme-client':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/acme-client
'@certd/basic':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/basic
'@certd/pipeline':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/pipeline
'@certd/plugin-lib':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../plugin-lib
'@google-cloud/publicca':
specifier: ^1.3.0
@ -680,10 +680,10 @@ importers:
specifier: ^1.7.10
version: 1.8.0
'@certd/basic':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/basic
'@certd/pipeline':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/pipeline
'@kubernetes/client-node':
specifier: 0.21.0
@ -771,19 +771,19 @@ importers:
packages/pro/commercial-core:
dependencies:
'@certd/basic':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../../core/basic
'@certd/lib-server':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../../libs/lib-server
'@certd/pipeline':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../../core/pipeline
'@certd/plugin-plus':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../plugin-plus
'@certd/plus-core':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../plus-core
'@midwayjs/core':
specifier: ~3.20.3
@ -868,22 +868,22 @@ importers:
specifier: ^1.0.2
version: 1.0.2
'@certd/basic':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../../core/basic
'@certd/lib-k8s':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../../libs/lib-k8s
'@certd/pipeline':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../../core/pipeline
'@certd/plugin-cert':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../../plugins/plugin-cert
'@certd/plugin-lib':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../../plugins/plugin-lib
'@certd/plus-core':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../plus-core
ali-oss:
specifier: ^6.21.0
@ -980,7 +980,7 @@ importers:
packages/pro/plus-core:
dependencies:
'@certd/basic':
specifier: ^1.33.0
specifier: ^1.32.0
version: link:../../core/basic
dayjs:
specifier: ^1.11.7
@ -1223,6 +1223,9 @@ importers:
sortablejs:
specifier: ^1.15.3
version: 1.15.6
spark-md5:
specifier: ^3.0.2
version: 3.0.2
tailwind-merge:
specifier: ^3.0.2
version: 3.0.2
@ -1267,10 +1270,10 @@ importers:
version: 0.1.3(zod@3.24.2)
devDependencies:
'@certd/lib-iframe':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../libs/lib-iframe
'@certd/pipeline':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/pipeline
'@rollup/plugin-commonjs':
specifier: ^25.0.7
@ -1450,44 +1453,44 @@ importers:
specifier: ^3.705.0
version: 3.758.0(aws-crt@1.25.3)
'@certd/acme-client':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/acme-client
'@certd/basic':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/basic
'@certd/commercial-core':
specifier: ^1.33.0
version: link:../../pro/commercial-core
specifier: ^1.33.2
version: 1.33.2(better-sqlite3@11.8.1)(encoding@0.1.13)(mysql2@3.14.0)(pg@8.13.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@18.19.80)(typescript@5.8.2))
'@certd/jdcloud':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../libs/lib-jdcloud
'@certd/lib-huawei':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../libs/lib-huawei
'@certd/lib-k8s':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../libs/lib-k8s
'@certd/lib-server':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../libs/lib-server
'@certd/midway-flyway-js':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../libs/midway-flyway-js
'@certd/pipeline':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../core/pipeline
'@certd/plugin-cert':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../plugins/plugin-cert
'@certd/plugin-lib':
specifier: ^1.33.0
specifier: ^1.33.2
version: link:../../plugins/plugin-lib
'@certd/plugin-plus':
specifier: ^1.33.0
version: link:../../pro/plugin-plus
specifier: ^1.33.2
version: 1.33.2(encoding@0.1.13)
'@certd/plus-core':
specifier: ^1.33.0
version: link:../../pro/plus-core
specifier: ^1.33.2
version: 1.33.2
'@corsinvest/cv4pve-api-javascript':
specifier: ^8.3.0
version: 8.3.0
@ -2617,6 +2620,15 @@ packages:
'@better-scroll/zoom@2.5.1':
resolution: {integrity: sha512-aGvFY5ooeZWS4RcxQLD+pGLpQHQxpPy0sMZV3yadcd2QK53PK9gS4Dp+BYfRv8lZ4/P2LoNEhr6Wq1DN6+uPlA==}
'@certd/commercial-core@1.33.2':
resolution: {integrity: sha512-LTRvwRAkMEU+knG+/eA8QbyK3EE2Z2eyn4763ILadCY/qHLAWqxg7NUD+fwsuAoByvsr3l5qLPPOF73p5s1iEA==}
'@certd/plugin-plus@1.33.2':
resolution: {integrity: sha512-x8qdJ1qtYfqVNBQ3uWJuFVYUHHnteoaH/3vnYKjgAnEAcosruZAz8h5RwwOjDhR+Vsdda/sSRdjlTCVDLidHZg==}
'@certd/plus-core@1.33.2':
resolution: {integrity: sha512-CsD+/P3Ycne3KzxrZAkFXGpCO9ZQNY4p28WAA2XfKQ6sLIYLuuDBP18V/E7EygfJEZ/c9KJWMkYgx/4wGGvb6w==}
'@colors/colors@1.5.0':
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
@ -14791,6 +14803,75 @@ snapshots:
dependencies:
'@better-scroll/core': 2.5.1
'@certd/commercial-core@1.33.2(better-sqlite3@11.8.1)(encoding@0.1.13)(mysql2@3.14.0)(pg@8.13.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@18.19.80)(typescript@5.8.2))':
dependencies:
'@certd/basic': link:packages/core/basic
'@certd/lib-server': link:packages/libs/lib-server
'@certd/pipeline': link:packages/core/pipeline
'@certd/plugin-plus': 1.33.2(encoding@0.1.13)
'@certd/plus-core': 1.33.2
'@midwayjs/core': 3.20.3
'@midwayjs/koa': 3.20.3
'@midwayjs/logger': 3.4.2
'@midwayjs/typeorm': 3.20.3
alipay-sdk: 4.13.0
dayjs: 1.11.13
typeorm: 0.3.21(better-sqlite3@11.8.1)(mysql2@3.14.0)(pg@8.13.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@18.19.80)(typescript@5.8.2))
wechatpay-node-v3: 2.2.1
transitivePeerDependencies:
- '@google-cloud/spanner'
- '@sap/hana-client'
- better-sqlite3
- encoding
- hdb-pool
- ioredis
- mongodb
- mssql
- mysql2
- oracledb
- pg
- pg-native
- pg-query-stream
- proxy-agent
- redis
- reflect-metadata
- sql.js
- sqlite3
- supports-color
- ts-node
- typeorm-aurora-data-api-driver
'@certd/plugin-plus@1.33.2(encoding@0.1.13)':
dependencies:
'@alicloud/pop-core': 1.8.0
'@baiducloud/sdk': 1.0.2
'@certd/basic': link:packages/core/basic
'@certd/lib-k8s': link:packages/libs/lib-k8s
'@certd/pipeline': link:packages/core/pipeline
'@certd/plugin-cert': link:packages/plugins/plugin-cert
'@certd/plugin-lib': link:packages/plugins/plugin-lib
'@certd/plus-core': 1.33.2
ali-oss: 6.22.0
baidu-aip-sdk: 4.16.16
basic-ftp: 5.0.5
cos-nodejs-sdk-v5: 2.14.6
crypto-js: 4.2.0
dayjs: 1.11.13
form-data: 4.0.2
https-proxy-agent: 7.0.6
jsencrypt: 3.3.2
qiniu: 7.14.0
tencentcloud-sdk-nodejs: 4.0.1045(encoding@0.1.13)
transitivePeerDependencies:
- encoding
- proxy-agent
- supports-color
'@certd/plus-core@1.33.2':
dependencies:
'@certd/basic': link:packages/core/basic
dayjs: 1.11.13
'@colors/colors@1.5.0':
optional: true