diff --git a/docker/run/docker-compose.yaml b/docker/run/docker-compose.yaml index cf5bd70ff..420cf0b1c 100644 --- a/docker/run/docker-compose.yaml +++ b/docker/run/docker-compose.yaml @@ -28,18 +28,14 @@ services: # 设置环境变量即可自定义certd配置 # 配置项见: packages/ui/certd-server/src/config/config.default.ts # 配置规则: certd_ + 配置项, 点号用_代替 - - # ↓↓↓↓ ------------------------------------ 这里可以设置http代理 - #- HTTPS_PROXY=http://xxxxxx:xx - #- HTTP_PROXY=http://xxxxxx:xx # ↓↓↓↓ ----------------------------- 如果忘记管理员密码,可以设置为true,重启之后,管理员密码将改成123456,然后请及时修改回false - certd_system_resetAdminPasswd=false - # ↓↓↓↓ -------------------------- 如果设置为true,启动后所有配置了cron的流水线任务都将被立即触发一次 - - certd_cron_immediateTriggerOnce=false - # ↓↓↓↓ -------------------------------- 配置证书和key,则表示https方式启动,使用https协议访问,https://your.domain:7001 - #- certd_koa_key=./data/ssl/cert.key - #- certd_koa_cert=./data/ssl/cert.crt - + # ↓↓↓↓ -------------------------------- 默认同时启动https,https访问地址https://your.domain:7002 + #- certd_https_key=./data/ssl/cert.key + #- certd_https_cert=./data/ssl/cert.crt + #- certd_https_enabled=true + #- certd_https_port=7002 + - # ↓↓↓↓ ------------------------------- 使用postgresql数据库 # - certd_flyway_scriptDir=./db/migration-pg # 升级脚本目录 # - certd_typeorm_dataSource_default_type=postgres # 数据库类型 diff --git a/docs/guide/use/tencent/images/delete.png b/docs/guide/use/tencent/images/delete.png new file mode 100644 index 000000000..c480c97ab Binary files /dev/null and b/docs/guide/use/tencent/images/delete.png differ diff --git a/docs/guide/use/tencent/images/delete2.png b/docs/guide/use/tencent/images/delete2.png new file mode 100644 index 000000000..67ef7895a Binary files /dev/null and b/docs/guide/use/tencent/images/delete2.png differ diff --git a/docs/guide/use/tencent/index.md b/docs/guide/use/tencent/index.md index 2dcebee6a..9dbf6a399 100644 --- a/docs/guide/use/tencent/index.md +++ b/docs/guide/use/tencent/index.md @@ -6,3 +6,16 @@ 打开 https://console.cloud.tencent.com/cam/capi 然后按如下方式获取腾讯云的API密钥 ![](./tencent-access.png) + + +## 如何避免收到腾讯云证书过期邮件 + +腾讯云在证书有效期还剩28天时会发送过期通知邮件 +您可以通过配置“腾讯云过期证书删除”任务来避免收到此类邮件。 + +![](./images/delete.png) + +注意点: +> 1. 选择腾讯云授权,需授权`服务角色SSL_QCSLinkedRoleInReplaceLoadCertificate`权限 +> 2. `1.26.14`版本之前Certd创建的证书流水线默认是到期前20天才更新证书,需要将之前创建的证书申请任务的更新天数修改为35天,保证删除之前就已经替换掉即将过期证书 +![](./images/delete2.png) \ No newline at end of file diff --git a/packages/core/pipeline/src/plugin/api.ts b/packages/core/pipeline/src/plugin/api.ts index 5fd306091..2b86b0e5e 100644 --- a/packages/core/pipeline/src/plugin/api.ts +++ b/packages/core/pipeline/src/plugin/api.ts @@ -111,6 +111,12 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin { return this._result.files; } + checkSignal() { + if (this.ctx.signal && this.ctx.signal.aborted) { + throw new Error("用户取消"); + } + } + setCtx(ctx: TaskInstanceContext) { this.ctx = ctx; this.logger = ctx.logger; diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts index ec3b7cbe7..8efcdf09c 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts @@ -61,7 +61,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { @TaskInput({ title: "更新天数", - value: 20, + value: 35, component: { name: "a-input-number", vModel: "value", @@ -212,7 +212,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { this.logger.info("input hash 有变更,检查是否需要重新申请证书"); //判断域名有没有变更 /** - * "renewDays": 20, + * "renewDays": 35, * "certApplyPlugin": "CertApply", * "sslProvider": "letsencrypt", * "privateKeyType": "rsa_2048_pkcs1", diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts index 2c842da4c..435a1a6ad 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts @@ -33,7 +33,7 @@ export type DomainsVerifyPlanInput = { desc: "免费通配符域名证书申请,支持多个域名打到同一个证书上", default: { input: { - renewDays: 20, + renewDays: 35, forceUpdate: false, }, strategy: { diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts index 881a7ac7b..a2c13c712 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts @@ -17,7 +17,7 @@ export type { CertInfo }; desc: "支持海量DNS解析提供商,推荐使用,一样的免费通配符域名证书申请,支持多个域名打到同一个证书上", default: { input: { - renewDays: 20, + renewDays: 35, forceUpdate: false, }, strategy: { diff --git a/packages/ui/certd-client/public/static/images/logo/logo.png b/packages/ui/certd-client/public/static/images/logo/logo.png new file mode 100644 index 000000000..cb42f7473 Binary files /dev/null and b/packages/ui/certd-client/public/static/images/logo/logo.png differ diff --git a/packages/ui/certd-client/src/layout/layout-framework.vue b/packages/ui/certd-client/src/layout/layout-framework.vue index d399ca7c7..e764310f9 100644 --- a/packages/ui/certd-client/src/layout/layout-framework.vue +++ b/packages/ui/certd-client/src/layout/layout-framework.vue @@ -252,8 +252,6 @@ const { token } = useToken(); justify-content: flex-end; align-items: center; display: flex; - } - .header-menu { flex: 1; } } diff --git a/packages/ui/certd-client/src/style/common.less b/packages/ui/certd-client/src/style/common.less index 7a4fa9213..a33cacbd1 100644 --- a/packages/ui/certd-client/src/style/common.less +++ b/packages/ui/certd-client/src/style/common.less @@ -178,6 +178,14 @@ h1, h2, h3, h4, h5, h6 { color: #1890ff; } +.color-red { + color: red; +} + +.color-green { + color: green; +} + .iconify{ //font-size: 16px; } diff --git a/packages/ui/certd-client/src/views/certd/access/access-selector/access/secret-plain-getter.vue b/packages/ui/certd-client/src/views/certd/access/access-selector/access/secret-plain-getter.vue new file mode 100644 index 000000000..cf05c662f --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/access/access-selector/access/secret-plain-getter.vue @@ -0,0 +1,32 @@ + + + diff --git a/packages/ui/certd-client/src/views/certd/access/api.ts b/packages/ui/certd-client/src/views/certd/access/api.ts index 903f9c316..ca9d47c62 100644 --- a/packages/ui/certd-client/src/views/certd/access/api.ts +++ b/packages/ui/certd-client/src/views/certd/access/api.ts @@ -43,6 +43,14 @@ export function createAccessApi(from = "user") { }); }, + async GetSecretPlain(id: number, key: string) { + return await request({ + url: apiPrefix + "/getSecretPlain", + method: "post", + data: { id, key } + }); + }, + async GetProviderDefine(type: string) { return await request({ url: apiPrefix + "/define", diff --git a/packages/ui/certd-client/src/views/certd/access/common.tsx b/packages/ui/certd-client/src/views/certd/access/common.tsx index 99258f648..5b2c3ebec 100644 --- a/packages/ui/certd-client/src/views/certd/access/common.tsx +++ b/packages/ui/certd-client/src/views/certd/access/common.tsx @@ -1,9 +1,11 @@ import { ColumnCompositionProps, dict } from "@fast-crud/fast-crud"; -import { computed, ref, toRef } from "vue"; +import { computed, provide, ref, toRef } from "vue"; import { useReference } from "/@/use/use-refrence"; import { forEach, get, merge, set } from "lodash-es"; +import SecretPlainGetter from "/@/views/certd/access/access-selector/access/secret-plain-getter.vue"; export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) { + provide("accessApi", api); const AccessTypeDictRef = dict({ url: "/pi/access/accessTypeDict" }); @@ -32,6 +34,13 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) { }; const column = merge({ title: key }, defaultPluginConfig, field); + if (value.encrypt === true) { + column.suffixRender = (scope: { form: any; key: string }) => { + const { form, key } = scope; + const inputKey = scope.key.replace("access.", ""); + return ; + }; + } //eval useReference(column); diff --git a/packages/ui/certd-client/src/views/certd/pipeline/crud.tsx b/packages/ui/certd-client/src/views/certd/pipeline/crud.tsx index 7b83032db..c1632098e 100644 --- a/packages/ui/certd-client/src/views/certd/pipeline/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/pipeline/crud.tsx @@ -124,7 +124,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp { title: "申请证书", input: { - renewDays: 20, + renewDays: 35, ...form }, strategy: { diff --git a/packages/ui/certd-server/src/controller/pipeline/access-controller.ts b/packages/ui/certd-server/src/controller/pipeline/access-controller.ts index 3ee6e7372..4b1df6831 100644 --- a/packages/ui/certd-server/src/controller/pipeline/access-controller.ts +++ b/packages/ui/certd-server/src/controller/pipeline/access-controller.ts @@ -67,6 +67,12 @@ export class AccessController extends CrudController { return this.ok(access); } + @Post('/getSecretPlain', { summary: Constants.per.authOnly }) + async getSecretPlain(@Body(ALL) body: { id: number; key: string }) { + const value = await this.service.getById(body.id, this.getUserId()); + return this.ok(value[body.key]); + } + @Post('/accessTypeDict', { summary: Constants.per.authOnly }) async getAccessTypeDict() { const list = this.service.getDefineList(); diff --git a/packages/ui/certd-server/src/middleware/reset-passwd/middleware.ts b/packages/ui/certd-server/src/middleware/reset-passwd/middleware.ts index 56fa7bbbe..eba30c11e 100644 --- a/packages/ui/certd-server/src/middleware/reset-passwd/middleware.ts +++ b/packages/ui/certd-server/src/middleware/reset-passwd/middleware.ts @@ -30,6 +30,7 @@ export class ResetPasswdMiddleware implements IWebMiddleware { logger.info('开始重置1号管理员用户的密码'); const newPasswd = '123456'; await this.userService.resetPassword(1, newPasswd); + await this.userService.updateStatus(1, 1); logger.info(`重置1号管理员用户的密码完成,新密码为:${newPasswd}`); } } diff --git a/packages/ui/certd-server/src/modules/sys/authority/service/user-service.ts b/packages/ui/certd-server/src/modules/sys/authority/service/user-service.ts index a6a0d27b8..5748a6f42 100644 --- a/packages/ui/certd-server/src/modules/sys/authority/service/user-service.ts +++ b/packages/ui/certd-server/src/modules/sys/authority/service/user-service.ts @@ -198,6 +198,9 @@ export class UserService extends BaseService { } async resetPassword(userId: any, newPasswd: string) { + if (!userId) { + throw new CommonException('userId不能为空'); + } const param = { id: userId, password: newPasswd, @@ -210,15 +213,19 @@ export class UserService extends BaseService { ids = ids.split(','); ids = ids.map(id => parseInt(id)); } - if (ids instanceof Array) { - if (ids.includes(1)) { - throw new CommonException('不能删除管理员'); - } + if (ids.length === 0) { + return; + } + if (ids.includes(1)) { + throw new CommonException('不能删除管理员'); } await super.delete(ids); } async isAdmin(userId: any) { + if (!userId) { + throw new CommonException('userId不能为空'); + } const userRoles = await this.userRoleService.find({ where: { userId, @@ -229,4 +236,13 @@ export class UserService extends BaseService { return true; } } + + async updateStatus(id: number, status: number) { + if (!id) { + throw new CommonException('userId不能为空'); + } + await this.repository.update(id, { + status, + }); + } } diff --git a/packages/ui/certd-server/src/plugins/plugin-tencent/lib/index.ts b/packages/ui/certd-server/src/plugins/plugin-tencent/lib/index.ts index 8777c920d..8022ed914 100644 --- a/packages/ui/certd-server/src/plugins/plugin-tencent/lib/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-tencent/lib/index.ts @@ -55,4 +55,21 @@ export class TencentSslClient { this.checkRet(res); return res; } + + async DescribeCertificates(params: any) { + const client = await this.getSslClient(); + const res = await client.DescribeCertificates(params); + this.checkRet(res); + return res; + } + + async doRequest(action: string, params: any) { + const client = await this.getSslClient(); + if (!client[action]) { + throw new Error(`action ${action} not found`); + } + const res = await client[action](params); + this.checkRet(res); + return res; + } } diff --git a/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/delete-expiring-cert/index.ts b/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/delete-expiring-cert/index.ts new file mode 100644 index 000000000..998274930 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/delete-expiring-cert/index.ts @@ -0,0 +1,202 @@ +import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline'; +import { AbstractPlusTaskPlugin, TencentAccess } from '@certd/plugin-plus'; +import { TencentSslClient } from '../../lib/index.js'; +import dayjs from 'dayjs'; +import { remove } from 'lodash-es'; + +@IsTaskPlugin({ + name: 'TencentDeleteExpiringCert', + title: '删除腾讯云即将过期证书', + icon: 'svg:icon-tencentcloud', + group: pluginGroups.tencent.key, + desc: '仅删除未使用的证书', + default: { + strategy: { + runStrategy: RunStrategy.AlwaysRun, + }, + }, + needPlus: true, +}) +export class TencentDeleteExpiringCert extends AbstractPlusTaskPlugin { + @TaskInput({ + title: 'Access提供者', + helper: 'access 授权', + component: { + name: 'access-selector', + type: 'tencent', + }, + required: true, + }) + accessId!: string; + + @TaskInput({ + title: '关键字筛选', + helper: '仅匹配ID、备注名称、域名包含关键字的证书,可以不填', + required: false, + component: { + name: 'a-input', + }, + }) + searchKey!: string; + + @TaskInput({ + title: '最大删除数量', + helper: '单次运行最大删除数量', + value: 100, + component: { + name: 'a-input-number', + vModel: 'value', + }, + required: true, + }) + maxCount!: number; + + @TaskInput({ + title: '即将过期天数', + helper: + '仅删除有效期小于此天数的证书,\n注意:`1.26.14`版本之前Certd创建的证书流水线默认是到期前20天才更新证书,需要将之前创建的证书申请任务的更新天数改为35天,保证删除之前就已经替换掉即将过期证书', + value: 30, + component: { + name: 'a-input-number', + vModel: 'value', + }, + required: true, + }) + expiringDays!: number; + + @TaskInput({ + title: '检查超时时间', + helper: '检查删除任务结果超时时间,单位分钟', + value: 10, + component: { + name: 'a-input-number', + vModel: 'value', + }, + required: true, + }) + checkTimeout!: number; + + async onInstance() {} + + async execute(): Promise { + const access = await this.accessService.getById(this.accessId); + const sslClient = new TencentSslClient({ + access, + logger: this.logger, + }); + + const params = { + Limit: this.maxCount ?? 100, + SearchKey: this.searchKey, + ExpirationSort: 'ASC', + FilterSource: 'upload', + // FilterExpiring: 1, + }; + const res = await sslClient.DescribeCertificates(params); + let certificates = res?.Certificates; + if (!certificates && !certificates.length) { + this.logger.info('没有找到证书'); + return; + } + + certificates = certificates.filter((item: any) => { + const endTime = item.CertEndTime; + return dayjs(endTime).add(this.expiringDays, 'day').isBefore(dayjs()); + }); + for (const certificate of certificates) { + this.logger.info(`证书ID:${certificate.CertificateId}, 过期时间:${certificate.CertEndTime},Alias:${certificate.Alias},证书域名:${certificate.Domain}`); + } + this.logger.info(`即将过期的证书数量:${certificates.length}`); + if (certificates.length === 0) { + this.logger.info('没有即将过期的证书, 无需删除'); + return; + } + const certIds = certificates.map((cert: any) => cert.CertificateId); + + const deleteRes = await sslClient.doRequest('DeleteCertificates', { + CertificateIds: certIds, + IsSync: true, + }); + this.logger.info('删除任务已提交: ', JSON.stringify(deleteRes)); + const ids = deleteRes?.CertTaskIds; + if (!ids && !ids.length) { + this.logger.error('没有找到任务ID'); + return; + } + const taskIds = ids.map((id: any) => id.TaskId); + const startTime = Date.now(); + const results = {}; + + const statusCount = { + success: 0, + failed: 0, + unauthorized: 0, + unbind: 0, + timeout: 0, + }; + const total = taskIds.length; + + while (Date.now() < startTime + this.checkTimeout * 60 * 1000) { + this.checkSignal(); + const taskResultRes = await sslClient.doRequest('DescribeDeleteCertificatesTaskResult', { + TaskIds: taskIds, + }); + const result = taskResultRes.DeleteTaskResult; + if (!result || result.length === 0) { + this.logger.info('暂未获取到有效的任务结果'); + continue; + } + for (const item of result) { + //遍历结果 + const status = item.Status; + if (status !== 0) { + remove(taskIds, id => id === item.TaskId); + } + // Status : 0表示任务进行中、 1表示任务成功、 2表示任务失败、3表示未授权服务角色导致任务失败、4表示有未解绑的云资源导致任务失败、5表示查询关联云资源超时导致任务失败 + if (status === 0) { + this.logger.info(`任务${item.TaskId}<${item.CertId}>: 进行中`); + } else if (status === 1) { + this.logger.info(`任务${item.TaskId}<${item.CertId}>: 成功`); + results[item.TaskId] = '成功'; + statusCount.success++; + } else if (status === 2) { + this.logger.error(`任务${item.TaskId}<${item.CertId}>: 失败`); + results[item.TaskId] = '失败'; + statusCount.failed++; + } else if (status === 3) { + this.logger.error(`任务${item.TaskId}<${item.CertId}>: 未授权服务角色导致任务失败`); + results[item.TaskId] = '未授权服务角色导致任务失败'; + statusCount.unauthorized++; + } else if (status === 4) { + this.logger.error(`任务${item.TaskId}<${item.CertId}>: 有未解绑的云资源导致任务失败`); + results[item.TaskId] = '有未解绑的云资源导致任务失败'; + statusCount.unbind++; + } else if (status === 5) { + this.logger.error(`任务${item.TaskId}<${item.CertId}>: 查询关联云资源超时导致任务失败`); + results[item.TaskId] = '查询关联云资源超时导致任务失败'; + statusCount.timeout++; + } else { + this.logger.info(`任务${item.TaskId}<${item.CertId}>: 未知状态:${status}`); + statusCount.failed++; + } + } + this.logger.info( + // eslint-disable-next-line max-len + `任务总数:${total}, 进行中:${taskIds.length}, 成功:${statusCount.success}, 未授权服务角色导致失败:${statusCount.unauthorized}, 未解绑关联资源失败:${statusCount.unbind}, 查询关联资源超时:${statusCount.timeout},未知原因失败:${statusCount.failed}` + ); + if (taskIds.length === 0) { + this.logger.info('任务已全部完成'); + + if (statusCount.unauthorized > 0) { + throw new Error('有未授权服务角色导致任务失败,需给Access授权服务角色SSL_QCSLinkedRoleInReplaceLoadCertificate'); + } + + return; + } + await this.ctx.utils.sleep(10000); + } + this.logger.error('检查任务结果超时', JSON.stringify(results)); + } +} + +new TencentDeleteExpiringCert(); diff --git a/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/index.ts b/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/index.ts index aadbe339d..758b2d3ae 100644 --- a/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-tencent/plugin/index.ts @@ -5,3 +5,4 @@ export * from './deploy-to-cdn-v2/index.js'; export * from './upload-to-tencent/index.js'; export * from './deploy-to-cos/index.js'; export * from './deploy-to-eo/index.js'; +export * from './delete-expiring-cert/index.js';