Compare commits

...

41 Commits

Author SHA1 Message Date
xiaojunnuo
198a97b00c v1.33.5 2025-04-17 23:35:14 +08:00
xiaojunnuo
3ea4e917e8 build: prepare to build 2025-04-17 23:32:08 +08:00
xiaojunnuo
60ad077172 pref: 任务日志查看页面,增加强制重新运行按钮 2025-04-17 23:31:43 +08:00
xiaojunnuo
356ad28e41 chore: 2025-04-17 23:11:04 +08:00
xiaojunnuo
e241141220 build: prepare to build 2025-04-17 23:07:14 +08:00
xiaojunnuo
14bb1b467a chore: 2025-04-17 23:06:17 +08:00
xiaojunnuo
2bbea6fd3f chore: 2025-04-17 23:05:52 +08:00
xiaojunnuo
48aef25b3f perf: 登录支持双重认证 2025-04-17 22:34:21 +08:00
xiaojunnuo
8e50e5dee3 chore: plus 2025-04-17 13:41:08 +08:00
xiaojunnuo
d5d54d4d3b chore: 2FA 2025-04-17 01:15:55 +08:00
xiaojunnuo
412e8a32dd Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2025-04-17 00:06:58 +08:00
xiaojunnuo
0f82cf409b perf: 多重认证登录 2025-04-17 00:06:49 +08:00
xiaojunnuo
79df39acab perf: 优化部署到华为云CDN,支持先上传到ccm,再使用证书id部署,修复offline状态下导致部署报错的bug 2025-04-16 09:34:04 +08:00
xiaojunnuo
8786bae7dc chore: 2025-04-16 00:03:13 +08:00
xiaojunnuo
4b3f8ca361 chore: 2025-04-16 00:02:58 +08:00
xiaojunnuo
03183218f7 chore: 2025-04-16 00:01:52 +08:00
xiaojunnuo
95b6db57e1 chore: 2025-04-15 23:57:50 +08:00
xiaojunnuo
bbe0c2457b build: publish 2025-04-15 23:50:06 +08:00
xiaojunnuo
c894c53e69 build: trigger build image 2025-04-15 23:49:48 +08:00
xiaojunnuo
5b3fb7387d v1.33.4 2025-04-15 23:48:00 +08:00
xiaojunnuo
feac310caf build: prepare to build 2025-04-15 23:45:18 +08:00
xiaojunnuo
d67ec3feb3 chore: 2025-04-15 23:45:11 +08:00
xiaojunnuo
cf8abb4528 perf: 插件支持导入导出 2025-04-15 23:43:01 +08:00
xiaojunnuo
d66de26de4 Merge branch 'v2' into v2-dev 2025-04-15 21:34:13 +08:00
greper
7edf3f6147 fix: 修复腾讯云部署到任意资源插件,无法使用之前已上传的腾讯云证书问题 from xinghejd/dev @xinghejd
fix: 修复腾讯云部署到任意资源插件,无法使用之前已上传的腾讯云证书问题
2025-04-15 21:16:49 +08:00
xinghejd
2143dff2ae fix: 补充类型断言 2025-04-15 08:54:28 +00:00
xinghejd
32c714d1b6 fix: 修复腾讯云部署到任意资源插件,无法使用之前已上传的腾讯云证书问题 2025-04-15 08:47:57 +00:00
xiaojunnuo
84e699ee24 chore: 思维导图 2025-04-15 11:22:42 +08:00
xiaojunnuo
7fdb572b8b chore: 思维导图 2025-04-15 11:07:27 +08:00
xiaojunnuo
cfd3b66be9 perf: 支持上传证书到华为云CCM 2025-04-14 23:31:59 +08:00
xiaojunnuo
75c4f9dea8 build: publish 2025-04-14 22:27:24 +08:00
xiaojunnuo
a76a32230d build: trigger build image 2025-04-14 22:26:59 +08:00
xiaojunnuo
0730f5ff4f v1.33.3 2025-04-14 22:25:16 +08:00
xiaojunnuo
c43d0a684c build: prepare to build 2025-04-14 22:22:17 +08:00
xiaojunnuo
66f1eda6cf chore: 2025-04-14 22:22:01 +08:00
xiaojunnuo
bf4d191c8b fix: 修复登录错误次数过多阻止再次登录逻辑 2025-04-14 18:09:54 +08:00
xiaojunnuo
d76d56fcce pref: 安全特性支持,站点隐藏功能 2025-04-14 17:40:23 +08:00
xiaojunnuo
251b0c58de chore: 2025-04-14 10:06:58 +08:00
xiaojunnuo
073cca4e8e chore: 2025-04-14 09:54:24 +08:00
xiaojunnuo
a4ad99f189 build: publish 2025-04-13 01:33:57 +08:00
xiaojunnuo
d37b910889 build: trigger build image 2025-04-13 01:33:28 +08:00
111 changed files with 2784 additions and 847 deletions

View File

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
### Performance Improvements
* 登录支持双重认证 ([48aef25](https://github.com/certd/certd/commit/48aef25b3f6499d674ca4e4ef16f4c62399fb735))
* 多重认证登录 ([0f82cf4](https://github.com/certd/certd/commit/0f82cf409bc60706ab07e4ca4f272b9a1ca7eecb))
* 优化部署到华为云CDN支持先上传到ccm再使用证书id部署修复offline状态下导致部署报错的bug ([79df39a](https://github.com/certd/certd/commit/79df39acabab10ae7e1864dadcdc186bb007a3c5))
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
### Bug Fixes
* 补充类型断言 ([2143dff](https://github.com/certd/certd/commit/2143dff2ae96e6a78bef9f0498e36f8cd9e6941f))
* 修复腾讯云部署到任意资源插件,无法使用之前已上传的腾讯云证书问题 ([32c714d](https://github.com/certd/certd/commit/32c714d1b6e68c71a74a7452115040c87ac4bfdc))
### Performance Improvements
* 插件支持导入导出 ([cf8abb4](https://github.com/certd/certd/commit/cf8abb45282070c8ba91469f93fd379fabf1f74a))
* 支持上传证书到华为云CCM ([cfd3b66](https://github.com/certd/certd/commit/cfd3b66be9ebf53a26693057e70ed60c3f116be9))
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
### Bug Fixes
* 修复登录错误次数过多阻止再次登录逻辑 ([bf4d191](https://github.com/certd/certd/commit/bf4d191c8bd2f9209eb6768f662b9c77de99e998))
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
### Bug Fixes

128
README.md
View File

@@ -9,13 +9,16 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系
本项目不仅支持证书申请过程自动化,还可以自动化部署更新证书,让你的证书永不过期。
* 全自动申请证书(支持所有注册商注册的域名)
* 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等目前已支持60+部署插件)
* 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等70+部署插件)
* 支持DNS-01、HTTP-01、CNAME代理等多种域名验证方式
* 支持通配符域名/泛域名支持多个域名打到一个证书上支持pem、pfx、der、jks等多种证书格式
* 邮件通知、webhook通知
* 私有化部署数据保存本地授权信息加密存储镜像由Github Actions构建过程公开透明
* 支持SQLitePostgreSQL、MySQL数据库
![](./docs/images/intro/intro.svg)
>
> 流水线数量现已调整为无限制,欢迎大家使用
>
@@ -23,7 +26,7 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系
> 关于证书续期:
>* 实际上没有办法不改变证书文件本身情况下直接续期或者续签。
>* 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去。
>* 免费证书过期时间90天以后可能还会缩短所以自动化部署必不可少
## 二、在线体验
@@ -62,7 +65,7 @@ https://certd.handfree.work/
-------> [点我查看详细使用步骤演示](./step.md) <--------
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
更多教程请访问文档网站 [certd.docmirror.cn](https://certd.docmirror.cn/)
更多教程请访问官方文档 [certd.docmirror.cn](https://certd.docmirror.cn/guide/)
@@ -72,10 +75,10 @@ https://certd.handfree.work/
您可以根据实际情况从如下方式中选择一种方式进行私有化部署:
1. [宝塔面板方式部署](https://certd.docmirror.cn/guide/install/docker/)
2. [1Panel面板方式部署](https://certd.docmirror.cn/guide/install/1panel/)
3. [Docker方式部署](https://certd.docmirror.cn/guide/install/docker/)
4. [源码方式部署](https://certd.docmirror.cn/guide/install/source/)
1. [宝塔面板方式部署 推荐](https://certd.docmirror.cn/guide/install/docker/)
2. [1Panel面板方式部署 推荐](https://certd.docmirror.cn/guide/install/1panel/)
3. [Docker方式部署 推荐](https://certd.docmirror.cn/guide/install/docker/)
4. [源码方式部署 不建议](https://certd.docmirror.cn/guide/install/source/)
#### Docker镜像说明
* 国内镜像地址:
@@ -94,87 +97,27 @@ https://certd.handfree.work/
![](./docs/images/action/action-build.jpg)
## 五、 升级
### docker-compose方式部署
#### 1. 如果使用固定版本号
1. 修改`docker-compose.yaml`中的镜像版本号
2. 运行`docker compose up -d` 即可
#### 2. 如果需要使用最新版本
```shell
#重新拉取镜像
docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
# 重新启动容器
docker compose down
docker compose up -d
```
> 数据默认存在`/data/certd`目录下,不用担心数据丢失
### 自动升级(仅限尝鲜建议非生产使用)
```yaml
version: '3.3'
services:
certd:
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
container_name: certd
restart: unless-stopped
volumes:
- /data/certd:/app/data
ports:
- "7001:7001"
- "7002:7002"
environment:
- certd_system_resetAdminPasswd=false
labels:
com.centurylinklabs.watchtower.enable: "true"
certd-updater: # 添加 Watchtower 服务
image: containrrr/watchtower:latest
container_name: certd-updater
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# 配置 自动更新
environment:
- WATCHTOWER_CLEANUP=true # 自动清理旧版本容器
- WATCHTOWER_INCLUDE_STOPPED=false # 不更新已停止的容器
- WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新
- WATCHTOWER_POLL_INTERVAL=300 # 每 5 分钟检查一次更新
```
### 其他部署方式升级方法
请参考 https://certd.docmirror.cn/guide/install/upgrade.html
> 注意:
> * 本应用存储的证书、授权信息等属于高度敏感数据,请做好安全防护
> * 请务必使用HTTPS协议访问本应用避免被中间人攻击
> * 请务必使用web应用防火墙防护本应用防止XSS、SQL注入等攻击
> * 请务必做好服务器本身的安全防护,防止数据库泄露
> * 请务必做好数据备份,避免数据丢失
### 更新日志:
[CHANGELOG](./CHANGELOG.md)
## 五、更多帮助
请访问官方文档:[https://certd.docmirror.cn/](https://certd.docmirror.cn/guide/)
* 升级方法:[升级方法](https://certd.docmirror.cn/guide/install/upgrade/)
* 常见问题:[忘记密码](https://certd.docmirror.cn/guide/use/forgotpasswd/)
* 多数据库:[多数据库配置](https://certd.docmirror.cn/guide/install/database/)
* 站点安全:[站点安全特性](https://certd.docmirror.cn/guide/feature/safe/)
* 更新日志:[CHANGELOG](./CHANGELOG.md)
## 六、一些说明
* 本项目ssl证书提供商为letencrypt/Google/ZeroSSL
* 申请过程遵循acme协议
* 证书续期:
* 实际上没有办法不改变证书文件本身情况下直接续期或者续签。
* 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去。
* 免费证书过期时间90天以后可能还会缩短所以自动化部署必不可少
* 设置每天自动运行当证书过期前35天会自动重新申请证书并部署
## 七、不同平台的设置说明
* 已迁移到新的文档网站,请到常见问题章节查看
* [最新文档站链接 https://certd.docmirror.cn](https://certd.docmirror.cn/)
## 八、问题处理
### 7.1 忘记管理员密码
[重置管理员密码方法](https://certd.docmirror.cn/guide/use/forgotpasswd/)
## 九、联系作者
## 六、联系作者
如有疑问欢迎加入群聊请备注certd
| 加群 | 微信群 | QQ群 |
@@ -188,7 +131,7 @@ services:
| 二维码 | <img height="230" src="./docs/guide/contact/images/me.png"> |
## 、捐赠
## 、捐赠
************************
支持开源,为爱发电,我已入驻爱发电
https://afdian.com/a/greper
@@ -197,8 +140,6 @@ https://afdian.com/a/greper
1. 可加入发电专属群,可以获得作者一对一技术支持
2. 您的需求我们将优先实现,并且将作为专业版功能提供
3. 一年期专业版激活码
4. 赠送国外免费服务器部署方案0成本使用Certd可能需要翻墙不过现在性能越来越差了
专业版特权对比
@@ -214,33 +155,26 @@ https://afdian.com/a/greper
************************
## 十一、贡献代码
## 、贡献代码
1. 本地开发 [贡献插件](https://certd.docmirror.cn/guide/development/)
1. 本地开发请参考 [贡献插件向导](https://certd.docmirror.cn/guide/development/)
2. 作为贡献者,代表您同意您贡献的代码如下许可:
1. 可以调整开源协议以使其更严格或更宽松。
2. 可以用于商业用途。
## 十二、 开源许可
## 、 开源许可
* 本项目遵循 GNU Affero General Public LicenseAGPL开源协议。
* 允许个人和公司内部自由使用、复制、修改和分发本项目,未获得商业授权情况下禁止任何形式的商业用途
* 未获得商业授权情况下禁止任何对logo、版权信息及授权许可相关代码的修改。
* 如需商业授权,请联系作者。
## 十三、我的其他项目求Star
## 十、我的其他项目求Star
| 项目名称 | stars | 项目描述 |
|---------------------------------------------------------|-------------------------------------------------------------------------------------------------------|-----------------------------------|
| [袖手AI](https://ai.handsfree.work/) | | 袖手GPT国内可用无需FQ每日免费额度 |
| [fast-crud](https://gitee.com/fast-crud/fast-crud/) | <img alt="GitHub stars" src="https://img.shields.io/github/stars/fast-crud/fast-crud?logo=github"/> | 基于vue3的crud快速开发框架 |
| [dev-sidecar](https://github.com/docmirror/dev-sidecar/) | <img alt="GitHub stars" src="https://img.shields.io/github/stars/docmirror/dev-sidecar?logo=github"/> | 直连访问github工具无需FQ解决github无法访问的问题 |
## 十四、更新日志
更新日志:[CHANGELOG](./CHANGELOG.md)

View File

@@ -1 +1 @@
01:02
23:49

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> "
}
}
}
});

View File

@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
### Bug Fixes
* 补充类型断言 ([2143dff](https://github.com/certd/certd/commit/2143dff2ae96e6a78bef9f0498e36f8cd9e6941f))
* 修复腾讯云部署到任意资源插件,无法使用之前已上传的腾讯云证书问题 ([32c714d](https://github.com/certd/certd/commit/32c714d1b6e68c71a74a7452115040c87ac4bfdc))
### Performance Improvements
* 插件支持导入导出 ([cf8abb4](https://github.com/certd/certd/commit/cf8abb45282070c8ba91469f93fd379fabf1f74a))
* 支持上传证书到华为云CCM ([cfd3b66](https://github.com/certd/certd/commit/cfd3b66be9ebf53a26693057e70ed60c3f116be9))
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
### Bug Fixes
* 修复登录错误次数过多阻止再次登录逻辑 ([bf4d191](https://github.com/certd/certd/commit/bf4d191c8bd2f9209eb6768f662b9c77de99e998))
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
### Bug Fixes
* 修复某些情况下无法输出日志的bug ([70101bf](https://github.com/certd/certd/commit/70101bfa7ade65678d9202c804bbae2cb808b594))
### Performance Improvements
* 修复内置插件分页查询逻辑 ([a2710dd](https://github.com/certd/certd/commit/a2710ddc2525e4e637fd157f0180e6d3b801c8be))
## [1.33.1](https://github.com/certd/certd/compare/v1.33.0...v1.33.1) (2025-04-12)
### Bug Fixes

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: 70 KiB

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

View File

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

View File

@@ -17,6 +17,8 @@ Certd 是一款开源、免费、全自动申请和部署更新SSL证书的工
* 支持SQLite、Postgresql、MySQL数据库
![](../images/intro/intro.svg)
## 二、一些说明
* 本项目申请证书过程遵循acme协议
* 需要验证域名所有权,一般有两种方式

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.33.2"
"version": "1.33.5"
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/publishlab/node-acme-client/compare/v1.33.4...v1.33.5) (2025-04-17)
**Note:** Version bump only for package @certd/acme-client
## [1.33.4](https://github.com/publishlab/node-acme-client/compare/v1.33.3...v1.33.4) (2025-04-15)
**Note:** Version bump only for package @certd/acme-client
## [1.33.3](https://github.com/publishlab/node-acme-client/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/acme-client
## [1.33.2](https://github.com/publishlab/node-acme-client/compare/v1.33.1...v1.33.2) (2025-04-12)
**Note:** Version bump only for package @certd/acme-client

View File

@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.33.2",
"version": "1.33.5",
"type": "module",
"module": "scr/index.js",
"main": "src/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.33.2",
"@certd/basic": "^1.33.5",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.7.2",
@@ -67,5 +67,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
### Performance Improvements
* 多重认证登录 ([0f82cf4](https://github.com/certd/certd/commit/0f82cf409bc60706ab07e4ca4f272b9a1ca7eecb))
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
**Note:** Version bump only for package @certd/basic
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/basic
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
### Bug Fixes

View File

@@ -1 +1 @@
01:29
23:32

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.33.2",
"version": "1.33.5",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -44,5 +44,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

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

@@ -1,4 +1,4 @@
import { customAlphabet } from "nanoid";
export const randomNumber = customAlphabet("1234567890", 4);
export const simpleNanoId = customAlphabet("1234567890abcdefghijklmopqrstuvwxyz", 12);
export const simpleNanoId = customAlphabet("1234567890abcdefghijklmopqrstuvwxyzABCDEFGHIJKLMOPQRSTUVWXYZ", 12);

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
**Note:** Version bump only for package @certd/pipeline
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
**Note:** Version bump only for package @certd/pipeline
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/pipeline
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
**Note:** Version bump only for package @certd/pipeline

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.33.2",
"version": "1.33.5",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,8 +16,8 @@
"test": "mocha --loader=ts-node/esm"
},
"dependencies": {
"@certd/basic": "^1.33.2",
"@certd/plus-core": "^1.33.2",
"@certd/basic": "^1.33.5",
"@certd/plus-core": "^1.33.5",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -43,5 +43,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
**Note:** Version bump only for package @certd/lib-huawei
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
**Note:** Version bump only for package @certd/lib-huawei
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/lib-huawei
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
**Note:** Version bump only for package @certd/lib-huawei

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.33.2",
"version": "1.33.5",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -23,5 +23,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
**Note:** Version bump only for package @certd/lib-iframe
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
**Note:** Version bump only for package @certd/lib-iframe
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/lib-iframe
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
**Note:** Version bump only for package @certd/lib-iframe

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.33.2",
"version": "1.33.5",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -30,5 +30,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
**Note:** Version bump only for package @certd/jdcloud
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
**Note:** Version bump only for package @certd/jdcloud
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/jdcloud
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
**Note:** Version bump only for package @certd/jdcloud

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.33.2",
"version": "1.33.5",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -60,5 +60,5 @@
"fetch"
]
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
**Note:** Version bump only for package @certd/lib-k8s
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
**Note:** Version bump only for package @certd/lib-k8s
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/lib-k8s
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
**Note:** Version bump only for package @certd/lib-k8s

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.33.2",
"version": "1.33.5",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,7 +16,7 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/basic": "^1.33.2",
"@certd/basic": "^1.33.5",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
### Performance Improvements
* 登录支持双重认证 ([48aef25](https://github.com/certd/certd/commit/48aef25b3f6499d674ca4e4ef16f4c62399fb735))
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
**Note:** Version bump only for package @certd/lib-server
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/lib-server
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
**Note:** Version bump only for package @certd/lib-server

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.33.2",
"version": "1.33.5",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -27,10 +27,10 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.33.2",
"@certd/basic": "^1.33.2",
"@certd/pipeline": "^1.33.2",
"@certd/plus-core": "^1.33.2",
"@certd/acme-client": "^1.33.5",
"@certd/basic": "^1.33.5",
"@certd/pipeline": "^1.33.5",
"@certd/plus-core": "^1.33.5",
"@midwayjs/cache": "~3.14.0",
"@midwayjs/core": "~3.20.3",
"@midwayjs/i18n": "~3.20.3",
@@ -61,5 +61,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

View File

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

View File

@@ -1,10 +1,19 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { TextException } from "./common-exception.js";
/**
* 授权异常
*/
export class AuthException extends BaseException {
constructor(message) {
constructor(message?:string) {
super('AuthException', Constants.res.auth.code, message ? message : Constants.res.auth.message);
}
}
export class Need2FAException extends TextException {
constructor(message:string,data:any) {
super('Need2FAException', Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message,data);
}
}

View File

@@ -3,9 +3,11 @@
*/
export class BaseException extends Error {
code: number;
constructor(name, code, message) {
data?:any
constructor(name, code, message,data?:any) {
super(message);
this.name = name;
this.code = code;
this.data = data;
}
}

View File

@@ -1,16 +1,23 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 通用异常
*/
export class CommonException extends BaseException {
constructor(message) {
super('CommonException', Constants.res.error.code, message ? message : Constants.res.error.message);
super("CommonException", Constants.res.error.code, message ? message : Constants.res.error.message);
}
}
export class CodeException extends BaseException {
constructor(res: { code: number; message: string }) {
super('CodeException', res.code, res.message);
super("CodeException", res.code, res.message);
}
}
export class TextException extends BaseException {
constructor(name, code,message, data?) {
super(name, code, message, data);
}
}

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

@@ -2,14 +2,15 @@ export class Result<T> {
code: number;
msg: string;
data: T;
constructor(code, msg, data?) {
this.code = code;
this.msg = msg;
this.data = data;
}
static error(code = 1, msg) {
return new Result(code, msg);
static error(code = 1, msg, data?: any) {
return new Result(code, msg, data);
}
static success(msg, data?) {

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

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
**Note:** Version bump only for package @certd/midway-flyway-js

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.33.2",
"version": "1.33.5",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
**Note:** Version bump only for package @certd/plugin-cert
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
**Note:** Version bump only for package @certd/plugin-cert
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/plugin-cert
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
**Note:** Version bump only for package @certd/plugin-cert

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.33.2",
"version": "1.33.5",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -15,10 +15,10 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/acme-client": "^1.33.2",
"@certd/basic": "^1.33.2",
"@certd/pipeline": "^1.33.2",
"@certd/plugin-lib": "^1.33.2",
"@certd/acme-client": "^1.33.5",
"@certd/basic": "^1.33.5",
"@certd/pipeline": "^1.33.5",
"@certd/plugin-lib": "^1.33.5",
"@google-cloud/publicca": "^1.3.0",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
@@ -41,5 +41,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
**Note:** Version bump only for package @certd/plugin-lib
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
### Performance Improvements
* 插件支持导入导出 ([cf8abb4](https://github.com/certd/certd/commit/cf8abb45282070c8ba91469f93fd379fabf1f74a))
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/plugin-lib
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
**Note:** Version bump only for package @certd/plugin-lib

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.33.2",
"version": "1.33.5",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -16,8 +16,8 @@
},
"dependencies": {
"@alicloud/pop-core": "^1.7.10",
"@certd/basic": "^1.33.2",
"@certd/pipeline": "^1.33.2",
"@certd/basic": "^1.33.5",
"@certd/pipeline": "^1.33.5",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.21.0",
"basic-ftp": "^5.0.5",
@@ -48,5 +48,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "64244af2cce396be5e2831937e5e3024a22899ed"
"gitHead": "5b3fb7387df65ed67811623ef9a2c5adadc8bf4f"
}

View File

@@ -1,7 +1,4 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
import { ConnectConfig } from "ssh2";
import { SshClient } from "./ssh.js";
@IsAccess({
name: "ssh",
title: "主机登录授权",
@@ -9,7 +6,7 @@ import { SshClient } from "./ssh.js";
icon: "clarity:host-line",
input: {},
})
export class SshAccess extends BaseAccess implements ConnectConfig {
export class SshAccess extends BaseAccess {
@AccessInput({
title: "主机地址",
component: {
@@ -125,6 +122,7 @@ export class SshAccess extends BaseAccess implements ConnectConfig {
testRequest = true;
async onTestRequest() {
const { SshClient } = await import("./ssh.js");
const client = new SshClient(this.ctx.logger);
await client.exec({

View File

@@ -1,22 +1,33 @@
// @ts-ignore
import ssh2, { ConnectConfig, ExecOptions } from "ssh2";
import ssh2Constants from "ssh2/lib/protocol/constants.js";
import path from "path";
import * as _ from "lodash-es";
import { isArray } from "lodash-es";
import { ILogger } from "@certd/basic";
import { SshAccess } from "./ssh-access.js";
import stripAnsi from "strip-ansi";
import { SocksClient } from "socks";
import { SocksProxy, SocksProxyType } from "socks/typings/common/constants.js";
import fs from "fs";
import { SocksProxyType } from "socks/typings/common/constants";
export type TransportItem = { localPath: string; remotePath: string };
export interface SocksProxy {
ipaddress?: string;
host?: string;
port: number;
type: any;
userId?: string;
password?: string;
custom_auth_method?: number;
custom_auth_request_handler?: () => Promise<Buffer>;
custom_auth_response_size?: number;
custom_auth_response_handler?: (data: Buffer) => Promise<boolean>;
}
export type SshConnectConfig = {
sock?: any;
};
export class AsyncSsh2Client {
conn: ssh2.Client;
conn: any;
logger: ILogger;
connConf: SshAccess & ssh2.ConnectConfig;
connConf: SshAccess & SshConnectConfig;
windows = false;
encoding: string;
constructor(connConf: SshAccess, logger: ILogger) {
@@ -40,7 +51,10 @@ export class AsyncSsh2Client {
if (typeof this.connConf.port === "string") {
this.connConf.port = parseInt(this.connConf.port);
}
const proxyOption: SocksProxy = this.parseSocksProxyFromUri(this.connConf.socksProxy);
const { SocksClient } = await import("socks");
const proxyOption = this.parseSocksProxyFromUri(this.connConf.socksProxy);
const info = await SocksClient.createConnection({
proxy: proxyOption,
command: "connect",
@@ -53,10 +67,12 @@ export class AsyncSsh2Client {
this.connConf.sock = info.socket;
}
const { SUPPORTED_KEX, SUPPORTED_SERVER_HOST_KEY, SUPPORTED_CIPHER, SUPPORTED_MAC } = ssh2Constants;
const ssh2 = await import("ssh2");
const ssh2Constants = await import("ssh2/lib/protocol/constants.js");
const { SUPPORTED_KEX, SUPPORTED_SERVER_HOST_KEY, SUPPORTED_CIPHER, SUPPORTED_MAC } = ssh2Constants.default;
return new Promise((resolve, reject) => {
try {
const conn = new ssh2.Client();
const conn = new ssh2.default.Client();
conn
.on("error", (err: any) => {
this.logger.error("连接失败", err);
@@ -197,6 +213,8 @@ export class AsyncSsh2Client {
}
async shell(script: string | string[]): Promise<string> {
const stripAnsiModule = await import("strip-ansi");
const stripAnsi = stripAnsiModule.default;
return new Promise<any>((resolve, reject) => {
this.logger.info(`执行shell脚本[${this.connConf.host}][shell]: ` + script);
this.conn.shell((err: Error, stream: any) => {
@@ -449,7 +467,7 @@ export class SshClient {
script = script.join(" && ");
} else {
const newLine = isLinux ? "\n" : "\r\n";
if (_.isArray(script)) {
if (isArray(script)) {
script = script as Array<string>;
script = script.join(newLine);
}
@@ -465,7 +483,7 @@ export class SshClient {
async shell(options: { connectConf: SshAccess; script: string | Array<string> }): Promise<string> {
let { script } = options;
const { connectConf } = options;
if (_.isArray(script)) {
if (isArray(script)) {
script = script as Array<string>;
if (connectConf.windows) {
script = script.join("\r\n");

View File

@@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
### Performance Improvements
* 登录支持双重认证 ([48aef25](https://github.com/certd/certd/commit/48aef25b3f6499d674ca4e4ef16f4c62399fb735))
* 多重认证登录 ([0f82cf4](https://github.com/certd/certd/commit/0f82cf409bc60706ab07e4ca4f272b9a1ca7eecb))
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
### Performance Improvements
* 插件支持导入导出 ([cf8abb4](https://github.com/certd/certd/commit/cf8abb45282070c8ba91469f93fd379fabf1f74a))
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
**Note:** Version bump only for package @certd/ui-client
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
### Performance Improvements

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.33.2",
"version": "1.33.5",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -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",
@@ -100,8 +101,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.33.2",
"@certd/pipeline": "^1.33.2",
"@certd/lib-iframe": "^1.33.5",
"@certd/pipeline": "^1.33.5",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",

View File

@@ -3,6 +3,17 @@ import { get } from "lodash-es";
import { errorLog, errorCreate } from "./tools";
import { env } from "/src/utils/util.env";
import { useUserStore } from "/@/store/user";
export class CodeError extends Error {
code: number;
data?: any;
constructor(message: string, code: number, data?: any) {
super(message);
this.code = code;
this.data = data;
}
}
/**
* @description 创建请求实例
*/
@@ -56,12 +67,13 @@ function createService() {
const errorMessage = dataAxios.msg || dataAxios.message || "未知错误";
// @ts-ignore
if (response?.config?.onError) {
// @ts-ignore
response.config.onError(new Error(errorMessage));
const err = new CodeError(errorMessage, dataAxios.code, dataAxios.data);
response.config.onError(err);
return;
}
//@ts-ignore
const showErrorNotify = response?.config?.showErrorNotify;
errorCreate(`${errorMessage}: ${response.config.url}`, showErrorNotify);
errorCreate(`${errorMessage}: ${response.config.url}`, showErrorNotify, dataAxios);
return dataAxios;
}
}

View File

@@ -4,6 +4,7 @@
* @param {String} defaultValue 默认值
*/
import { uiContext } from "@fast-crud/fast-crud";
import { CodeError } from "/@/api/service";
export function parse(jsonString = "{}", defaultValue = {}) {
let result = defaultValue;
@@ -68,8 +69,8 @@ export function errorLog(error: any, notify = true) {
* @description 创建一个错误
* @param {String} msg 错误信息
*/
export function errorCreate(msg: string, notify = true) {
const err = new Error(msg);
export function errorCreate(msg: string, notify = true, data?: any) {
const err = new CodeError(msg, data.code, data.data);
console.error("errorCreate", err);
if (notify) {
uiContext.get().notification.error({ message: err.message });

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

@@ -143,6 +143,17 @@ export const certdResources = [
keepAlive: true,
},
},
{
title: "认证安全设置",
name: "UserSecurity",
path: "/certd/mine/security",
component: "/certd/mine/security/index.vue",
meta: {
icon: "fluent:shield-keyhole-16-regular",
auth: true,
isMenu: true,
},
},
{
title: "账号信息",
name: "UserProfile",

View File

@@ -66,3 +66,11 @@ export async function mine(): Promise<UserInfoRes> {
method: "post",
});
}
export async function loginByTwoFactor(data: any) {
return await request({
url: "/loginByTwoFactor",
method: "post",
data,
});
}

View File

@@ -51,7 +51,7 @@ export const useUserStore = defineStore({
setUserInfo(info: UserInfoRes) {
this.userInfo = info;
const userStore = vbenUserStore();
userStore.setUserInfo(info);
userStore.setUserInfo(info as any);
LocalStorage.set(USER_INFO_KEY, info);
},
resetState() {
@@ -71,23 +71,18 @@ export const useUserStore = defineStore({
* @description: login
*/
async login(loginType: string, params: LoginReq | SmsLoginReq): Promise<any> {
try {
let loginRes: any = null;
if (loginType === "sms") {
loginRes = await UserApi.loginBySms(params as SmsLoginReq);
} else {
loginRes = await UserApi.login(params as LoginReq);
}
const { token, expire } = loginRes;
// save token
this.setToken(token, expire);
// get user info
return await this.onLoginSuccess(loginRes);
} catch (error) {
console.error(error);
return null;
let loginRes: any = null;
if (loginType === "sms") {
loginRes = await UserApi.loginBySms(params as SmsLoginReq);
} else {
loginRes = await UserApi.login(params as LoginReq);
}
return await this.onLoginSuccess(loginRes);
},
async loginByTwoFactor(form: any) {
const loginRes = await UserApi.loginByTwoFactor(form);
return await this.onLoginSuccess(loginRes);
},
async getUserInfoAction(): Promise<UserInfoRes> {
const userInfo = await UserApi.mine();
@@ -100,9 +95,13 @@ export const useUserStore = defineStore({
},
async onLoginSuccess(loginData: any) {
const { token, expire } = loginData;
// save token
this.setToken(token, expire);
// get user info
// await this.getUserInfoAction();
// const userInfo = await this.getUserInfoAction();
mitter.emit("app.login", { token: loginData });
mitter.emit("app.login", { ...loginData });
await router.replace("/");
},

View File

@@ -99,4 +99,25 @@ span.fs-icon-svg{
.ant-btn .fs-icon:last-child{
margin-right:0px
}
.fs-iconify fs-icon{
svg{
vertical-align:0 !important;
}
}
.fs-button{
span{
&:first-child{
margin-right: 5px;
}
&:last-child{
margin-left: 5px;
}
}
.fs-icon,.fs-button-icon{
margin: 0 !important;
}
}

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

@@ -0,0 +1,47 @@
// @ts-ignore
import { request } from "/@/api/service";
const apiPrefix = "/user/settings";
export type UserTwoFactorSetting = {
authenticator: {
enabled: boolean;
verified: boolean;
};
};
export type AuthenticatorSaveReq = {
verifyCode?: string;
};
export async function TwoFactorSettingsGet() {
const res = await request({
url: apiPrefix + "/twoFactor/get",
method: "post",
});
if (!res) {
return {};
}
return res as UserTwoFactorSetting;
}
export async function TwoFactorAuthenticatorGet() {
const res = await request({
url: apiPrefix + "/twoFactor/authenticator/qrcode",
method: "post",
});
return res as string; //base64
}
export async function TwoFactorAuthenticatorSave(req: AuthenticatorSaveReq) {
return await request({
url: apiPrefix + "/twoFactor/authenticator/save",
method: "post",
data: req,
});
}
export async function TwoFactorAuthenticatorOff() {
return await request({
url: apiPrefix + "/twoFactor/authenticator/off",
method: "post",
});
}

View File

@@ -0,0 +1,162 @@
<template>
<fs-page class="page-user-settings page-two-factor">
<template #header>
<div class="title">认证安全设置</div>
</template>
<div class="user-settings-form settings-form">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off">
<a-form-item label="2FA多重验证登录" :name="['authenticator', 'enabled']">
<div class="flex mt-5">
<a-switch v-model:checked="formState.authenticator.enabled" :disabled="!settingsStore.isPlus" @change="onAuthenticatorEnabledChanged" />
<a-button
v-if="formState.authenticator.enabled && formState.authenticator.verified"
:disabled="authenticatorOpenRef || !settingsStore.isPlus"
size="small"
class="ml-5"
type="primary"
@click="authenticatorForm.open = true"
>
重新绑定
</a-button>
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">是否开启多重验证登录</div>
</a-form-item>
<a-form-item v-if="authenticatorOpenRef" label="绑定设备" class="authenticator-config">
<h3 class="font-bold m-5">1. 安装任意一款支持Authenticator的验证APP比如</h3>
<div class="ml-20">
<ul>
<li>
<a-tooltip title="如果报没有找到谷歌服务的错误您可以安装KK谷歌助手">
<a href="https://appgallery.huawei.com/app/C100262999" target="_blank"> Microsoft Authenticator</a>
</a-tooltip>
</li>
<li>
<a href="https://sj.qq.com/appdetail/com.tencent.authenticator" target="_blank">腾讯身份验证器</a>
</li>
<li>
<a href="https://www.synology.cn/zh-cn/dsm/feature/authentication" target="_blank">群晖身份验证器</a>
</li>
<li>
<a-tooltip title="如果报没有找到谷歌服务的错误您可以安装KK谷歌助手">
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank">Google Authenticator</a>
</a-tooltip>
</li>
<li>
<a href="https://play.google.com/store/apps/details?id=com.authy.authy" target="_blank">Authy</a>
</li>
</ul>
</div>
<h3 class="font-bold m-10">2. 扫描二维码添加账号</h3>
<div v-if="authenticatorForm.qrcodeSrc" class="qrcode">
<div class="ml-20">
<img class="full-w" :src="authenticatorForm.qrcodeSrc" />
</div>
</div>
<h3 class="font-bold m-10">3. 输入验证码</h3>
<div class="ml-20">
<a-input v-model:value="authenticatorForm.verifyCode" placeholder="请输入验证码" />
</div>
<div class="ml-20 flex mt-10">
<loading-button type="primary" html-type="button" :click="doAuthenticatorSave">确认</loading-button>
<a-button class="ml-1" @click="authenticatorForm.open = false">取消</a-button>
</div>
</a-form-item>
</a-form>
</div>
</fs-page>
</template>
<script setup lang="tsx">
import { computed, reactive, watch } from "vue";
import * as api from "./api";
import { UserTwoFactorSetting } from "./api";
import { Modal, notification } from "ant-design-vue";
import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings";
const settingsStore = useSettingStore();
defineOptions({
name: "UserSecurity",
});
const formState = reactive<Partial<UserTwoFactorSetting>>({
authenticator: {
enabled: false,
verified: false,
},
});
const authenticatorForm = reactive({
qrcodeSrc: "",
verifyCode: "",
open: false,
});
const authenticatorOpenRef = computed(() => {
return formState.authenticator.enabled && (authenticatorForm.open || !formState.authenticator.verified);
});
watch(
() => {
return authenticatorOpenRef.value;
},
async open => {
if (open) {
//base64 转图片
authenticatorForm.qrcodeSrc = await api.TwoFactorAuthenticatorGet();
} else {
authenticatorForm.qrcodeSrc = "";
authenticatorForm.verifyCode = "";
}
}
);
async function loadUserSettings() {
const data: any = await api.TwoFactorSettingsGet();
merge(formState, data);
}
loadUserSettings();
const doAuthenticatorSave = async (form: any) => {
await api.TwoFactorAuthenticatorSave({
verifyCode: authenticatorForm.verifyCode,
});
notification.success({
message: "保存成功",
});
authenticatorForm.open = false;
};
function onAuthenticatorEnabledChanged(value: any) {
if (!value) {
//要关闭
if (formState.authenticator.verified) {
Modal.confirm({
title: "确认",
content: `确定要关闭多重验证登录吗?`,
async onOk() {
await api.TwoFactorAuthenticatorOff();
notification.success({
message: "关闭成功",
});
loadUserSettings();
},
onCancel() {
formState.authenticator.enabled = true;
},
});
}
}
}
</script>
<style lang="less">
.page-user-settings {
.user-settings-form {
width: 600px;
margin: 20px;
}
}
</style>

View File

@@ -4,10 +4,14 @@
<a-tab-pane v-for="item of detail.nodes" :key="item.node.id">
<template #tab>
<div class="tab-title flex-between" :title="item.node.title">
<span class="tab-title-text flex items-center md:w-40">
<span class="tab-title-text flex items-center md:w-48">
<pi-status-show class="mr-1" :status="item.node.status?.result" type="icon"></pi-status-show>
<!-- <fs-icon icon="ion:chevron-forward-circle" class="text-md mr-1"></fs-icon>-->
{{ item.node.title }}
<span class="flex-1 ellipsis">{{ item.node.title }}</span>
<a-tooltip title="强制重新执行此步骤">
<fs-icon class="pointer color-blue ml-1" style="font-size: 16px" title="强制重新执行此步骤" icon="icon-park-outline:replay-music" @click="triggerRun(item.node.id)"></fs-icon>
</a-tooltip>
</span>
</div>
</template>
@@ -31,13 +35,14 @@ export default {
name: "PiTaskView",
components: { PiStatusShow },
props: {},
emits: ["run"],
setup(props: any, ctx: any) {
const taskModal = ref({
open: false,
onOk() {
taskViewClose();
},
cancelText: "关闭"
cancelText: "关闭",
});
const { isMobile } = usePreferences();
const tabPosition = computed(() => {
@@ -65,7 +70,7 @@ export default {
node: step,
type: "步骤",
tab: 2,
logs: []
logs: [],
});
}
for (let node of nodes) {
@@ -82,7 +87,7 @@ export default {
list.push({
time,
content,
color
color,
});
}
return list;
@@ -111,12 +116,12 @@ export default {
if (isBottom && el) {
el?.scrollTo({
top: el.scrollHeight,
behavior: "smooth"
behavior: "smooth",
});
}
},
{
immediate: true
immediate: true,
}
);
}
@@ -135,15 +140,21 @@ export default {
taskModal.value.open = false;
};
function triggerRun(id: string) {
ctx.emit("run", id);
taskModal.value.open = false;
}
return {
detail,
taskModal,
activeKey,
taskViewOpen,
taskViewClose,
tabPosition
tabPosition,
triggerRun,
};
}
},
};
</script>
@@ -154,7 +165,7 @@ export default {
.tab-title-text {
display: flex;
max-width: 180px;
//max-width: 180px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -249,7 +249,7 @@
<pi-task-form ref="taskFormRef" :edit-mode="editMode"></pi-task-form>
<pi-trigger-form ref="triggerFormRef" :edit-mode="editMode"></pi-trigger-form>
<pi-task-view ref="taskViewRef"></pi-task-view>
<pi-task-view ref="taskViewRef" @run="run"></pi-task-view>
<PiNotificationForm ref="notificationFormRef" :edit-mode="editMode"></PiNotificationForm>
</fs-page>
</template>

View File

@@ -1,26 +0,0 @@
// @ts-ignore
import { request } from "/@/api/service";
const apiPrefix = "/user/settings";
export type UserSettings = {
defaultNotification?: number;
defaultCron?: string;
};
export async function UserSettingsGet() {
const res = await request({
url: apiPrefix + "/getDefault",
method: "post",
});
if (!res) {
return {};
}
return res;
}
export async function UserSettingsSave(setting: any) {
return await request({
url: apiPrefix + "/saveDefault",
method: "post",
data: setting,
});
}

View File

@@ -1,74 +0,0 @@
<template>
<fs-page class="page-user-settings">
<template #header>
<div class="title">设置</div>
</template>
<div class="user-settings-form settings-form">
<a-form
:model="formState"
name="basic"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
autocomplete="off"
@finish="onFinish"
@finish-failed="onFinishFailed"
>
<a-form-item label="默认定时设置" name="defaultCron">
<notification-selector v-model="formState.defaultCron" />
<div class="helper">创建流水线时默认使用此定时时间</div>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button :loading="saveLoading" type="primary" html-type="submit">保存</a-button>
</a-form-item>
</a-form>
</div>
</fs-page>
</template>
<script setup lang="tsx">
import { reactive, ref } from "vue";
import * as api from "./api";
import { UserSettings } from "./api";
import { notification } from "ant-design-vue";
import { merge } from "lodash-es";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
defineOptions({
name: "UserSettings"
});
const formState = reactive<Partial<UserSettings>>({});
async function loadUserSettings() {
const data: any = await api.UserSettingsGet();
merge(formState, data);
}
const saveLoading = ref(false);
loadUserSettings();
const onFinish = async (form: any) => {
try {
saveLoading.value = true;
await api.UserSettingsSave(form);
notification.success({
message: "保存成功"
});
} finally {
saveLoading.value = false;
}
};
const onFinishFailed = (errorInfo: any) => {
// console.log("Failed:", errorInfo);
};
</script>
<style lang="less">
.page-user-settings {
.user-settings-form {
width: 500px;
margin: 20px;
}
}
</style>

View File

@@ -1,14 +1,6 @@
<template>
<div class="main login-page">
<a-form
ref="formRef"
class="user-layout-login"
name="custom-validation"
:model="formState"
v-bind="layout"
@finish="handleFinish"
@finish-failed="handleFinishFailed"
>
<a-form v-if="!twoFactor.loginId" ref="formRef" class="user-layout-login" name="custom-validation" :model="formState" v-bind="layout" @finish="handleFinish" @finish-failed="handleFinishFailed">
<!-- <div class="login-title">登录</div>-->
<a-tabs v-model:active-key="formState.loginType" :tab-bar-style="{ textAlign: 'center', borderBottom: 'unset' }">
<a-tab-pane key="password" tab="密码登录" :disabled="sysPublicSettings.passwordLoginEnabled !== true">
@@ -44,13 +36,7 @@
</a-form-item>
<a-form-item name="smsCode" :rules="rules.smsCode">
<sms-code
v-model:value="formState.smsCode"
:img-code="formState.imgCode"
:mobile="formState.mobile"
:phone-code="formState.phoneCode"
:random-str="formState.randomStr"
/>
<sms-code v-model:value="formState.smsCode" :img-code="formState.imgCode" :mobile="formState.mobile" :phone-code="formState.phoneCode" :random-str="formState.randomStr" />
</a-form-item>
</template>
</a-tab-pane>
@@ -63,6 +49,23 @@
<router-link v-if="hasRegisterTypeEnabled()" class="register" :to="{ name: 'register' }"> 注册 </router-link>
</a-form-item>
</a-form>
<a-form v-else ref="twoFactorFormRef" class="user-layout-login" :model="twoFactor" v-bind="layout">
<div class="mb-10 flex flex-center">请打开您的Authenticator APP获取动态验证码</div>
<a-form-item name="verifyCode">
<a-input v-model:value="twoFactor.verifyCode" placeholder="请输入动态验证码" allow-clear>
<template #prefix>
<fs-icon icon="ion:lock-closed-outline"></fs-icon>
</template>
</a-input>
</a-form-item>
<a-form-item>
<loading-button type="primary" size="large" html-type="button" class="login-button" :click="handleTwoFactorSubmit">OTP验证登录</loading-button>
</a-form-item>
<a-form-item class="user-login-other">
<a class="register" @click="twoFactor.loginId = null"> 返回 </a>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts">
@@ -89,42 +92,51 @@ export default defineComponent({
loginType: "password", //password
imgCode: "",
smsCode: "",
randomStr: ""
randomStr: "",
});
const rules = {
mobile: [
{
required: true,
message: "请输入手机号"
}
message: "请输入手机号",
},
],
username: [
{
required: true,
message: "请输入用户名"
}
message: "请输入用户名",
},
],
password: [
{
required: true,
message: "请输入登录密码"
}
message: "请输入登录密码",
},
],
smsCode: [
{
required: true,
message: "请输入短信验证码"
}
]
message: "请输入短信验证码",
},
],
};
const layout = {
labelCol: {
span: 0
span: 0,
},
wrapperCol: {
span: 24
}
span: 24,
},
};
const twoFactor = reactive({
loginId: "",
verifyCode: "",
});
const handleTwoFactorSubmit = async () => {
await userStore.loginByTwoFactor(twoFactor);
};
const handleFinish = async (values: any) => {
@@ -132,6 +144,14 @@ export default defineComponent({
try {
const loginType = formState.loginType;
await userStore.login(loginType, toRaw(formState));
} catch (e: any) {
//@ts-ignore
if (e.code === 10020) {
//@ts-ignore
twoFactor.loginId = e.data;
} else {
throw e;
}
} finally {
loading.value = false;
}
@@ -163,9 +183,11 @@ export default defineComponent({
resetForm,
isLoginError,
sysPublicSettings,
hasRegisterTypeEnabled
hasRegisterTypeEnabled,
twoFactor,
handleTwoFactorSubmit,
};
}
},
});
</script>

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

@@ -66,6 +66,22 @@ export async function SetDisabled(data: { id?: number; name?: string; type?: str
});
}
export async function ExportPlugin(id: number) {
return await request({
url: apiPrefix + "/export",
method: "post",
data: { id },
});
}
export async function ImportPlugin(body: any) {
return await request({
url: apiPrefix + "/import",
method: "post",
data: body,
});
}
export type PluginConfigBean = {
name: string;
disabled: boolean;

View File

@@ -2,8 +2,8 @@ import * as api from "./api";
import { useI18n } from "vue-i18n";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { Modal } from "ant-design-vue";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { Modal, notification } from "ant-design-vue";
//@ts-ignore
import yaml from "js-yaml";
@@ -36,7 +36,74 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
const { openCrudFormDialog } = useFormWrapper();
async function openImportDialog() {
function createCrudOptions() {
return {
crudOptions: {
columns: {
content: {
title: "插件文件",
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 8,
},
},
col: {
span: 24,
},
helper: "选择插件文件",
},
},
override: {
title: "同名覆盖",
type: "dict-switch",
dict: dict({
data: [
{
value: true,
label: "覆盖",
},
{
value: false,
label: "不覆盖",
},
],
}),
form: {
value: false,
col: {
span: 24,
},
helper: "如果已有相同名称插件,直接覆盖",
},
},
},
form: {
wrapper: {
title: "导入插件",
saveRemind: false,
},
afterSubmit() {
notification.success({ message: "操作成功" });
},
async doSubmit({ form }: any) {
return await api.ImportPlugin({
...form,
});
},
},
},
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
}
return {
crudOptions: {
settings: {
@@ -65,8 +132,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
buttons: {
add: {
show: true,
icon: "ion:ios-add-circle-outline",
text: "自定义插件",
},
import: {
show: true,
icon: "ion:cloud-upload-outline",
text: "导入",
type: "primary",
async click() {
await openImportDialog();
},
},
},
},
rowHandle: {
@@ -85,10 +162,33 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}),
},
remove: {
order: 999,
show: compute(({ row }) => {
return row.type === "custom";
}),
},
export: {
text: null,
icon: "ion:cloud-download-outline",
title: "导出",
type: "link",
show: compute(({ row }) => {
return row.type === "custom";
}),
async click({ row }) {
//将文本内容,作为文件下载
const content = await api.ExportPlugin(row.id);
if (content) {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${row.name}.yaml`;
link.click();
URL.revokeObjectURL(url);
}
},
},
},
},
table: {
@@ -182,8 +282,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
form: {
show: true,
helper: "必须为英文,驼峰命名,类型作为前缀\n例如AliyunDeployToCDN\n插件一旦被使用不要修改名称",
rules: [{ required: true }],
helper: "必须为英文或数字,驼峰命名,类型作为前缀\n例如AliyunDeployToCDN\n插件一旦被使用不要修改名称",
rules: [
{ required: true },
{
type: "regexp",
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
message: "必须为英文或数字,驼峰命名,类型作为前缀",
},
],
},
column: {
width: 250,
@@ -205,7 +312,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
form: {
show: true,
helper: "上传到插件商店时,将作为插件名称前缀,例如greper/pluginName",
rules: [{ required: true }],
rules: [
{ required: true },
{
type: "regexp",
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
message: "必须为英文字母或数字",
},
],
},
column: {
width: 200,

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

@@ -66,34 +66,34 @@ import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue";
defineOptions({
name: "SettingRegister"
name: "SettingRegister",
});
const testMobile = ref("");
async function testSendSms() {
if (!testMobile.value) {
notification.error({
message: "请输入测试手机号"
message: "请输入测试手机号",
});
return;
}
await api.TestSms({
mobile: testMobile.value
mobile: testMobile.value,
});
notification.success({
message: "发送成功"
message: "发送成功",
});
}
const formState = reactive<Partial<SysSettings>>({
public: {
registerEnabled: false
registerEnabled: false,
},
private: {
sms: {
type: "aliyun",
config: {}
}
}
config: {},
},
},
});
const rules = {
@@ -103,13 +103,13 @@ const rules = {
return Promise.reject("密码登录和手机号登录至少开启一个");
}
return Promise.resolve();
}
},
},
required: {
required: true,
trigger: "change",
message: "此项必填"
}
message: "此项必填",
},
};
async function smsTypeChange(value: string) {
@@ -124,13 +124,13 @@ async function loadTypeDefine(type: string) {
const define: any = await api.GetSmsTypeDefine(type);
const keys = Object.keys(define.input);
const inputs: any = {};
keys.forEach((key) => {
keys.forEach(key => {
const value = define.input[key];
value.simpleKey = key;
value.key = "private.sms.config." + key;
if (!value.component) {
value.component = {
name: "a-input"
name: "a-input",
};
}
if (!value.component.name) {
@@ -165,7 +165,7 @@ const onFinish = async (form: any) => {
await api.SysSettingsSave(form);
await settingsStore.loadSysSettings();
notification.success({
message: "保存成功"
message: "保存成功",
});
} finally {
saveLoading.value = false;

View File

@@ -0,0 +1,161 @@
<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">
<h2>站点隐藏</h2>
<a-form-item label="启用站点隐藏" :name="['hidden', 'enabled']" :required="true">
<div class="flex">
<a-switch v-model:checked="formState.hidden.enabled" />
</div>
<div class="helper">
可以在平时关闭站点的可访问性需要时再打开增强站点安全性
<a href="https://certd.docmirror.cn/guide/feature/safe/hidden" class="flex items-center" target="_blank">
<span>帮助说明</span>
<fs-icon class="ml-1" icon="mingcute:question-line"></fs-icon
></a>
</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";
import { useSettingStore } from "/@/store/settings";
defineOptions({
name: "SettingSafe",
});
const settingsStore = useSettingStore();
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

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
### Performance Improvements
* 登录支持双重认证 ([48aef25](https://github.com/certd/certd/commit/48aef25b3f6499d674ca4e4ef16f4c62399fb735))
* 多重认证登录 ([0f82cf4](https://github.com/certd/certd/commit/0f82cf409bc60706ab07e4ca4f272b9a1ca7eecb))
* 优化部署到华为云CDN支持先上传到ccm再使用证书id部署修复offline状态下导致部署报错的bug ([79df39a](https://github.com/certd/certd/commit/79df39acabab10ae7e1864dadcdc186bb007a3c5))
## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15)
### Bug Fixes
* 补充类型断言 ([2143dff](https://github.com/certd/certd/commit/2143dff2ae96e6a78bef9f0498e36f8cd9e6941f))
* 修复腾讯云部署到任意资源插件,无法使用之前已上传的腾讯云证书问题 ([32c714d](https://github.com/certd/certd/commit/32c714d1b6e68c71a74a7452115040c87ac4bfdc))
### Performance Improvements
* 插件支持导入导出 ([cf8abb4](https://github.com/certd/certd/commit/cf8abb45282070c8ba91469f93fd379fabf1f74a))
* 支持上传证书到华为云CCM ([cfd3b66](https://github.com/certd/certd/commit/cfd3b66be9ebf53a26693057e70ed60c3f116be9))
## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14)
### Bug Fixes
* 修复登录错误次数过多阻止再次登录逻辑 ([bf4d191](https://github.com/certd/certd/commit/bf4d191c8bd2f9209eb6768f662b9c77de99e998))
## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12)
### Performance Improvements

View File

@@ -0,0 +1,53 @@
// 扫描目录,列出文件,然后加载为模块
import { join } from 'path';
import fs from 'fs'
import { pathToFileURL } from "node:url";
import path from 'path'
function scanDir(dir) {
const files = fs.readdirSync(dir);
const result = [];
// 扫描目录及子目录
for (const file of files) {
if (file.includes("index.js")) {
continue;
}
const filePath = join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
result.push(...scanDir(filePath));
} else {
if (!file.endsWith(".js")) {
continue;
}
result.push(filePath);
}
}
return result
}
export default async function loadModules(dir) {
const files = scanDir(dir);
const modules = {}
for (const file of files) {
try {
// 转换为 file:// URLWindows 必需)
const moduleUrl = pathToFileURL(file).href
const module = await import(moduleUrl)
// 如果模块有默认导出,优先使用
modules[file] = module.default || module
} catch (err) {
console.error(`加载模块 ${file} 失败:`, err)
}
}
return modules;
}
const modules = await loadModules('./dist/plugins');
for (const key in modules) {
console.log(key)
}

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.33.2",
"version": "1.33.5",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -38,19 +38,19 @@
"@aws-sdk/client-acm": "^3.699.0",
"@aws-sdk/client-cloudfront": "^3.699.0",
"@aws-sdk/client-s3": "^3.705.0",
"@certd/acme-client": "^1.33.2",
"@certd/basic": "^1.33.2",
"@certd/commercial-core": "^1.33.2",
"@certd/jdcloud": "^1.33.2",
"@certd/lib-huawei": "^1.33.2",
"@certd/lib-k8s": "^1.33.2",
"@certd/lib-server": "^1.33.2",
"@certd/midway-flyway-js": "^1.33.2",
"@certd/pipeline": "^1.33.2",
"@certd/plugin-cert": "^1.33.2",
"@certd/plugin-lib": "^1.33.2",
"@certd/plugin-plus": "^1.33.2",
"@certd/plus-core": "^1.33.2",
"@certd/acme-client": "^1.33.5",
"@certd/basic": "^1.33.5",
"@certd/commercial-core": "^1.33.5",
"@certd/jdcloud": "^1.33.5",
"@certd/lib-huawei": "^1.33.5",
"@certd/lib-k8s": "^1.33.5",
"@certd/lib-server": "^1.33.5",
"@certd/midway-flyway-js": "^1.33.5",
"@certd/pipeline": "^1.33.5",
"@certd/plugin-cert": "^1.33.5",
"@certd/plugin-lib": "^1.33.5",
"@certd/plugin-plus": "^1.33.5",
"@certd/plus-core": "^1.33.5",
"@corsinvest/cv4pve-api-javascript": "^8.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",
@@ -97,9 +97,11 @@
"nanoid": "^5.0.7",
"node-forge": "^1.3.1",
"nodemailer": "^6.9.16",
"otplib": "^12.0.1",
"pg": "^8.12.0",
"psl": "^1.9.0",
"qiniu": "^7.12.0",
"qrcode": "^1.5.4",
"qs": "^6.13.1",
"querystring": "^0.2.1",
"reflect-metadata": "^0.2.2",

View File

@@ -19,12 +19,18 @@ 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);
// 在这里可以添加日志记录、发送错误通知等操作
});
@Configuration({
// detectorOptions: {
// ignore: [
// '**/plugins/**'
// ]
// },
imports: [
koa,
orm,
@@ -77,6 +83,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

@@ -1,10 +1,11 @@
import { Provide, Controller, Post, Inject, Body, Query, ALL } from '@midwayjs/core';
import { UserService } from '../../../modules/sys/authority/service/user-service.js';
import { CrudController } from '@certd/lib-server';
import { RoleService } from '../../../modules/sys/authority/service/role-service.js';
import { PermissionService } from '../../../modules/sys/authority/service/permission-service.js';
import { Constants } from '@certd/lib-server';
import { In } from 'typeorm';
import {Provide, Controller, Post, Inject, Body, Query, ALL} from '@midwayjs/core';
import {UserService} from '../../../modules/sys/authority/service/user-service.js';
import {CrudController} from '@certd/lib-server';
import {RoleService} from '../../../modules/sys/authority/service/role-service.js';
import {PermissionService} from '../../../modules/sys/authority/service/permission-service.js';
import {Constants} from '@certd/lib-server';
import {In} from 'typeorm';
import {LoginService} from "../../../modules/login/service/login-service.js";
/**
* 系统用户
@@ -20,11 +21,14 @@ export class UserController extends CrudController<UserService> {
@Inject()
permissionService: PermissionService;
@Inject()
loginService: LoginService;
getService() {
return this.service;
}
@Post('/getSimpleUserByIds', { summary: 'sys:auth:user:add' })
@Post('/getSimpleUserByIds', {summary: 'sys:auth:user:add'})
async getSimpleUserByIds(@Body('ids') ids: number[]) {
const users = await this.service.find({
select: {
@@ -42,10 +46,10 @@ export class UserController extends CrudController<UserService> {
return this.ok(users);
}
@Post('/page', { summary: 'sys:auth:user:view' })
@Post('/page', {summary: 'sys:auth:user:view'})
async page(
@Body(ALL)
body
body
) {
const ret = await super.page(body);
@@ -74,25 +78,26 @@ export class UserController extends CrudController<UserService> {
return ret;
}
@Post('/add', { summary: 'sys:auth:user:add' })
@Post('/add', {summary: 'sys:auth:user:add'})
async add(
@Body(ALL)
bean
bean
) {
return await super.add(bean);
}
@Post('/update', { summary: 'sys:auth:user:edit' })
@Post('/update', {summary: 'sys:auth:user:edit'})
async update(
@Body(ALL)
bean
bean
) {
return await super.update(bean);
}
@Post('/delete', { summary: 'sys:auth:user:remove' })
@Post('/delete', {summary: 'sys:auth:user:remove'})
async delete(
@Query('id')
id: number
id: number
) {
if (id === 1) {
throw new Error('不能删除默认的管理员角色');
@@ -103,10 +108,23 @@ export class UserController extends CrudController<UserService> {
return await super.delete(id);
}
/**
* 解除登录锁定
*/
@Post('/unlockBlock', {summary: "sys:auth:user:edit"})
public async unlockBlock(@Body('id') id: number) {
const info = await this.service.info(id, ['password']);
this.loginService.clearCacheOnSuccess(info.username)
if (info.mobile) {
this.loginService.clearCacheOnSuccess(info.mobile)
}
return this.ok(info);
}
/**
* 当前登录用户的个人信息
*/
@Post('/mine', { summary: Constants.per.authOnly })
@Post('/mine', {summary: Constants.per.authOnly})
public async mine() {
const id = this.getUserId();
const info = await this.service.info(id, ['password']);
@@ -116,7 +134,7 @@ export class UserController extends CrudController<UserService> {
/**
* 当前登录用户的权限列表
*/
@Post('/permissions', { summary: Constants.per.authOnly })
@Post('/permissions', {summary: Constants.per.authOnly})
public async permissions() {
const id = this.getUserId();
const permissions = await this.service.getUserPermissions(id);
@@ -126,11 +144,13 @@ export class UserController extends CrudController<UserService> {
/**
* 当前登录用户的权限树形列表
*/
@Post('/permissionTree', { summary: Constants.per.authOnly })
@Post('/permissionTree', {summary: Constants.per.authOnly})
public async permissionTree() {
const id = this.getUserId();
const permissions = await this.service.getUserPermissions(id);
const tree = this.permissionService.buildTree(permissions);
return this.ok(tree);
}
}

View File

@@ -1,7 +1,7 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { merge } from 'lodash-es';
import { CrudController } from '@certd/lib-server';
import { PluginService } from '../../../modules/plugin/service/plugin-service.js';
import { PluginImportReq, PluginService } from "../../../modules/plugin/service/plugin-service.js";
import { CommPluginConfig, PluginConfigService } from '../../../modules/plugin/service/plugin-config-service.js';
/**
* 插件
@@ -82,4 +82,17 @@ export class PluginController extends CrudController<PluginService> {
const res = await this.pluginConfigService.saveCommPluginConfig(body);
return this.ok(res);
}
@Post('/import', { summary: 'sys:settings:edit' })
async import(@Body(ALL) body: PluginImportReq) {
const res = await this.service.importPlugin(body);
return this.ok(res);
}
@Post('/export', { summary: 'sys:settings:edit' })
async export(@Body('id') id: number) {
const res = await this.service.exportPlugin(id);
return this.ok(res);
}
}

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

@@ -23,13 +23,16 @@ export class LoginController extends BaseController {
user: any
) {
const token = await this.loginService.loginByPassword(user);
this.ctx.cookies.set('token', token.token, {
maxAge: 1000 * token.expire,
});
this.writeTokenCookie(token);
return this.ok(token);
}
private writeTokenCookie(token: { expire: any; token: any }) {
this.ctx.cookies.set("token", token.token, {
maxAge: 1000 * token.expire
});
}
@Post('/loginBySms', { summary: Constants.per.guest })
public async loginBySms(
@Body(ALL)
@@ -48,10 +51,23 @@ export class LoginController extends BaseController {
randomStr: body.randomStr,
});
this.ctx.cookies.set('token', token.token, {
maxAge: 1000 * token.expire,
this.writeTokenCookie(token);
return this.ok(token);
}
@Post('/loginByTwoFactor', { summary: Constants.per.guest })
public async loginByTwoFactor(
@Body(ALL)
body: any
) {
const token = await this.loginService.loginByTwoFactor({
loginId: body.loginId,
verifyCode: body.verifyCode,
});
this.writeTokenCookie(token);
return this.ok(token);
}

View File

@@ -0,0 +1,75 @@
import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core";
import { BaseController, Constants } from "@certd/lib-server";
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
import { UserTwoFactorSetting } from "../../../modules/mine/service/models.js";
import { merge } from "lodash-es";
import { TwoFactorService } from "../../../modules/mine/service/two-factor-service.js";
import {isPlus} from "@certd/plus-core";
/**
*/
@Provide()
@Controller("/api/user/settings/twoFactor")
export class UserTwoFactorSettingController extends BaseController {
@Inject()
service: UserSettingsService;
@Inject()
twoFactorService: TwoFactorService;
@Post("/get", { summary: Constants.per.authOnly })
async get() {
const userId = this.getUserId();
const setting = await this.service.getSetting<UserTwoFactorSetting>(userId, UserTwoFactorSetting);
return this.ok(setting);
}
@Post("/save", { summary: Constants.per.authOnly })
async save(@Body(ALL) bean: any) {
if (!isPlus()) {
throw new Error('本功能需要开通专业版')
}
const userId = this.getUserId();
const setting = new UserTwoFactorSetting();
merge(setting, bean);
// 禁用时清除
if(!setting.authenticator.enabled){
setting.authenticator.secret = null;
setting.authenticator.verified = false;
}
await this.service.saveSetting(userId, setting);
return this.ok({});
}
@Post("/authenticator/qrcode", { summary: Constants.per.authOnly })
async authenticatorQrcode() {
const userId = this.getUserId();
const qrcode = await this.twoFactorService.getAuthenticatorQrCode(userId);
return this.ok(qrcode);
}
@Post("/authenticator/save", { summary: Constants.per.authOnly })
async authenticatorSave(@Body(ALL) bean: any) {
if (!isPlus()) {
throw new Error('本功能需要开通专业版')
}
const userId = this.getUserId();
await this.twoFactorService.saveAuthenticator({
userId,
verifyCode: bean.verifyCode,
});
return this.ok();
}
@Post("/authenticator/off", { summary: Constants.per.authOnly })
async authenticatorOff() {
const userId = this.getUserId();
await this.twoFactorService.offAuthenticator(userId);
return this.ok();
}
}

View File

@@ -1,8 +1,7 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { CrudController } from '@certd/lib-server';
import { Constants } from '@certd/lib-server';
import { UserSettingsService } from '../../../modules/mine/service/user-settings-service.js';
import { UserSettingsEntity } from '../../../modules/mine/entity/user-settings.js';
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { Constants, CrudController } from "@certd/lib-server";
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
import { UserSettingsEntity } from "../../../modules/mine/entity/user-settings.js";
/**
*/
@@ -66,4 +65,6 @@ export class UserSettingsController extends CrudController<UserSettingsService>
const entity = await this.service.getByKey(key, this.getUserId());
return this.ok(entity);
}
}

View File

@@ -1,7 +1,7 @@
import { Provide } from '@midwayjs/core';
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
import { logger } from '@certd/basic';
import { Result } from '@certd/lib-server';
import { Result, TextException } from "@certd/lib-server";
@Provide()
export class GlobalExceptionMiddleware implements IWebMiddleware {
@@ -14,12 +14,15 @@ export class GlobalExceptionMiddleware implements IWebMiddleware {
await next();
logger.info('请求完成:', url, Date.now() - startTime + 'ms');
} catch (err) {
if(err instanceof TextException){
delete err.stack
}
logger.error('请求异常:', url, Date.now() - startTime + 'ms', err);
ctx.status = 200;
if (err.code == null || typeof err.code !== 'number') {
err.code = 1;
}
ctx.body = Result.error(err.code, err.message);
ctx.body = Result.error(err.code, err.message,err.data);
}
};
}

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

@@ -1,20 +1,23 @@
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { UserService } from '../../sys/authority/service/user-service.js';
import {Config, Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {UserService} from '../../sys/authority/service/user-service.js';
import jwt from 'jsonwebtoken';
import { CommonException } from '@certd/lib-server';
import { RoleService } from '../../sys/authority/service/role-service.js';
import { UserEntity } from '../../sys/authority/entity/user.js';
import { SysSettingsService } from '@certd/lib-server';
import { SysPrivateSettings } from '@certd/lib-server';
import { cache } from '@certd/basic';
import { LoginErrorException } from '@certd/lib-server/dist/basic/exception/login-error-exception.js';
import { CodeService } from '../../basic/service/code-service.js';
import {AuthException, CommonException, Need2FAException} from "@certd/lib-server";
import {RoleService} from '../../sys/authority/service/role-service.js';
import {UserEntity} from '../../sys/authority/entity/user.js';
import {SysSettingsService} from '@certd/lib-server';
import {SysPrivateSettings} from '@certd/lib-server';
import {cache, utils} from '@certd/basic';
import {LoginErrorException} from '@certd/lib-server/dist/basic/exception/login-error-exception.js';
import {CodeService} from '../../basic/service/code-service.js';
import {TwoFactorService} from "../../mine/service/two-factor-service.js";
import {UserSettingsService} from '../../mine/service/user-settings-service.js';
import {isPlus} from "@certd/plus-core";
/**
* 系统用户
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@Scope(ScopeEnum.Request, {allowDowngrade: true})
export class LoginService {
@Inject()
userService: UserService;
@@ -28,52 +31,77 @@ export class LoginService {
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
userSettingsService: UserSettingsService;
@Inject()
twoFactorService: TwoFactorService;
checkErrorTimes(username: string, errorMessage: string) {
const cacheKey = `login_error_times:${username}`;
const blockTimesKey = `login_block_times:${username}`;
let blockTimes = cache.get(blockTimesKey);
let maxWaitMin = 2;
const maxRetryTimes = 5;
if (blockTimes == null) {
blockTimes = 1;
checkIsBlocked(username: string) {
const blockDurationKey = `login_block_duration:${username}`;
const value = cache.get(blockDurationKey);
if (value) {
const ttl = cache.getRemainingTTL(blockDurationKey)
const leftMin = Math.ceil(ttl / 1000 / 60);
throw new CommonException(`账号被锁定,请${leftMin}分钟后重试`);
}
maxWaitMin = maxWaitMin * blockTimes;
let ttl = maxWaitMin * 60 * 1000;
}
let errorTimes = cache.get(cacheKey);
clearCacheOnSuccess(username: string) {
cache.delete(`login_error_times:${username}`);
cache.delete(`login_block_times:${username}`);
cache.delete(`login_block_duration:${username}`);
}
addErrorTimes(username: string, errorMessage: string) {
const errorTimesKey = `login_error_times:${username}`;
const blockTimesKey = `login_block_times:${username}`;
const blockDurationKey = `login_block_duration:${username}`;
let blockTimes = cache.get(blockTimesKey);
// let maxWaitMin = 2;
const maxRetryTimes = blockTimes > 1 ? 3 : 5;
if (blockTimes == null) {
blockTimes = 0;
}
// maxWaitMin = maxWaitMin * blockTimes;
// let ttl = maxWaitMin * 60 * 1000;
let errorTimes = cache.get(errorTimesKey);
if (errorTimes == null) {
errorTimes = 0;
} else {
const remainingTTL = cache.getRemainingTTL(cacheKey);
if (remainingTTL > 0) {
ttl = remainingTTL;
}
}
errorTimes += 1;
cache.set(cacheKey, errorTimes, {
ttl: ttl,
const ttl24H = 24 * 60 * 60 * 1000;
cache.set(errorTimesKey, errorTimes, {
ttl: ttl24H,
});
if (errorTimes >= maxRetryTimes) {
if (errorTimes === maxRetryTimes) {
blockTimes += 1;
cache.set(blockTimesKey, blockTimes, {
ttl: 24 * 60 * 60 * 1000,
});
}
if (errorTimes > maxRetryTimes) {
blockTimes += 1;
cache.set(blockTimesKey, blockTimes, {
ttl: ttl24H,
});
//按照block次数指数递增最长24小时
const ttl = Math.min(blockTimes * blockTimes * 60 * 1000, ttl24H);
const leftMin = Math.ceil(ttl / 1000 / 60);
cache.set(blockDurationKey, 1, {
ttl: ttl,
})
// 清除error次数
cache.delete(errorTimesKey);
throw new LoginErrorException(`登录失败次数过多,请${leftMin}分钟后重试`, 0);
}
const leftTimes = maxRetryTimes - errorTimes;
if (leftTimes < 3) {
throw new LoginErrorException(`登录失败,剩余尝试次数:${leftTimes}`, leftTimes);
throw new LoginErrorException(`登录失败(${errorMessage}),剩余尝试次数:${leftTimes}`, leftTimes);
}
throw new LoginErrorException(errorMessage, leftTimes);
}
async loginBySmsCode(req: { mobile: string; phoneCode: string; smsCode: string; randomStr: string }) {
this.checkIsBlocked(req.mobile)
const smsChecked = await this.codeService.checkSmsCode({
mobile: req.mobile,
phoneCode: req.phoneCode,
@@ -82,11 +110,11 @@ export class LoginService {
throwError: false,
});
const { mobile, phoneCode } = req;
const {mobile, phoneCode} = req;
if (!smsChecked) {
this.checkErrorTimes(mobile, '验证码错误');
this.addErrorTimes(mobile, '验证码错误');
}
let info = await this.userService.findOne({ phoneCode, mobile: mobile });
let info = await this.userService.findOne({phoneCode, mobile: mobile});
if (info == null) {
//用户不存在,注册
info = await this.userService.register('mobile', {
@@ -95,52 +123,78 @@ export class LoginService {
password: '',
} as any);
}
this.clearCacheOnSuccess(mobile);
return this.onLoginSuccess(info);
}
async loginByPassword(req: { username: string; password: string; phoneCode: string }) {
const { username, password, phoneCode } = req;
const info = await this.userService.findOne([{ username: username }, { email: username }, { phoneCode, mobile: username }]);
this.checkIsBlocked(req.username)
const {username, password, phoneCode} = req;
const info = await this.userService.findOne([{username: username}, {email: username}, {
phoneCode,
mobile: username
}]);
if (info == null) {
throw new CommonException('用户名或密码错误');
}
const right = await this.userService.checkPassword(password, info.password, info.passwordVersion);
if (!right) {
this.checkErrorTimes(username, '用户名或密码错误');
this.addErrorTimes(username, '用户名或密码错误');
}
this.clearCacheOnSuccess(username);
return this.onLoginSuccess(info);
}
// /**
// * login
// */
// async login(user) {
// console.assert(user.username != null, '用户名不能为空');
// const info = await this.userService.findOne({ username: user.username });
// if (info == null) {
// throw new CommonException('用户名或密码错误');
// }
// const right = await this.userService.checkPassword(user.password, info.password, info.passwordVersion);
// if (!right) {
// this.checkErrorTimes(user.username, '用户名或密码错误');
// }
// return await this.onLoginSuccess(info);
// }
async checkTwoFactorEnabled(userId: number) {
//检查是否开启多重认证
if (!isPlus()) {
return true
}
const twoFactorSetting = await this.twoFactorService.getSetting(userId)
const authenticatorSetting = twoFactorSetting.authenticator
if (authenticatorSetting.enabled) {
//要检查
const randomKey = utils.id.simpleNanoId(12)
cache.set(`login_2fa_code:${randomKey}`, userId, {
ttl: 60 * 1000 * 2,
})
throw new Need2FAException('已开启多重认证请在2分钟内输入OPT验证码',randomKey)
}
}
async loginByTwoFactor(req: { loginId: string; verifyCode: string }) {
//检查是否开启多重认证
if (!isPlus()) {
throw new Error('本功能需要开通专业版')
}
const userId = cache.get(`login_2fa_code:${req.loginId}`)
if (!userId) {
throw new AuthException('已超时,请返回重新登录')
}
await this.twoFactorService.verifyAuthenticatorCode(userId, req.verifyCode)
return this.generateToken(await this.userService.findOne(userId))
}
private async onLoginSuccess(info: UserEntity) {
if (info.status === 0) {
throw new CommonException('用户已被禁用');
}
const roleIds = await this.roleService.getRoleIdsByUserId(info.id);
return this.generateToken(info, roleIds);
await this.checkTwoFactorEnabled(info.id)
return this.generateToken(info);
}
/**
* 生成token
* @param user 用户对象
* @param roleIds
*/
async generateToken(user: UserEntity, roleIds: number[]) {
async generateToken(user: UserEntity) {
const roleIds = await this.roleService.getRoleIdsByUserId(user.id);
const tokenInfo = {
username: user.username,
id: user.id,

View File

@@ -0,0 +1,21 @@
import { BaseSettings } from "@certd/lib-server";
export type TwoFactorAuthenticator = {
enabled: boolean;
secret?: string;
type?: string;
verified?:boolean;
}
export class UserTwoFactorSetting extends BaseSettings {
static __title__ = "用户多重认证设置";
static __key__ = "user.two.factor";
authenticator: TwoFactorAuthenticator = {
enabled:false,
verified:false,
};
}

Some files were not shown because too many files have changed in this diff Show More