mirror of https://github.com/certd/certd
Merge remote-tracking branch 'origin/v2-dev' into v2-dev
commit
5c992c3214
|
@ -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 # 数据库类型
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -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)
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -33,7 +33,7 @@ export type DomainsVerifyPlanInput = {
|
|||
desc: "免费通配符域名证书申请,支持多个域名打到同一个证书上",
|
||||
default: {
|
||||
input: {
|
||||
renewDays: 20,
|
||||
renewDays: 35,
|
||||
forceUpdate: false,
|
||||
},
|
||||
strategy: {
|
||||
|
|
|
@ -17,7 +17,7 @@ export type { CertInfo };
|
|||
desc: "支持海量DNS解析提供商,推荐使用,一样的免费通配符域名证书申请,支持多个域名打到同一个证书上",
|
||||
default: {
|
||||
input: {
|
||||
renewDays: 20,
|
||||
renewDays: 35,
|
||||
forceUpdate: false,
|
||||
},
|
||||
strategy: {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -252,8 +252,6 @@ const { token } = useToken();
|
|||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.header-menu {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -178,6 +178,14 @@ h1, h2, h3, h4, h5, h6 {
|
|||
color: #1890ff;
|
||||
}
|
||||
|
||||
.color-red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.color-green {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.iconify{
|
||||
//font-size: 16px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<a-button class="cd-secret-plain-getter ml-5">
|
||||
<fs-icon class="pointer" :icon="computedIcon" @click="showPlain" />
|
||||
</a-button>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from "vue";
|
||||
defineOptions({
|
||||
name: "SecretPlainGetter"
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string;
|
||||
accessId?: number;
|
||||
inputKey: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
const showRef = ref(false);
|
||||
const computedIcon = computed(() => {
|
||||
return showRef.value ? "ion:eye-outline" : "ion:eye-off-outline";
|
||||
});
|
||||
const accessApi: any = inject("accessApi");
|
||||
async function showPlain() {
|
||||
showRef.value = true;
|
||||
if (props.accessId) {
|
||||
const plain = await accessApi.GetSecretPlain(props.accessId, props.inputKey);
|
||||
emit("update:modelValue", plain);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less"></style>
|
|
@ -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",
|
||||
|
|
|
@ -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 <SecretPlainGetter accessId={form.id} inputKey={inputKey} v-model={form[key]} />;
|
||||
};
|
||||
}
|
||||
//eval
|
||||
useReference(column);
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
|
|||
{
|
||||
title: "申请证书",
|
||||
input: {
|
||||
renewDays: 20,
|
||||
renewDays: 35,
|
||||
...form
|
||||
},
|
||||
strategy: {
|
||||
|
|
|
@ -67,6 +67,12 @@ export class AccessController extends CrudController<AccessService> {
|
|||
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();
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,6 +198,9 @@ export class UserService extends BaseService<UserEntity> {
|
|||
}
|
||||
|
||||
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<UserEntity> {
|
|||
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<UserEntity> {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async updateStatus(id: number, status: number) {
|
||||
if (!id) {
|
||||
throw new CommonException('userId不能为空');
|
||||
}
|
||||
await this.repository.update(id, {
|
||||
status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<span class="color-red">注意:`1.26.14`版本之前Certd创建的证书流水线默认是到期前20天才更新证书,需要将之前创建的证书申请任务的更新天数改为35天,保证删除之前就已经替换掉即将过期证书</span>',
|
||||
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<void> {
|
||||
const access = await this.accessService.getById<TencentAccess>(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();
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue